mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-06-22 10:02:15 +00:00
chore: refactor REST API permission check
This commit is contained in:
parent
172e1d75cf
commit
b296496356
36 changed files with 1068 additions and 427 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -191,15 +191,13 @@ func checkPermission(check func(ctx apiv1_permissions.Context)) func(*context.AP
|
|||
}
|
||||
|
||||
// must be used within a group with a call to commentAssignment() to set ctx.Comment
|
||||
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
|
||||
func reqValidCommentID() func(*context.APIContext) {
|
||||
apiv1_permissions_tests.RecordSignature(apiv1_permissions.ReqValidCommentID)
|
||||
return func(ctx *context.APIContext) {
|
||||
if ctx.Comment == nil {
|
||||
panic("reqValidCommentID requires commentAssignment to be called first")
|
||||
}
|
||||
apiv1_permissions.ReqValidCommentID(ctx, ctx.Comment)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -228,64 +226,25 @@ func commentAssignment(idParam string) func(ctx *context.APIContext) {
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) {
|
||||
apiv1_permissions_tests.RecordSignature(apiv1_permissions.ReqPackageAccess, accessMode)
|
||||
return func(ctx *context.APIContext) {
|
||||
apiv1_permissions.ReqPackageAccess(ctx, accessMode)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
func checkTokenPublicOnly() func(*context.APIContext) {
|
||||
apiv1_permissions_tests.RecordSignature(apiv1_permissions.CheckTokenPublicOnly)
|
||||
return func(ctx *context.APIContext) {
|
||||
var packageOwner *user_model.User
|
||||
if ctx.Package != nil {
|
||||
packageOwner = ctx.Package.Owner
|
||||
}
|
||||
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
|
||||
var org *user_model.User
|
||||
if ctx.Org != nil && ctx.Org.Organization != nil {
|
||||
org = ctx.Org.Organization.AsUser()
|
||||
}
|
||||
apiv1_permissions.CheckTokenPublicOnly(ctx, ctx.ContextUser, org, packageOwner)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -300,285 +259,123 @@ func requiredScopeLevel(ctx *context.APIContext) auth_model.AccessTokenScopeLeve
|
|||
return requiredScopeLevel
|
||||
}
|
||||
|
||||
func TokenRequiresScopes(ctx Context, requiredScopeCategories []auth_model.AccessTokenScopeCategory, requiredScopeLevel auth_model.AccessTokenScopeLevel) {
|
||||
// no scope required
|
||||
if len(requiredScopeCategories) == 0 {
|
||||
return
|
||||
func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeCategory) func(*context.APIContext) {
|
||||
apiv1_permissions_tests.RecordSignature(apiv1_permissions.TokenRequiresScopes, requiredScopeCategories)
|
||||
return func(ctx *context.APIContext) {
|
||||
apiv1_permissions.TokenRequiresScopes(ctx, requiredScopeCategories, requiredScopeLevel(ctx))
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Middleware that dynamically checks either the organization or user scope, depending on the owner type of the
|
||||
// repository (requires `repoAssignment()` middleware to be used before this).
|
||||
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
|
||||
func tokenRequiresRepoOwnerScope() func(*context.APIContext) {
|
||||
apiv1_permissions_tests.RecordSignature(apiv1_permissions.TokenRequiresRepoOwnerScope)
|
||||
return func(ctx *context.APIContext) {
|
||||
apiv1_permissions.TokenRequiresRepoOwnerScope(ctx, ctx.Repo.Owner, requiredScopeLevel(ctx))
|
||||
}
|
||||
TokenRequiresScopes(ctx, []auth_model.AccessTokenScopeCategory{category}, requiredScopeLevel)
|
||||
}
|
||||
|
||||
// Contexter middleware already checks token for user sign in process.
|
||||
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")
|
||||
func reqToken() func(ctx *context.APIContext) {
|
||||
return checkPermission(apiv1_permissions.ReqToken)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
func reqExploreSignIn() func(ctx *context.APIContext) {
|
||||
return checkPermission(apiv1_permissions.ReqExploreSignIn)
|
||||
}
|
||||
|
||||
func ReqUsersExploreEnabled(ctx Context) {
|
||||
if setting.Service.Explore.DisableUsersPage {
|
||||
ctx.NotFound()
|
||||
}
|
||||
func reqUsersExploreEnabled() func(ctx *context.APIContext) {
|
||||
return checkPermission(apiv1_permissions.ReqUsersExploreEnabled)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
func reqBasicOrRevProxyAuth() func(ctx *context.APIContext) {
|
||||
return checkPermission(apiv1_permissions.ReqBasicOrRevProxyAuth)
|
||||
}
|
||||
|
||||
func ReqSiteAdmin(ctx Context) {
|
||||
if !IsUserSiteAdmin(ctx) {
|
||||
ctx.Error(http.StatusForbidden, "reqSiteAdmin", "user should be the site admin")
|
||||
return
|
||||
}
|
||||
func reqSiteAdmin() func(ctx *context.APIContext) {
|
||||
return checkPermission(apiv1_permissions.ReqSiteAdmin)
|
||||
}
|
||||
|
||||
// reqOwner requires that the current user is either the owner of the repository or an administrator. If one or more
|
||||
// unitTypes are given, it also requires that at least one the respective unitTypes is enabled.
|
||||
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
|
||||
func reqOwner(unitTypes ...unit.Type) func(ctx *context.APIContext) {
|
||||
apiv1_permissions_tests.RecordSignature(apiv1_permissions.ReqOwner, unitTypes)
|
||||
return func(ctx *context.APIContext) {
|
||||
apiv1_permissions.ReqOwner(ctx, unitTypes)
|
||||
}
|
||||
}
|
||||
|
||||
// reqSelfOrAdmin doer should be the same as the contextUser or site admin
|
||||
func ReqSelfOrAdmin(ctx Context) {
|
||||
getName := func(user *user_model.User) string {
|
||||
if user == nil {
|
||||
return ""
|
||||
}
|
||||
return user.Name
|
||||
}
|
||||
|
||||
if !IsUserSiteAdmin(ctx) && getName(ctx.GetUser()) != getName(ctx.GetDoer()) {
|
||||
ctx.Error(http.StatusForbidden, "reqSelfOrAdmin", "doer should be the site admin or be same as the contextUser")
|
||||
return
|
||||
}
|
||||
func reqSelfOrAdmin() func(ctx *context.APIContext) {
|
||||
return checkPermission(apiv1_permissions.ReqSelfOrAdmin)
|
||||
}
|
||||
|
||||
// reqAdmin user should be an owner or a collaborator with admin write of a repository, or site admin. If one or more
|
||||
// unitTypes are given, it also requires that at least one the respective unitTypes is enabled.
|
||||
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
|
||||
func reqAdmin(unitTypes ...unit.Type) func(ctx *context.APIContext) {
|
||||
apiv1_permissions_tests.RecordSignature(apiv1_permissions.ReqAdmin, unitTypes)
|
||||
return func(ctx *context.APIContext) {
|
||||
apiv1_permissions.ReqAdmin(ctx, unitTypes)
|
||||
}
|
||||
}
|
||||
|
||||
// reqRepoWriter requires that the current user has permission to write to a repository or that it is an administrator.
|
||||
// One or more unitTypes have to be specified, and at least one of them has to be enabled.
|
||||
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
|
||||
func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) {
|
||||
apiv1_permissions_tests.RecordSignature(apiv1_permissions.ReqRepoWriter, unitTypes)
|
||||
return func(ctx *context.APIContext) {
|
||||
apiv1_permissions.ReqRepoWriter(ctx, unitTypes)
|
||||
}
|
||||
}
|
||||
|
||||
// reqRepoBranchWriter user should have a permission to write to a branch, or be a site admin
|
||||
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")
|
||||
func reqRepoBranchWriter() func(*context.APIContext) {
|
||||
apiv1_permissions_tests.RecordSignature(apiv1_permissions.ReqRepoBranchWriter)
|
||||
return func(ctx *context.APIContext) {
|
||||
options, ok := web.GetForm(ctx).(api.FileOptionInterface)
|
||||
if !ok {
|
||||
ctx.Error(http.StatusForbidden, "reqRepoBranchWriter", "user should have a permission to write to this branch")
|
||||
return
|
||||
}
|
||||
apiv1_permissions.ReqRepoBranchWriter(ctx, options.Branch())
|
||||
}
|
||||
}
|
||||
|
||||
// reqRepoReader user should have specific read permission or be a repo admin or a site admin
|
||||
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
|
||||
func reqRepoReader(unitType unit.Type) func(*context.APIContext) {
|
||||
apiv1_permissions_tests.RecordSignature(apiv1_permissions.ReqRepoReader, unitType)
|
||||
return func(ctx *context.APIContext) {
|
||||
apiv1_permissions.ReqRepoReader(ctx, unitType)
|
||||
}
|
||||
}
|
||||
|
||||
// reqAnyRepoReader user should have any permission to read repository or permissions of site admin
|
||||
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
|
||||
}
|
||||
func reqAnyRepoReader() func(ctx *context.APIContext) {
|
||||
return checkPermission(apiv1_permissions.ReqAnyRepoReader)
|
||||
}
|
||||
|
||||
// reqOrgOwnership user should be an organization owner, or a site admin
|
||||
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
|
||||
}
|
||||
func reqOrgOwnership() func(ctx *context.APIContext) {
|
||||
return checkPermission(apiv1_permissions.ReqOrgOwnership)
|
||||
}
|
||||
|
||||
// reqTeamMembership user should be an team member, or a site admin
|
||||
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
|
||||
}
|
||||
func reqTeamMembership() func(ctx *context.APIContext) {
|
||||
return checkPermission(apiv1_permissions.ReqTeamMembership)
|
||||
}
|
||||
|
||||
// reqOrgMembership user should be an organization member, or a site admin
|
||||
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
|
||||
}
|
||||
func reqOrgMembership() func(ctx *context.APIContext) {
|
||||
return checkPermission(apiv1_permissions.ReqOrgMembership)
|
||||
}
|
||||
|
||||
func ReqGitHook(ctx Context) {
|
||||
if !ctx.GetDoer().CanEditGitHook() {
|
||||
ctx.Error(http.StatusForbidden, "", "must be allowed to edit Git hooks")
|
||||
return
|
||||
}
|
||||
func reqGitHook() func(ctx *context.APIContext) {
|
||||
return checkPermission(apiv1_permissions.ReqGitHook)
|
||||
}
|
||||
|
||||
// reqWebhooksEnabled requires webhooks to be enabled by admin.
|
||||
func ReqWebhooksEnabled(ctx Context) {
|
||||
if setting.DisableWebhooks {
|
||||
ctx.Error(http.StatusForbidden, "", "webhooks disabled by administrator")
|
||||
return
|
||||
}
|
||||
func reqWebhooksEnabled() func(ctx *context.APIContext) {
|
||||
return checkPermission(apiv1_permissions.ReqWebhooksEnabled)
|
||||
}
|
||||
|
||||
func orgAssignment(args ...bool) func(ctx *context.APIContext) {
|
||||
|
|
@ -630,115 +427,35 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) {
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
func mustEnableIssues() func(ctx *context.APIContext) {
|
||||
return checkPermission(apiv1_permissions.MustEnableIssues)
|
||||
}
|
||||
|
||||
func mustEnableIssuesOrPulls() func(ctx *context.APIContext) {
|
||||
return checkPermission(apiv1_permissions.MustEnableIssuesOrPulls)
|
||||
}
|
||||
|
||||
func mustAllowPulls() func(ctx *context.APIContext) {
|
||||
return checkPermission(apiv1_permissions.MustAllowPulls)
|
||||
}
|
||||
|
||||
func mustEnableLocalIssuesIfIsIssue() func(*context.APIContext) {
|
||||
apiv1_permissions_tests.RecordSignature(apiv1_permissions.MustEnableLocalIssuesIfIsIssue)
|
||||
return func(ctx *context.APIContext) {
|
||||
apiv1_permissions.MustEnableLocalIssuesIfIsIssue(ctx, ctx.ParamsInt64(":index"))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
func mustEnableWiki() func(ctx *context.APIContext) {
|
||||
return checkPermission(apiv1_permissions.MustEnableWiki)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
func mustNotBeArchived() func(ctx *context.APIContext) {
|
||||
return checkPermission(apiv1_permissions.MustNotBeArchived)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func MustEnableWiki(ctx Context) {
|
||||
if !(ctx.GetPermission().CanRead(unit.TypeWiki)) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func MustNotBeArchived(ctx Context) {
|
||||
if ctx.GetRepository().IsArchived {
|
||||
ctx.Error(http.StatusLocked, "RepoArchived", fmt.Errorf("%s is archived", ctx.GetRepository().LogString()))
|
||||
}
|
||||
}
|
||||
|
||||
func MustEnableAttachments(ctx Context) {
|
||||
if !setting.Attachment.Enabled {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
func mustEnableAttachments() func(ctx *context.APIContext) {
|
||||
return checkPermission(apiv1_permissions.MustEnableAttachments)
|
||||
}
|
||||
|
||||
// bind binding an obj to a func(ctx *context.APIContext)
|
||||
|
|
@ -754,22 +471,8 @@ func bind[T any](_ T) any {
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
func individualPermsChecker() func(ctx *context.APIContext) {
|
||||
return checkPermission(apiv1_permissions.IndividualPermsChecker)
|
||||
}
|
||||
|
||||
// Routes registers all v1 APIs routes to web application.
|
||||
|
|
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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()))
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue