mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-26 06:16:56 +09:00
refactor: carry the API token on the doer (#39690)
The token a request authenticated with now lives on the doer, so permission and visibility checks apply its scope from the doer alone. Replaces https://github.com/go-gitea/gitea/pull/39167, following https://github.com/go-gitea/gitea/pull/39167#issuecomment-6063839619. 1. Public-only tokens get 404 instead of 403 for hidden users, orgs and repos, like GitHub. 2. Org ownership, site admin, repo and org creation, generation and transfer endpoints reject public-only tokens. 3. API pull request responses set `head.repo` to `null` and `head.repo_id` to `-1` when the caller cannot see the head repository. 4. Public-only tokens see org memberships like anonymous callers, so concealed members stay hidden. 5. Site admin access in activity feeds, follower lists, org members, Actions run lists and packages needs a session or an `all`-scoped token. Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
1 parent
7e62dda791
commit
5264304a34
96 files changed
+583
-686
No files matched your search
+3
-12
@@ -13,7 +13,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/cache"
|
||||
@@ -44,17 +43,9 @@ type APIContext struct {
|
||||
|
||||
ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer
|
||||
|
||||
Repo *Repository
|
||||
Org *APIOrganization
|
||||
Package *Package
|
||||
PublicOnly bool // Whether the request is for a public endpoint
|
||||
}
|
||||
|
||||
// TokenCanAccessRepo reports whether the current API token is allowed to access the repository.
|
||||
// A public-only token cannot reach a private repo or a repo owned by a non-public (limited or
|
||||
// private) owner; any other token is unrestricted by this check.
|
||||
func (ctx *APIContext) TokenCanAccessRepo(repo *repo_model.Repository) bool {
|
||||
return !ctx.PublicOnly || !publicOnlyTokenDeniedRepo(ctx, repo)
|
||||
Repo *Repository
|
||||
Org *APIOrganization
|
||||
Package *Package
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -37,7 +37,7 @@ func RedirectToUser(ctx *Base, doer *user_model.User, userName string, redirectU
|
||||
}
|
||||
|
||||
// Handle Visibility
|
||||
if user.Visibility != structs.VisibleTypePublic && doer == nil {
|
||||
if user.Visibility != structs.VisibleTypePublic && !user_model.DoerCanViewPrivate(doer) {
|
||||
// We must be signed in to see limited or private organizations
|
||||
ctx.HTTPError(http.StatusNotFound, "user does not exist")
|
||||
return
|
||||
|
||||
@@ -91,7 +91,7 @@ func OrgAssignment(orgAssignmentOpts OrgAssignmentOptions) func(ctx *Context) {
|
||||
org := ctx.Org.Organization
|
||||
|
||||
// Handle Visibility
|
||||
if org.Visibility != structs.VisibleTypePublic && !ctx.IsSigned {
|
||||
if org.Visibility != structs.VisibleTypePublic && !user_model.DoerCanViewPrivate(ctx.Doer) {
|
||||
// We must be signed in to see limited or private organizations
|
||||
ctx.NotFound(err)
|
||||
return
|
||||
|
||||
@@ -110,6 +110,13 @@ func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, string)) *Pack
|
||||
}
|
||||
|
||||
func determineAccessMode(ctx *Base, pkgOwner, doer *user_model.User) (perm.AccessMode, error) {
|
||||
if !user_model.DoerTokenAllowsOwner(doer, pkgOwner) {
|
||||
return perm.AccessModeNone, nil
|
||||
}
|
||||
if user_model.DoerIsSiteAdminFullAccess(doer) {
|
||||
return perm.AccessModeOwner, nil
|
||||
}
|
||||
|
||||
if setting.Service.RequireSignInViewStrict && (doer == nil || doer.IsGhost()) {
|
||||
return perm.AccessModeNone, nil
|
||||
}
|
||||
|
||||
@@ -23,4 +23,9 @@ func TestDeterminePackageAccessModeForLimitedOwner(t *testing.T) {
|
||||
accessMode, err = determineAccessMode(&Base{}, owner, &user.User{ID: 3, IsActive: true, IsRestricted: true})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, perm.AccessModeNone, accessMode)
|
||||
|
||||
publicOnlyAdmin := &user.User{ID: 4, IsActive: true, IsAdmin: true, ExtDoerData: user.NewTokenExtDoerData(user.CredentialAccessToken, 1, "public-only,read:package")}
|
||||
accessMode, err = determineAccessMode(&Base{}, owner, publicOnlyAdmin)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, perm.AccessModeNone, accessMode)
|
||||
}
|
||||
@@ -4,63 +4,21 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
)
|
||||
|
||||
// isOwnerHidden reports whether repo's owner is not publicly visible (a limited or private owner), so
|
||||
// the owner's repositories must be hidden from callers that may only reach genuinely public resources.
|
||||
func isOwnerHidden(ctx context.Context, repo *repo_model.Repository) bool {
|
||||
if err := repo.LoadOwner(ctx); err != nil || repo.Owner == nil {
|
||||
return true // fail closed if the owner visibility can't be determined
|
||||
}
|
||||
return !repo.Owner.Visibility.IsPublic()
|
||||
}
|
||||
|
||||
// publicOnlyTokenDeniedRepo reports whether a public-only API token must be denied access to
|
||||
// repo. A public-only token may only reach genuinely public resources, so it is denied for
|
||||
// private repos and for repos owned by a non-public (limited or private) owner.
|
||||
func publicOnlyTokenDeniedRepo(ctx context.Context, repo *repo_model.Repository) bool {
|
||||
if repo == nil {
|
||||
return false
|
||||
}
|
||||
return repo.IsPrivate || isOwnerHidden(ctx, repo)
|
||||
}
|
||||
|
||||
// TokenIsPublicOnly reports whether the request is authenticated by a public-only API token. A
|
||||
// non-token request, or a token with no recorded scope, is not public-only.
|
||||
func TokenIsPublicOnly(ctx *Context) bool {
|
||||
scope, hasApiTokenScope := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
|
||||
if !hasApiTokenScope {
|
||||
return false
|
||||
}
|
||||
publicOnly, _ := scope.PublicOnly()
|
||||
return publicOnly
|
||||
}
|
||||
|
||||
// CheckTokenScopes checks whether the authenticated API token contains any of the given scopes.
|
||||
func CheckTokenScopes(ctx *Context, repo *repo_model.Repository, scopes ...auth_model.AccessTokenScope) {
|
||||
scope, hasApiTokenScope := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
|
||||
func CheckTokenScopes(ctx *Context, scopes ...auth_model.AccessTokenScope) {
|
||||
scope, hasApiTokenScope := user_model.GetDoerTokenScope(ctx.Doer)
|
||||
if !hasApiTokenScope {
|
||||
return
|
||||
}
|
||||
|
||||
publicOnly, err := scope.PublicOnly()
|
||||
if err != nil {
|
||||
ctx.ServerError("PublicOnly", err)
|
||||
return
|
||||
}
|
||||
|
||||
if publicOnly && publicOnlyTokenDeniedRepo(ctx, repo) {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
scopeMatched, err := scope.HasAnyScope(scopes...)
|
||||
if err != nil {
|
||||
ctx.ServerError("HasAnyScope", err)
|
||||
@@ -118,6 +76,6 @@ func RequireUnitReader(unitTypes ...unit.Type) func(ctx *Context) {
|
||||
}
|
||||
|
||||
// CheckRepoScopedToken checks whether the authenticated API token has repo scope.
|
||||
func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_model.AccessTokenScopeLevel) {
|
||||
CheckTokenScopes(ctx, repo, auth_model.GetRequiredScopes(level, auth_model.AccessTokenScopeCategoryRepository)...)
|
||||
func CheckRepoScopedToken(ctx *Context, level auth_model.AccessTokenScopeLevel) {
|
||||
CheckTokenScopes(ctx, auth_model.GetRequiredScopes(level, auth_model.AccessTokenScopeCategoryRepository)...)
|
||||
}
|
||||
@@ -55,8 +55,13 @@ func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, string)) (
|
||||
} else {
|
||||
errCb(http.StatusInternalServerError, fmt.Sprintf("GetUserByName: %v", err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if !user_model.DoerTokenAllowsOwner(doer, contextUser) {
|
||||
errCb(http.StatusNotFound, user_model.ErrUserRedirectNotExist{Name: username}.Error())
|
||||
return nil
|
||||
}
|
||||
return contextUser
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user