mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-06-22 10:02:15 +00:00
feat: improve REST API permissions functions test coverage (#12512)
- Add a storage interface for REST API middleware functions checking permissions - Refactor those middleware functions to use this interface instead of directly accessing data members and move them into their own package (syntactic only refactor, no modification to the logic) - Add tests for this new package - The tests directory has a README explaining the test architecture, debugging tips and hints to collect coverage Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12512 Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
This commit is contained in:
commit
7e04aa1e97
83 changed files with 5114 additions and 639 deletions
|
|
@ -219,6 +219,54 @@ forgejo.org/modules/zstd
|
|||
Writer.Write
|
||||
Writer.Close
|
||||
|
||||
forgejo.org/routers/api/v1/permissions
|
||||
Permissions.GetContext
|
||||
Permissions.SetContext
|
||||
Permissions.GetToken
|
||||
Permissions.SetToken
|
||||
Permissions.GetRepository
|
||||
Permissions.SetRepository
|
||||
Permissions.GetDoer
|
||||
Permissions.SetDoer
|
||||
Permissions.GetUser
|
||||
Permissions.SetUser
|
||||
Permissions.GetOrg
|
||||
Permissions.SetOrg
|
||||
Permissions.GetTeam
|
||||
Permissions.SetTeam
|
||||
Permissions.GetPackageOwner
|
||||
Permissions.SetPackageOwner
|
||||
Permissions.GetPackageAccessMode
|
||||
Permissions.SetPackageAccessMode
|
||||
Permissions.GetPermission
|
||||
Permissions.SetPermission
|
||||
Permissions.GetIsSigned
|
||||
Permissions.SetIsSigned
|
||||
Permissions.GetPublicOnly
|
||||
Permissions.SetPublicOnly
|
||||
Permissions.GetReducer
|
||||
Permissions.SetReducer
|
||||
Permissions.GetAuthentication
|
||||
Permissions.SetAuthentication
|
||||
Permissions.GetRequiredScopeCategories
|
||||
Permissions.SetRequiredScopeCategories
|
||||
Permissions.GetStatus
|
||||
Permissions.SetStatus
|
||||
Permissions.GetMessage
|
||||
Permissions.SetMessage
|
||||
Permissions.GetError
|
||||
Permissions.Error
|
||||
Permissions.NotFound
|
||||
Permissions.InternalServerError
|
||||
Permissions.WrittenStatus
|
||||
Permissions.String
|
||||
Permissions.Strings
|
||||
|
||||
forgejo.org/routers/api/v1/permissions/tests
|
||||
GetSignatureStringToSignature
|
||||
GetUniquePermissionsSequences
|
||||
GetShortestPermissionSequenceForEachSignature
|
||||
|
||||
forgejo.org/routers/web/org
|
||||
MustEnableProjects
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/web/middleware"
|
||||
apiv1_permissions_tests "forgejo.org/routers/api/v1/permissions/tests"
|
||||
|
||||
"code.forgejo.org/go-chi/binding"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
|
@ -43,6 +45,9 @@ type Route struct {
|
|||
|
||||
// NewRoute creates a new route
|
||||
func NewRoute() *Route {
|
||||
if setting.IsInTesting {
|
||||
apiv1_permissions_tests.Reset()
|
||||
}
|
||||
r := chi.NewRouter()
|
||||
return &Route{R: r}
|
||||
}
|
||||
|
|
@ -62,6 +67,9 @@ func (r *Route) Group(pattern string, fn func(), middlewares ...any) {
|
|||
previousMiddlewares := r.curMiddlewares
|
||||
r.curGroupPrefix += pattern
|
||||
r.curMiddlewares = append(r.curMiddlewares, middlewares...)
|
||||
if setting.IsInTesting {
|
||||
defer apiv1_permissions_tests.RestorePermissionsSequence()()
|
||||
}
|
||||
|
||||
fn()
|
||||
|
||||
|
|
@ -105,6 +113,10 @@ func (r *Route) wrapMiddlewareAndHandler(h []any) ([]func(http.Handler) http.Han
|
|||
// If any method is invalid, the lower level router will panic.
|
||||
func (r *Route) Methods(methods, pattern string, h ...any) {
|
||||
middlewares, handlerFunc := r.wrapMiddlewareAndHandler(h)
|
||||
if setting.IsInTesting {
|
||||
apiv1_permissions_tests.CollectPermissionsMiddlewares(h[len(h)-1], methods, r.getPattern(pattern))
|
||||
apiv1_permissions_tests.RestoreLastPermissionsSequence()
|
||||
}
|
||||
fullPattern := r.getPattern(pattern)
|
||||
if strings.Contains(methods, ",") {
|
||||
methods := strings.SplitSeq(methods, ",")
|
||||
|
|
@ -171,7 +183,7 @@ func (r *Route) NotFound(h ...any) {
|
|||
|
||||
// Combo delegates requests to Combo
|
||||
func (r *Route) Combo(pattern string, h ...any) *Combo {
|
||||
return &Combo{r, pattern, h}
|
||||
return &Combo{r, pattern, h, apiv1_permissions_tests.GetSignatures()}
|
||||
}
|
||||
|
||||
// Combo represents a tiny group routes with same pattern
|
||||
|
|
@ -179,34 +191,41 @@ type Combo struct {
|
|||
r *Route
|
||||
pattern string
|
||||
h []any
|
||||
|
||||
permissionsSequence apiv1_permissions_tests.Sequence
|
||||
}
|
||||
|
||||
// Get delegates Get method
|
||||
func (c *Combo) Get(h ...any) *Combo {
|
||||
c.r.Get(c.pattern, append(c.h, h...)...)
|
||||
apiv1_permissions_tests.SetSignatures(c.permissionsSequence)
|
||||
return c
|
||||
}
|
||||
|
||||
// Post delegates Post method
|
||||
func (c *Combo) Post(h ...any) *Combo {
|
||||
c.r.Post(c.pattern, append(c.h, h...)...)
|
||||
apiv1_permissions_tests.SetSignatures(c.permissionsSequence)
|
||||
return c
|
||||
}
|
||||
|
||||
// Delete delegates Delete method
|
||||
func (c *Combo) Delete(h ...any) *Combo {
|
||||
c.r.Delete(c.pattern, append(c.h, h...)...)
|
||||
apiv1_permissions_tests.SetSignatures(c.permissionsSequence)
|
||||
return c
|
||||
}
|
||||
|
||||
// Put delegates Put method
|
||||
func (c *Combo) Put(h ...any) *Combo {
|
||||
c.r.Put(c.pattern, append(c.h, h...)...)
|
||||
apiv1_permissions_tests.SetSignatures(c.permissionsSequence)
|
||||
return c
|
||||
}
|
||||
|
||||
// Patch delegates Patch method
|
||||
func (c *Combo) Patch(h ...any) *Combo {
|
||||
c.r.Patch(c.pattern, append(c.h, h...)...)
|
||||
apiv1_permissions_tests.SetSignatures(c.permissionsSequence)
|
||||
return c
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ func (info *FuncInfo) String() string {
|
|||
return fmt.Sprintf("%s:%d(%s)", info.shortFile, info.line, info.shortName)
|
||||
}
|
||||
|
||||
func GetFuncShortName(fn any) string {
|
||||
return GetFuncInfo(fn).shortName
|
||||
}
|
||||
|
||||
// GetFuncInfo returns the FuncInfo for a provided function and friendlyname
|
||||
func GetFuncInfo(fn any, friendlyName ...string) *FuncInfo {
|
||||
// ptr represents the memory position of the function passed in as v.
|
||||
|
|
|
|||
|
|
@ -11,10 +11,11 @@ import (
|
|||
auth_model "forgejo.org/models/auth"
|
||||
"forgejo.org/modules/log"
|
||||
"forgejo.org/modules/setting"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
apiv1_permissions_tests "forgejo.org/routers/api/v1/permissions/tests"
|
||||
"forgejo.org/routers/common"
|
||||
"forgejo.org/services/auth"
|
||||
auth_method "forgejo.org/services/auth/method"
|
||||
"forgejo.org/services/authz"
|
||||
"forgejo.org/services/context"
|
||||
|
||||
"github.com/go-chi/cors"
|
||||
|
|
@ -38,7 +39,7 @@ func Middlewares() (stack []any) {
|
|||
checkDeprecatedAuthMethods,
|
||||
// Get user from session if logged in.
|
||||
apiAuthentication(buildAuthGroup()),
|
||||
apiAuthorization,
|
||||
apiAuthorization(),
|
||||
verifyAuthWithOptions(&common.VerifyOptions{
|
||||
SignInRequired: setting.Service.RequireSignInView,
|
||||
}),
|
||||
|
|
@ -97,28 +98,10 @@ func apiAuthentication(authMethod auth.Method) func(*context.APIContext) {
|
|||
}
|
||||
}
|
||||
|
||||
func apiAuthorization(ctx *context.APIContext) {
|
||||
if hasScope, scope := ctx.Authentication.Scope().Get(); hasScope {
|
||||
publicOnly, err := scope.PublicOnly()
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
ctx.PublicOnly = publicOnly
|
||||
}
|
||||
|
||||
reducer := ctx.Authentication.Reducer()
|
||||
if reducer != nil {
|
||||
ctx.Reducer = reducer
|
||||
} else {
|
||||
// No Reducer will be populated if the auth method wasn't an PAT. In this case, we populate `ctx.Reducer` so no
|
||||
// nil checks are needed, and we respect the scope `PublicOnly()` so that it it's safe to just rely on
|
||||
// `ctx.Reducer` to account for public-only access:
|
||||
if ctx.PublicOnly {
|
||||
ctx.Reducer = &authz.PublicReposAuthorizationReducer{}
|
||||
} else {
|
||||
ctx.Reducer = &authz.AllAccessAuthorizationReducer{}
|
||||
}
|
||||
func apiAuthorization() func(ctx *context.APIContext) {
|
||||
apiv1_permissions_tests.RecordSignature(apiv1_permissions.APIAuthorization)
|
||||
return func(ctx *context.APIContext) {
|
||||
apiv1_permissions.APIAuthorization(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
35
routers/api/v1/permissions/api_authorization.go
Normal file
35
routers/api/v1/permissions/api_authorization.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forgejo.org/services/authz"
|
||||
)
|
||||
|
||||
func APIAuthorization(ctx Context) {
|
||||
if hasScope, scope := ctx.GetAuthentication().Scope().Get(); hasScope {
|
||||
publicOnly, err := scope.PublicOnly()
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
ctx.SetPublicOnly(publicOnly)
|
||||
}
|
||||
|
||||
reducer := ctx.GetAuthentication().Reducer()
|
||||
if reducer != nil {
|
||||
ctx.SetReducer(reducer)
|
||||
} else {
|
||||
// No Reducer will be populated if the auth method wasn't an PAT. In this case, we populate `ctx.Reducer` so no
|
||||
// nil checks are needed, and we respect the scope `PublicOnly()` so that it it's safe to just rely on
|
||||
// `ctx.Reducer` to account for public-only access:
|
||||
if ctx.GetPublicOnly() {
|
||||
ctx.SetReducer(&authz.PublicReposAuthorizationReducer{})
|
||||
} else {
|
||||
ctx.SetReducer(&authz.AllAccessAuthorizationReducer{})
|
||||
}
|
||||
}
|
||||
}
|
||||
66
routers/api/v1/permissions/check_token_public_only.go
Normal file
66
routers/api/v1/permissions/check_token_public_only.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
user_model "forgejo.org/models/user"
|
||||
api "forgejo.org/modules/structs"
|
||||
)
|
||||
|
||||
func CheckTokenPublicOnly(ctx Context, user, org, packageOwner *user_model.User) {
|
||||
if !ctx.GetPublicOnly() {
|
||||
return
|
||||
}
|
||||
|
||||
requiredScopeCategories := ctx.GetRequiredScopeCategories()
|
||||
if len(requiredScopeCategories) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// public Only permission check
|
||||
switch {
|
||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository):
|
||||
if ctx.GetRepository() != nil && ctx.GetRepository().IsPrivate {
|
||||
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public repos")
|
||||
return
|
||||
}
|
||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryIssue):
|
||||
if ctx.GetRepository() != nil && ctx.GetRepository().IsPrivate {
|
||||
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public issues")
|
||||
return
|
||||
}
|
||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryOrganization):
|
||||
if org != nil && org.Visibility != api.VisibleTypePublic {
|
||||
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public orgs")
|
||||
return
|
||||
}
|
||||
if user != nil && user.IsOrganization() && user.Visibility != api.VisibleTypePublic {
|
||||
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public orgs")
|
||||
return
|
||||
}
|
||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryUser):
|
||||
if user != nil && user.IsUser() && user.Visibility != api.VisibleTypePublic {
|
||||
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public users")
|
||||
return
|
||||
}
|
||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryActivityPub):
|
||||
if user != nil && user.IsUser() && user.Visibility != api.VisibleTypePublic {
|
||||
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public activitypub")
|
||||
return
|
||||
}
|
||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryNotification):
|
||||
if ctx.GetRepository() != nil && ctx.GetRepository().IsPrivate {
|
||||
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public notifications")
|
||||
return
|
||||
}
|
||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryPackage):
|
||||
if packageOwner != nil && packageOwner.Visibility.IsPrivate() {
|
||||
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public packages")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
28
routers/api/v1/permissions/helpers.go
Normal file
28
routers/api/v1/permissions/helpers.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"forgejo.org/models/unit"
|
||||
)
|
||||
|
||||
func IsUserSiteAdmin(ctx Context) bool {
|
||||
if !ctx.GetReducer().AllowAdminOverride() {
|
||||
return false
|
||||
}
|
||||
return ctx.GetIsSigned() && ctx.GetDoer().IsAdmin
|
||||
}
|
||||
|
||||
func IsUserRepoAdmin(ctx Context) bool {
|
||||
if !ctx.GetReducer().AllowAdminOverride() {
|
||||
return false
|
||||
}
|
||||
return ctx.GetPermission().IsAdmin()
|
||||
}
|
||||
|
||||
func IsUserRepoWriter(ctx Context, unitTypes []unit.Type) bool {
|
||||
return slices.ContainsFunc(unitTypes, ctx.GetPermission().CanWrite)
|
||||
}
|
||||
26
routers/api/v1/permissions/individual_perms_checker.go
Normal file
26
routers/api/v1/permissions/individual_perms_checker.go
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
api "forgejo.org/modules/structs"
|
||||
)
|
||||
|
||||
func IndividualPermsChecker(ctx Context) {
|
||||
// org permissions have been checked in context.OrgAssignment(), but individual permissions haven't been checked.
|
||||
if ctx.GetUser().IsIndividual() {
|
||||
switch ctx.GetUser().Visibility {
|
||||
case api.VisibleTypePrivate:
|
||||
if ctx.GetDoer() == nil || (ctx.GetUser().ID != ctx.GetDoer().ID && !IsUserSiteAdmin(ctx)) {
|
||||
ctx.NotFound("Visit Project", nil)
|
||||
return
|
||||
}
|
||||
case api.VisibleTypeLimited:
|
||||
if ctx.GetDoer() == nil {
|
||||
ctx.NotFound("Visit Project", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
routers/api/v1/permissions/interface.go
Normal file
57
routers/api/v1/permissions/interface.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
org_model "forgejo.org/models/organization"
|
||||
"forgejo.org/models/perm"
|
||||
access_model "forgejo.org/models/perm/access"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/services/auth"
|
||||
"forgejo.org/services/authz"
|
||||
)
|
||||
|
||||
type Context interface {
|
||||
GetContext() context.Context
|
||||
|
||||
GetRepository() *repo_model.Repository
|
||||
|
||||
GetDoer() *user_model.User
|
||||
|
||||
GetUser() *user_model.User
|
||||
|
||||
GetOrg() *org_model.Organization
|
||||
|
||||
GetTeam() *org_model.Team
|
||||
|
||||
GetPackageOwner() *user_model.User
|
||||
GetPackageAccessMode() perm.AccessMode
|
||||
|
||||
GetPermission() *access_model.Permission
|
||||
SetPermission(*access_model.Permission)
|
||||
|
||||
GetIsSigned() bool
|
||||
|
||||
GetPublicOnly() bool
|
||||
SetPublicOnly(bool)
|
||||
|
||||
GetReducer() authz.AuthorizationReducer
|
||||
SetReducer(authz.AuthorizationReducer)
|
||||
|
||||
GetAuthentication() auth.AuthenticationResult
|
||||
|
||||
GetRequiredScopeCategories() []auth_model.AccessTokenScopeCategory
|
||||
SetRequiredScopeCategories([]auth_model.AccessTokenScopeCategory)
|
||||
|
||||
Error(status int, title string, obj any)
|
||||
InternalServerError(err error)
|
||||
NotFound(objs ...any)
|
||||
|
||||
GetError() error
|
||||
WrittenStatus() int
|
||||
}
|
||||
32
routers/api/v1/permissions/must_allow_pulls.go
Normal file
32
routers/api/v1/permissions/must_allow_pulls.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"forgejo.org/models/unit"
|
||||
"forgejo.org/modules/log"
|
||||
)
|
||||
|
||||
func MustAllowPulls(ctx Context) {
|
||||
if !ctx.GetRepository().CanEnablePulls() || !ctx.GetPermission().CanRead(unit.TypePullRequests) {
|
||||
if ctx.GetRepository().CanEnablePulls() && log.IsTrace() {
|
||||
if ctx.GetIsSigned() {
|
||||
log.Trace("Permission Denied: User %-v cannot read %-v in Repo %-v\n"+
|
||||
"User in Repo has Permissions: %-+v",
|
||||
ctx.GetDoer(),
|
||||
unit.TypePullRequests,
|
||||
ctx.GetRepository(),
|
||||
ctx.GetPermission())
|
||||
} else {
|
||||
log.Trace("Permission Denied: Anonymous user cannot read %-v in Repo %-v\n"+
|
||||
"Anonymous user in Repo has Permissions: %-+v",
|
||||
unit.TypePullRequests,
|
||||
ctx.GetRepository(),
|
||||
ctx.GetPermission())
|
||||
}
|
||||
}
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
}
|
||||
15
routers/api/v1/permissions/must_enable_attachments.go
Normal file
15
routers/api/v1/permissions/must_enable_attachments.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"forgejo.org/modules/setting"
|
||||
)
|
||||
|
||||
func MustEnableAttachments(ctx Context) {
|
||||
if !setting.Attachment.Enabled {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
}
|
||||
32
routers/api/v1/permissions/must_enable_issues.go
Normal file
32
routers/api/v1/permissions/must_enable_issues.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"forgejo.org/models/unit"
|
||||
"forgejo.org/modules/log"
|
||||
)
|
||||
|
||||
func MustEnableIssues(ctx Context) {
|
||||
if !ctx.GetPermission().CanRead(unit.TypeIssues) {
|
||||
if log.IsTrace() {
|
||||
if ctx.GetIsSigned() {
|
||||
log.Trace("Permission Denied: User %-v cannot read %-v in Repo %-v\n"+
|
||||
"User in Repo has Permissions: %-+v",
|
||||
ctx.GetDoer(),
|
||||
unit.TypeIssues,
|
||||
ctx.GetRepository(),
|
||||
ctx.GetPermission())
|
||||
} else {
|
||||
log.Trace("Permission Denied: Anonymous user cannot read %-v in Repo %-v\n"+
|
||||
"Anonymous user in Repo has Permissions: %-+v",
|
||||
unit.TypeIssues,
|
||||
ctx.GetRepository(),
|
||||
ctx.GetPermission())
|
||||
}
|
||||
}
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
}
|
||||
34
routers/api/v1/permissions/must_enable_issues_or_pulls.go
Normal file
34
routers/api/v1/permissions/must_enable_issues_or_pulls.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"forgejo.org/models/unit"
|
||||
"forgejo.org/modules/log"
|
||||
)
|
||||
|
||||
func MustEnableIssuesOrPulls(ctx Context) {
|
||||
if !ctx.GetPermission().CanRead(unit.TypeIssues) &&
|
||||
(!ctx.GetRepository().CanEnablePulls() || !ctx.GetPermission().CanRead(unit.TypePullRequests)) {
|
||||
if ctx.GetRepository().CanEnablePulls() && log.IsTrace() {
|
||||
if ctx.GetIsSigned() {
|
||||
log.Trace("Permission Denied: User %-v cannot read %-v and %-v in Repo %-v\n"+
|
||||
"User in Repo has Permissions: %-+v",
|
||||
ctx.GetDoer(),
|
||||
unit.TypeIssues,
|
||||
unit.TypePullRequests,
|
||||
ctx.GetRepository(),
|
||||
ctx.GetPermission())
|
||||
} else {
|
||||
log.Trace("Permission Denied: Anonymous user cannot read %-v and %-v in Repo %-v\n"+
|
||||
"Anonymous user in Repo has Permissions: %-+v",
|
||||
unit.TypeIssues,
|
||||
unit.TypePullRequests,
|
||||
ctx.GetRepository(),
|
||||
ctx.GetPermission())
|
||||
}
|
||||
}
|
||||
ctx.NotFound()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
issues_model "forgejo.org/models/issues"
|
||||
"forgejo.org/models/unit"
|
||||
)
|
||||
|
||||
func MustEnableLocalIssuesIfIsIssue(ctx Context, index int64) {
|
||||
if ctx.GetRepository().UnitEnabled(ctx.GetContext(), unit.TypeIssues) {
|
||||
return
|
||||
}
|
||||
|
||||
issue, err := issues_model.GetIssueByIndex(ctx.GetContext(), ctx.GetRepository().ID, index)
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.NotFound()
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !issue.IsPull {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
}
|
||||
15
routers/api/v1/permissions/must_enable_wiki.go
Normal file
15
routers/api/v1/permissions/must_enable_wiki.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"forgejo.org/models/unit"
|
||||
)
|
||||
|
||||
func MustEnableWiki(ctx Context) {
|
||||
if !(ctx.GetPermission().CanRead(unit.TypeWiki)) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
}
|
||||
15
routers/api/v1/permissions/must_not_be_archived.go
Normal file
15
routers/api/v1/permissions/must_not_be_archived.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func MustNotBeArchived(ctx Context) {
|
||||
if ctx.GetRepository().IsArchived {
|
||||
ctx.Error(http.StatusLocked, "RepoArchived", fmt.Errorf("%s is archived", ctx.GetRepository().LogString()))
|
||||
}
|
||||
}
|
||||
326
routers/api/v1/permissions/permissions.go
Normal file
326
routers/api/v1/permissions/permissions.go
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
org_model "forgejo.org/models/organization"
|
||||
"forgejo.org/models/perm"
|
||||
access_model "forgejo.org/models/perm/access"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/services/auth"
|
||||
"forgejo.org/services/authz"
|
||||
)
|
||||
|
||||
type Permissions struct {
|
||||
ctx context.Context
|
||||
|
||||
token *auth_model.AccessToken
|
||||
doer *user_model.User
|
||||
user *user_model.User
|
||||
team *org_model.Team
|
||||
org *org_model.Organization
|
||||
isSigned bool
|
||||
authentication auth.AuthenticationResult
|
||||
requiredScopeCategories []auth_model.AccessTokenScopeCategory
|
||||
reducer authz.AuthorizationReducer
|
||||
publicOnly bool
|
||||
repository *repo_model.Repository
|
||||
permission *access_model.Permission
|
||||
packageOwner *user_model.User
|
||||
packageAccessMode perm.AccessMode
|
||||
|
||||
status int
|
||||
message string
|
||||
}
|
||||
|
||||
func (o *Permissions) GetContext() context.Context {
|
||||
return o.ctx
|
||||
}
|
||||
|
||||
func (o *Permissions) SetContext(ctx context.Context) {
|
||||
o.ctx = ctx
|
||||
}
|
||||
|
||||
func (o *Permissions) GetToken() *auth_model.AccessToken {
|
||||
return o.token
|
||||
}
|
||||
|
||||
func (o *Permissions) SetToken(token *auth_model.AccessToken) {
|
||||
o.token = token
|
||||
}
|
||||
|
||||
func (o *Permissions) GetRepository() *repo_model.Repository {
|
||||
return o.repository
|
||||
}
|
||||
|
||||
func (o *Permissions) SetRepository(repository *repo_model.Repository) {
|
||||
o.repository = repository
|
||||
}
|
||||
|
||||
func (o *Permissions) GetDoer() *user_model.User {
|
||||
return o.doer
|
||||
}
|
||||
|
||||
func (o *Permissions) SetDoer(doer *user_model.User) {
|
||||
o.doer = doer
|
||||
}
|
||||
|
||||
func (o *Permissions) GetUser() *user_model.User {
|
||||
return o.user
|
||||
}
|
||||
|
||||
func (o *Permissions) SetUser(user *user_model.User) {
|
||||
o.user = user
|
||||
}
|
||||
|
||||
func (o *Permissions) GetOrg() *org_model.Organization {
|
||||
return o.org
|
||||
}
|
||||
|
||||
func (o *Permissions) SetOrg(org *org_model.Organization) {
|
||||
o.org = org
|
||||
}
|
||||
|
||||
func (o *Permissions) GetTeam() *org_model.Team {
|
||||
return o.team
|
||||
}
|
||||
|
||||
func (o *Permissions) SetTeam(team *org_model.Team) {
|
||||
o.team = team
|
||||
}
|
||||
|
||||
func (o *Permissions) GetPackageOwner() *user_model.User {
|
||||
return o.packageOwner
|
||||
}
|
||||
|
||||
func (o *Permissions) SetPackageOwner(packageOwner *user_model.User) {
|
||||
o.packageOwner = packageOwner
|
||||
}
|
||||
|
||||
func (o *Permissions) GetPackageAccessMode() perm.AccessMode {
|
||||
return o.packageAccessMode
|
||||
}
|
||||
|
||||
func (o *Permissions) SetPackageAccessMode(packageAccessMode perm.AccessMode) {
|
||||
o.packageAccessMode = packageAccessMode
|
||||
}
|
||||
|
||||
func (o *Permissions) GetPermission() *access_model.Permission {
|
||||
return o.permission
|
||||
}
|
||||
|
||||
func (o *Permissions) SetPermission(permission *access_model.Permission) {
|
||||
o.permission = permission
|
||||
}
|
||||
|
||||
func (o *Permissions) GetIsSigned() bool {
|
||||
return o.isSigned
|
||||
}
|
||||
|
||||
func (o *Permissions) SetIsSigned(isSigned bool) {
|
||||
o.isSigned = isSigned
|
||||
}
|
||||
|
||||
func (o *Permissions) GetPublicOnly() bool {
|
||||
return o.publicOnly
|
||||
}
|
||||
|
||||
func (o *Permissions) SetPublicOnly(publicOnly bool) {
|
||||
o.publicOnly = publicOnly
|
||||
}
|
||||
|
||||
func (o *Permissions) GetReducer() authz.AuthorizationReducer {
|
||||
return o.reducer
|
||||
}
|
||||
|
||||
func (o *Permissions) SetReducer(reducer authz.AuthorizationReducer) {
|
||||
o.reducer = reducer
|
||||
}
|
||||
|
||||
func (o *Permissions) GetAuthentication() auth.AuthenticationResult {
|
||||
return o.authentication
|
||||
}
|
||||
|
||||
func (o *Permissions) SetAuthentication(authentication auth.AuthenticationResult) {
|
||||
o.authentication = authentication
|
||||
}
|
||||
|
||||
func (o *Permissions) GetRequiredScopeCategories() []auth_model.AccessTokenScopeCategory {
|
||||
return o.requiredScopeCategories
|
||||
}
|
||||
|
||||
func (o *Permissions) SetRequiredScopeCategories(requiredScopeCategories []auth_model.AccessTokenScopeCategory) {
|
||||
o.requiredScopeCategories = requiredScopeCategories
|
||||
}
|
||||
|
||||
func (o *Permissions) GetStatus() int {
|
||||
return o.status
|
||||
}
|
||||
|
||||
func (o *Permissions) SetStatus(status int) {
|
||||
o.status = status
|
||||
}
|
||||
|
||||
func (o *Permissions) GetMessage() string {
|
||||
return o.message
|
||||
}
|
||||
|
||||
func (o *Permissions) SetMessage(message string) {
|
||||
o.message = message
|
||||
}
|
||||
|
||||
func (o *Permissions) GetError() error {
|
||||
if o.status == 0 {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%d: %s", o.status, o.message)
|
||||
}
|
||||
|
||||
func (o *Permissions) Error(status int, title string, obj any) {
|
||||
var message string
|
||||
if err, ok := obj.(error); ok {
|
||||
message = err.Error()
|
||||
} else {
|
||||
message = fmt.Sprintf("%s", obj)
|
||||
}
|
||||
o.status = status
|
||||
o.message = fmt.Sprintf("%s: %s", title, message)
|
||||
}
|
||||
|
||||
func (o *Permissions) NotFound(objs ...any) {
|
||||
errors := make([]string, 0)
|
||||
message := "Not Found"
|
||||
for _, obj := range objs {
|
||||
// Ignore nil
|
||||
if obj == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err, ok := obj.(error); ok {
|
||||
errors = append(errors, err.Error())
|
||||
} else {
|
||||
message = obj.(string)
|
||||
}
|
||||
}
|
||||
o.Error(http.StatusNotFound, message, strings.Join(errors, ","))
|
||||
}
|
||||
|
||||
func (o *Permissions) InternalServerError(err error) {
|
||||
o.Error(http.StatusInternalServerError, "InternalServerError", err.Error())
|
||||
}
|
||||
|
||||
func (o *Permissions) WrittenStatus() int {
|
||||
return o.status
|
||||
}
|
||||
|
||||
func (o *Permissions) String() string {
|
||||
return strings.Join(o.Strings(), " ")
|
||||
}
|
||||
|
||||
func (o *Permissions) Strings() []string {
|
||||
var s []string
|
||||
if o.token != nil {
|
||||
s = append(s, fmt.Sprintf("%T(ID=%d Token=%s)", o.token, o.token.ID, o.token.Token))
|
||||
}
|
||||
if o.doer != nil {
|
||||
s = append(s, fmt.Sprintf("%T(Name=%s)", o.doer, o.doer.Name))
|
||||
}
|
||||
if o.user != nil {
|
||||
s = append(s, fmt.Sprintf("%T(Name=%s)", o.user, o.user.Name))
|
||||
}
|
||||
if o.team != nil {
|
||||
s = append(s, fmt.Sprintf("%T(Name=%s)", o.team, o.team.Name))
|
||||
}
|
||||
if o.org != nil {
|
||||
s = append(s, fmt.Sprintf("%T(Name=%s)", o.org, o.org.Name))
|
||||
}
|
||||
if o.isSigned {
|
||||
s = append(s, fmt.Sprintf("isSigned(%v)", o.isSigned))
|
||||
}
|
||||
if o.authentication != nil {
|
||||
var sa []string
|
||||
user := o.authentication.User()
|
||||
if user != nil {
|
||||
sa = append(sa, fmt.Sprintf("%T(Name=%s)", user, user.Name))
|
||||
}
|
||||
if has, scope := o.authentication.Scope().Get(); has {
|
||||
sa = append(sa, fmt.Sprintf("%T(%s)", scope, scope))
|
||||
}
|
||||
if o.authentication.Reducer() != nil {
|
||||
sa = append(sa, fmt.Sprintf("%T", o.authentication.Reducer()))
|
||||
}
|
||||
if o.authentication.IsPasswordAuthentication() {
|
||||
sa = append(sa, fmt.Sprintf("IsPasswordAuthentication(%v)", o.authentication.IsPasswordAuthentication()))
|
||||
}
|
||||
if o.authentication.IsReverseProxyAuthentication() {
|
||||
sa = append(sa, fmt.Sprintf("IsReverseProxyAuthentication(%v)", o.authentication.IsReverseProxyAuthentication()))
|
||||
}
|
||||
if has, oauth2scopes := o.authentication.OAuth2GrantScopes().Get(); has {
|
||||
sa = append(sa, fmt.Sprintf("%T(%s)", oauth2scopes, oauth2scopes))
|
||||
}
|
||||
if has, taskID := o.authentication.ActionsTaskID().Get(); has {
|
||||
sa = append(sa, fmt.Sprintf("ActionsTaskID(%d)", taskID))
|
||||
}
|
||||
s = append(s, fmt.Sprintf("%T(%s)", o.authentication, strings.Join(sa, " ")))
|
||||
}
|
||||
if len(o.requiredScopeCategories) > 0 {
|
||||
s = append(s, fmt.Sprintf("%T(%v)", o.requiredScopeCategories, o.requiredScopeCategories))
|
||||
}
|
||||
if o.reducer != nil {
|
||||
s = append(s, fmt.Sprintf("%T", o.reducer))
|
||||
}
|
||||
if o.publicOnly {
|
||||
s = append(s, fmt.Sprintf("publicOnly(%v)", o.publicOnly))
|
||||
}
|
||||
if o.repository != nil {
|
||||
var sa []string
|
||||
if o.repository.IsPrivate {
|
||||
sa = append(sa, fmt.Sprintf("IsPrivate(%v)", o.repository.IsPrivate))
|
||||
}
|
||||
if o.repository.IsArchived {
|
||||
sa = append(sa, fmt.Sprintf("IsArchived(%v)", o.repository.IsArchived))
|
||||
}
|
||||
if o.repository.IsMirror {
|
||||
sa = append(sa, fmt.Sprintf("IsMirror(%v)", o.repository.IsMirror))
|
||||
}
|
||||
if len(o.repository.Units) > 0 {
|
||||
var units []string
|
||||
for _, repoUnit := range o.repository.Units {
|
||||
units = append(units, repoUnit.Type.String())
|
||||
}
|
||||
sa = append(sa, fmt.Sprintf("Unit(%s)", strings.Join(units, ",")))
|
||||
}
|
||||
s = append(s, fmt.Sprintf("%T(ID=%d Name=%s OwnerName=%s %s)", o.repository, o.repository.ID, o.repository.Name, o.repository.OwnerName, strings.Join(sa, " ")))
|
||||
}
|
||||
if o.permission != nil {
|
||||
var sa []string
|
||||
if len(o.permission.Units) > 0 {
|
||||
var units []string
|
||||
for _, repoUnit := range o.permission.Units {
|
||||
unitString := repoUnit.Type.String()
|
||||
if o.permission.UnitsMode != nil {
|
||||
if mode, has := o.permission.UnitsMode[repoUnit.Type]; has {
|
||||
unitString = fmt.Sprintf("%s:%s", unitString, mode)
|
||||
}
|
||||
}
|
||||
units = append(units, unitString)
|
||||
}
|
||||
sa = append(sa, fmt.Sprintf("Unit(%s)", strings.Join(units, ",")))
|
||||
}
|
||||
sa = append(sa, fmt.Sprintf("AccessMode(%s)", o.permission.AccessMode))
|
||||
s = append(s, fmt.Sprintf("%T(%s)", o.permission, strings.Join(sa, " ")))
|
||||
}
|
||||
if o.packageOwner != nil {
|
||||
s = append(s, fmt.Sprintf("packageOwner %T(Name=%s)", o.packageOwner, o.packageOwner.Name))
|
||||
s = append(s, fmt.Sprintf("packageMode %T(%s)", o.packageAccessMode, o.packageAccessMode))
|
||||
}
|
||||
return s
|
||||
}
|
||||
57
routers/api/v1/permissions/repo_access.go
Normal file
57
routers/api/v1/permissions/repo_access.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
actions_model "forgejo.org/models/actions"
|
||||
"forgejo.org/models/perm"
|
||||
access_model "forgejo.org/models/perm/access"
|
||||
"forgejo.org/models/unit"
|
||||
user_model "forgejo.org/models/user"
|
||||
)
|
||||
|
||||
func RepoAccess(ctx Context) {
|
||||
if ctx.GetDoer() != nil && ctx.GetDoer().ID == user_model.ActionsUserID && ctx.GetAuthentication().ActionsTaskID().Has() {
|
||||
_, taskID := ctx.GetAuthentication().ActionsTaskID().Get()
|
||||
task, err := actions_model.GetTaskByID(ctx.GetContext(), taskID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "actions_model.GetTaskByID", err)
|
||||
return
|
||||
}
|
||||
if task.RepoID != ctx.GetRepository().ID {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if task.IsForkPullRequest {
|
||||
ctx.GetPermission().AccessMode = perm.AccessModeRead
|
||||
} else {
|
||||
ctx.GetPermission().AccessMode = perm.AccessModeWrite
|
||||
}
|
||||
|
||||
if err := ctx.GetRepository().LoadUnits(ctx.GetContext()); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadUnits", err)
|
||||
return
|
||||
}
|
||||
ctx.GetPermission().Units = ctx.GetRepository().Units
|
||||
ctx.GetPermission().UnitsMode = make(map[unit.Type]perm.AccessMode)
|
||||
for _, u := range ctx.GetRepository().Units {
|
||||
ctx.GetPermission().UnitsMode[u.Type] = ctx.GetPermission().AccessMode
|
||||
}
|
||||
} else {
|
||||
permission, err := access_model.GetUserRepoPermissionWithReducer(ctx.GetContext(), ctx.GetRepository(), ctx.GetDoer(), ctx.GetReducer())
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermissionWithReducer", err)
|
||||
return
|
||||
}
|
||||
ctx.SetPermission(&permission)
|
||||
}
|
||||
|
||||
if !ctx.GetPermission().HasAccess() {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
}
|
||||
24
routers/api/v1/permissions/req_admin.go
Normal file
24
routers/api/v1/permissions/req_admin.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"forgejo.org/models/unit"
|
||||
)
|
||||
|
||||
func ReqAdmin(ctx Context, unitTypes []unit.Type) {
|
||||
if len(unitTypes) > 0 && !slices.ContainsFunc(unitTypes, func(unitType unit.Type) bool {
|
||||
return ctx.GetRepository().UnitEnabled(ctx.GetContext(), unitType)
|
||||
}) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
if !IsUserRepoAdmin(ctx) && !IsUserSiteAdmin(ctx) {
|
||||
ctx.Error(http.StatusForbidden, "reqAdmin", "user should be an owner or a collaborator with admin write of a repository")
|
||||
return
|
||||
}
|
||||
}
|
||||
15
routers/api/v1/permissions/req_any_repo_reader.go
Normal file
15
routers/api/v1/permissions/req_any_repo_reader.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func ReqAnyRepoReader(ctx Context) {
|
||||
if !ctx.GetPermission().HasAccess() && !IsUserSiteAdmin(ctx) {
|
||||
ctx.Error(http.StatusForbidden, "reqAnyRepoReader", "user should have any permission to read repository or permissions of site admin")
|
||||
return
|
||||
}
|
||||
}
|
||||
23
routers/api/v1/permissions/req_basic_or_rev_proxy_auth.go
Normal file
23
routers/api/v1/permissions/req_basic_or_rev_proxy_auth.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forgejo.org/modules/setting"
|
||||
)
|
||||
|
||||
func ReqBasicOrRevProxyAuth(ctx Context) {
|
||||
if ctx.GetIsSigned() && setting.Service.EnableReverseProxyAuthAPI && ctx.GetAuthentication().IsReverseProxyAuthentication() {
|
||||
return
|
||||
}
|
||||
|
||||
// Require basic authorization method to be used and that basic
|
||||
// authorization used password login to verify the user.
|
||||
if !ctx.GetAuthentication().IsPasswordAuthentication() {
|
||||
ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth method not allowed")
|
||||
return
|
||||
}
|
||||
}
|
||||
16
routers/api/v1/permissions/req_explore_sign_in.go
Normal file
16
routers/api/v1/permissions/req_explore_sign_in.go
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forgejo.org/modules/setting"
|
||||
)
|
||||
|
||||
func ReqExploreSignIn(ctx Context) {
|
||||
if (setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView) && !ctx.GetIsSigned() {
|
||||
ctx.Error(http.StatusUnauthorized, "reqExploreSignIn", "you must be signed in to search for users")
|
||||
}
|
||||
}
|
||||
15
routers/api/v1/permissions/req_git_hook.go
Normal file
15
routers/api/v1/permissions/req_git_hook.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func ReqGitHook(ctx Context) {
|
||||
if !ctx.GetDoer().CanEditGitHook() {
|
||||
ctx.Error(http.StatusForbidden, "", "must be allowed to edit Git hooks")
|
||||
return
|
||||
}
|
||||
}
|
||||
38
routers/api/v1/permissions/req_org_membership.go
Normal file
38
routers/api/v1/permissions/req_org_membership.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forgejo.org/models/organization"
|
||||
)
|
||||
|
||||
func ReqOrgMembership(ctx Context) {
|
||||
if IsUserSiteAdmin(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
var orgID int64
|
||||
if ctx.GetOrg() != nil {
|
||||
orgID = ctx.GetOrg().ID
|
||||
} else if ctx.GetTeam() != nil {
|
||||
orgID = ctx.GetTeam().OrgID
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "", "reqOrgMembership: unprepared context")
|
||||
return
|
||||
}
|
||||
|
||||
if isMember, err := organization.IsOrganizationMember(ctx.GetContext(), orgID, ctx.GetDoer().ID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsOrganizationMember", err)
|
||||
return
|
||||
} else if !isMember {
|
||||
if ctx.GetOrg() != nil {
|
||||
ctx.Error(http.StatusForbidden, "", "Must be an organization member")
|
||||
} else {
|
||||
ctx.NotFound()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
39
routers/api/v1/permissions/req_org_ownership.go
Normal file
39
routers/api/v1/permissions/req_org_ownership.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forgejo.org/models/organization"
|
||||
)
|
||||
|
||||
func ReqOrgOwnership(ctx Context) {
|
||||
if IsUserSiteAdmin(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
var orgID int64
|
||||
if ctx.GetOrg() != nil {
|
||||
orgID = ctx.GetOrg().ID
|
||||
} else if ctx.GetTeam() != nil {
|
||||
orgID = ctx.GetTeam().OrgID
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "", "reqOrgOwnership: unprepared context")
|
||||
return
|
||||
}
|
||||
|
||||
isOwner, err := organization.IsOrganizationOwner(ctx.GetContext(), orgID, ctx.GetDoer().ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsOrganizationOwner", err)
|
||||
return
|
||||
} else if !isOwner {
|
||||
if ctx.GetOrg() != nil {
|
||||
ctx.Error(http.StatusForbidden, "", "Must be an organization owner")
|
||||
} else {
|
||||
ctx.NotFound()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
24
routers/api/v1/permissions/req_owner.go
Normal file
24
routers/api/v1/permissions/req_owner.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"forgejo.org/models/unit"
|
||||
)
|
||||
|
||||
func ReqOwner(ctx Context, unitTypes []unit.Type) {
|
||||
if len(unitTypes) > 0 && !slices.ContainsFunc(unitTypes, func(unitType unit.Type) bool {
|
||||
return ctx.GetRepository().UnitEnabled(ctx.GetContext(), unitType)
|
||||
}) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
if !ctx.GetPermission().IsOwner() && !IsUserSiteAdmin(ctx) {
|
||||
ctx.Error(http.StatusForbidden, "reqOwner", "user should be the owner of the repo")
|
||||
return
|
||||
}
|
||||
}
|
||||
17
routers/api/v1/permissions/req_package_access.go
Normal file
17
routers/api/v1/permissions/req_package_access.go
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forgejo.org/models/perm"
|
||||
)
|
||||
|
||||
func ReqPackageAccess(ctx Context, accessMode perm.AccessMode) {
|
||||
if ctx.GetPackageAccessMode() < accessMode && !IsUserSiteAdmin(ctx) {
|
||||
ctx.Error(http.StatusForbidden, "reqPackageAccess", "user should have specific permission or be a site admin")
|
||||
return
|
||||
}
|
||||
}
|
||||
16
routers/api/v1/permissions/req_repo_branch_writer.go
Normal file
16
routers/api/v1/permissions/req_repo_branch_writer.go
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
issues_model "forgejo.org/models/issues"
|
||||
)
|
||||
|
||||
func ReqRepoBranchWriter(ctx Context, branch string) {
|
||||
if !issues_model.CanMaintainerWriteToBranch(ctx.GetContext(), *ctx.GetPermission(), branch, ctx.GetDoer()) && !IsUserSiteAdmin(ctx) {
|
||||
ctx.Error(http.StatusForbidden, "reqRepoBranchWriter", "user should have a permission to write to this branch")
|
||||
}
|
||||
}
|
||||
21
routers/api/v1/permissions/req_repo_reader.go
Normal file
21
routers/api/v1/permissions/req_repo_reader.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forgejo.org/models/unit"
|
||||
)
|
||||
|
||||
func ReqRepoReader(ctx Context, unitType unit.Type) {
|
||||
if !ctx.GetRepository().UnitEnabled(ctx.GetContext(), unitType) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
if !ctx.GetPermission().CanRead(unitType) && !IsUserRepoAdmin(ctx) && !IsUserSiteAdmin(ctx) {
|
||||
ctx.Error(http.StatusForbidden, "reqRepoReader", "user should have specific read permission or be a repo admin or a site admin")
|
||||
return
|
||||
}
|
||||
}
|
||||
24
routers/api/v1/permissions/req_repo_writer.go
Normal file
24
routers/api/v1/permissions/req_repo_writer.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"forgejo.org/models/unit"
|
||||
)
|
||||
|
||||
func ReqRepoWriter(ctx Context, unitTypes []unit.Type) {
|
||||
if !slices.ContainsFunc(unitTypes, func(unitType unit.Type) bool {
|
||||
return ctx.GetRepository().UnitEnabled(ctx.GetContext(), unitType)
|
||||
}) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
if !IsUserRepoWriter(ctx, unitTypes) && !IsUserRepoAdmin(ctx) && !IsUserSiteAdmin(ctx) {
|
||||
ctx.Error(http.StatusForbidden, "reqRepoWriter", "user should have a permission to write to a repo")
|
||||
return
|
||||
}
|
||||
}
|
||||
24
routers/api/v1/permissions/req_self_or_admin.go
Normal file
24
routers/api/v1/permissions/req_self_or_admin.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
user_model "forgejo.org/models/user"
|
||||
)
|
||||
|
||||
func ReqSelfOrAdmin(ctx Context) {
|
||||
getID := func(user *user_model.User) int64 {
|
||||
if user == nil {
|
||||
return 0
|
||||
}
|
||||
return user.ID
|
||||
}
|
||||
|
||||
if !IsUserSiteAdmin(ctx) && getID(ctx.GetUser()) != getID(ctx.GetDoer()) {
|
||||
ctx.Error(http.StatusForbidden, "reqSelfOrAdmin", "doer should be the site admin or be same as the contextUser")
|
||||
return
|
||||
}
|
||||
}
|
||||
15
routers/api/v1/permissions/req_site_admin.go
Normal file
15
routers/api/v1/permissions/req_site_admin.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func ReqSiteAdmin(ctx Context) {
|
||||
if !IsUserSiteAdmin(ctx) {
|
||||
ctx.Error(http.StatusForbidden, "reqSiteAdmin", "user should be the site admin")
|
||||
return
|
||||
}
|
||||
}
|
||||
44
routers/api/v1/permissions/req_team_membership.go
Normal file
44
routers/api/v1/permissions/req_team_membership.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forgejo.org/models/organization"
|
||||
)
|
||||
|
||||
func ReqTeamMembership(ctx Context) {
|
||||
if IsUserSiteAdmin(ctx) {
|
||||
return
|
||||
}
|
||||
if ctx.GetTeam() == nil {
|
||||
ctx.Error(http.StatusInternalServerError, "", "reqTeamMembership: unprepared context")
|
||||
return
|
||||
}
|
||||
|
||||
orgID := ctx.GetTeam().OrgID
|
||||
isOwner, err := organization.IsOrganizationOwner(ctx.GetContext(), orgID, ctx.GetDoer().ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsOrganizationOwner", err)
|
||||
return
|
||||
} else if isOwner {
|
||||
return
|
||||
}
|
||||
|
||||
if isTeamMember, err := organization.IsTeamMember(ctx.GetContext(), orgID, ctx.GetTeam().ID, ctx.GetDoer().ID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsTeamMember", err)
|
||||
return
|
||||
} else if !isTeamMember {
|
||||
isOrgMember, err := organization.IsOrganizationMember(ctx.GetContext(), orgID, ctx.GetDoer().ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsOrganizationMember", err)
|
||||
} else if isOrgMember {
|
||||
ctx.Error(http.StatusForbidden, "", "Must be a team member")
|
||||
} else {
|
||||
ctx.NotFound()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
20
routers/api/v1/permissions/req_token.go
Normal file
20
routers/api/v1/permissions/req_token.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func ReqToken(ctx Context) {
|
||||
// If actions token is present
|
||||
if ctx.GetAuthentication().ActionsTaskID().Has() {
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.GetIsSigned() {
|
||||
return
|
||||
}
|
||||
ctx.Error(http.StatusUnauthorized, "reqToken", "token is required")
|
||||
}
|
||||
14
routers/api/v1/permissions/req_users_explore_enabled.go
Normal file
14
routers/api/v1/permissions/req_users_explore_enabled.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"forgejo.org/modules/setting"
|
||||
)
|
||||
|
||||
func ReqUsersExploreEnabled(ctx Context) {
|
||||
if setting.Service.Explore.DisableUsersPage {
|
||||
ctx.NotFound()
|
||||
}
|
||||
}
|
||||
20
routers/api/v1/permissions/req_valid_comment_id.go
Normal file
20
routers/api/v1/permissions/req_valid_comment_id.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
issues_model "forgejo.org/models/issues"
|
||||
)
|
||||
|
||||
func ReqValidCommentID(ctx Context, comment *issues_model.Comment) {
|
||||
if comment.Issue == nil || comment.Issue.RepoID != ctx.GetRepository().ID {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.GetPermission().CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
}
|
||||
17
routers/api/v1/permissions/req_webhooks_enabled.go
Normal file
17
routers/api/v1/permissions/req_webhooks_enabled.go
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forgejo.org/modules/setting"
|
||||
)
|
||||
|
||||
func ReqWebhooksEnabled(ctx Context) {
|
||||
if setting.DisableWebhooks {
|
||||
ctx.Error(http.StatusForbidden, "", "webhooks disabled by administrator")
|
||||
return
|
||||
}
|
||||
}
|
||||
106
routers/api/v1/permissions/tests/README.md
Normal file
106
routers/api/v1/permissions/tests/README.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
Tests for `routers/api/v1/permissions`
|
||||
|
||||
Each permission function implemented in the `routers/api/v1/permissions` has a matching test in this package. For instance:
|
||||
|
||||
- the `ReqGitHook` function in `routers/api/v1/permissions/req_git_hook.go`
|
||||
- is tested in `routers/api/v1/permissions/tests/req_git_hook_test.go`
|
||||
|
||||
To keep the tests maintainable despite the large number of fixtures and permission sequences, the tests for a function are described in a structure instead of being implemented by a `Test...` function.
|
||||
|
||||
```go
|
||||
type functionTest struct {
|
||||
fixtures []*fixtureType
|
||||
fulfillNeeds func(t *testing.T, data *fixtureData)
|
||||
interpret func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData)
|
||||
|
||||
protect func() func()
|
||||
call func(t *testing.T, ctx apiv1_permissions.Context, permissions *apiv1_permissions.Permissions, data *fixtureData, signature []any)
|
||||
sequenceFilter []string
|
||||
}
|
||||
```
|
||||
|
||||
- The test registers the struct and it will be used when the `TestAPIv1Permissions` test runs
|
||||
- The `fixtures` are described using a `map[string]string` using conventions that are to be interpreted by the fixture helpers. For instance `"doer": "regularuser"` will be interpreted by the `fixtureSetDoer` helper and create the user `regularuser`.
|
||||
- The `fulfillNeeds` function will be called for each function that comes earlier in the sequence, to ensure it gets sensible defaults allowing it to run successfully. For instance if the sequence is `APIAuthorization,TokenRequiresScopes,ReqOrgOwnership`, the `fulfillNeeds` function of `TokenRequiresScopes` is expected to set a sensible default for the scope, such as `"scope": "read:repository"`
|
||||
- After `fulfillNeeds` is called and the fixture data is assumed to have all the necessary defaults, it will be acted upon by each `interpret` function in the sequence. For instance, `APIAuthorization` will interpret the `"doer": "regularuser"` data by calling `fixtureSetDoer` to ensure it is created.
|
||||
- If global variables need protection (for instance when changing a setting), they are to be protected by the `protect` function
|
||||
- Once the fixture has been interpreted, each permission function in the sequence is called in order. They are all expected to complete successfully. Except for the last one, which is the function under test, that may error out if the fixture is designed for that purpose.
|
||||
- The `call` function, if it exists, is expected to call the function for which the test is designed. Fos instance the `call` for `ReqValidCommentID` runs `apiv1_permissions.ReqValidCommentID(ctx, comment)`. The function must not have any side effect. Instead it must use whatever data has been created by the fixture (using the `interpret` function).
|
||||
- The `sequenceFilter` only keeps some permissions function in the sequence leading to the function under test. For instance when testing `ReqOrgOwnership` the sequence `APIAuthorization,TokenRequiresScopes,ReqOrgOwnership` will be used. In some cases it is useful to simplify the tests in case the shortest sequence leading to a function contains functions that will interfere if a particular fixture is set.
|
||||
|
||||
## Permission function signatures
|
||||
|
||||
The signature of every permission function has at least one argument which is a `routers/api/v1/permissions.Context` interface. It may also have additional arguments provided when building the routes. For instance `TokenRequiresScopes` may be given a list of scope categories. Such arguments do not vary depending on the context because they are preset when the route is built. In addition the function may have arguments that are extracted from the environment. For instance `ReqValidCommentID` may be given the content of the `id` field from the body of a JSON payload.
|
||||
|
||||
The string representation of the signature is:
|
||||
|
||||
- The function name if there are no arguments provided when building the routes. For instance `APIAuthorization`
|
||||
- The function name followed by a whitespace list of arguments provided when building the routes. For instance `TokenRequiresScopes Repository User`
|
||||
|
||||
## Fixtures helpers
|
||||
|
||||
All fixtures are dynamically created (they are not using the global fixtures found in `models/fixtures`). The `fixtures_test.go` file contains all the helpers to create those fixtures.
|
||||
|
||||
## Debugging
|
||||
|
||||
- Running the tests in verbose mode `make GOTESTFLAGS=-v GO_TEST_PACKAGES=forgejo.org/routers/api/v1/permissions/tests/... 'test#Test' `
|
||||
- Browsing the tests such as
|
||||
```
|
||||
...
|
||||
=== RUN TestAPIv1Permissions/APIAuthorization,TokenRequiresScopes_Admin_fixture_0
|
||||
functions_test.go:95: creating fixture data from doer:doerregular,level:read,scope:read:admin
|
||||
functions_test.go:98: created fixture data doer:doerregular,level:read,scope:read:admin
|
||||
functions_test.go:105: *auth.AccessToken(ID=10 Token=e26bfc1190efcf8c36ef640659af33e87073032c)
|
||||
functions_test.go:105: *user.User(Name=doerregular)
|
||||
functions_test.go:105: isSigned(true)
|
||||
functions_test.go:105: *tests_test.accessTokenAuthenticationResult(*user.User(Name=doerregular) auth.AccessTokenScope(read:admin) *authz.AllAccessAuthorizationReducer)
|
||||
fixture_test.go:637: calling permissions.APIAuthorization(ctx)
|
||||
functions_test.go:131: + *authz.AllAccessAuthorizationReducer
|
||||
token_requires_scopes_test.go:67: calling TokenRequiresScopes(ctx, [1], 1)
|
||||
functions_test.go:131: + []auth.AccessTokenScopeCategory([1])
|
||||
...
|
||||
```
|
||||
- The name of the test is the sequence of middleware under test (`APIAuthorization`, `TokenRequiresScopes`)
|
||||
- It is followed by the index of the fixture being used for running the test, as found in the test file of the last function in the sequence (`TokenRequiresScopes` in the example)
|
||||
- The `creating fixture` line shows all the data contained in that fixture
|
||||
- The `created fixture` line shows the data added after calling the `fulfillNeeds` function for each function in the sequence
|
||||
- The indented lines that follow shows the content of the `routers/api/v1/permissions.Permission` object before calling a permission function. To reduce the verbosity modifications are shown in a diff style fashion.
|
||||
- The `calling` line shows the function and its arguments before it is called
|
||||
- Running a single test (note the `/` is replaced with a `.` in the test name to comply with the Makefile rule) `make RACE_ENABLED=true GOTESTFLAGS=-v GO_TEST_PACKAGES=forgejo.org/routers/api/v1/permissions/tests/... 'test#TestAPIv1Permissions.APIAuthorization,TokenRequiresScopes_Repository,RepoAccess,CheckTokenPublicOnly,ReqToken,ReqRepoReader_TypeCode,CheckForkDestination'`
|
||||
|
||||
## The call function
|
||||
|
||||
`func(t *testing.T, ctx apiv1_permissions.Context, permissions *apiv1_permissions.Permissions, data *fixtureData, signature []any)`
|
||||
|
||||
It is responsible for:
|
||||
|
||||
- Calling `t.Logf` to display the call about to be made
|
||||
- Calling the function using `ctx` as a first argument
|
||||
|
||||
The `permissions` and `data` arguments are provided, as computed by the `interpret` function.
|
||||
|
||||
The `signature[0]` is the function itself and could be called with `signature[0].Call`.
|
||||
|
||||
The `signature[1:]` list are the mandatory arguments to the function call.
|
||||
|
||||
## Test coverage
|
||||
|
||||
### `routers/api/v1/permissions`
|
||||
|
||||
- At the root of the source tree
|
||||
- `COVERAGE_TEST_PACKAGES="forgejo.org/routers/api/v1/permissions forgejo.org/routers/api/v1/permissions/tests" make coverage-run coverage-show-percentage | grep v1/permissions | grep -v v1/permissions/permissions.go | grep -v v1/permissions/tests | sed -e 's/\t\t*/ /g' -e 's|forgejo.org/routers/||'`
|
||||
- `uncover coverage/textfmt.out ReqOrgOwnership`
|
||||
|
||||
### Forgejo development branch
|
||||
|
||||
- Run https://codeberg.org/forgejo/forgejo/actions?workflow=coverage.yml
|
||||
- Download the `coverage.zip` artifact
|
||||
- Extract it in `/tmp/coverage/merged`
|
||||
- At the root of the source tree checked out at the same SHA that coverage used
|
||||
- Convert with `go tool covdata textfmt -i=/tmp/coverage/merged -o=/tmp/coverage/textfmt.out`
|
||||
- Show percentages per function `go tool cover -func=/tmp/coverage/textfmt.out`
|
||||
- Show line covered and missed for a function `uncover /tmp/coverage/textfmt.out repoAssignment`
|
||||
|
||||
## References
|
||||
|
||||
Design discussion https://codeberg.org/forgejo/design/issues/63
|
||||
41
routers/api/v1/permissions/tests/api_authorization_test.go
Normal file
41
routers/api/v1/permissions/tests/api_authorization_test.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
user_model "forgejo.org/models/user"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTest(apiv1_permissions.APIAuthorization, functionTest{
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
data.SetDefault("doer", "doerregular")
|
||||
if data.Get("doer") == user_model.ActionsUserName {
|
||||
data.SetDefault("repository", "userowner/repositorypublic")
|
||||
}
|
||||
data.SetDefault("scope", "read:repository")
|
||||
data.SetDefault("level", "read")
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
if data.Has("repository") && data.Get("doer") == user_model.ActionsUserName {
|
||||
fixtureSetRepository(t, permissions, data)
|
||||
}
|
||||
fixtureSetDoer(t, permissions, data)
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "anonymous",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doerregular",
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
213
routers/api/v1/permissions/tests/check_token_public_only_test.go
Normal file
213
routers/api/v1/permissions/tests/check_token_public_only_test.go
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
org_model "forgejo.org/models/organization"
|
||||
user_model "forgejo.org/models/user"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
const (
|
||||
categoryActivityPub = "activitypub"
|
||||
categoryAdmin = "admin"
|
||||
categoryNotification = "notification"
|
||||
categoryOrganization = "organization"
|
||||
categoryPackage = "package"
|
||||
categoryIssue = "issue"
|
||||
categoryRepository = "repository"
|
||||
categoryUser = "user"
|
||||
)
|
||||
|
||||
var categoryStringToCategory = map[string]auth_model.AccessTokenScopeCategory{
|
||||
categoryActivityPub: auth_model.AccessTokenScopeCategoryActivityPub,
|
||||
categoryAdmin: auth_model.AccessTokenScopeCategoryAdmin,
|
||||
categoryNotification: auth_model.AccessTokenScopeCategoryNotification,
|
||||
categoryOrganization: auth_model.AccessTokenScopeCategoryOrganization,
|
||||
categoryPackage: auth_model.AccessTokenScopeCategoryPackage,
|
||||
categoryIssue: auth_model.AccessTokenScopeCategoryIssue,
|
||||
categoryRepository: auth_model.AccessTokenScopeCategoryRepository,
|
||||
categoryUser: auth_model.AccessTokenScopeCategoryUser,
|
||||
}
|
||||
|
||||
var _ = registerFunctionTestWithCall(apiv1_permissions.CheckTokenPublicOnly, functionTest{
|
||||
sequenceFilter: []string{
|
||||
"APIAuthorization",
|
||||
"CheckTokenPublicOnly",
|
||||
},
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
data.SetDefault("doer", "regularuser")
|
||||
data.SetDefault("repository", "regularuser/repositorypublic")
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
if data.Has("user") {
|
||||
fixtureCreateUser(t, &user_model.User{Name: data.Get("user")})
|
||||
}
|
||||
if data.Has("org") {
|
||||
fixtureCreateOrg(t, &org_model.Organization{Name: data.Get("org")}, &user_model.User{Name: data.Get("doer")})
|
||||
}
|
||||
if data.Has("packageOwner") {
|
||||
fixtureCreateUser(t, &user_model.User{Name: data.Get("packageOwner")})
|
||||
}
|
||||
if data.Has("requiredScopeCategories") {
|
||||
var categories []auth_model.AccessTokenScopeCategory
|
||||
for categoryString := range strings.SplitSeq(data.Get("requiredScopeCategories"), ",") {
|
||||
categories = append(categories, categoryStringToCategory[categoryString])
|
||||
}
|
||||
permissions.SetRequiredScopeCategories(categories)
|
||||
}
|
||||
fixtureSetRepository(t, permissions, data)
|
||||
},
|
||||
call: func(t *testing.T, ctx apiv1_permissions.Context, data *fixtureData, _ []any) {
|
||||
t.Helper()
|
||||
var user *user_model.User
|
||||
if data.Has("user") {
|
||||
user = fixtureGetUser(t, data.Get("user"))
|
||||
}
|
||||
var org *org_model.Organization
|
||||
if data.Has("org") {
|
||||
if data.Has("orgAsUser") {
|
||||
user = fixtureGetUser(t, data.Get("org"))
|
||||
} else {
|
||||
org = fixtureGetOrg(t, data.Get("org"))
|
||||
}
|
||||
}
|
||||
var packageOwner *user_model.User
|
||||
if data.Has("packageOwner") {
|
||||
packageOwner = fixtureGetUser(t, data.Get("packageOwner"))
|
||||
}
|
||||
t.Logf("calling CheckTokenPublicOnly(ctx, %+v, %+v, %+v)", user, org, packageOwner)
|
||||
apiv1_permissions.CheckTokenPublicOnly(ctx, user, org.AsUser(), packageOwner)
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"repository": "userowner/repositorypublic",
|
||||
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"repository": "userowner/repositorypublic",
|
||||
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
|
||||
"requiredScopeCategories": categoryRepository,
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"repository": "userowner/repositoryprivate",
|
||||
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
|
||||
"requiredScopeCategories": categoryRepository,
|
||||
}),
|
||||
error: "token scope is limited to public repos",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"repository": "userowner/repositorypublic",
|
||||
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
|
||||
"requiredScopeCategories": categoryIssue,
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"repository": "userowner/repositoryprivate",
|
||||
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
|
||||
"requiredScopeCategories": categoryIssue,
|
||||
}),
|
||||
error: "token scope is limited to public issues",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"repository": "userowner/repositorypublic",
|
||||
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
|
||||
"requiredScopeCategories": categoryNotification,
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"repository": "userowner/repositoryprivate",
|
||||
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
|
||||
"requiredScopeCategories": categoryNotification,
|
||||
}),
|
||||
error: "token scope is limited to public notifications",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"user": "regularuser",
|
||||
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
|
||||
"requiredScopeCategories": categoryUser,
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"user": "privateuser",
|
||||
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
|
||||
"requiredScopeCategories": categoryUser,
|
||||
}),
|
||||
error: "token scope is limited to public users",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"user": "regularuser",
|
||||
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
|
||||
"requiredScopeCategories": categoryActivityPub,
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"user": "privateuser",
|
||||
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
|
||||
"requiredScopeCategories": categoryActivityPub,
|
||||
}),
|
||||
error: "token scope is limited to public activitypub",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"org": "regularorg",
|
||||
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
|
||||
"requiredScopeCategories": categoryOrganization,
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"org": "privateorg",
|
||||
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
|
||||
"requiredScopeCategories": categoryOrganization,
|
||||
}),
|
||||
error: "token scope is limited to public orgs",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"org": "privateorg",
|
||||
"orgAsUser": "true",
|
||||
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
|
||||
"requiredScopeCategories": categoryOrganization,
|
||||
}),
|
||||
error: "token scope is limited to public orgs",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"packageOwner": "regularuser",
|
||||
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
|
||||
"requiredScopeCategories": categoryPackage,
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"packageOwner": "privateuser",
|
||||
"scope": fmt.Sprintf("%s", auth_model.AccessTokenScopePublicOnly),
|
||||
"requiredScopeCategories": categoryPackage,
|
||||
}),
|
||||
error: "token scope is limited to public packages",
|
||||
},
|
||||
},
|
||||
})
|
||||
622
routers/api/v1/permissions/tests/fixture_test.go
Normal file
622
routers/api/v1/permissions/tests/fixture_test.go
Normal file
|
|
@ -0,0 +1,622 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
// See README.md for a documentation of the test logic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
actions_model "forgejo.org/models/actions"
|
||||
auth_model "forgejo.org/models/auth"
|
||||
"forgejo.org/models/db"
|
||||
issues_model "forgejo.org/models/issues"
|
||||
org_model "forgejo.org/models/organization"
|
||||
"forgejo.org/models/perm"
|
||||
access_model "forgejo.org/models/perm/access"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
unit_model "forgejo.org/models/unit"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/modules/git"
|
||||
"forgejo.org/modules/optional"
|
||||
"forgejo.org/modules/structs"
|
||||
"forgejo.org/modules/util"
|
||||
"forgejo.org/modules/web/routing"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
apiv1_permissions_tests "forgejo.org/routers/api/v1/permissions/tests"
|
||||
"forgejo.org/services/auth"
|
||||
"forgejo.org/services/authz"
|
||||
issue_service "forgejo.org/services/issue"
|
||||
packages_service "forgejo.org/services/packages"
|
||||
pull_service "forgejo.org/services/pull"
|
||||
"forgejo.org/tests/forgery"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func fixtureCreateToken(t *testing.T, user *user_model.User, scope auth_model.AccessTokenScope, repoIDs ...int64) (*auth_model.AccessToken, error) {
|
||||
t.Helper()
|
||||
scope, err := scope.Normalize()
|
||||
require.NoError(t, err)
|
||||
resourceAllRepos := len(repoIDs) == 0
|
||||
accessToken := &auth_model.AccessToken{
|
||||
UID: user.ID,
|
||||
Name: util.CryptoRandomString(10),
|
||||
Scope: scope,
|
||||
ResourceAllRepos: resourceAllRepos,
|
||||
}
|
||||
require.NoError(t, auth_model.NewAccessToken(t.Context(), accessToken))
|
||||
if len(repoIDs) > 0 {
|
||||
var resourceRepos []*auth_model.AccessTokenResourceRepo
|
||||
for _, repoID := range repoIDs {
|
||||
resourceRepos = append(resourceRepos, &auth_model.AccessTokenResourceRepo{
|
||||
TokenID: accessToken.ID,
|
||||
RepoID: repoID,
|
||||
})
|
||||
}
|
||||
require.NoError(t, auth_model.InsertAccessTokenResourceRepos(t.Context(), accessToken.ID, resourceRepos))
|
||||
}
|
||||
return accessToken, nil
|
||||
}
|
||||
|
||||
func fixtureCreateIssue(t *testing.T, user *user_model.User, repo *repo_model.Repository, title, content string) *issues_model.Issue {
|
||||
t.Helper()
|
||||
issue := &issues_model.Issue{
|
||||
RepoID: repo.ID,
|
||||
Title: title,
|
||||
Content: content,
|
||||
PosterID: user.ID,
|
||||
Poster: user,
|
||||
}
|
||||
|
||||
err := issue_service.NewIssue(t.Context(), repo, issue, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
return issue
|
||||
}
|
||||
|
||||
func fixtureGetUser(t *testing.T, name string) *user_model.User {
|
||||
t.Helper()
|
||||
existingUser, err := user_model.GetUserByName(t.Context(), name)
|
||||
if err == nil {
|
||||
return existingUser
|
||||
} else if !user_model.IsErrUserNotExist(err) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fixtureGetOrg(t *testing.T, name string) *org_model.Organization {
|
||||
t.Helper()
|
||||
return (*org_model.Organization)(fixtureGetUser(t, name))
|
||||
}
|
||||
|
||||
func fixtureCreateUser(t *testing.T, user *user_model.User) *user_model.User {
|
||||
t.Helper()
|
||||
if existingUser := fixtureGetUser(t, user.Name); existingUser != nil {
|
||||
return existingUser
|
||||
}
|
||||
overwriteDefault := &user_model.CreateUserOverwriteOptions{}
|
||||
user.Email = user.Name + "@test.forgejo.org"
|
||||
user.Passwd = "password"
|
||||
if strings.Contains(user.Name, "private") {
|
||||
visibility := structs.VisibleTypePrivate
|
||||
overwriteDefault.Visibility = &visibility
|
||||
} else if strings.Contains(user.Name, "limited") {
|
||||
visibility := structs.VisibleTypeLimited
|
||||
overwriteDefault.Visibility = &visibility
|
||||
}
|
||||
require.NoError(t, user_model.CreateUser(t.Context(), user, overwriteDefault))
|
||||
return user
|
||||
}
|
||||
|
||||
func fixtureCreateOrg(t *testing.T, org *org_model.Organization, owner *user_model.User) *org_model.Organization {
|
||||
t.Helper()
|
||||
if existing := fixtureGetOrg(t, org.Name); existing != nil {
|
||||
return existing
|
||||
}
|
||||
owner = fixtureCreateUser(t, owner)
|
||||
if strings.Contains(org.Name, "private") {
|
||||
org.Visibility = structs.VisibleTypePrivate
|
||||
}
|
||||
require.NoError(t, org_model.CreateOrganization(t.Context(), org, owner))
|
||||
return org
|
||||
}
|
||||
|
||||
func fixtureCreateTeams(t *testing.T, org *org_model.Organization, teams string) {
|
||||
t.Helper()
|
||||
|
||||
for team := range strings.SplitSeq(teams, ",") {
|
||||
teamName, memberName, found := strings.Cut(team, ":")
|
||||
require.True(t, found)
|
||||
fixtureCreateTeam(t, org, memberName, &forgery.CreateTeamOptions{
|
||||
Name: teamName,
|
||||
Mode: perm.AccessModeWrite,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func fixtureCreateTeam(t *testing.T, org *org_model.Organization, memberName string, opts *forgery.CreateTeamOptions) *org_model.Team {
|
||||
t.Helper()
|
||||
|
||||
member := fixtureCreateUser(t, &user_model.User{Name: memberName})
|
||||
opts.Members = []*user_model.User{member}
|
||||
team := forgery.CreateTeam(t, org, opts)
|
||||
require.NotNil(t, team)
|
||||
return team
|
||||
}
|
||||
|
||||
func fixtureSetPackageOwner(t *testing.T, permissions *apiv1_permissions.Permissions, fixtureData *fixtureData) {
|
||||
t.Helper()
|
||||
if !fixtureData.Has("packageOwner") {
|
||||
return
|
||||
}
|
||||
owner := fixtureCreateUser(t, &user_model.User{Name: fixtureData.Get("packageOwner")})
|
||||
permissions.SetPackageOwner(owner)
|
||||
mode, err := packages_service.DeterminePackageAccessMode(permissions.GetContext(), permissions.GetPackageOwner(), permissions.GetDoer())
|
||||
require.NoError(t, err)
|
||||
permissions.SetPackageAccessMode(mode)
|
||||
}
|
||||
|
||||
func fixtureSetDoer(t *testing.T, permissions *apiv1_permissions.Permissions, fixtureData *fixtureData) {
|
||||
t.Helper()
|
||||
if !fixtureData.Has("doer") {
|
||||
return
|
||||
}
|
||||
if doer := permissions.GetDoer(); doer != nil {
|
||||
if doer.Name != fixtureData.Get("doer") {
|
||||
panic(fmt.Sprintf("attempting to override already doer %s with %s", doer.Name, fixtureData.Get("doer")))
|
||||
}
|
||||
return
|
||||
}
|
||||
name := fixtureData.Get("doer")
|
||||
if name == user_model.ActionsUserName {
|
||||
fixtureSetDoerActionsUser(t, permissions, fixtureData)
|
||||
} else {
|
||||
fixtureSetDoerRegularUser(t, permissions, fixtureData)
|
||||
}
|
||||
}
|
||||
|
||||
var _ auth.AuthenticationResult = &actionsTaskTokenAuthenticationResult{}
|
||||
|
||||
type actionsTaskTokenAuthenticationResult struct {
|
||||
*auth.BaseAuthenticationResult
|
||||
user *user_model.User
|
||||
taskID int64
|
||||
}
|
||||
|
||||
func (r *actionsTaskTokenAuthenticationResult) Scope() optional.Option[auth_model.AccessTokenScope] {
|
||||
return optional.None[auth_model.AccessTokenScope]()
|
||||
}
|
||||
|
||||
func (r *actionsTaskTokenAuthenticationResult) User() *user_model.User {
|
||||
return r.user
|
||||
}
|
||||
|
||||
func (r *actionsTaskTokenAuthenticationResult) ActionsTaskID() optional.Option[int64] {
|
||||
return optional.Some(r.taskID)
|
||||
}
|
||||
|
||||
func fixtureSetDoerActionsUser(t *testing.T, permissions *apiv1_permissions.Permissions, fixtureData *fixtureData) {
|
||||
permissions.SetDoer(user_model.NewActionsUser())
|
||||
repository := permissions.GetRepository()
|
||||
require.NotNil(t, repository)
|
||||
repositoryID := repository.ID
|
||||
if fixtureData.Get("task.RepoID") == "unrelated" {
|
||||
repositoryID = 13245
|
||||
}
|
||||
task := &actions_model.ActionTask{
|
||||
RepoID: repositoryID,
|
||||
}
|
||||
if fixtureData.Get("task.IsForkPullRequest") == "true" {
|
||||
task.IsForkPullRequest = true
|
||||
}
|
||||
task.GenerateToken()
|
||||
{
|
||||
_, err := db.GetEngine(t.Context()).Insert(task)
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, task.ID)
|
||||
}
|
||||
|
||||
permissions.SetAuthentication(&actionsTaskTokenAuthenticationResult{user: permissions.GetDoer(), taskID: task.ID})
|
||||
permissions.SetReducer(&authz.AllAccessAuthorizationReducer{})
|
||||
permission, err := access_model.GetUserRepoPermissionWithReducer(permissions.GetContext(), permissions.GetRepository(), permissions.GetDoer(), permissions.GetReducer())
|
||||
require.NoError(t, err)
|
||||
permissions.SetPermission(&permission)
|
||||
}
|
||||
|
||||
var _ auth.AuthenticationResult = &basicPasswordAuthenticationResult{}
|
||||
|
||||
type basicPasswordAuthenticationResult struct {
|
||||
*auth.BaseAuthenticationResult
|
||||
user *user_model.User
|
||||
}
|
||||
|
||||
func (*basicPasswordAuthenticationResult) IsPasswordAuthentication() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *basicPasswordAuthenticationResult) User() *user_model.User {
|
||||
return r.user
|
||||
}
|
||||
|
||||
var _ auth.AuthenticationResult = &accessTokenAuthenticationResult{}
|
||||
|
||||
type accessTokenAuthenticationResult struct {
|
||||
*auth.BaseAuthenticationResult
|
||||
user *user_model.User
|
||||
scope auth_model.AccessTokenScope
|
||||
reducer authz.AuthorizationReducer
|
||||
}
|
||||
|
||||
func (r *accessTokenAuthenticationResult) User() *user_model.User {
|
||||
return r.user
|
||||
}
|
||||
|
||||
func (r *accessTokenAuthenticationResult) Scope() optional.Option[auth_model.AccessTokenScope] {
|
||||
return optional.Some(r.scope)
|
||||
}
|
||||
|
||||
func (r *accessTokenAuthenticationResult) Reducer() authz.AuthorizationReducer {
|
||||
return r.reducer
|
||||
}
|
||||
|
||||
var _ auth.AuthenticationResult = &reverseProxyAuthenticationResult{}
|
||||
|
||||
type reverseProxyAuthenticationResult struct {
|
||||
*auth.BaseAuthenticationResult
|
||||
user *user_model.User
|
||||
}
|
||||
|
||||
func (r *reverseProxyAuthenticationResult) User() *user_model.User {
|
||||
return r.user
|
||||
}
|
||||
|
||||
func (*reverseProxyAuthenticationResult) IsReverseProxyAuthentication() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func fixtureSetDoerRegularUser(t *testing.T, permissions *apiv1_permissions.Permissions, fixtureData *fixtureData) {
|
||||
var scope auth_model.AccessTokenScope
|
||||
if fixtureData.Has("scope") {
|
||||
scope = auth_model.AccessTokenScope(fixtureData.Get("scope"))
|
||||
} else {
|
||||
scope = auth_model.AccessTokenScopeAll
|
||||
}
|
||||
if fixtureData.Has("doer") {
|
||||
doer := fixtureData.Get("doer")
|
||||
if doer != "anonymous" {
|
||||
isAdmin := strings.Contains(doer, "admin")
|
||||
user := &user_model.User{
|
||||
Name: doer,
|
||||
IsAdmin: isAdmin,
|
||||
}
|
||||
fixtureCreateUser(t, user)
|
||||
permissions.SetDoer(user)
|
||||
}
|
||||
} else {
|
||||
panic(fmt.Errorf("attempting to set doer with no name"))
|
||||
}
|
||||
|
||||
if permissions.GetDoer() == nil {
|
||||
permissions.SetAuthentication(&auth.UnauthenticatedResult{})
|
||||
} else {
|
||||
token, err := fixtureCreateToken(t, permissions.GetDoer(), scope)
|
||||
require.NoError(t, err)
|
||||
tokenReducer, err := authz.GetAuthorizationReducerForAccessToken(t.Context(), token)
|
||||
require.NoError(t, err)
|
||||
permissions.SetIsSigned(true)
|
||||
switch fixtureData.Get("authentication") {
|
||||
case "basic":
|
||||
permissions.SetAuthentication(&basicPasswordAuthenticationResult{user: permissions.GetDoer()})
|
||||
case "proxy":
|
||||
permissions.SetAuthentication(&reverseProxyAuthenticationResult{user: permissions.GetDoer()})
|
||||
default:
|
||||
permissions.SetToken(token)
|
||||
permissions.SetAuthentication(&accessTokenAuthenticationResult{user: permissions.GetDoer(), scope: token.Scope, reducer: tokenReducer})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fixtureCreateBranch(t *testing.T, permissions *apiv1_permissions.Permissions, branch string) {
|
||||
t.Helper()
|
||||
repository := permissions.GetRepository()
|
||||
require.NotNil(t, repository)
|
||||
|
||||
gitRepo, err := git.OpenRepository(t.Context(), repository.RepoPath())
|
||||
require.NoError(t, err)
|
||||
defaultBranch, err := git.GetDefaultBranch(t.Context(), repository.RepoPath())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, gitRepo.CreateBranch(branch, defaultBranch))
|
||||
}
|
||||
|
||||
func fixtureCreatePullRequest(t *testing.T, permissions *apiv1_permissions.Permissions, fixtureData *fixtureData) {
|
||||
t.Helper()
|
||||
if !fixtureData.Has("pullRequest") {
|
||||
return
|
||||
}
|
||||
|
||||
repository := permissions.GetRepository()
|
||||
require.NotNil(t, repository)
|
||||
|
||||
poster := fixtureGetUser(t, fixtureData.Get("pullRequestAuthor"))
|
||||
require.NotNil(t, poster)
|
||||
|
||||
ctx, committer, err := db.TxContext(t.Context())
|
||||
require.NoError(t, err)
|
||||
defer committer.Close()
|
||||
|
||||
idx, err := db.GetNextResourceIndex(ctx, "issue_index", repository.ID)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("generate issue index failed: %w", err))
|
||||
}
|
||||
issue := &issues_model.Issue{
|
||||
Index: idx,
|
||||
RepoID: repository.ID,
|
||||
IsPull: true,
|
||||
Title: fixtureData.Get("pullRequest"),
|
||||
PosterID: poster.ID,
|
||||
Poster: poster,
|
||||
}
|
||||
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
if _, err = sess.NoAutoTime().Insert(issue); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
issue.PullRequest = &issues_model.PullRequest{}
|
||||
|
||||
pr := issue.PullRequest
|
||||
pr.Index = issue.Index
|
||||
pr.IssueID = issue.ID
|
||||
pr.HeadRepoID = repository.ID
|
||||
pr.BaseRepoID = repository.ID
|
||||
pr.HeadBranch = fixtureData.Get("pullRequestBranch")
|
||||
_, err = sess.NoAutoTime().Insert(pr)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, committer.Commit())
|
||||
require.NoError(t, pr.LoadBaseRepo(ctx))
|
||||
require.NoError(t, pr.LoadHeadRepo(ctx))
|
||||
require.NoError(t, pull_service.PushToBaseRepo(ctx, pr))
|
||||
}
|
||||
|
||||
func fixtureSetRepository(t *testing.T, permissions *apiv1_permissions.Permissions, fixtureData *fixtureData) {
|
||||
t.Helper()
|
||||
if !fixtureData.Has("repository") {
|
||||
return
|
||||
}
|
||||
if repository := permissions.GetRepository(); repository != nil {
|
||||
if repository.FullName() != fixtureData.Get("repository") {
|
||||
panic(fmt.Sprintf("attempting to override already repository %s with %s", repository.FullName(), fixtureData.Get("repository")))
|
||||
}
|
||||
return
|
||||
}
|
||||
ownerName, repoName, found := strings.Cut(fixtureData.Get("repository"), "/")
|
||||
require.True(t, found)
|
||||
owner := fixtureCreateUser(t, &user_model.User{Name: ownerName})
|
||||
opts := &forgery.CreateRepositoryOptions{
|
||||
Name: repoName,
|
||||
IsPrivate: strings.Contains(repoName, "private"),
|
||||
}
|
||||
if fixtureData.Get("repository-init") == "true" {
|
||||
opts.Files = forgery.FilesInit{}
|
||||
}
|
||||
repository := forgery.CreateRepository(t, owner, opts)
|
||||
// some of it is redundant with the config but that makes
|
||||
// the tests immune to changes in the defaults
|
||||
for _, unitType := range unit_model.DefaultRepoUnits {
|
||||
forgery.EnableRepoUnit(t, repository, unitType, nil)
|
||||
}
|
||||
if strings.Contains(repoName, "archived") {
|
||||
require.NoError(t, repo_model.SetArchiveRepoState(t.Context(), repository, true))
|
||||
}
|
||||
permissions.SetRepository(repository)
|
||||
}
|
||||
|
||||
func dataToString(t *testing.T, fixtureData *fixtureData, key string) string {
|
||||
t.Helper()
|
||||
require.True(t, fixtureData.Has(key))
|
||||
return fixtureData.Get(key)
|
||||
}
|
||||
|
||||
func fixtureGetIssue(t *testing.T, fixtureData *fixtureData) *issues_model.Issue {
|
||||
t.Helper()
|
||||
var issue issues_model.Issue
|
||||
found, err := db.GetEngine(t.Context()).Where("name = ?", dataToString(t, fixtureData, "issue")).Get(&issue)
|
||||
require.NoError(t, err)
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
return &issue
|
||||
}
|
||||
|
||||
func fixtureSetIssue(t *testing.T, permissions *apiv1_permissions.Permissions, fixtureData *fixtureData) {
|
||||
t.Helper()
|
||||
if fixtureGetIssue(t, fixtureData) == nil {
|
||||
authorName := fixtureData.Get("issueAuthor")
|
||||
author := fixtureCreateUser(t, &user_model.User{Name: authorName})
|
||||
_ = fixtureCreateIssue(t, author, permissions.GetRepository(), dataToString(t, fixtureData, "issue"), "issue description")
|
||||
}
|
||||
}
|
||||
|
||||
func fixtureGetComment(t *testing.T, fixtureData *fixtureData) *issues_model.Comment {
|
||||
var comment issues_model.Comment
|
||||
found, err := db.GetEngine(t.Context()).Where("content = ?", dataToString(t, fixtureData, "comment")).Get(&comment)
|
||||
require.NoError(t, err)
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
comment.LoadIssue(t.Context())
|
||||
return &comment
|
||||
}
|
||||
|
||||
func fixtureCreateComment(t *testing.T, permissions *apiv1_permissions.Permissions, fixtureData *fixtureData) {
|
||||
t.Helper()
|
||||
if fixtureGetComment(t, fixtureData) == nil {
|
||||
authorName := fixtureData.Get("issueAuthor")
|
||||
author := fixtureCreateUser(t, &user_model.User{Name: authorName})
|
||||
_, err := issues_model.CreateComment(t.Context(), &issues_model.CreateCommentOptions{
|
||||
Type: issues_model.CommentTypeComment,
|
||||
Doer: author,
|
||||
Issue: fixtureGetIssue(t, fixtureData),
|
||||
Repo: permissions.GetRepository(),
|
||||
Content: dataToString(t, fixtureData, "comment"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func fixtureDisableRepoUnit(t *testing.T, permissions *apiv1_permissions.Permissions, unitType unit_model.Type) {
|
||||
t.Helper()
|
||||
repo := permissions.GetRepository()
|
||||
require.NotNil(t, repo)
|
||||
forgery.DisableRepoUnits(t, repo, unitType)
|
||||
}
|
||||
|
||||
func fixtureDisableUnits(t *testing.T, permissions *apiv1_permissions.Permissions, fixtureData *fixtureData) {
|
||||
t.Helper()
|
||||
if !fixtureData.Has("disable-units") {
|
||||
return
|
||||
}
|
||||
for unit := range strings.SplitSeq(fixtureData.Get("disable-units"), ",") {
|
||||
unitType := unit_model.TypeFromKey(unit)
|
||||
if unitType == unit_model.TypeInvalid {
|
||||
panic(fmt.Errorf("unable to find a unit matching '%s'", unit))
|
||||
}
|
||||
fixtureDisableRepoUnit(t, permissions, unitType)
|
||||
}
|
||||
}
|
||||
|
||||
type fixtureData struct {
|
||||
entries map[string]string
|
||||
}
|
||||
|
||||
func (o *fixtureData) Set(key, value string) {
|
||||
o.entries[key] = value
|
||||
}
|
||||
|
||||
func (o *fixtureData) SetDefault(key, value string) {
|
||||
if !o.Has(key) {
|
||||
o.Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *fixtureData) Get(key string) string {
|
||||
return o.entries[key]
|
||||
}
|
||||
|
||||
func (o *fixtureData) Has(key string) bool {
|
||||
_, has := o.entries[key]
|
||||
return has
|
||||
}
|
||||
|
||||
func (o *fixtureData) String() string {
|
||||
var s []string
|
||||
for k, e := range o.entries {
|
||||
s = append(s, fmt.Sprintf("%s:%s", k, e))
|
||||
}
|
||||
slices.Sort(s)
|
||||
return strings.Join(s, ",")
|
||||
}
|
||||
|
||||
func newFixtureData(data map[string]string) *fixtureData {
|
||||
fixtureData := &fixtureData{
|
||||
entries: make(map[string]string, 10),
|
||||
}
|
||||
for key, value := range data {
|
||||
fixtureData.Set(key, value)
|
||||
}
|
||||
return fixtureData
|
||||
}
|
||||
|
||||
func (o *fixtureData) Clone() *fixtureData {
|
||||
return &fixtureData{entries: maps.Clone(o.entries)}
|
||||
}
|
||||
|
||||
type fixtureType struct {
|
||||
data *fixtureData
|
||||
error string
|
||||
|
||||
used bool
|
||||
}
|
||||
|
||||
func (o *fixtureType) Clone() *fixtureType {
|
||||
f := *o
|
||||
f.data = o.data.Clone()
|
||||
return &f
|
||||
}
|
||||
|
||||
// See README.md for a documentation of the test logic that uses
|
||||
// this test description.
|
||||
type functionTest struct {
|
||||
// The fixture will be constructed, when this function is the last
|
||||
// one of the chain. It will go through the fulfillNeeds and
|
||||
// interpret of the previous functions in the chain, as well as its
|
||||
// own interpret.
|
||||
fixtures []*fixtureType
|
||||
|
||||
// List the settings which might be updated while interpreting the fixtureData
|
||||
// so that they are restored upon test completion.
|
||||
protectSettingsBool []*bool
|
||||
|
||||
// number of static arguments to pass to call's last argument
|
||||
staticArgs int
|
||||
// call the middleware (set automatically by [registerFunctionTest])
|
||||
call func(t *testing.T, ctx apiv1_permissions.Context, data *fixtureData, staticArgs []any)
|
||||
|
||||
sequenceFilter []string
|
||||
fulfillNeeds func(t *testing.T, data *fixtureData)
|
||||
interpret func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData)
|
||||
}
|
||||
|
||||
func buildSignatureStringToFunctionTest(t *testing.T) {
|
||||
for signatureString, signature := range apiv1_permissions_tests.GetSignatureStringToSignature() {
|
||||
for prefix, builder := range prefixToFunctionTestBuilder {
|
||||
if strings.HasPrefix(signatureString, prefix) {
|
||||
builder(t, signatureString, signature)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func registerFunctionTest(fun func(apiv1_permissions.Context), test functionTest) bool {
|
||||
shortName := routing.GetFuncShortName(fun)
|
||||
test.call = func(t *testing.T, ctx apiv1_permissions.Context, _ *fixtureData, _ []any) {
|
||||
t.Logf("calling %s(ctx)", shortName)
|
||||
fun(ctx)
|
||||
}
|
||||
return registerFunctionTestWithCall(fun, test)
|
||||
}
|
||||
|
||||
func registerFunctionTestWithCall(fun any, test functionTest) bool {
|
||||
signatureString := apiv1_permissions_tests.SignatureToString([]any{fun})
|
||||
if _, has := signatureStringToFunctionTest[signatureString]; has {
|
||||
panic(fmt.Errorf("attempt to register %s twice", signatureString))
|
||||
}
|
||||
if test.call == nil {
|
||||
panic("'call' field is required")
|
||||
}
|
||||
signatureStringToFunctionTest[signatureString] = test
|
||||
return true
|
||||
}
|
||||
|
||||
var signatureStringToFunctionTest = map[string]functionTest{}
|
||||
|
||||
type functionTestBuilder func(t *testing.T, signatureString string, signature []any)
|
||||
|
||||
func registerFunctionTestBuilder(prefixes []string, builder functionTestBuilder) bool {
|
||||
for _, prefix := range prefixes {
|
||||
if _, has := prefixToFunctionTestBuilder[prefix]; has {
|
||||
panic(fmt.Errorf("attempt to register %s twice", prefix))
|
||||
}
|
||||
prefixToFunctionTestBuilder[prefix] = builder
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var prefixToFunctionTestBuilder = map[string]functionTestBuilder{}
|
||||
267
routers/api/v1/permissions/tests/functions_test.go
Normal file
267
routers/api/v1/permissions/tests/functions_test.go
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
// See README.md for a documentation of the test logic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"forgejo.org/models/unittest"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/test"
|
||||
"forgejo.org/modules/web/routing"
|
||||
apiv1 "forgejo.org/routers/api/v1"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
apiv1_permissions_tests "forgejo.org/routers/api/v1/permissions/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func createFixture(t *testing.T, signatures [][]any, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
for _, signature := range signatures[:len(signatures)-1] {
|
||||
signatureString := apiv1_permissions_tests.SignatureToString(signature)
|
||||
functionTest, has := signatureStringToFunctionTest[signatureString]
|
||||
require.True(t, has)
|
||||
if functionTest.fulfillNeeds != nil {
|
||||
functionTest.fulfillNeeds(t, data)
|
||||
}
|
||||
}
|
||||
for _, signature := range signatures {
|
||||
signatureString := apiv1_permissions_tests.SignatureToString(signature)
|
||||
functionTest, has := signatureStringToFunctionTest[signatureString]
|
||||
require.True(t, has)
|
||||
if functionTest.interpret != nil {
|
||||
functionTest.interpret(t, permissions, data)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getFunctionTest(t *testing.T, signatures [][]any) functionTest {
|
||||
lastSignature := signatures[len(signatures)-1]
|
||||
lastSignatureString := apiv1_permissions_tests.SignatureToString(lastSignature)
|
||||
test, has := signatureStringToFunctionTest[lastSignatureString]
|
||||
require.True(t, has, lastSignatureString)
|
||||
return test
|
||||
}
|
||||
|
||||
func getFixtures(t *testing.T, signatures [][]any) []*fixtureType {
|
||||
return getFunctionTest(t, signatures).fixtures
|
||||
}
|
||||
|
||||
func protectVariables(t *testing.T, signatures [][]any) {
|
||||
for _, signature := range signatures {
|
||||
signatureString := apiv1_permissions_tests.SignatureToString(signature)
|
||||
functionTest, has := signatureStringToFunctionTest[signatureString]
|
||||
require.True(t, has)
|
||||
for _, b := range functionTest.protectSettingsBool {
|
||||
t.Cleanup(test.MockProtect(b))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testSequence(t *testing.T, signatures [][]any, onlyForSuccess bool) int {
|
||||
t.Helper()
|
||||
signaturesString := apiv1_permissions_tests.SignaturesToString(signatures)
|
||||
var fixtures []*fixtureType
|
||||
if onlyForSuccess {
|
||||
for _, fixture := range getFixtures(t, signatures) {
|
||||
if fixture.error == "" {
|
||||
fixtures = []*fixtureType{fixture}
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotEmpty(t, fixtures, "%s must have at least one fixture with no error", signaturesString)
|
||||
} else {
|
||||
fixtures = getFixtures(t, signatures)
|
||||
}
|
||||
for i, fixture := range fixtures {
|
||||
runName := signaturesString
|
||||
if len(fixtures) > 1 {
|
||||
runName = fmt.Sprintf("%s fixture %d", signaturesString, i)
|
||||
}
|
||||
t.Run(runName, func(t *testing.T) {
|
||||
protectVariables(t, signatures)
|
||||
|
||||
unittest.LoadFixtures() // reset the database to clear any side effect of running the test
|
||||
permissions := &apiv1_permissions.Permissions{}
|
||||
permissions.SetContext(t.Context())
|
||||
t.Logf("creating fixture data from %v", fixture.data)
|
||||
modifiedFixture := fixture.Clone()
|
||||
createFixture(t, signatures, permissions, modifiedFixture.data)
|
||||
t.Logf("created fixture data %v", modifiedFixture.data)
|
||||
|
||||
var previousPerms []string
|
||||
showPermissionsDiff := func() {
|
||||
newPerms := permissions.Strings()
|
||||
if previousPerms == nil {
|
||||
for _, s := range newPerms {
|
||||
t.Logf("\t%s", s)
|
||||
}
|
||||
} else {
|
||||
// easier to compute the additions first (since we can destroy previousPerms)
|
||||
// nicer to show the additions after the deletion
|
||||
var additions []string
|
||||
for _, s := range newPerms {
|
||||
if i := slices.Index(previousPerms, s); i >= 0 {
|
||||
if i == 0 {
|
||||
// most frequent case
|
||||
previousPerms = previousPerms[1:]
|
||||
continue
|
||||
}
|
||||
last := len(previousPerms) - 1
|
||||
if i != last {
|
||||
previousPerms[i] = previousPerms[last]
|
||||
}
|
||||
previousPerms = previousPerms[:last]
|
||||
continue
|
||||
}
|
||||
additions = append(additions, s)
|
||||
}
|
||||
for _, s := range previousPerms {
|
||||
t.Logf("\t- %s", s)
|
||||
}
|
||||
for _, s := range additions {
|
||||
t.Logf("\t+ %s", s)
|
||||
}
|
||||
}
|
||||
previousPerms = newPerms
|
||||
}
|
||||
showPermissionsDiff()
|
||||
var permissionsContext apiv1_permissions.Context = permissions
|
||||
for i, signature := range signatures {
|
||||
signatureString := apiv1_permissions_tests.SignatureToString(signature)
|
||||
functionTest, has := signatureStringToFunctionTest[signatureString]
|
||||
require.True(t, has)
|
||||
|
||||
args := signature[1:]
|
||||
if len(args) != functionTest.staticArgs {
|
||||
t.Fatalf("%s expects %d static arguments, got %d: %#v", routing.GetFuncShortName(signature[0]), functionTest.staticArgs, len(args), args)
|
||||
}
|
||||
functionTest.call(t, permissionsContext, modifiedFixture.data, args)
|
||||
showPermissionsDiff()
|
||||
if i == len(signatures)-1 {
|
||||
fixture.used = true
|
||||
if fixture.error != "" {
|
||||
assert.NotZero(t, permissions.GetStatus())
|
||||
assert.Contains(t, permissions.GetMessage(), fixture.error)
|
||||
} else {
|
||||
assert.Zero(t, permissions.GetStatus(), permissions.GetMessage())
|
||||
}
|
||||
} else {
|
||||
assert.Zero(t, permissions.GetStatus(), permissions.GetMessage())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
return len(fixtures)
|
||||
}
|
||||
|
||||
func getPermissionSequenceForFunction(t *testing.T, sequence [][]any) [][]any {
|
||||
sequenceString := apiv1_permissions_tests.SignaturesToString(sequence)
|
||||
sequenceFilter := getFunctionTest(t, sequence).sequenceFilter
|
||||
if sequenceFilter == nil {
|
||||
return sequence
|
||||
}
|
||||
var filteredSequence [][]any
|
||||
if len(sequenceFilter) > len(sequence) {
|
||||
panic(fmt.Errorf("%s is longer than %v", sequenceString, sequenceFilter))
|
||||
}
|
||||
for _, signature := range sequence {
|
||||
if len(sequenceFilter) == 0 {
|
||||
break
|
||||
}
|
||||
signatureString := apiv1_permissions_tests.SignatureToString(signature)
|
||||
if signatureString == sequenceFilter[0] || strings.HasPrefix(signatureString, sequenceFilter[0]+" ") {
|
||||
filteredSequence = append(filteredSequence, signature)
|
||||
sequenceFilter = sequenceFilter[1:]
|
||||
}
|
||||
}
|
||||
if len(sequenceFilter) > 0 {
|
||||
panic(fmt.Errorf("%s filtered by %v does not consume all filters and %v is left", sequenceString, getFunctionTest(t, sequence).sequenceFilter, sequenceFilter))
|
||||
}
|
||||
if len(filteredSequence) == 0 {
|
||||
panic(fmt.Errorf("%s filtered by %v is an empty sequence", sequenceString, getFunctionTest(t, sequence).sequenceFilter))
|
||||
}
|
||||
getPrefix := func(signature []any) string {
|
||||
signatureString := apiv1_permissions_tests.SignatureToString(signature)
|
||||
prefix, _, _ := strings.Cut(signatureString, " ")
|
||||
return prefix
|
||||
}
|
||||
lastFilteredFunctionString := getPrefix(filteredSequence[len(filteredSequence)-1])
|
||||
lastFunctionString := getPrefix(sequence[len(sequence)-1])
|
||||
if lastFilteredFunctionString != lastFunctionString {
|
||||
panic(fmt.Errorf("%s filtered by %v ends with the function %s instead of %s", sequenceString, getFunctionTest(t, sequence).sequenceFilter, lastFilteredFunctionString, lastFunctionString))
|
||||
}
|
||||
return filteredSequence
|
||||
}
|
||||
|
||||
func getPermissionSequencesForFunctions(t *testing.T) [][][]any {
|
||||
var sequences [][][]any
|
||||
for _, sequence := range apiv1_permissions_tests.GetShortestPermissionSequenceForEachSignature() {
|
||||
sequences = append(sequences, getPermissionSequenceForFunction(t, sequence))
|
||||
}
|
||||
return sequences
|
||||
}
|
||||
|
||||
func TestAPIv1Permissions(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Service.DefaultAllowCreateOrganization, true)()
|
||||
defer test.MockVariableValue(&setting.IsInTesting, true)()
|
||||
defer test.MockVariableValue(&setting.DisableGitHooks, false)()
|
||||
|
||||
unittest.PrepareTestEnv(t)
|
||||
|
||||
// because setting.IsInTesting == true, it will record the
|
||||
// middleware sequence of each route it builds
|
||||
apiv1.Routes()
|
||||
|
||||
buildSignatureStringToFunctionTest(t)
|
||||
|
||||
runs := 0
|
||||
t.Logf("running all fixtures for each permission function")
|
||||
for _, sequence := range getPermissionSequencesForFunctions(t) {
|
||||
runs += testSequence(t, sequence, false)
|
||||
}
|
||||
t.Logf("verify all unique permission sequences can run successfully")
|
||||
uniqueSequences := apiv1_permissions_tests.GetUniquePermissionsSequences()
|
||||
for _, sequence := range uniqueSequences {
|
||||
runs += testSequence(t, sequence, true)
|
||||
}
|
||||
|
||||
hasRunArg := func() bool {
|
||||
for _, arg := range os.Args {
|
||||
if arg != "-test.run=Test" && strings.HasPrefix(arg, "-test.run") {
|
||||
return true
|
||||
}
|
||||
if arg != "-run=Test" && strings.HasPrefix(arg, "-run") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// this sanity check will fail if only a selection of tests is run
|
||||
// with -run=TestMyTest
|
||||
if !hasRunArg() {
|
||||
unusedFixtures := false
|
||||
for signatureString, test := range signatureStringToFunctionTest {
|
||||
for _, fixture := range test.fixtures {
|
||||
if !fixture.used {
|
||||
t.Logf("%s fixture %v not used", signatureString, fixture.data)
|
||||
unusedFixtures = true
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.False(t, unusedFixtures)
|
||||
}
|
||||
t.Logf("%d unique sequence of permission functions", len(uniqueSequences))
|
||||
t.Logf("%d permissions functions", len(signatureStringToFunctionTest))
|
||||
t.Logf("used a total of %d fixtures", runs)
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
user_model "forgejo.org/models/user"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTest(apiv1_permissions.IndividualPermsChecker, functionTest{
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
data.SetDefault("user", data.Get("doer"))
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
if data.Has("user") && data.Get("user") != "anonymous" {
|
||||
name := data.Get("user")
|
||||
fixtureCreateUser(t, &user_model.User{Name: name})
|
||||
permissions.SetUser(fixtureGetUser(t, name))
|
||||
}
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"user": "IndividualPermsChecker",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"user": "IndividualPermsCheckerprivate",
|
||||
}),
|
||||
error: "Visit Project",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "anonymous",
|
||||
"user": "IndividualPermsCheckerlimited",
|
||||
}),
|
||||
error: "Visit Project",
|
||||
},
|
||||
},
|
||||
})
|
||||
16
routers/api/v1/permissions/tests/main_test.go
Normal file
16
routers/api/v1/permissions/tests/main_test.go
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forgejo.org/models/unittest"
|
||||
|
||||
_ "forgejo.org/modules/testimport"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m)
|
||||
}
|
||||
38
routers/api/v1/permissions/tests/must_allow_pulls_test.go
Normal file
38
routers/api/v1/permissions/tests/must_allow_pulls_test.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTest(apiv1_permissions.MustAllowPulls, functionTest{
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
data.Set("repository-init", "true")
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
fixtureDisableUnits(t, permissions, data)
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doerregular",
|
||||
"repository": "userowner/repositorypublic",
|
||||
"repository-init": "true",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doerregular",
|
||||
"repository": "userowner/repositorypublic",
|
||||
"repository-init": "true",
|
||||
"disable-units": "repo.pulls",
|
||||
}),
|
||||
error: "Not Found",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forgejo.org/modules/setting"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTest(apiv1_permissions.MustEnableAttachments, functionTest{
|
||||
protectSettingsBool: []*bool{
|
||||
&setting.Attachment.Enabled,
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
setting.Attachment.Enabled = data.Get("Attachment.Enabled") != "false"
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"Attachment.Enabled": "false",
|
||||
}),
|
||||
error: "Not Found",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTest(apiv1_permissions.MustEnableIssuesOrPulls, functionTest{
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
data.Set("repository-init", "true")
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
fixtureDisableUnits(t, permissions, data)
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doerregular",
|
||||
"repository": "userowner/repositorypublic",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doerregular",
|
||||
"repository": "userowner/repositorypublic",
|
||||
"repository-init": "true",
|
||||
"disable-units": "repo.pulls,repo.issues",
|
||||
}),
|
||||
error: "Not Found",
|
||||
},
|
||||
},
|
||||
})
|
||||
32
routers/api/v1/permissions/tests/must_enable_issues_test.go
Normal file
32
routers/api/v1/permissions/tests/must_enable_issues_test.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTest(apiv1_permissions.MustEnableIssues, functionTest{
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
fixtureDisableUnits(t, permissions, data)
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doerregular",
|
||||
"repository": "userowner/repositorypublic",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doerregular",
|
||||
"repository": "userowner/repositorypublic",
|
||||
"disable-units": "repo.issues",
|
||||
}),
|
||||
error: "Not Found",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
user_model "forgejo.org/models/user"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTestWithCall(apiv1_permissions.MustEnableLocalIssuesIfIsIssue, functionTest{
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
data.SetDefault("issue", "issueOne")
|
||||
data.SetDefault("issueAuthor", "issueAuthor")
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
fixtureDisableUnits(t, permissions, data)
|
||||
if data.Has("pullRequest") {
|
||||
require.True(t, data.Has("pullRequestBranch"))
|
||||
fixtureCreateBranch(t, permissions, data.Get("pullRequestBranch"))
|
||||
require.True(t, data.Has("pullRequestAuthor"))
|
||||
require.True(t, data.Has("pullRequest"))
|
||||
fixtureCreatePullRequest(t, permissions, data)
|
||||
require.Equal(t, data.Get("issue"), data.Get("pullRequest"))
|
||||
} else {
|
||||
fixtureCreateUser(t, &user_model.User{Name: data.Get("issueAuthor")})
|
||||
fixtureSetIssue(t, permissions, data)
|
||||
}
|
||||
},
|
||||
call: func(t *testing.T, ctx apiv1_permissions.Context, data *fixtureData, _ []any) {
|
||||
t.Helper()
|
||||
index := fixtureGetIssue(t, data).Index
|
||||
t.Logf("calling MustEnableLocalIssuesIfIsIssue(ctx, %d)", index)
|
||||
apiv1_permissions.MustEnableLocalIssuesIfIsIssue(ctx, index)
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doerregular",
|
||||
"repository": "userowner/repositorypublic",
|
||||
"issue": "issue5000",
|
||||
"issueAuthor": "issueAuthor",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doerregular",
|
||||
"repository": "userowner/repositorypublic",
|
||||
"issue": "issue5000",
|
||||
"issueAuthor": "issueAuthor",
|
||||
"disable-units": "repo.issues",
|
||||
}),
|
||||
error: "Not Found",
|
||||
},
|
||||
{ // does not fail because it is an issue instead of a pull request
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "userowner",
|
||||
"repository": "userowner/repositorypublic",
|
||||
"repository-init": "true",
|
||||
"pullRequestAuthor": "userowner",
|
||||
"pullRequestBranch": "MustEnableLocalIssuesIfIsIssue",
|
||||
"pullRequest": "MustEnableLocalIssuesIfIsIssue",
|
||||
"issue": "MustEnableLocalIssuesIfIsIssue",
|
||||
"disable-units": "repo.issues",
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
32
routers/api/v1/permissions/tests/must_enable_wiki_test.go
Normal file
32
routers/api/v1/permissions/tests/must_enable_wiki_test.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTest(apiv1_permissions.MustEnableWiki, functionTest{
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
fixtureDisableUnits(t, permissions, data)
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doerregular",
|
||||
"repository": "userowner/repositorypublic",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doerregular",
|
||||
"repository": "userowner/repositorypublic",
|
||||
"disable-units": "repo.wiki",
|
||||
}),
|
||||
error: "Not Found",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTest(apiv1_permissions.MustNotBeArchived, functionTest{
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"repository": "userowner/repositorypublic",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"repository": "userowner/repositoryarchived",
|
||||
}),
|
||||
error: "is archived",
|
||||
},
|
||||
},
|
||||
})
|
||||
76
routers/api/v1/permissions/tests/repo_access_test.go
Normal file
76
routers/api/v1/permissions/tests/repo_access_test.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
user_model "forgejo.org/models/user"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTest(apiv1_permissions.RepoAccess, functionTest{
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
data.SetDefault("repository", "userowner/repositorypublic")
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
fixtureSetRepository(t, permissions, data)
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doerregular",
|
||||
"repository": "userowner/repositorypublic",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "anonymous",
|
||||
"repository": "userowner/repositorypublic",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doeradmin",
|
||||
"repository": "userowner/repositoryprivate",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doerregular",
|
||||
"repository": "userowner/repositoryprivate",
|
||||
}),
|
||||
error: "Not Found",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "anonymous",
|
||||
"repository": "userowner/repositoryprivate",
|
||||
}),
|
||||
error: "Not Found",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": user_model.ActionsUserName,
|
||||
"repository": "userowner/repositorypublic",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": user_model.ActionsUserName,
|
||||
"repository": "userowner/repositorypublic",
|
||||
"task.RepoID": "unrelated",
|
||||
}),
|
||||
error: "Not Found",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": user_model.ActionsUserName,
|
||||
"repository": "userowner/repositorypublic",
|
||||
"task.IsForkPullRequest": "true",
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
69
routers/api/v1/permissions/tests/req_admin_test.go
Normal file
69
routers/api/v1/permissions/tests/req_admin_test.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
unit_model "forgejo.org/models/unit"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTestBuilder([]string{"ReqAdmin ", "ReqAdmin"}, func(t *testing.T, signatureString string, signature []any) {
|
||||
t.Helper()
|
||||
unitTypes := signature[1].([]unit_model.Type)
|
||||
fixtures := []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"repository": "userowner/repositorypublic",
|
||||
"doer": "doeradmin",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"repository": "userowner/repositorypublic",
|
||||
"doer": "userowner",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"repository": "userowner/repositorypublic",
|
||||
"doer": "regularuser",
|
||||
}),
|
||||
error: "user should be an owner or a collaborator with admin write of a repository",
|
||||
},
|
||||
}
|
||||
for _, unitType := range unitTypes {
|
||||
unit := unitsTypeToString(unitType)
|
||||
fixtures = append(fixtures, &fixtureType{
|
||||
data: newFixtureData(map[string]string{
|
||||
"repository": "userowner/repositorypublic",
|
||||
"doer": "doeradmin",
|
||||
"disable-units": unit,
|
||||
}),
|
||||
error: "Not Found",
|
||||
})
|
||||
}
|
||||
signatureStringToFunctionTest[signatureString] = functionTest{
|
||||
sequenceFilter: []string{
|
||||
"APIAuthorization",
|
||||
"RepoAccess",
|
||||
signatureString,
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
fixtureDisableUnits(t, permissions, data)
|
||||
},
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
data.Set("doer", "doeradmin")
|
||||
},
|
||||
fixtures: fixtures,
|
||||
staticArgs: 1,
|
||||
call: func(t *testing.T, ctx apiv1_permissions.Context, _ *fixtureData, args []any) {
|
||||
unitTypes := args[0].([]unit_model.Type)
|
||||
t.Logf("calling ReqAdmin(ctx, %+v)", unitTypes)
|
||||
apiv1_permissions.ReqAdmin(ctx, unitTypes)
|
||||
},
|
||||
}
|
||||
})
|
||||
40
routers/api/v1/permissions/tests/req_any_repo_reader_test.go
Normal file
40
routers/api/v1/permissions/tests/req_any_repo_reader_test.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTest(apiv1_permissions.ReqAnyRepoReader, functionTest{
|
||||
sequenceFilter: []string{
|
||||
"APIAuthorization",
|
||||
"RepoAccess",
|
||||
"ReqAnyRepoReader",
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doerregular",
|
||||
"repository": "userowner/repositorypublic",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doeradmin",
|
||||
"repository": "userowner/repositoryprivate",
|
||||
}),
|
||||
},
|
||||
// This fixture is unreachable because this permissions function is always used after
|
||||
// a RepoAccess that enforces the same restriction for non admin users
|
||||
// {
|
||||
// data: newFixtureData(map[string]string{
|
||||
// "doer": "doerregular",
|
||||
// "repository": "userowner/repositoryprivate",
|
||||
// }),
|
||||
// error: "Denied",
|
||||
// },
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forgejo.org/modules/setting"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTest(apiv1_permissions.ReqBasicOrRevProxyAuth, functionTest{
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
data.SetDefault("doer", "regularuser")
|
||||
data.SetDefault("Service.EnableReverseProxyAuthAPI", "true")
|
||||
data.SetDefault("authentication", "proxy")
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
fixtureSetDoer(t, permissions, data)
|
||||
setting.Service.EnableReverseProxyAuthAPI = data.Get("Service.EnableReverseProxyAuthAPI") == "true"
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "regularuser",
|
||||
"Service.EnableReverseProxyAuthAPI": "true",
|
||||
"authentication": "proxy",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "regularuser",
|
||||
"Service.EnableReverseProxyAuthAPI": "false",
|
||||
"authentication": "basic",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "regularuser",
|
||||
"Service.EnableReverseProxyAuthAPI": "true",
|
||||
"authentication": "token",
|
||||
}),
|
||||
error: "auth method not allowed",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "regularuser",
|
||||
"Service.EnableReverseProxyAuthAPI": "false",
|
||||
"authentication": "token",
|
||||
}),
|
||||
error: "auth method not allowed",
|
||||
},
|
||||
},
|
||||
})
|
||||
48
routers/api/v1/permissions/tests/req_explore_sign_in_test.go
Normal file
48
routers/api/v1/permissions/tests/req_explore_sign_in_test.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forgejo.org/modules/setting"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTest(apiv1_permissions.ReqExploreSignIn, functionTest{
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
data.SetDefault("doer", "regularuser")
|
||||
},
|
||||
protectSettingsBool: []*bool{
|
||||
&setting.Service.RequireSignInView,
|
||||
&setting.Service.Explore.RequireSigninView,
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
fixtureSetDoer(t, permissions, data)
|
||||
setting.Service.RequireSignInView = data.Get("Service.RequireSignInView") == "true"
|
||||
setting.Service.Explore.RequireSigninView = data.Get("Service.Explore.RequireSigninView") == "true"
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "regularuser",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "anonymous",
|
||||
"Service.RequireSignInView": "true",
|
||||
}),
|
||||
error: "you must be signed in",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "anonymous",
|
||||
"Service.Explore.RequireSigninView": "true",
|
||||
}),
|
||||
error: "you must be signed in",
|
||||
},
|
||||
},
|
||||
})
|
||||
38
routers/api/v1/permissions/tests/req_git_hook_test.go
Normal file
38
routers/api/v1/permissions/tests/req_git_hook_test.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forgejo.org/modules/setting"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTest(apiv1_permissions.ReqGitHook, functionTest{
|
||||
protectSettingsBool: []*bool{
|
||||
&setting.DisableGitHooks,
|
||||
},
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
data.SetDefault("doer", "doeradmin")
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
setting.DisableGitHooks = data.Get("DisableGitHooks") == "true"
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doeradmin",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doeradmin",
|
||||
"DisableGitHooks": "true",
|
||||
}),
|
||||
error: "must be allowed to edit Git hooks",
|
||||
},
|
||||
},
|
||||
})
|
||||
100
routers/api/v1/permissions/tests/req_org_membership_test.go
Normal file
100
routers/api/v1/permissions/tests/req_org_membership_test.go
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
org_model "forgejo.org/models/organization"
|
||||
user_model "forgejo.org/models/user"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTest(apiv1_permissions.ReqOrgMembership, functionTest{
|
||||
sequenceFilter: []string{
|
||||
"APIAuthorization",
|
||||
"TokenRequiresScopes",
|
||||
"ReqOrgMembership",
|
||||
},
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
data.SetDefault("org", "ReqOrgMembership")
|
||||
data.SetDefault("setOrg", "true")
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
orgOwner := data.Get("doer")
|
||||
if data.Has("orgOwner") {
|
||||
orgOwner = data.Get("orgOwner")
|
||||
}
|
||||
var org *org_model.Organization
|
||||
if data.Has("org") {
|
||||
fixtureCreateUser(t, &user_model.User{Name: orgOwner})
|
||||
org = fixtureCreateOrg(t, &org_model.Organization{Name: data.Get("org")}, &user_model.User{Name: orgOwner})
|
||||
}
|
||||
|
||||
if data.Get("setOrg") == "true" {
|
||||
permissions.SetOrg(org)
|
||||
}
|
||||
|
||||
if data.Get("setTeam") == "true" {
|
||||
team, err := org_model.GetTeam(t.Context(), org.ID, org_model.OwnerTeamName)
|
||||
require.NoError(t, err)
|
||||
permissions.SetTeam(team)
|
||||
}
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"org": "ReqOrgMembershipOrg",
|
||||
"setOrg": "true",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doeradmin",
|
||||
"setOrg": "true",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"org": "ReqOrgMembershipOrg",
|
||||
"doer": "regularuser",
|
||||
"setOrg": "true",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"org": "ReqOrgMembershipOrg",
|
||||
"orgOwner": "ReqOrgMembershipOrgOwner",
|
||||
"doer": "regularuser",
|
||||
"setOrg": "true",
|
||||
}),
|
||||
error: "Must be an organization member",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"org": "ReqOrgMembershipOrg",
|
||||
"doer": "regularuser",
|
||||
"setTeam": "true",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"org": "ReqOrgMembershipOrg",
|
||||
"orgOwner": "ReqOrgMembershipOrgOwner",
|
||||
"doer": "regularuser",
|
||||
"setTeam": "true",
|
||||
}),
|
||||
error: "Not Found",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"setOrg": "true",
|
||||
}),
|
||||
error: "unprepared context",
|
||||
},
|
||||
},
|
||||
})
|
||||
100
routers/api/v1/permissions/tests/req_org_ownership_test.go
Normal file
100
routers/api/v1/permissions/tests/req_org_ownership_test.go
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
org_model "forgejo.org/models/organization"
|
||||
user_model "forgejo.org/models/user"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTest(apiv1_permissions.ReqOrgOwnership, functionTest{
|
||||
sequenceFilter: []string{
|
||||
"APIAuthorization",
|
||||
"TokenRequiresScopes",
|
||||
"ReqOrgOwnership",
|
||||
},
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
data.SetDefault("org", "ReqOrgOwnershipOrg")
|
||||
data.SetDefault("setOrg", "true")
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
orgOwner := data.Get("doer")
|
||||
if data.Has("orgOwner") {
|
||||
orgOwner = data.Get("orgOwner")
|
||||
}
|
||||
var org *org_model.Organization
|
||||
if data.Has("org") {
|
||||
fixtureCreateUser(t, &user_model.User{Name: orgOwner})
|
||||
org = fixtureCreateOrg(t, &org_model.Organization{Name: data.Get("org")}, &user_model.User{Name: orgOwner})
|
||||
}
|
||||
|
||||
if data.Get("setOrg") == "true" {
|
||||
permissions.SetOrg(org)
|
||||
}
|
||||
|
||||
if data.Get("setTeam") == "true" {
|
||||
team, err := org_model.GetTeam(t.Context(), org.ID, org_model.OwnerTeamName)
|
||||
require.NoError(t, err)
|
||||
permissions.SetTeam(team)
|
||||
}
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"org": "ReqOrgOwnershipOrg",
|
||||
"setOrg": "true",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doeradmin",
|
||||
"setOrg": "true",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"org": "ReqOrgOwnershipOrg",
|
||||
"doer": "regularuser",
|
||||
"setOrg": "true",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"org": "ReqOrgOwnershipOrg",
|
||||
"orgOwner": "ReqOrgOwnershipOrgOwner",
|
||||
"doer": "regularuser",
|
||||
"setOrg": "true",
|
||||
}),
|
||||
error: "Must be an organization owner",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"org": "ReqOrgOwnershipOrg",
|
||||
"doer": "regularuser",
|
||||
"setTeam": "true",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"org": "ReqOrgOwnershipOrg",
|
||||
"orgOwner": "ReqOrgOwnershipOrgOwner",
|
||||
"doer": "regularuser",
|
||||
"setTeam": "true",
|
||||
}),
|
||||
error: "Not Found",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"setOrg": "true",
|
||||
}),
|
||||
error: "reqOrgOwnership: unprepared context",
|
||||
},
|
||||
},
|
||||
})
|
||||
63
routers/api/v1/permissions/tests/req_owner_test.go
Normal file
63
routers/api/v1/permissions/tests/req_owner_test.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
unit_model "forgejo.org/models/unit"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTestBuilder([]string{"ReqOwner ", "ReqOwner"}, func(t *testing.T, signatureString string, signature []any) {
|
||||
t.Helper()
|
||||
unitTypes := signature[1].([]unit_model.Type)
|
||||
fixtures := []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "userowner",
|
||||
"repository": "userowner/repositorypublic",
|
||||
"scope": "read:user,write:repository",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "regular",
|
||||
"repository": "userowner/repositorypublic",
|
||||
"scope": "read:user,write:repository",
|
||||
}),
|
||||
error: "user should be the owner of the repo",
|
||||
},
|
||||
}
|
||||
for _, unitType := range unitTypes {
|
||||
unit := unitsTypeToString(unitType)
|
||||
fixtures = append(fixtures, &fixtureType{
|
||||
data: newFixtureData(map[string]string{
|
||||
"disable-units": unit,
|
||||
}),
|
||||
error: "Not Found",
|
||||
})
|
||||
}
|
||||
signatureStringToFunctionTest[signatureString] = functionTest{
|
||||
sequenceFilter: []string{
|
||||
"APIAuthorization",
|
||||
"RepoAccess",
|
||||
"ReqOwner",
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
fixtureDisableUnits(t, permissions, data)
|
||||
},
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
data.Set("doer", "doeradmin")
|
||||
},
|
||||
fixtures: fixtures,
|
||||
staticArgs: 1,
|
||||
call: func(t *testing.T, ctx apiv1_permissions.Context, _ *fixtureData, args []any) {
|
||||
unitTypes := args[0].([]unit_model.Type)
|
||||
t.Logf("calling ReqOwner(ctx, %+v)", unitTypes)
|
||||
apiv1_permissions.ReqOwner(ctx, unitTypes)
|
||||
},
|
||||
}
|
||||
})
|
||||
52
routers/api/v1/permissions/tests/req_package_access_test.go
Normal file
52
routers/api/v1/permissions/tests/req_package_access_test.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forgejo.org/models/perm"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTestBuilder([]string{"ReqPackageAccess "}, func(_ *testing.T, signatureString string, signature []any) {
|
||||
signatureStringToFunctionTest[signatureString] = functionTest{
|
||||
sequenceFilter: []string{
|
||||
"APIAuthorization",
|
||||
signatureString,
|
||||
},
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
data.SetDefault("doer", "doerregular")
|
||||
if data.Get("packageOwner") == "doer" {
|
||||
data.Set("packageOwner", data.Get("doer"))
|
||||
}
|
||||
data.SetDefault("packageOwner", data.Get("doer"))
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
fixtureSetPackageOwner(t, permissions, data)
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"packageOwner": "doer",
|
||||
"doer": "doeradmin",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "userregular",
|
||||
"packageOwner": "userprivate",
|
||||
}),
|
||||
error: "user should have specific permission or be a site admin",
|
||||
},
|
||||
},
|
||||
staticArgs: 1,
|
||||
call: func(t *testing.T, ctx apiv1_permissions.Context, _ *fixtureData, args []any) {
|
||||
mode := args[0].(perm.AccessMode)
|
||||
t.Logf("calling ReqPackageAccess(ctx, %s)", mode)
|
||||
apiv1_permissions.ReqPackageAccess(ctx, mode)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTestWithCall(apiv1_permissions.ReqRepoBranchWriter, functionTest{
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
require.True(t, data.Has("pullRequestBranch"))
|
||||
fixtureCreateBranch(t, permissions, data.Get("pullRequestBranch"))
|
||||
require.True(t, data.Has("pullRequestAuthor"))
|
||||
require.True(t, data.Has("pullRequest"))
|
||||
fixtureCreatePullRequest(t, permissions, data)
|
||||
},
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
owner, _, found := strings.Cut(data.Get("repository"), "/")
|
||||
require.True(t, found)
|
||||
data.Set("doer", owner)
|
||||
data.SetDefault("repository-init", "true")
|
||||
data.SetDefault("pullRequestAuthor", owner)
|
||||
data.SetDefault("pullRequestBranch", "ReqRepoBranchWriter")
|
||||
data.SetDefault("pullRequest", "ReqRepoBranchWriter")
|
||||
},
|
||||
call: func(t *testing.T, ctx apiv1_permissions.Context, data *fixtureData, _ []any) {
|
||||
branch := data.Get("pullRequestBranch")
|
||||
t.Logf("calling ReqRepoBranchWriter(ctx, %s)", branch)
|
||||
apiv1_permissions.ReqRepoBranchWriter(ctx, branch)
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "userowner",
|
||||
"repository": "userowner/repositorypublic",
|
||||
"repository-init": "true",
|
||||
"pullRequestAuthor": "userowner",
|
||||
"pullRequestBranch": "ReqRepoBranchWriter",
|
||||
"pullRequest": "ReqRepoBranchWriter",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "regularuser",
|
||||
"repository": "userowner/repositorypublic",
|
||||
"repository-init": "true",
|
||||
"pullRequestAuthor": "userowner",
|
||||
"pullRequestBranch": "ReqRepoBranchWriter",
|
||||
"pullRequest": "ReqRepoBranchWriter",
|
||||
}),
|
||||
error: "user should have a permission to write to this branch",
|
||||
},
|
||||
},
|
||||
})
|
||||
53
routers/api/v1/permissions/tests/req_repo_reader_test.go
Normal file
53
routers/api/v1/permissions/tests/req_repo_reader_test.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
unit_model "forgejo.org/models/unit"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTestBuilder([]string{"ReqRepoReader "}, func(t *testing.T, signatureString string, signature []any) {
|
||||
t.Helper()
|
||||
unitType := signature[1].(unit_model.Type)
|
||||
unit := unitsTypeToString(unitType)
|
||||
signatureStringToFunctionTest[signatureString] = functionTest{
|
||||
sequenceFilter: []string{
|
||||
"APIAuthorization",
|
||||
"RepoAccess",
|
||||
signatureString,
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
fixtureDisableUnits(t, permissions, data)
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"disable-units": unit,
|
||||
}),
|
||||
error: "Not Found",
|
||||
},
|
||||
// This fixture is unreachable because this permissions function is always used after
|
||||
// a RepoAccess that enforces the same restriction for non admin users
|
||||
// {
|
||||
// data: newFixtureData(map[string]string{
|
||||
// "doer": "regularuser",
|
||||
// "repository": "userowner/repositoryprivate",
|
||||
// }),
|
||||
// error: "user should have specific read permission or be a repo admin or a site admin",
|
||||
// },
|
||||
},
|
||||
staticArgs: 1,
|
||||
call: func(t *testing.T, ctx apiv1_permissions.Context, _ *fixtureData, args []any) {
|
||||
unitType := args[0].(unit_model.Type)
|
||||
t.Logf("calling ReqRepoReader(ctx, %s)", unitType)
|
||||
apiv1_permissions.ReqRepoReader(ctx, unitType)
|
||||
},
|
||||
}
|
||||
})
|
||||
72
routers/api/v1/permissions/tests/req_repo_writer_test.go
Normal file
72
routers/api/v1/permissions/tests/req_repo_writer_test.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
unit_model "forgejo.org/models/unit"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTestBuilder([]string{"ReqRepoWriter "}, func(t *testing.T, signatureString string, signature []any) {
|
||||
t.Helper()
|
||||
unitTypes := signature[1].([]unit_model.Type)
|
||||
units := unitsTypeToString(unitTypes...)
|
||||
scopes := unitsToScopes(unitTypes, "write")
|
||||
signatureStringToFunctionTest[signatureString] = functionTest{
|
||||
sequenceFilter: []string{
|
||||
"APIAuthorization",
|
||||
"RepoAccess",
|
||||
signatureString,
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
fixtureDisableUnits(t, permissions, data)
|
||||
},
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
if data.Has("repository") {
|
||||
owner, _, found := strings.Cut(data.Get("repository"), "/")
|
||||
require.True(t, found)
|
||||
data.Set("doer", owner)
|
||||
} else {
|
||||
data.SetDefault("repository", "userowner/repositorypublic")
|
||||
data.SetDefault("doer", "userowner")
|
||||
}
|
||||
data.SetDefault("level", "write")
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"repository": "userowner/repositorypublic",
|
||||
"doer": "userowner",
|
||||
"scope": scopes,
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"disable-units": units,
|
||||
}),
|
||||
error: "Not Found",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "regularuser",
|
||||
"repository": "userowner/repositorypublic",
|
||||
"scope": "write:issue",
|
||||
}),
|
||||
error: "user should have a permission to write to a repo",
|
||||
},
|
||||
},
|
||||
staticArgs: 1,
|
||||
call: func(t *testing.T, ctx apiv1_permissions.Context, _ *fixtureData, args []any) {
|
||||
unitType := args[0].([]unit_model.Type)
|
||||
t.Logf("calling ReqRepoWriter(ctx, %s)", unitType)
|
||||
apiv1_permissions.ReqRepoWriter(ctx, unitType)
|
||||
},
|
||||
}
|
||||
})
|
||||
48
routers/api/v1/permissions/tests/req_self_or_admin_test.go
Normal file
48
routers/api/v1/permissions/tests/req_self_or_admin_test.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
user_model "forgejo.org/models/user"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTest(apiv1_permissions.ReqSelfOrAdmin, functionTest{
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
data.SetDefault("doer", "doeradmin")
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
if data.Has("user") && data.Get("user") != "anonymous" {
|
||||
name := data.Get("user")
|
||||
user := permissions.GetUser()
|
||||
if user == nil {
|
||||
fixtureCreateUser(t, &user_model.User{Name: name})
|
||||
permissions.SetUser(fixtureGetUser(t, name))
|
||||
}
|
||||
}
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doeradmin",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "regularuser",
|
||||
"user": "regularuser",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "regularuser",
|
||||
"user": "otheruser",
|
||||
}),
|
||||
error: "doer should be the site admin or be same as the contextUser",
|
||||
},
|
||||
},
|
||||
})
|
||||
30
routers/api/v1/permissions/tests/req_site_admin_test.go
Normal file
30
routers/api/v1/permissions/tests/req_site_admin_test.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTest(apiv1_permissions.ReqSiteAdmin, functionTest{
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
data.SetDefault("doer", "doeradmin")
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doeradmin",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "regularuser",
|
||||
}),
|
||||
error: "user should be the site admin",
|
||||
},
|
||||
},
|
||||
})
|
||||
98
routers/api/v1/permissions/tests/req_team_membership_test.go
Normal file
98
routers/api/v1/permissions/tests/req_team_membership_test.go
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
org_model "forgejo.org/models/organization"
|
||||
user_model "forgejo.org/models/user"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTest(apiv1_permissions.ReqTeamMembership, functionTest{
|
||||
sequenceFilter: []string{
|
||||
"APIAuthorization",
|
||||
"TokenRequiresScopes",
|
||||
"ReqTeamMembership",
|
||||
},
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
data.SetDefault("org", "ReqTeamMembership")
|
||||
data.SetDefault("team", org_model.OwnerTeamName)
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
orgOwner := data.Get("doer")
|
||||
if data.Has("orgOwner") {
|
||||
orgOwner = data.Get("orgOwner")
|
||||
}
|
||||
var org *org_model.Organization
|
||||
if data.Has("org") {
|
||||
fixtureCreateUser(t, &user_model.User{Name: orgOwner})
|
||||
org = fixtureCreateOrg(t, &org_model.Organization{Name: data.Get("org")}, &user_model.User{Name: orgOwner})
|
||||
}
|
||||
|
||||
if data.Has("teams") {
|
||||
fixtureCreateTeams(t, org, data.Get("teams"))
|
||||
}
|
||||
|
||||
if data.Has("team") {
|
||||
team, err := org_model.GetTeam(t.Context(), org.ID, data.Get("team"))
|
||||
require.NoError(t, err)
|
||||
permissions.SetTeam(team)
|
||||
}
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"org": "ReqTeamMembership",
|
||||
"team": org_model.OwnerTeamName,
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doeradmin",
|
||||
"org": "ReqTeamMembership",
|
||||
"team": org_model.OwnerTeamName,
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "regularuser",
|
||||
"orgOwner": "orgOwner",
|
||||
"org": "ReqTeamMembership",
|
||||
"teams": "team1:regularuser",
|
||||
"team": "team1",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "regularuser",
|
||||
"orgOwner": "orgOwner",
|
||||
"org": "ReqTeamMembership",
|
||||
"teams": "team1:regularuser,team2:otheruser",
|
||||
"team": "team2",
|
||||
}),
|
||||
error: "Must be a team member",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "regularuser",
|
||||
"orgOwner": "orgOwner",
|
||||
"org": "ReqTeamMembership",
|
||||
"teams": "team2:otheruser",
|
||||
"team": "team2",
|
||||
}),
|
||||
error: "Not Found",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"org": "ReqTeamMembership",
|
||||
}),
|
||||
error: "reqTeamMembership: unprepared context",
|
||||
},
|
||||
},
|
||||
})
|
||||
36
routers/api/v1/permissions/tests/req_token_test.go
Normal file
36
routers/api/v1/permissions/tests/req_token_test.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
user_model "forgejo.org/models/user"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTest(apiv1_permissions.ReqToken, functionTest{
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
data.SetDefault("doer", "doerregular")
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doerregular",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": user_model.ActionsUserName,
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "anonymous",
|
||||
}),
|
||||
error: "token is required",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forgejo.org/modules/setting"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTest(apiv1_permissions.ReqUsersExploreEnabled, functionTest{
|
||||
protectSettingsBool: []*bool{
|
||||
&setting.Service.Explore.DisableUsersPage,
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
setting.Service.Explore.DisableUsersPage = data.Get("Service.Explore.DisableUsersPage") == "true"
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"Service.Explore.DisableUsersPage": "true",
|
||||
}),
|
||||
error: "Not Found",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
user_model "forgejo.org/models/user"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTestWithCall(apiv1_permissions.ReqValidCommentID, functionTest{
|
||||
sequenceFilter: []string{
|
||||
"APIAuthorization",
|
||||
"RepoAccess",
|
||||
"ReqValidCommentID",
|
||||
},
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
data.SetDefault("issue", "issueOne")
|
||||
data.SetDefault("issueAuthor", "issueAuthor")
|
||||
data.SetDefault("comment", "comment for ReqValidCommentID")
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
fixtureCreateUser(t, &user_model.User{Name: data.Get("issueAuthor")})
|
||||
fixtureSetIssue(t, permissions, data)
|
||||
fixtureCreateComment(t, permissions, data)
|
||||
},
|
||||
call: func(t *testing.T, ctx apiv1_permissions.Context, data *fixtureData, _ []any) {
|
||||
t.Helper()
|
||||
comment := fixtureGetComment(t, data)
|
||||
if data.Has("NilIssue") {
|
||||
comment.Issue = nil
|
||||
}
|
||||
if data.Has("InconsistentID") {
|
||||
comment.Issue.RepoID = 123456
|
||||
}
|
||||
t.Logf("calling ReqValidCommentID(ctx, %+v)", comment)
|
||||
apiv1_permissions.ReqValidCommentID(ctx, comment)
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doerregular",
|
||||
"repository": "userowner/repositorypublic",
|
||||
"issue": "issueOne",
|
||||
"issueAuthor": "issueAuthor",
|
||||
"comment": "comment for ReqValidCommentID",
|
||||
}),
|
||||
},
|
||||
// This fixture is unreachable because this permissions function is always used after
|
||||
// a RepoAccess that enforces the same restriction for non admin users
|
||||
// {
|
||||
// data: newFixtureData(map[string]string{
|
||||
// "doer": "doerregular",
|
||||
// "repository": "userowner/repositoryprivate",
|
||||
// "issue": "issueOne",
|
||||
// "issueAuthor": "issueAuthor",
|
||||
// "comment": "comment for ReqValidCommentID",
|
||||
// }),
|
||||
// error: "Not Found",
|
||||
// },
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doerregular",
|
||||
"repository": "userowner/repositorypublic",
|
||||
"issue": "issueOne",
|
||||
"issueAuthor": "issueAuthor",
|
||||
"comment": "comment for ReqValidCommentID",
|
||||
|
||||
"NilIssue": "true",
|
||||
}),
|
||||
error: "Not Found",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doerregular",
|
||||
"repository": "userowner/repositorypublic",
|
||||
"issue": "issueOne",
|
||||
"issueAuthor": "issueAuthor",
|
||||
"comment": "comment for ReqValidCommentID",
|
||||
|
||||
"InconsistentID": "true",
|
||||
}),
|
||||
error: "Not Found",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forgejo.org/modules/setting"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTest(apiv1_permissions.ReqWebhooksEnabled, functionTest{
|
||||
protectSettingsBool: []*bool{
|
||||
&setting.DisableWebhooks,
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
setting.DisableWebhooks = data.Get("DisableWebhooks") == "true"
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"DisableWebhooks": "true",
|
||||
}),
|
||||
error: "webhooks disabled by administrator",
|
||||
},
|
||||
},
|
||||
})
|
||||
297
routers/api/v1/permissions/tests/testing.go
Normal file
297
routers/api/v1/permissions/tests/testing.go
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package tests
|
||||
|
||||
// See README.md for a documentation of the test logic
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"maps"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
"forgejo.org/models/perm"
|
||||
"forgejo.org/models/unit"
|
||||
"forgejo.org/modules/log"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/web/routing"
|
||||
)
|
||||
|
||||
// Helpers to keep track of the ordered sequence of permissions middleware used by
|
||||
// each REST API endpoint and use them for testing.
|
||||
//
|
||||
// When the routes are built, permission middleware are expected to call
|
||||
// the `RecordSignature` function so that it is added to the sequence of
|
||||
// permissions functions that will be called each time the API endpoint is
|
||||
// used.
|
||||
//
|
||||
// When all the permission functions have been accumulated in a
|
||||
// sequence for a given API endpoint, the
|
||||
// `CollectPermissionsMiddlewares` function is called to store it
|
||||
// in the `methodsAndPatternToSignatures` dictionary.
|
||||
//
|
||||
// The `Group` route function uses the `RestorePermissionsSequence`
|
||||
// function to save the permission sequence as it exists when entering the
|
||||
// `Group` route function and restore it when it returns.
|
||||
//
|
||||
// The `Combo` route function uses the `GetSignatures` function to
|
||||
// save the permission sequence as it exists when it creates the
|
||||
// object `Combo` object. It then uses the `SetSignatures` function to
|
||||
// restore it after delegating to the `Get`, `Delete`, etc. method.
|
||||
//
|
||||
// The `Get`, `Delete`, etc. route functions use the
|
||||
// `RestoreLastPermissionsSequence` to discard the permission sequence
|
||||
// specific to them (from the middleware list they were given in argument).
|
||||
//
|
||||
// A middleware can call `FollowedBy` to require that a permission check
|
||||
// registered with `RecordSignature` appears after it in the sequence. It
|
||||
// will fail if it appears before it or if it is not found.
|
||||
//
|
||||
// `Reset()` is called before building the routes to initialize all
|
||||
// global variables. They are all to be used when setting up the routes
|
||||
// and if a test directly calls `routers.NormalRoutes()`, all routes need
|
||||
// to be re-evaluated. This may be useful for instance when a setting
|
||||
// is modified and enables routes that were not enabled before.
|
||||
|
||||
type Sequence struct {
|
||||
signatures [][]any
|
||||
followedBy []string
|
||||
}
|
||||
|
||||
func (o Sequence) clone() Sequence {
|
||||
return Sequence{
|
||||
signatures: slices.Clone(o.signatures),
|
||||
followedBy: slices.Clone(o.followedBy),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
signatureStringToSignature map[string][]any
|
||||
methodsAndPatternToSignatures map[string][][]any
|
||||
|
||||
// used as temporary storage while building the routes
|
||||
sequence Sequence
|
||||
sequenceStack []Sequence
|
||||
|
||||
mutex = sync.Mutex{}
|
||||
)
|
||||
|
||||
// See above for the documentation
|
||||
func RecordSignature(signature ...any) {
|
||||
if !setting.IsInTesting {
|
||||
return
|
||||
}
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
sequence.signatures = append(sequence.signatures, signature)
|
||||
signatureStringToSignature[SignatureToString(signature)] = signature
|
||||
}
|
||||
|
||||
// See above for the documentation
|
||||
func FollowedBy(fun, followedBy any) {
|
||||
if !setting.IsInTesting {
|
||||
return
|
||||
}
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
followedByName := routing.GetFuncShortName(followedBy)
|
||||
funName := routing.GetFuncShortName(fun)
|
||||
for _, signature := range sequence.signatures {
|
||||
if routing.GetFuncShortName(signature[0]) == followedByName {
|
||||
panic(fmt.Errorf("%s must follow %s but precedes it", followedByName, funName))
|
||||
}
|
||||
}
|
||||
sequence.followedBy = append(sequence.followedBy, followedByName)
|
||||
}
|
||||
|
||||
// See above for the documentation
|
||||
func CollectPermissionsMiddlewares(endpoint any, methods, pattern string) {
|
||||
if !setting.IsInTesting {
|
||||
panic("must only be called if setting.IsInTesting is true")
|
||||
}
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
if len(sequence.signatures) == 0 {
|
||||
return
|
||||
}
|
||||
signaturesString := SignaturesToString(sequence.signatures)
|
||||
functionName := routing.GetFuncShortName(endpoint)
|
||||
methodsAndPattern := methods + " " + pattern
|
||||
followedByValidation(methodsAndPattern)
|
||||
if existingSignatures, has := methodsAndPatternToSignatures[methodsAndPattern]; has && SignaturesToString(existingSignatures) != signaturesString {
|
||||
panic(fmt.Errorf("function %s is invoked for %s with different permissions %s != %s", functionName, methodsAndPattern, SignaturesToString(existingSignatures), signaturesString))
|
||||
}
|
||||
methodsAndPatternToSignatures[methodsAndPattern] = slices.Clone(sequence.signatures)
|
||||
log.Debug("%s: permissions checks %v for function %v", methodsAndPattern, signaturesString, functionName)
|
||||
}
|
||||
|
||||
// See above for the documentation
|
||||
func RestorePermissionsSequence() func() {
|
||||
if !setting.IsInTesting {
|
||||
panic("must only be called if setting.IsInTesting is true")
|
||||
}
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
saved := sequence.clone()
|
||||
savedPermissionsSequenceStack := sequenceStack
|
||||
sequenceStack = append(sequenceStack, saved)
|
||||
return func() {
|
||||
sequenceStack = savedPermissionsSequenceStack
|
||||
if len(sequenceStack) > 0 {
|
||||
sequence = sequenceStack[len(sequenceStack)-1]
|
||||
} else {
|
||||
sequence = Sequence{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See above for the documentation
|
||||
func GetSignatures() Sequence {
|
||||
if !setting.IsInTesting {
|
||||
return Sequence{}
|
||||
}
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
return sequence.clone()
|
||||
}
|
||||
|
||||
// See above for the documentation
|
||||
func SetSignatures(s Sequence) {
|
||||
if !setting.IsInTesting {
|
||||
return
|
||||
}
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
sequence = s
|
||||
}
|
||||
|
||||
// See above for the documentation
|
||||
func RestoreLastPermissionsSequence() {
|
||||
if !setting.IsInTesting {
|
||||
panic("must only be called if setting.IsInTesting is true")
|
||||
}
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
if len(sequenceStack) > 0 {
|
||||
sequence = sequenceStack[len(sequenceStack)-1]
|
||||
}
|
||||
}
|
||||
|
||||
// See above for the documentation
|
||||
func Reset() {
|
||||
if !setting.IsInTesting {
|
||||
panic("must only be called if setting.IsInTesting is true")
|
||||
}
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
signatureStringToSignature = make(map[string][]any, 200)
|
||||
methodsAndPatternToSignatures = make(map[string][][]any, 200)
|
||||
sequence = Sequence{}
|
||||
sequenceStack = nil
|
||||
}
|
||||
|
||||
func followedByValidation(methodsAndPattern string) {
|
||||
signaturesString := SignaturesToString(sequence.signatures)
|
||||
followedBy := slices.Clone(sequence.followedBy)
|
||||
for _, signature := range sequence.signatures {
|
||||
if len(followedBy) == 0 {
|
||||
break
|
||||
}
|
||||
functionName := routing.GetFuncShortName(signature[0])
|
||||
if slices.Contains(followedBy, functionName) {
|
||||
followedBy = slices.DeleteFunc(followedBy, func(e string) bool {
|
||||
return e == functionName
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(followedBy) > 0 {
|
||||
panic(fmt.Errorf("%s: %s does not contain the required permissions check %v", methodsAndPattern, signaturesString, followedBy))
|
||||
}
|
||||
}
|
||||
|
||||
func SignatureToString(signature []any) string {
|
||||
fn := reflect.ValueOf(signature[0])
|
||||
if fn.Type().Kind() != reflect.Func {
|
||||
panic(fmt.Sprintf("handler must be a function, but got %s", fn.Type()))
|
||||
}
|
||||
function := strings.TrimPrefix(routing.GetFuncShortName(signature[0]), "permissions.")
|
||||
argStrings := []string{function}
|
||||
for _, arg := range signature[1:] {
|
||||
switch typedArg := arg.(type) {
|
||||
case []auth_model.AccessTokenScopeCategory:
|
||||
argStrings = append(argStrings, " "+requiredScopesToString(typedArg...))
|
||||
case []unit.Type:
|
||||
slices.Sort(typedArg)
|
||||
for _, unitType := range typedArg {
|
||||
argStrings = append(argStrings, " "+unitType.String())
|
||||
}
|
||||
case unit.Type:
|
||||
argStrings = append(argStrings, " "+typedArg.String())
|
||||
case perm.AccessMode:
|
||||
argStrings = append(argStrings, " "+typedArg.String())
|
||||
default:
|
||||
panic(fmt.Errorf("unsupported type %T", arg))
|
||||
}
|
||||
}
|
||||
return strings.Join(argStrings, "")
|
||||
}
|
||||
|
||||
func SignaturesToString(signatures [][]any) string {
|
||||
var signatureStrings []string
|
||||
for _, s := range signatures {
|
||||
signatureStrings = append(signatureStrings, SignatureToString(s))
|
||||
}
|
||||
return strings.Join(signatureStrings, ",")
|
||||
}
|
||||
|
||||
func GetSignatureStringToSignature() map[string][]any {
|
||||
return signatureStringToSignature
|
||||
}
|
||||
|
||||
func GetUniquePermissionsSequences() [][][]any {
|
||||
signaturesStringToSignatures := make(map[string][][]any, 200)
|
||||
for _, signatures := range methodsAndPatternToSignatures {
|
||||
signaturesStringToSignatures[SignaturesToString(signatures)] = signatures
|
||||
}
|
||||
sequences := slices.Collect(maps.Values(signaturesStringToSignatures))
|
||||
slices.SortFunc(sequences, func(a, b [][]any) int {
|
||||
return cmp.Compare(SignaturesToString(a), SignaturesToString(b))
|
||||
})
|
||||
return sequences
|
||||
}
|
||||
|
||||
func GetShortestPermissionSequenceForEachSignature() [][][]any {
|
||||
signaturesStringToShortest := make(map[string][][]any, 200)
|
||||
for _, signatures := range methodsAndPatternToSignatures {
|
||||
for len(signatures) > 0 {
|
||||
last := signatures[len(signatures)-1]
|
||||
lastString := SignatureToString(last)
|
||||
if existingSignatures, has := signaturesStringToShortest[lastString]; has {
|
||||
if len(signatures) < len(existingSignatures) {
|
||||
signaturesStringToShortest[lastString] = signatures
|
||||
}
|
||||
} else {
|
||||
signaturesStringToShortest[lastString] = signatures
|
||||
}
|
||||
signatures = signatures[:len(signatures)-1]
|
||||
}
|
||||
}
|
||||
sequences := slices.Collect(maps.Values(signaturesStringToShortest))
|
||||
slices.SortFunc(sequences, func(a, b [][]any) int {
|
||||
return cmp.Compare(SignaturesToString(a), SignaturesToString(b))
|
||||
})
|
||||
return sequences
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
org_model "forgejo.org/models/organization"
|
||||
user_model "forgejo.org/models/user"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTestWithCall(apiv1_permissions.TokenRequiresRepoOwnerScope, functionTest{
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
if !data.Has("owner") {
|
||||
if data.Has("repository") {
|
||||
owner, _, found := strings.Cut(data.Get("repository"), "/")
|
||||
require.True(t, found)
|
||||
data.Set("owner", owner)
|
||||
} else {
|
||||
data.Set("owner", "doerregular")
|
||||
}
|
||||
}
|
||||
data.SetDefault("level", "read")
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
ownerName := data.Get("owner")
|
||||
if strings.Contains(ownerName, "org") {
|
||||
fixtureCreateOrg(t, &org_model.Organization{Name: ownerName}, &user_model.User{Name: "orgOwner" + ownerName})
|
||||
require.NotNil(t, fixtureGetUser(t, ownerName))
|
||||
} else {
|
||||
fixtureCreateUser(t, &user_model.User{Name: ownerName})
|
||||
}
|
||||
},
|
||||
call: func(t *testing.T, ctx apiv1_permissions.Context, data *fixtureData, _ []any) {
|
||||
t.Helper()
|
||||
owner := fixtureGetUser(t, data.Get("owner"))
|
||||
level := levelStringToLevel(data.Get("level"))
|
||||
t.Logf("calling TokenRequiresRepoOwnerScope(ctx, %+v, %v)", owner, level)
|
||||
apiv1_permissions.TokenRequiresRepoOwnerScope(ctx, owner, level)
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"owner": "doerregular",
|
||||
"scope": "read:user",
|
||||
"level": "read",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"owner": "doerregular",
|
||||
"scope": "read:user",
|
||||
"level": "write",
|
||||
}),
|
||||
error: "token does not have at least one of required scope(s): [write:user]",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"owner": "regularorg",
|
||||
"scope": "read:organization",
|
||||
"level": "read",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"owner": "regularorg",
|
||||
"scope": "read:organization",
|
||||
"level": "write",
|
||||
}),
|
||||
error: "token does not have at least one of required scope(s): [write:organization]",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
)
|
||||
|
||||
var _ = registerFunctionTestBuilder([]string{"TokenRequiresScopes "}, func(t *testing.T, signatureString string, signature []any) {
|
||||
t.Helper()
|
||||
categories := signature[1].([]auth_model.AccessTokenScopeCategory)
|
||||
var scopes []string
|
||||
for _, category := range categories {
|
||||
var scope auth_model.AccessTokenScope
|
||||
switch category {
|
||||
case auth_model.AccessTokenScopeCategoryActivityPub:
|
||||
scope = auth_model.AccessTokenScopeReadActivityPub
|
||||
case auth_model.AccessTokenScopeCategoryAdmin:
|
||||
scope = auth_model.AccessTokenScopeReadAdmin
|
||||
case auth_model.AccessTokenScopeCategoryNotification:
|
||||
scope = auth_model.AccessTokenScopeReadNotification
|
||||
case auth_model.AccessTokenScopeCategoryOrganization:
|
||||
scope = auth_model.AccessTokenScopeReadOrganization
|
||||
case auth_model.AccessTokenScopeCategoryPackage:
|
||||
scope = auth_model.AccessTokenScopeReadPackage
|
||||
case auth_model.AccessTokenScopeCategoryIssue:
|
||||
scope = auth_model.AccessTokenScopeReadIssue
|
||||
case auth_model.AccessTokenScopeCategoryRepository:
|
||||
scope = auth_model.AccessTokenScopeReadRepository
|
||||
case auth_model.AccessTokenScopeCategoryUser:
|
||||
scope = auth_model.AccessTokenScopeReadUser
|
||||
default:
|
||||
panic(fmt.Errorf("unexpected category %v", category))
|
||||
}
|
||||
scopes = append(scopes, string(scope))
|
||||
}
|
||||
readscope := strings.Join(scopes, ",")
|
||||
t.Logf("%s scopes %s", signatureString, readscope)
|
||||
signatureStringToFunctionTest[signatureString] = functionTest{
|
||||
fulfillNeeds: func(t *testing.T, data *fixtureData) {
|
||||
t.Helper()
|
||||
data.SetDefault("repository", "userowner/repositorypublic")
|
||||
data.SetDefault("doer", "doerregular")
|
||||
if data.Has("scope") {
|
||||
scope := data.Get("scope")
|
||||
if !strings.Contains(scope, readscope) {
|
||||
writescope := strings.ReplaceAll(readscope, "read", "write")
|
||||
data.Set("scope", strings.Join([]string{scope, writescope}, ","))
|
||||
}
|
||||
} else {
|
||||
data.Set("scope", readscope)
|
||||
}
|
||||
|
||||
data.SetDefault("level", "read")
|
||||
},
|
||||
interpret: func(t *testing.T, permissions *apiv1_permissions.Permissions, data *fixtureData) {
|
||||
fixtureSetRepository(t, permissions, data)
|
||||
},
|
||||
staticArgs: 1,
|
||||
call: func(t *testing.T, ctx apiv1_permissions.Context, data *fixtureData, args []any) {
|
||||
level := levelStringToLevel(data.Get("level"))
|
||||
categories := args[0].([]auth_model.AccessTokenScopeCategory)
|
||||
t.Logf("calling TokenRequiresScopes(ctx, %v, %v)", categories, level)
|
||||
apiv1_permissions.TokenRequiresScopes(ctx, categories, level)
|
||||
},
|
||||
fixtures: []*fixtureType{
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doerregular",
|
||||
"scope": readscope,
|
||||
"level": "read",
|
||||
}),
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doerregular",
|
||||
"scope": readscope,
|
||||
"level": "write",
|
||||
}),
|
||||
error: "token does not have at least one of required scope(s)",
|
||||
},
|
||||
{
|
||||
data: newFixtureData(map[string]string{
|
||||
"doer": "doerregular",
|
||||
"scope": "read:misc",
|
||||
"level": "read",
|
||||
}),
|
||||
error: "token does not have at least one of required scope(s)",
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
42
routers/api/v1/permissions/tests/utils.go
Normal file
42
routers/api/v1/permissions/tests/utils.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
)
|
||||
|
||||
func requiredScopesToString(scopeCategories ...auth_model.AccessTokenScopeCategory) string {
|
||||
var categories []string
|
||||
for _, category := range scopeCategories {
|
||||
switch category {
|
||||
case auth_model.AccessTokenScopeCategoryActivityPub:
|
||||
categories = append(categories, "ActivityPub")
|
||||
case auth_model.AccessTokenScopeCategoryAdmin:
|
||||
categories = append(categories, "Admin")
|
||||
case auth_model.AccessTokenScopeCategoryMisc:
|
||||
categories = append(categories, "Misc")
|
||||
case auth_model.AccessTokenScopeCategoryNotification:
|
||||
categories = append(categories, "Notification")
|
||||
case auth_model.AccessTokenScopeCategoryOrganization:
|
||||
categories = append(categories, "Organization")
|
||||
case auth_model.AccessTokenScopeCategoryPackage:
|
||||
categories = append(categories, "Package")
|
||||
case auth_model.AccessTokenScopeCategoryIssue:
|
||||
categories = append(categories, "Issue")
|
||||
case auth_model.AccessTokenScopeCategoryRepository:
|
||||
categories = append(categories, "Repository")
|
||||
case auth_model.AccessTokenScopeCategoryUser:
|
||||
categories = append(categories, "User")
|
||||
default:
|
||||
panic(fmt.Errorf("unkwnon scope category %v", category))
|
||||
}
|
||||
}
|
||||
slices.Sort(categories)
|
||||
return strings.Join(categories, "")
|
||||
}
|
||||
67
routers/api/v1/permissions/tests/utils_test.go
Normal file
67
routers/api/v1/permissions/tests/utils_test.go
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
unit_model "forgejo.org/models/unit"
|
||||
)
|
||||
|
||||
func levelStringToLevel(levelString string) auth_model.AccessTokenScopeLevel {
|
||||
level := auth_model.Read
|
||||
if levelString != "" {
|
||||
switch levelString {
|
||||
case "read":
|
||||
level = auth_model.Read
|
||||
case "write":
|
||||
level = auth_model.Write
|
||||
case "noaccess":
|
||||
level = auth_model.NoAccess
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected level '%s'", levelString))
|
||||
}
|
||||
}
|
||||
return level
|
||||
}
|
||||
|
||||
func unitsTypeToString(unitTypes ...unit_model.Type) string {
|
||||
var unitStrings []string
|
||||
for _, unitType := range unitTypes {
|
||||
var unit *unit_model.Unit
|
||||
for _, u := range unit_model.Units {
|
||||
if u.Type == unitType {
|
||||
unit = &u
|
||||
break
|
||||
}
|
||||
}
|
||||
if unit == nil {
|
||||
panic(fmt.Errorf("unable to find a unit with type %v", unitType))
|
||||
}
|
||||
unitStrings = append(unitStrings, unit.NameKey)
|
||||
}
|
||||
return strings.Join(unitStrings, ",")
|
||||
}
|
||||
|
||||
func unitsToScopes(unitTypes []unit_model.Type, levelString string) string {
|
||||
var scopeStrings []string
|
||||
for _, unitType := range unitTypes {
|
||||
unit := strings.TrimPrefix(unitsTypeToString(unitType), "repo.")
|
||||
var scope string
|
||||
switch unit {
|
||||
case "issues":
|
||||
scope = "issue"
|
||||
case "code", "pulls", "wiki", "project", "actions", "releases":
|
||||
scope = "repository"
|
||||
case "packages":
|
||||
scope = "package"
|
||||
default:
|
||||
panic(fmt.Errorf("unexpected unit type %v", unitType))
|
||||
}
|
||||
scopeStrings = append(scopeStrings, fmt.Sprintf("%s:%s", levelString, scope))
|
||||
}
|
||||
return strings.Join(scopeStrings, ",")
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
auth_model "forgejo.org/models/auth"
|
||||
user_model "forgejo.org/models/user"
|
||||
)
|
||||
|
||||
func TokenRequiresRepoOwnerScope(ctx Context, owner *user_model.User, requiredScopeLevel auth_model.AccessTokenScopeLevel) {
|
||||
var category auth_model.AccessTokenScopeCategory
|
||||
if owner.IsOrganization() {
|
||||
category = auth_model.AccessTokenScopeCategoryOrganization
|
||||
} else {
|
||||
category = auth_model.AccessTokenScopeCategoryUser
|
||||
}
|
||||
TokenRequiresScopes(ctx, []auth_model.AccessTokenScopeCategory{category}, requiredScopeLevel)
|
||||
}
|
||||
39
routers/api/v1/permissions/token_requires_scopes.go
Normal file
39
routers/api/v1/permissions/token_requires_scopes.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
)
|
||||
|
||||
func TokenRequiresScopes(ctx Context, requiredScopeCategories []auth_model.AccessTokenScopeCategory, requiredScopeLevel auth_model.AccessTokenScopeLevel) {
|
||||
// no scope required
|
||||
if len(requiredScopeCategories) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Need OAuth2 token to be present.
|
||||
hasScope, scope := ctx.GetAuthentication().Scope().Get()
|
||||
if !hasScope {
|
||||
return
|
||||
}
|
||||
|
||||
// get the required scope for the given access level and category
|
||||
requiredScopes := auth_model.GetRequiredScopes(requiredScopeLevel, requiredScopeCategories...)
|
||||
allow, err := scope.HasScope(requiredScopes...)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusForbidden, "tokenRequiresScope", "checking scope failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !allow {
|
||||
ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s): %v", requiredScopes))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetRequiredScopeCategories(requiredScopeCategories)
|
||||
}
|
||||
|
|
@ -10,11 +10,15 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
issues_model "forgejo.org/models/issues"
|
||||
org_model "forgejo.org/models/organization"
|
||||
"forgejo.org/models/perm"
|
||||
access_model "forgejo.org/models/perm/access"
|
||||
quota_model "forgejo.org/models/quota"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
"forgejo.org/models/unit"
|
||||
user_model "forgejo.org/models/user"
|
||||
mc "forgejo.org/modules/cache"
|
||||
|
|
@ -25,6 +29,7 @@ import (
|
|||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/web"
|
||||
web_types "forgejo.org/modules/web/types"
|
||||
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
||||
"forgejo.org/services/auth"
|
||||
"forgejo.org/services/authz"
|
||||
|
||||
|
|
@ -178,6 +183,88 @@ func (ctx *APIContext) ServerError(title string, err error) {
|
|||
ctx.Error(http.StatusInternalServerError, title, err)
|
||||
}
|
||||
|
||||
func (ctx *APIContext) GetContext() context.Context {
|
||||
return ctx.originCtx
|
||||
}
|
||||
|
||||
func (ctx *APIContext) GetRepository() *repo_model.Repository {
|
||||
return ctx.Repo.Repository
|
||||
}
|
||||
|
||||
func (ctx *APIContext) GetDoer() *user_model.User {
|
||||
return ctx.Doer
|
||||
}
|
||||
|
||||
func (ctx *APIContext) GetUser() *user_model.User {
|
||||
return ctx.ContextUser
|
||||
}
|
||||
|
||||
func (ctx *APIContext) GetOrg() *org_model.Organization {
|
||||
return ctx.Org.Organization
|
||||
}
|
||||
|
||||
func (ctx *APIContext) GetTeam() *org_model.Team {
|
||||
return ctx.Org.Team
|
||||
}
|
||||
|
||||
func (ctx *APIContext) GetPackageOwner() *user_model.User {
|
||||
if ctx.Package == nil {
|
||||
return nil
|
||||
}
|
||||
return ctx.Package.Owner
|
||||
}
|
||||
|
||||
func (ctx *APIContext) GetPackageAccessMode() perm.AccessMode {
|
||||
if ctx.Package == nil {
|
||||
return perm.AccessModeNone
|
||||
}
|
||||
return ctx.Package.AccessMode
|
||||
}
|
||||
|
||||
func (ctx *APIContext) GetPermission() *access_model.Permission {
|
||||
return &ctx.Repo.Permission
|
||||
}
|
||||
|
||||
func (ctx *APIContext) SetPermission(permission *access_model.Permission) {
|
||||
ctx.Repo.Permission = *permission
|
||||
}
|
||||
|
||||
func (ctx *APIContext) GetIsSigned() bool {
|
||||
return ctx.IsSigned
|
||||
}
|
||||
|
||||
func (ctx *APIContext) GetPublicOnly() bool {
|
||||
return ctx.PublicOnly
|
||||
}
|
||||
|
||||
func (ctx *APIContext) SetPublicOnly(publicOnly bool) {
|
||||
ctx.PublicOnly = publicOnly
|
||||
}
|
||||
|
||||
func (ctx *APIContext) GetReducer() authz.AuthorizationReducer {
|
||||
return ctx.Reducer
|
||||
}
|
||||
|
||||
func (ctx *APIContext) SetReducer(reducer authz.AuthorizationReducer) {
|
||||
ctx.Reducer = reducer
|
||||
}
|
||||
|
||||
func (ctx *APIContext) GetAuthentication() auth.AuthenticationResult {
|
||||
return ctx.Authentication
|
||||
}
|
||||
|
||||
func (ctx *APIContext) GetRequiredScopeCategories() []auth_model.AccessTokenScopeCategory {
|
||||
requiredScopeCategories, ok := ctx.Data["requiredScopeCategories"].([]auth_model.AccessTokenScopeCategory)
|
||||
if !ok || len(requiredScopeCategories) == 0 {
|
||||
return nil
|
||||
}
|
||||
return requiredScopeCategories
|
||||
}
|
||||
|
||||
func (ctx *APIContext) SetRequiredScopeCategories(requiredScopeCategories []auth_model.AccessTokenScopeCategory) {
|
||||
ctx.Data["requiredScopeCategories"] = requiredScopeCategories
|
||||
}
|
||||
|
||||
// Error responds with an error message to client with given obj as the message.
|
||||
// If status is 500, also it prints error to log.
|
||||
func (ctx *APIContext) Error(status int, title string, obj any) {
|
||||
|
|
@ -218,6 +305,10 @@ func (ctx *APIContext) InternalServerError(err error) {
|
|||
})
|
||||
}
|
||||
|
||||
func (ctx *APIContext) GetError() error {
|
||||
return errors.New("unexpected call to APIContext.GetError")
|
||||
}
|
||||
|
||||
type apiContextKeyType struct{}
|
||||
|
||||
var apiContextKey = apiContextKeyType{}
|
||||
|
|
@ -452,23 +543,17 @@ func (ctx *APIContext) NotFoundOrServerError(logMsg string, errCheck func(error)
|
|||
|
||||
// IsUserSiteAdmin returns true if current user is a site admin
|
||||
func (ctx *APIContext) IsUserSiteAdmin() bool {
|
||||
if !ctx.Reducer.AllowAdminOverride() {
|
||||
return false
|
||||
}
|
||||
return ctx.IsSigned && ctx.Doer.IsAdmin
|
||||
return apiv1_permissions.IsUserSiteAdmin(ctx)
|
||||
}
|
||||
|
||||
// IsUserRepoAdmin returns true if current user is admin in current repo
|
||||
func (ctx *APIContext) IsUserRepoAdmin() bool {
|
||||
if !ctx.Reducer.AllowAdminOverride() {
|
||||
return false
|
||||
}
|
||||
return ctx.Repo.IsAdmin()
|
||||
return apiv1_permissions.IsUserRepoAdmin(ctx)
|
||||
}
|
||||
|
||||
// IsUserRepoWriter returns true if current user has write privilege in current repo
|
||||
func (ctx *APIContext) IsUserRepoWriter(unitTypes []unit.Type) bool {
|
||||
return slices.ContainsFunc(unitTypes, ctx.Repo.CanWrite)
|
||||
return apiv1_permissions.IsUserRepoWriter(ctx, unitTypes)
|
||||
}
|
||||
|
||||
// Returns true when the requests indicates that it accepts a Github response.
|
||||
|
|
|
|||
|
|
@ -7,14 +7,11 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"forgejo.org/models/organization"
|
||||
packages_model "forgejo.org/models/packages"
|
||||
"forgejo.org/models/perm"
|
||||
"forgejo.org/models/unit"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/structs"
|
||||
"forgejo.org/modules/templates"
|
||||
packages_service "forgejo.org/services/packages"
|
||||
)
|
||||
|
||||
// Package contains owner, access mode and optional the package descriptor
|
||||
|
|
@ -62,7 +59,7 @@ func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, string, any))
|
|||
Owner: ctx.ContextUser,
|
||||
}
|
||||
var err error
|
||||
pkg.AccessMode, err = determineAccessMode(ctx.Base, pkg, ctx.Doer)
|
||||
pkg.AccessMode, err = packages_service.DeterminePackageAccessMode(ctx.Base, pkg.Owner, ctx.Doer)
|
||||
if err != nil {
|
||||
errCb(http.StatusInternalServerError, "determineAccessMode", err)
|
||||
return pkg
|
||||
|
|
@ -92,62 +89,6 @@ func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, string, any))
|
|||
return pkg
|
||||
}
|
||||
|
||||
func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.AccessMode, error) {
|
||||
if setting.Service.RequireSignInView && (doer == nil || doer.IsGhost()) {
|
||||
return perm.AccessModeNone, nil
|
||||
}
|
||||
|
||||
if doer != nil && !doer.IsGhost() && !doer.IsAccessAllowed(ctx) {
|
||||
return perm.AccessModeNone, nil
|
||||
}
|
||||
|
||||
// TODO: ActionUser permission check
|
||||
accessMode := perm.AccessModeNone
|
||||
if pkg.Owner.IsOrganization() {
|
||||
org := organization.OrgFromUser(pkg.Owner)
|
||||
|
||||
if doer != nil && !doer.IsGhost() {
|
||||
// 1. If user is logged in, check all team packages permissions
|
||||
var err error
|
||||
accessMode, err = org.GetOrgUserMaxAuthorizeLevel(ctx, doer.ID)
|
||||
if err != nil {
|
||||
return accessMode, err
|
||||
}
|
||||
// If access mode is less than write check every team for more permissions
|
||||
// The minimum possible access mode is read for org members
|
||||
if accessMode < perm.AccessModeWrite {
|
||||
teams, err := organization.GetUserOrgTeams(ctx, org.ID, doer.ID)
|
||||
if err != nil {
|
||||
return accessMode, err
|
||||
}
|
||||
for _, t := range teams {
|
||||
perm := t.UnitAccessMode(ctx, unit.TypePackages)
|
||||
if accessMode < perm {
|
||||
accessMode = perm
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if accessMode == perm.AccessModeNone && organization.HasOrgOrUserVisible(ctx, pkg.Owner, doer) {
|
||||
// 2. If user is unauthorized or no org member, check if org is visible
|
||||
accessMode = perm.AccessModeRead
|
||||
}
|
||||
} else {
|
||||
if doer != nil && !doer.IsGhost() {
|
||||
// 1. Check if user is package owner
|
||||
if doer.ID == pkg.Owner.ID {
|
||||
accessMode = perm.AccessModeOwner
|
||||
} else if pkg.Owner.Visibility == structs.VisibleTypePublic || pkg.Owner.Visibility == structs.VisibleTypeLimited { // 2. Check if package owner is public or limited
|
||||
accessMode = perm.AccessModeRead
|
||||
}
|
||||
} else if pkg.Owner.Visibility == structs.VisibleTypePublic { // 3. Check if package owner is public
|
||||
accessMode = perm.AccessModeRead
|
||||
}
|
||||
}
|
||||
|
||||
return accessMode, nil
|
||||
}
|
||||
|
||||
// PackageContexter initializes a package context for a request.
|
||||
func PackageContexter() func(next http.Handler) http.Handler {
|
||||
renderer := templates.HTMLRenderer()
|
||||
|
|
|
|||
72
services/packages/perm.go
Normal file
72
services/packages/perm.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// Copyright 2026 The Forgejo Authors.
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package packages
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forgejo.org/models/organization"
|
||||
"forgejo.org/models/perm"
|
||||
"forgejo.org/models/unit"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/modules/setting"
|
||||
api "forgejo.org/modules/structs"
|
||||
)
|
||||
|
||||
func DeterminePackageAccessMode(ctx context.Context, owner, doer *user_model.User) (perm.AccessMode, error) {
|
||||
if setting.Service.RequireSignInView && (doer == nil || doer.IsGhost()) {
|
||||
return perm.AccessModeNone, nil
|
||||
}
|
||||
|
||||
if doer != nil && !doer.IsGhost() && !doer.IsAccessAllowed(ctx) {
|
||||
return perm.AccessModeNone, nil
|
||||
}
|
||||
|
||||
// TODO: ActionUser permission check
|
||||
accessMode := perm.AccessModeNone
|
||||
if owner.IsOrganization() {
|
||||
org := organization.OrgFromUser(owner)
|
||||
|
||||
if doer != nil && !doer.IsGhost() {
|
||||
// 1. If user is logged in, check all team packages permissions
|
||||
var err error
|
||||
accessMode, err = org.GetOrgUserMaxAuthorizeLevel(ctx, doer.ID)
|
||||
if err != nil {
|
||||
return accessMode, err
|
||||
}
|
||||
// If access mode is less than write check every team for more permissions
|
||||
// The minimum possible access mode is read for org members
|
||||
if accessMode < perm.AccessModeWrite {
|
||||
teams, err := organization.GetUserOrgTeams(ctx, org.ID, doer.ID)
|
||||
if err != nil {
|
||||
return accessMode, err
|
||||
}
|
||||
for _, t := range teams {
|
||||
perm := t.UnitAccessMode(ctx, unit.TypePackages)
|
||||
if accessMode < perm {
|
||||
accessMode = perm
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if accessMode == perm.AccessModeNone && organization.HasOrgOrUserVisible(ctx, owner, doer) {
|
||||
// 2. If user is unauthorized or no org member, check if org is visible
|
||||
accessMode = perm.AccessModeRead
|
||||
}
|
||||
} else {
|
||||
if doer != nil && !doer.IsGhost() {
|
||||
// 1. Check if user is package owner
|
||||
if doer.ID == owner.ID {
|
||||
accessMode = perm.AccessModeOwner
|
||||
} else if owner.Visibility == api.VisibleTypePublic || owner.Visibility == api.VisibleTypeLimited { // 2. Check if package owner is public or limited
|
||||
accessMode = perm.AccessModeRead
|
||||
}
|
||||
} else if owner.Visibility == api.VisibleTypePublic { // 3. Check if package owner is public
|
||||
accessMode = perm.AccessModeRead
|
||||
}
|
||||
}
|
||||
|
||||
return accessMode, nil
|
||||
}
|
||||
73
tests/forgery/team.go
Normal file
73
tests/forgery/team.go
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
package forgery
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forgejo.org/models"
|
||||
"forgejo.org/models/db"
|
||||
org_model "forgejo.org/models/organization"
|
||||
"forgejo.org/models/perm"
|
||||
unit_model "forgejo.org/models/unit"
|
||||
user_model "forgejo.org/models/user"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type CreateTeamOptions struct {
|
||||
Name string
|
||||
CanCreateOrgRepo bool
|
||||
|
||||
Mode perm.AccessMode
|
||||
|
||||
Members []*user_model.User
|
||||
}
|
||||
|
||||
func CreateTeam(t *testing.T, org *org_model.Organization, opts *CreateTeamOptions) *org_model.Team {
|
||||
t.Helper()
|
||||
|
||||
if opts == nil {
|
||||
opts = &CreateTeamOptions{
|
||||
Mode: perm.AccessModeRead,
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Name == "" {
|
||||
opts.Name = "team-" + uniqueSafeName(t.Name())
|
||||
}
|
||||
|
||||
team := &org_model.Team{
|
||||
OrgID: org.ID,
|
||||
Name: opts.Name,
|
||||
LowerName: opts.Name,
|
||||
IncludesAllRepositories: true,
|
||||
AccessMode: opts.Mode,
|
||||
CanCreateOrgRepo: opts.CanCreateOrgRepo,
|
||||
}
|
||||
require.NoError(t, db.Insert(t.Context(), team))
|
||||
|
||||
units := make([]org_model.TeamUnit, 0, len(unit_model.AllRepoUnitTypes))
|
||||
for _, tp := range unit_model.AllRepoUnitTypes {
|
||||
up := opts.Mode
|
||||
if tp == unit_model.TypeExternalTracker || tp == unit_model.TypeExternalWiki {
|
||||
up = perm.AccessModeRead
|
||||
}
|
||||
units = append(units, org_model.TeamUnit{
|
||||
OrgID: org.ID,
|
||||
TeamID: team.ID,
|
||||
Type: tp,
|
||||
AccessMode: up,
|
||||
})
|
||||
}
|
||||
|
||||
require.NoError(t, db.Insert(t.Context(), &units))
|
||||
|
||||
for _, user := range opts.Members {
|
||||
_, err := models.InsertTeamMember(t.Context(), team, user.ID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return team
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue