diff --git a/models/activities/action.go b/models/activities/action.go index 52e733b8dba..5ba27fad6b2 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -436,12 +436,6 @@ type GetFeedsOptions struct { DontCount bool // do counting in GetFeeds } -func (opts *GetFeedsOptions) ApplyPublicOnly(publicOnly bool) { - if publicOnly { - opts.IncludePrivate = false - } -} - // ActivityReadable return whether doer can read activities of user func ActivityReadable(user, doer *user_model.User) bool { return !user.KeepActivityPrivate || @@ -478,13 +472,13 @@ func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder. } // check activity visibility for actor ( similar to activityReadable() ) - if opts.Actor == nil { + if !user_model.DoerCanViewPrivate(opts.Actor) { cond = cond.And(builder.In("act_user_id", builder.Select("`user`.id").Where( builder.Eq{"keep_activity_private": false, "visibility": structs.VisibleTypePublic}, ).From("`user`"), )) - } else if !opts.Actor.IsAdmin { + } else if !user_model.DoerIsSiteAdminFullAccess(opts.Actor) { uidCond := builder.Select("`user`.id").From("`user`").Where( builder.Eq{"keep_activity_private": false}. And(builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))). @@ -509,7 +503,7 @@ func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder. } // check readable repositories by doer/actor - if opts.Actor == nil || !opts.Actor.IsAdmin { + if !user_model.DoerIsSiteAdminFullAccess(opts.Actor) { cond = cond.And(builder.In("repo_id", repo_model.AccessibleRepoIDsQuery(opts.Actor))) } @@ -539,7 +533,7 @@ func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder. } } - if !opts.IncludePrivate { + if !opts.IncludePrivate || user_model.IsPublicOnlyDoer(opts.Actor) { cond = cond.And(builder.Eq{"`action`.is_private": false}) } if !opts.IncludeDeleted { diff --git a/models/activities/action_list.go b/models/activities/action_list.go index f1c617f5942..12df409fc78 100644 --- a/models/activities/action_list.go +++ b/models/activities/action_list.go @@ -212,7 +212,7 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err var err error var cond builder.Cond // if the actor is the requested user or is an administrator, we can skip the ActivityQueryCondition - if opts.Actor != nil && opts.RequestedUser != nil && (opts.Actor.IsAdmin || opts.Actor.ID == opts.RequestedUser.ID) { + if opts.RequestedUser != nil && user_model.DoerCanViewPrivate(opts.Actor) && (user_model.DoerIsSiteAdminFullAccess(opts.Actor) || opts.Actor.ID == opts.RequestedUser.ID) { cond = builder.Eq{ "user_id": opts.RequestedUser.ID, }.And( diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go index 0c2c3bfc45b..7fedab54b41 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -283,6 +283,7 @@ func applyConditions(sess db.Session, opts *IssuesOptions) { sess.And(repo_model.UserOwnedRepoCond(opts.Owner.ID)) } + sess.And(repo_model.DoerTokenRepoCond(opts.Doer)) if opts.Doer != nil && !opts.Doer.IsAdmin { sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.Doer.ID, opts.Owner, opts.Team, opts.IsPull.Value())) } diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go index 9d975d998ed..2227ae26a96 100644 --- a/models/issues/issue_test.go +++ b/models/issues/issue_test.go @@ -16,6 +16,7 @@ import ( repo_model "gitea.dev/models/repo" "gitea.dev/models/unittest" user_model "gitea.dev/models/user" + "gitea.dev/modules/optional" "gitea.dev/modules/setting" "gitea.dev/modules/test" @@ -150,6 +151,8 @@ func TestUpdateIssueCols(t *testing.T) { func TestIssues(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) + publicOnlyUser2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + publicOnlyUser2.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialAccessToken, 1, "public-only,read:issue") for _, test := range []struct { Opts issues_model.IssuesOptions ExpectedIssueIDs []int64 @@ -204,6 +207,15 @@ func TestIssues(t *testing.T) { }, []int64{11, 5, 9, 8, 3, 2, 1}, }, + { + issues_model.IssuesOptions{ + RepoCond: builder.In("repo_id", 1, 2), + Doer: publicOnlyUser2, + IsPull: optional.Some(false), + SortType: "oldest", + }, + []int64{1, 5}, + }, } { issues, err := issues_model.Issues(t.Context(), &test.Opts) assert.NoError(t, err) diff --git a/models/organization/org.go b/models/organization/org.go index 42628132c80..c0afaa89426 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -178,7 +178,7 @@ type FindOrgMembersOpts struct { } func (opts FindOrgMembersOpts) PublicOnly() bool { - return opts.Doer == nil || !(opts.IsDoerMember || opts.Doer.IsAdmin) + return !user_model.DoerCanViewPrivate(opts.Doer) || !(opts.IsDoerMember || user_model.DoerIsSiteAdminFullAccess(opts.Doer)) } // applyKeywordFilter adds keyword search conditions to session @@ -456,8 +456,8 @@ func ownerVisibilitySatisfiesDoer(orgOrUser, user *user_model.User) bool { // HasOrgOrUserVisible tells if the given user can see the given org or user func HasOrgOrUserVisible(ctx context.Context, owner, doer *user_model.User) bool { - return ownerVisibilitySatisfiesDoer(owner, doer) || - (doer != nil && OrgFromUser(owner).HasMemberWithUserID(ctx, doer.ID)) + return user_model.DoerTokenAllowsOwner(doer, owner) && (ownerVisibilitySatisfiesDoer(owner, doer) || + (doer != nil && OrgFromUser(owner).HasMemberWithUserID(ctx, doer.ID))) } // HasOrgsVisible tells if the given user can see at least one of the orgs provided diff --git a/models/organization/org_list.go b/models/organization/org_list.go index 9f9ea69c7c7..9a64bbd4192 100644 --- a/models/organization/org_list.go +++ b/models/organization/org_list.go @@ -54,12 +54,6 @@ type FindOrgOptions struct { IncludeVisibility structs.VisibleType } -func (opts *FindOrgOptions) ApplyPublicOnly(publicOnly bool) { - if publicOnly { - opts.IncludeVisibility = structs.VisibleTypePublic - } -} - func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder { cond := builder.Eq{"uid": userID} if !includePrivate { @@ -83,7 +77,7 @@ func (opts FindOrgOptions) ToOrders() string { } func DoerViewOtherVisibility(doer, other *user_model.User) structs.VisibleType { - if doer == nil || other == nil { + if other == nil || !user_model.DoerCanViewPrivate(doer) { return structs.VisibleTypePublic } if doer.IsAdmin || doer.ID == other.ID { diff --git a/models/packages/package_blob.go b/models/packages/package_blob.go index e3db4527609..c0b68c504b3 100644 --- a/models/packages/package_blob.go +++ b/models/packages/package_blob.go @@ -123,7 +123,7 @@ func GetTotalUnreferencedBlobSize(ctx context.Context) (int64, error) { // IsBlobAccessibleForUser tests if the user has access to the blob func IsBlobAccessibleForUser(ctx context.Context, blobID int64, user *user_model.User) (bool, error) { - if user.IsAdmin { + if user_model.DoerIsSiteAdminFullAccess(user) { return true, nil } ownerVisibilities := []structs.VisibleType{structs.VisibleTypePublic} @@ -153,6 +153,9 @@ func IsBlobAccessibleForUser(ctx context.Context, blobID int64, user *user_model Or(builder.Eq{"`user`.type": user_model.UserTypeOrganization}. And(builder.Lte{strconv.Itoa(int(perm.AccessModeRead)): maxTeamAuthorize}.Or(builder.Lte{strconv.Itoa(int(perm.AccessModeRead)): maxTeamUnitAccessMode}))), ) + if !user_model.DoerCanViewPrivate(user) { + cond = cond.And(builder.Eq{"`user`.visibility": structs.VisibleTypePublic}) + } return db.GetEngine(ctx). Table("package_blob"). diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 7ce3f4f0168..ee3b2f27d63 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -424,6 +424,9 @@ func GetDoerRepoPermission(ctx context.Context, repo *repo_model.Repository, use // In most request paths, callers should use GetDoerRepoPermission instead. // Unlike GetDoerRepoPermission, this helper does not resolve Actions task users. func GetIndividualUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) { + if !repo_model.DoerTokenAllowsRepo(ctx, user, repo) { + return PermissionNoAccess(), nil + } defer func() { if err == nil { finalProcessRepoUnitPermission(user, &perm) @@ -675,10 +678,14 @@ func PermissionNoAccess() Permission { // permission on a repository. Producers and consumers must agree on it, so it lives here. func RepoUserPermissionCacheKey(repoID int64, doer *user_model.User) string { var doerID int64 + var doerExtData string if doer != nil { doerID = doer.ID + if doer.ExtDoerData != nil { + doerExtData = doer.ExtDoerData.EncodeToString() + } } - return fmt.Sprintf("%d-%d", repoID, doerID) + return fmt.Sprintf("%d-%d-%s", repoID, doerID, doerExtData) } // CanReadWorkflowCrossRepo checks whether the run can read workflow files from targetRepo. diff --git a/models/perm/access/repo_permission_test.go b/models/perm/access/repo_permission_test.go index 8eef8b010d8..cb076ec98bb 100644 --- a/models/perm/access/repo_permission_test.go +++ b/models/perm/access/repo_permission_test.go @@ -272,4 +272,21 @@ func testGetDoerRepoPermission(t *testing.T) { individualPerm, err := GetIndividualUserRepoPermission(ctx, repo1, regularUser) require.NoError(t, err) assert.Equal(t, individualPerm, doerPerm) + + t.Run("PublicOnlyTokenReachesOnlyPublicReposOfPublicOwners", func(t *testing.T) { + publicOnlyUser := *regularUser + publicOnlyUser.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialAccessToken, 1, "public-only,write:repository") + publicOnlyAdmin := *unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + publicOnlyAdmin.ExtDoerData = publicOnlyUser.ExtDoerData + for _, repoID := range []int64{2, 38} { + perm, err := GetDoerRepoPermission(ctx, unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}), &publicOnlyAdmin) + require.NoError(t, err) + assert.False(t, perm.HasAnyUnitAccessOrPublicAccess(), "repo %d", repoID) + } + + perm, err := GetDoerRepoPermission(ctx, repo1, &publicOnlyUser) + require.NoError(t, err) + assert.True(t, perm.CanWrite(unit.TypeCode)) + assert.NotEqual(t, RepoUserPermissionCacheKey(repo1.ID, regularUser), RepoUserPermissionCacheKey(repo1.ID, &publicOnlyUser)) + }) } diff --git a/models/repo/org_repo.go b/models/repo/org_repo.go index 5bf0a3e7d42..498b9103e1c 100644 --- a/models/repo/org_repo.go +++ b/models/repo/org_repo.go @@ -31,8 +31,7 @@ func GetOrgRepositoryIDs(ctx context.Context, orgID int64) (repoIDs []int64, _ e type SearchTeamRepoOptions struct { db.ListOptions TeamID int64 - // PublicOnly restricts the result (and count) to non-private repositories. - PublicOnly bool + Actor *user_model.User } func (opts *SearchTeamRepoOptions) toCond() builder.Cond { @@ -44,10 +43,7 @@ func (opts *SearchTeamRepoOptions) toCond() builder.Cond { Where(builder.Eq{"team_id": opts.TeamID}), )) } - if opts.PublicOnly { - cond = cond.And(builder.Eq{"is_private": false}) - } - return cond + return cond.And(DoerTokenRepoCond(opts.Actor)) } // GetTeamRepositories returns paginated repositories in team of organization. diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index 066f75df2c4..4266d3fde9a 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -212,13 +212,6 @@ type SearchRepoOptions struct { OnlyShowRelevant bool } -func (opts *SearchRepoOptions) ApplyPublicOnly(publicOnly bool) { - if publicOnly { - opts.Private = false - opts.AllLimited = false - } -} - // UserOwnedRepoCond returns user ownered repositories func UserOwnedRepoCond(userID int64) builder.Cond { return builder.Eq{ @@ -370,6 +363,9 @@ func UserOrgPublicUnitRepoCond(userID, orgID int64) builder.Cond { // SearchRepositoryCondition creates a query condition according search repository options func SearchRepositoryCondition(opts SearchRepoOptions) builder.Cond { + if user_model.IsPublicOnlyDoer(opts.Actor) { + opts.Private, opts.AllLimited = false, false + } cond := builder.NewCond() if opts.Private { @@ -663,6 +659,9 @@ func userAllPublicRepoCond(cond builder.Cond, ownerVisibilityLimit []structs.Vis // AccessibleRepositoryCondition takes a user a returns a condition for checking if a repository is accessible func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) builder.Cond { + if user_model.IsPublicOnlyDoer(user) && !user.IsRestricted { + return PublicRepoUnderPublicOwnerCond() + } cond := builder.NewCond() if user == nil || !user.IsRestricted || user.ID <= 0 { @@ -700,8 +699,7 @@ func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) bu cond = userAllPublicRepoCond(cond, orgVisibilityLimit) } } - - return cond + return cond.And(DoerTokenRepoCond(user)) } // SearchRepositoryByName takes keyword and part of repository name to search, @@ -766,6 +764,20 @@ func PublicRepoUnderPublicOwnerCond() builder.Cond { ) } +func DoerTokenAllowsRepo(ctx context.Context, doer *user_model.User, repo *Repository) bool { + if !user_model.IsPublicOnlyDoer(doer) { + return true + } + return !repo.IsPrivate && repo.LoadOwner(ctx) == nil && repo.Owner.Visibility.IsPublic() +} + +func DoerTokenRepoCond(doer *user_model.User) builder.Cond { + if user_model.IsPublicOnlyDoer(doer) { + return PublicRepoUnderPublicOwnerCond() + } + return builder.NewCond() +} + // NotPublicRepoUnderPublicOwnerCond complements PublicRepoUnderPublicOwnerCond. Spelled positively so // the owner subquery hashes the limited/private minority, not every public user. func NotPublicRepoUnderPublicOwnerCond() builder.Cond { @@ -780,23 +792,21 @@ func NotPublicRepoUnderPublicOwnerCond() builder.Cond { // - owner_id = ownerID: only that owner's repos. // - AccessibleRepositoryCondition(user, TypeActions): only repos whose Actions the user can read // (admin/owner teams are handled inside it; a site admin is not, callers must skip the filter for one). -// - publicOnly (a public-only token): additionally limit to public repos under a public owner. -func UserActionsAccessibleOwnerRepoCond(ownerID int64, user *user_model.User, publicOnly bool) builder.Cond { - cond := builder.NewCond().And( +func UserActionsAccessibleOwnerRepoCond(ownerID int64, user *user_model.User) builder.Cond { + return builder.And( builder.Eq{"`repository`.owner_id": ownerID}, AccessibleRepositoryCondition(user, unit.TypeActions), ) - if publicOnly { - cond = cond.And(PublicRepoUnderPublicOwnerCond()) - } - return cond } // FindUserActionsAccessibleOwnerRepoIDsSubQuery returns a subquery selecting the repository IDs the user -// can see for the given owner. Callers embed it in an `IN (...)` condition so that a large owner does not +// can see for the given owner, nil if unrestricted. Callers embed it in an `IN (...)` condition so that a large owner does not // materialize every repo ID into the SQL statement, which could exceed database parameter limits. -func FindUserActionsAccessibleOwnerRepoIDsSubQuery(ownerID int64, user *user_model.User, publicOnly bool) *builder.Builder { - return builder.Select("id").From("repository").Where(UserActionsAccessibleOwnerRepoCond(ownerID, user, publicOnly)) +func FindUserActionsAccessibleOwnerRepoIDsSubQuery(ownerID int64, user *user_model.User) *builder.Builder { + if ownerID <= 0 || user_model.DoerIsSiteAdminFullAccess(user) { + return nil + } + return builder.Select("id").From("repository").Where(UserActionsAccessibleOwnerRepoCond(ownerID, user)) } // GetUserRepositories returns a list of repositories of given user. @@ -806,10 +816,10 @@ func GetUserRepositories(ctx context.Context, opts SearchRepoOptions) (Repositor } cond := builder.NewCond() - if opts.Actor == nil { - return nil, 0, util.NewInvalidArgumentErrorf("GetUserRepositories: Actor is needed but not given") + if opts.OwnerID == 0 { + return nil, 0, util.NewInvalidArgumentErrorf("GetUserRepositories: OwnerID is needed but not given") } - cond = cond.And(builder.Eq{"owner_id": opts.Actor.ID}) + cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}, DoerTokenRepoCond(opts.Actor)) if !opts.Private { cond = cond.And(builder.Eq{"is_private": false}) } diff --git a/models/repo/repo_list_test.go b/models/repo/repo_list_test.go index 0bea4b23d59..44677425451 100644 --- a/models/repo/repo_list_test.go +++ b/models/repo/repo_list_test.go @@ -493,14 +493,21 @@ func TestFindUserActionsAccessibleOwnerRepoIDs(t *testing.T) { // org3 is a public org owning repo3 (private) and repo32 (public), both with the actions unit const orgID = 3 - all, err := repo_model.SearchRepositoryIDsByCondition(t.Context(), repo_model.UserActionsAccessibleOwnerRepoCond(orgID, user, false)) + all, err := repo_model.SearchRepositoryIDsByCondition(t.Context(), repo_model.UserActionsAccessibleOwnerRepoCond(orgID, user)) require.NoError(t, err) assert.Contains(t, all, int64(3), "without public-only the private repo's actions are listed") - publicOnly, err := repo_model.SearchRepositoryIDsByCondition(t.Context(), repo_model.UserActionsAccessibleOwnerRepoCond(orgID, user, true)) + scopedUser := *user + scopedUser.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialAccessToken, 1, "public-only,read:repository") + publicOnly, err := repo_model.SearchRepositoryIDsByCondition(t.Context(), repo_model.UserActionsAccessibleOwnerRepoCond(orgID, &scopedUser)) require.NoError(t, err) assert.NotContains(t, publicOnly, int64(3), "a public-only token must not list a private repo's actions") assert.Contains(t, publicOnly, int64(32), "a public repo under a public owner stays listed") + + admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + assert.Nil(t, repo_model.FindUserActionsAccessibleOwnerRepoIDsSubQuery(orgID, admin)) + admin.ExtDoerData = scopedUser.ExtDoerData + assert.NotNil(t, repo_model.FindUserActionsAccessibleOwnerRepoIDsSubQuery(orgID, admin)) } // TestUserOrgUnitRepoCondTeamAuthorize pins team.authorize vs team_unit.access_mode diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go index b0e11246503..8e5ad8ce3b5 100644 --- a/models/repo/user_repo.go +++ b/models/repo/user_repo.go @@ -31,12 +31,6 @@ func (opts *StarredReposOptions) ToOrders() string { return "`repository`.id" } -func (opts *StarredReposOptions) ApplyPublicOnly(publicOnly bool) { - if publicOnly { - opts.IncludePrivate = false - } -} - func (opts *StarredReposOptions) ToConds() builder.Cond { var cond builder.Cond = builder.Eq{ "star.uid": opts.StarrerID, @@ -84,12 +78,6 @@ func (opts *WatchedReposOptions) ToOrders() string { return "`repository`.id" } -func (opts *WatchedReposOptions) ApplyPublicOnly(publicOnly bool) { - if publicOnly { - opts.IncludePrivate = false - } -} - func (opts *WatchedReposOptions) ToConds() builder.Cond { var cond builder.Cond = builder.Eq{ "watch.user_id": opts.WatcherID, diff --git a/models/user/search.go b/models/user/search.go index 8520f8c9fd4..d9e2c5b70f3 100644 --- a/models/user/search.go +++ b/models/user/search.go @@ -60,12 +60,6 @@ func (opts *SearchUserOptions) ToOrders() string { return "id" } -func (opts *SearchUserOptions) ApplyPublicOnly(publicOnly bool) { - if publicOnly { - opts.Visible = []structs.VisibleType{structs.VisibleTypePublic} - } -} - func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) db.Session { var cond builder.Cond cond = builder.In("type", opts.Types) @@ -177,23 +171,17 @@ func SearchUsers(ctx context.Context, opts SearchUserOptions) (users []*User, _ // BuildCanSeeUserCondition creates a condition which can be used to restrict results to users/orgs the actor can see func BuildCanSeeUserCondition(actor *User) builder.Cond { - if actor != nil { - // If Admin - they see all users! - if !actor.IsAdmin { - // Users can see an organization they are a member of - cond := builder.In("`user`.id", builder.Select("org_id").From("org_user").Where(builder.Eq{"uid": actor.ID})) - if !actor.IsRestricted { - // Not-Restricted users can see public and limited users/organizations - cond = cond.Or(builder.In("`user`.visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited)) - } - // Don't forget about self - return cond.Or(builder.Eq{"`user`.id": actor.ID}) + canViewPrivate := DoerCanViewPrivate(actor) + cond := builder.NewCond() + if actor != nil && !actor.IsAdmin && (canViewPrivate || actor.IsRestricted) { + cond = builder.In("`user`.id", builder.Select("org_id").From("org_user").Where(builder.Eq{"uid": actor.ID})) + if !actor.IsRestricted { + cond = cond.Or(builder.In("`user`.visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited)) } - - return nil + cond = cond.Or(builder.Eq{"`user`.id": actor.ID}) } - - // Force visibility for privacy - // Not logged in - only public users - return builder.In("`user`.visibility", structs.VisibleTypePublic) + if !canViewPrivate { + cond = cond.And(builder.In("`user`.visibility", structs.VisibleTypePublic)) + } + return cond } diff --git a/models/user/user.go b/models/user/user.go index c839e41d170..0349a469f91 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -175,7 +175,8 @@ type User struct { // When the user model is used as a doer (all existing code does so), the doer can have extra details. // * Actions task doer needs to bind to the task // * Deploy-key doer needs to bind to the key - ExtDoerData ExtDoerData `xorm:"-"` + // * API token doer needs to bind to the token and its scope + ExtDoerData ExtDoerData `xorm:"-" json:"-"` } // Meta defines the meta information of a user, to be stored in the K/V table @@ -445,11 +446,6 @@ func (u *User) IsTypeBot() bool { return u.Type == UserTypeBot } -// IsTokenAccessAllowed returns whether the user is an individual or a bot (which allows for token access) -func (u *User) IsTokenAccessAllowed() bool { - return u.Type == UserTypeIndividual || u.Type == UserTypeBot -} - // EmailTo returns a string suitable to be put into a e-mail `To:` header. func (u *User) EmailTo() string { sanitizedDisplayName := globalVars().emailToReplacer.Replace(u.DisplayName()) @@ -1446,11 +1442,11 @@ func GetAdminUser(ctx context.Context) (*User, error) { } func isUserVisibleToViewerCond(viewer *User) builder.Cond { - if viewer != nil && viewer.IsAdmin { + if DoerIsSiteAdminFullAccess(viewer) { return builder.NewCond() } - if viewer == nil || viewer.IsRestricted { + if !DoerCanViewPrivate(viewer) || viewer.IsRestricted { return builder.Eq{ "`user`.visibility": structs.VisibleTypePublic, } @@ -1484,6 +1480,9 @@ func isUserVisibleToViewerCond(viewer *User) builder.Cond { // IsUserVisibleToViewer check if viewer is able to see user profile func IsUserVisibleToViewer(ctx context.Context, u, viewer *User) bool { + if !DoerTokenAllowsOwner(viewer, u) { + return false + } if viewer != nil && (viewer.IsAdmin || viewer.ID == u.ID) { return true } diff --git a/models/user/user_extdata.go b/models/user/user_extdata.go index 62d6f5d6a7e..217a5db9ea8 100644 --- a/models/user/user_extdata.go +++ b/models/user/user_extdata.go @@ -4,8 +4,11 @@ package user import ( + "errors" "strconv" "strings" + + "gitea.dev/models/auth" ) type ExtDoerData interface { @@ -44,3 +47,88 @@ func (e *extDoerDeployKey) DecodeFromString(s string) (err error) { e.DeployKeyID, err = strconv.ParseInt(idStr, 10, 64) return err } + +const ( + CredentialAccessToken = "access-token" + CredentialOAuth2Grant = "oauth2-grant" +) + +type extDoerToken struct { + credential string // empty for package registry tokens + scope auth.AccessTokenScope + publicOnly bool +} + +var _ ExtDoerData = (*extDoerToken)(nil) + +func NewTokenExtDoerData(credentialKind string, credentialID int64, scope auth.AccessTokenScope) ExtDoerData { + publicOnly, err := scope.PublicOnly() + e := &extDoerToken{scope: scope, publicOnly: publicOnly || err != nil} + if credentialKind != "" { + e.credential = credentialKind + ":" + strconv.FormatInt(credentialID, 10) + } + return e +} + +func (e *extDoerToken) EncodeToString() string { + return e.credential + "|" + string(e.scope) +} + +func (e *extDoerToken) DecodeFromString(s string) error { + credential, scope, ok := strings.Cut(s, "|") + if !ok { + return errors.New("invalid token doer data") + } + publicOnly, err := auth.AccessTokenScope(scope).PublicOnly() + if err != nil { + return err + } + e.credential, e.scope, e.publicOnly = credential, auth.AccessTokenScope(scope), publicOnly + return nil +} + +func getDoerToken(u *User) *extDoerToken { + if u == nil { + return nil + } + token, _ := u.ExtDoerData.(*extDoerToken) + return token +} + +func GetDoerTokenScope(u *User) (auth.AccessTokenScope, bool) { + if token := getDoerToken(u); token != nil { + return token.scope, true + } + return "", false +} + +func IsPublicOnlyDoer(u *User) bool { + token := getDoerToken(u) + return token != nil && token.publicOnly +} + +func DoerCanViewPrivate(doer *User) bool { + return doer != nil && !IsPublicOnlyDoer(doer) +} + +func DoerIsSiteAdminFullAccess(doer *User) bool { + if !DoerCanViewPrivate(doer) || !doer.IsAdmin { + return false + } + scope, hasScope := GetDoerTokenScope(doer) + return !hasScope || scope == auth.AccessTokenScopeAll +} + +func DoerTokenAllowsOwner(doer, owner *User) bool { + return !IsPublicOnlyDoer(doer) || owner.Visibility.IsPublic() +} + +func GetDoerCredential(u *User) string { + if u == nil || u.ExtDoerData == nil { + return "" + } + if token := getDoerToken(u); token != nil { + return token.credential + } + return u.ExtDoerData.EncodeToString() +} diff --git a/models/user/user_system.go b/models/user/user_system.go index 55b8e58e0a4..2c814144ad7 100644 --- a/models/user/user_system.go +++ b/models/user/user_system.go @@ -115,15 +115,17 @@ func GetSystemUserByID(uid int64) *User { return nil } -func GetDoerPermissionUser(ctx context.Context, id int64, extDoerData string) (u *User, _ error) { - if id > 0 { - return GetUserByID(ctx, id) - } - switch id { - case ActionsUserID: +func GetDoerPermissionUser(ctx context.Context, id int64, extDoerData string) (u *User, err error) { + switch { + case id > 0: + if u, err = GetUserByID(ctx, id); err != nil || extDoerData == "" { + return u, err + } + u.ExtDoerData = &extDoerToken{} + case id == ActionsUserID: u = NewActionsUser() u.ExtDoerData = &extDoerGiteaActions{} - case DeployKeyUserID: + case id == DeployKeyUserID: u = NewDeployKeyUser() u.ExtDoerData = &extDoerDeployKey{} default: diff --git a/models/user/user_system_test.go b/models/user/user_system_test.go index 8111e7b54c9..5e93b3628cd 100644 --- a/models/user/user_system_test.go +++ b/models/user/user_system_test.go @@ -6,6 +6,8 @@ package user import ( "testing" + "gitea.dev/models/unittest" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -37,3 +39,15 @@ func TestSystemUser(t *testing.T) { assert.Equal(t, int64(-1), uid) assert.Equal(t, "Ghost", u.Name) } + +func TestGetDoerPermissionUserCarriesToken(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + u, err := GetDoerPermissionUser(t.Context(), 2, NewTokenExtDoerData(CredentialAccessToken, 9, "public-only,read:repository").EncodeToString()) + require.NoError(t, err) + assert.True(t, IsPublicOnlyDoer(u)) + assert.Equal(t, "access-token:9", GetDoerCredential(u)) + + _, err = GetDoerPermissionUser(t.Context(), 2, "access-token:9|bogus") + assert.Error(t, err) +} diff --git a/models/user/user_test.go b/models/user/user_test.go index 92c344435f2..50a2eb70b4e 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -215,6 +215,11 @@ func TestSearchUsers(t *testing.T) { testUserSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: optional.Some(true)}, []int64{24}) + + restrictedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29}) + testUserSuccess(user_model.SearchUserOptions{Actor: restrictedUser}, []int64{29}) + restrictedUser.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialAccessToken, 1, "public-only,read:user") + testUserSuccess(user_model.SearchUserOptions{Actor: restrictedUser}, []int64{29}) } func TestEmailNotificationPreferences(t *testing.T) { @@ -525,6 +530,10 @@ func TestIsUserVisibleToViewer(t *testing.T) { test(user20, user1, true) test(user31, user1, true) test(user33, user1, true) + publicOnlyAdmin := *user1 + publicOnlyAdmin.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialAccessToken, 1, "public-only,read:user") + test(user31, &publicOnlyAdmin, false) + test(user33, &publicOnlyAdmin, false) // non admin viewer test(user4, user4, true) diff --git a/modules/web/middleware/data.go b/modules/web/middleware/data.go index dfe8e10a471..f7ea662cdfc 100644 --- a/modules/web/middleware/data.go +++ b/modules/web/middleware/data.go @@ -16,8 +16,6 @@ const ( ContextDataKeySignedUser = "SignedUser" // ContextDataKeyImpersonator holds the admin acting as the signed-in user, if any. ContextDataKeyImpersonator = "Impersonator" - // ContextDataKeyAuthCredential names the credential the request authenticated with, e.g. "access-token:42". - ContextDataKeyAuthCredential = "AuthCredential" ) func GetContextData(c context.Context) reqctx.ContextData { diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index ba7326bf519..5ece151ee24 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -8,6 +8,7 @@ import ( auth_model "gitea.dev/models/auth" "gitea.dev/models/perm" + user_model "gitea.dev/models/user" "gitea.dev/modules/log" npm_module "gitea.dev/modules/packages/npm" "gitea.dev/modules/setting" @@ -41,50 +42,20 @@ import ( func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) { return func(ctx *context.Context) { - scope, hasApiTokenScope := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) - if hasApiTokenScope { - { // request authenticated by a scoped token; enforce package scope restrictions - scopeMatched := false - var err error - switch accessMode { - case perm.AccessModeRead: - scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeReadPackage) - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, "HasScope", err.Error()) - return - } - case perm.AccessModeWrite: - scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeWritePackage) - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, "HasScope", err.Error()) - return - } - } - if !scopeMatched { - ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea Package API"`) - ctx.HTTPError(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin") - return - } - - // check if scope only applies to public resources - publicOnly, err := scope.PublicOnly() - if err != nil { - ctx.HTTPError(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error()) - return - } - - if publicOnly { - // a public-only token must not reach limited-visibility owners either, - // matching how orgs/users are enforced elsewhere in this file - if ctx.Package != nil && !ctx.Package.Owner.Visibility.IsPublic() { - ctx.HTTPError(http.StatusForbidden, "reqToken", "token scope is limited to public packages") - return - } - } + if scope, ok := user_model.GetDoerTokenScope(ctx.Doer); ok { + scopeMatched, err := scope.HasScope(auth_model.GetRequiredScopes(auth_model.GetScopeLevelFromAccessMode(accessMode), auth_model.AccessTokenScopeCategoryPackage)...) + if err != nil { + ctx.HTTPError(http.StatusInternalServerError, "HasScope", err.Error()) + return + } + if !scopeMatched { + ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea Package API"`) + ctx.HTTPError(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin") + return } } - if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() { + if ctx.Package.AccessMode < accessMode { ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea Package API"`) ctx.HTTPError(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin") return diff --git a/routers/api/packages/auth.go b/routers/api/packages/auth.go index ac239021669..20410d4bed0 100644 --- a/routers/api/packages/auth.go +++ b/routers/api/packages/auth.go @@ -24,7 +24,7 @@ func (a *Auth) Name() string { } // Verify extracts the user from the Bearer token -func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) { +func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, _ auth.DataStore, sess auth.SessionStore) (*user_model.User, error) { packageMeta, err := packages.ParseAuthorizationRequest(req) if err != nil { log.Trace("ParseAuthorizationToken: %v", err) @@ -49,11 +49,9 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS if err != nil { return nil, err } + if packageMeta.Scope != "" { + u.ExtDoerData = user_model.NewTokenExtDoerData("", 0, packageMeta.Scope) + } } - - if packageMeta.Scope != "" { - store.GetData()["ApiTokenScope"] = packageMeta.Scope - } - return u, nil } diff --git a/routers/api/packages/conan/conan.go b/routers/api/packages/conan/conan.go index ff6237de4bb..041080c7deb 100644 --- a/routers/api/packages/conan/conan.go +++ b/routers/api/packages/conan/conan.go @@ -125,7 +125,7 @@ func Authenticate(ctx *context.Context) { return } - packageScope := auth_service.GetAccessScope(ctx.Data) + packageScope := auth_service.GetAccessScope(ctx.Doer, ctx.Data) if has, err := packageScope.HasAnyScope( auth_model.AccessTokenScopeReadPackage, auth_model.AccessTokenScopeWritePackage, @@ -154,7 +154,7 @@ func CheckCredentials(ctx *context.Context) { return } - packageScope := auth_service.GetAccessScope(ctx.Data) + packageScope := auth_service.GetAccessScope(ctx.Doer, ctx.Data) if has, err := packageScope.HasAnyScope( auth_model.AccessTokenScopeReadPackage, auth_model.AccessTokenScopeWritePackage, diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index c1a4f3c3439..d800b1f3c22 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -153,7 +153,7 @@ func DetermineSupport(ctx *context.Context) { // If the current user is anonymous, the ghost user is used unless RequireSignInView is enabled. func Authenticate(ctx *context.Context) { u := ctx.Doer - packageScope := auth_service.GetAccessScope(ctx.Data) + packageScope := auth_service.GetAccessScope(ctx.Doer, ctx.Data) if u == nil { if setting.Service.RequireSignInViewStrict { APIUnauthorizedError(ctx) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 4ac65706447..8231dae185d 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -130,6 +130,7 @@ func sudo() func(ctx *context.APIContext) { audit.Record(ctx, audit_model.UserImpersonation, user) + user.ExtDoerData = ctx.Doer.ExtDoerData ctx.Doer = user // keep the audit actor in step with the effective doer, and keep the admin attached to it ctx.Data[middleware.ContextDataKeyImpersonator] = ctx.Data[middleware.ContextDataKeySignedUser] @@ -219,11 +220,6 @@ func repoAssignment() func(ctx *context.APIContext) { ctx.APIErrorNotFound() return } - - if !ctx.TokenCanAccessRepo(repo) { - ctx.APIErrorNotFound() - return - } } } @@ -243,73 +239,16 @@ func doerNeedTwoFactorAuth(ctx gocontext.Context, doer *user_model.User) (bool, func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) { return func(ctx *context.APIContext) { - if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() { + if ctx.Package.AccessMode < accessMode { ctx.APIError(http.StatusForbidden, "user should have specific permission or be a site admin") return } } } -func checkTokenPublicOnly() func(ctx *context.APIContext) { - return func(ctx *context.APIContext) { - if !ctx.PublicOnly { - return - } - - requiredScopeCategories, ok := ctx.Data["requiredScopeCategories"].([]auth_model.AccessTokenScopeCategory) - if !ok || len(requiredScopeCategories) == 0 { - return - } - - for _, category := range requiredScopeCategories { - switch category { - case auth_model.AccessTokenScopeCategoryRepository: - if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) { - ctx.APIError(http.StatusForbidden, "token scope is limited to public repos") - return - } - case auth_model.AccessTokenScopeCategoryIssue: - if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) { - ctx.APIError(http.StatusForbidden, "token scope is limited to public issues") - return - } - case auth_model.AccessTokenScopeCategoryOrganization: - orgPrivate := ctx.Org.Organization != nil && !ctx.Org.Organization.Visibility.IsPublic() - userOrgPrivate := ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() && !ctx.ContextUser.Visibility.IsPublic() - if orgPrivate || userOrgPrivate { - ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs") - return - } - case auth_model.AccessTokenScopeCategoryUser: - if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && !ctx.ContextUser.Visibility.IsPublic() { - ctx.APIError(http.StatusForbidden, "token scope is limited to public users") - return - } - case auth_model.AccessTokenScopeCategoryActivityPub: - if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && !ctx.ContextUser.Visibility.IsPublic() { - ctx.APIError(http.StatusForbidden, "token scope is limited to public activitypub") - return - } - case auth_model.AccessTokenScopeCategoryNotification: - if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) { - ctx.APIError(http.StatusForbidden, "token scope is limited to public notifications") - return - } - case auth_model.AccessTokenScopeCategoryPackage: - // a public-only token must not reach limited-visibility owners either, - // matching the org/user public-only enforcement above - if ctx.Package != nil && !ctx.Package.Owner.Visibility.IsPublic() { - ctx.APIError(http.StatusForbidden, "token scope is limited to public packages") - return - } - } - } - } -} - func rejectPublicOnly() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { - if !ctx.PublicOnly { + if !user_model.IsPublicOnlyDoer(ctx.Doer) { return } @@ -319,6 +258,10 @@ func rejectPublicOnly() func(ctx *context.APIContext) { func contextAuthenticatedUser() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { + if !user_model.DoerTokenAllowsOwner(ctx.Doer, ctx.Doer) { + ctx.APIError(http.StatusForbidden, "this endpoint is not available for public-only tokens") + return + } ctx.ContextUser = ctx.Doer } } @@ -333,7 +276,7 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC } // Need OAuth2 token to be present. - scope, scopeExists := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) + scope, scopeExists := user_model.GetDoerTokenScope(ctx.Doer) if !scopeExists { return } @@ -354,20 +297,7 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC if !allow { ctx.APIError(http.StatusForbidden, fmt.Sprintf("token does not have at least one of required scope(s), required=%v, token scope=%v", requiredScopes, scope)) - return } - - ctx.Data["requiredScopeCategories"] = requiredScopeCategories - - // check if scope only applies to public resources - publicOnly, err := scope.PublicOnly() - if err != nil { - ctx.APIError(http.StatusForbidden, "parsing public resource scope failed: "+err.Error()) - return - } - - // assign to true so that those searching should only filter public repositories/users/organizations - ctx.PublicOnly = publicOnly } } @@ -492,6 +422,9 @@ func reqAnyRepoReader() func(ctx *context.APIContext) { // reqOrgOwnership user should be an organization owner, or a site admin func reqOrgOwnership() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { + if rejectPublicOnly()(ctx); ctx.Written() { // org ownership reaches the org's private repos (hooks, teams, secrets) + return + } if ctx.IsUserSiteAdmin() { return } @@ -731,6 +664,10 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) { } } } + + if ctx.Org.Organization != nil && !user_model.DoerTokenAllowsOwner(ctx.Doer, ctx.Org.Organization.AsUser()) { + ctx.APIErrorNotFound() + } } } @@ -817,13 +754,6 @@ func mustEnableWiki(ctx *context.APIContext) { // visibility is too permissive for reads, org ownership too strict for writes. func reqProjectsUnitAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) { return func(ctx *context.APIContext) { - // "/users/{username}/projects" also accepts an organization, where checkTokenPublicOnly - // does nothing because IsTokenAccessAllowed is false for orgs. Enforce it here, before - // the admin bypass, so both spellings of the route answer alike. - if ctx.PublicOnly && ctx.ContextUser.IsOrganization() && !ctx.ContextUser.Visibility.IsPublic() { - ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs") - return - } if ctx.IsUserSiteAdmin() { return } @@ -1133,7 +1063,7 @@ func Routes() *web.Router { }, reqSelfOrAdmin(), reqBasicOrRevProxyAuth()) m.Get("/activities/feeds", user.ListUserActivityFeeds) - }, context.UserAssignmentAPI(), checkTokenPublicOnly(), individualPermsChecker) + }, context.UserAssignmentAPI(), individualPermsChecker) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser)) // Users (requires user scope) @@ -1151,7 +1081,7 @@ func Routes() *web.Router { m.Get("/starred", reqStarsEnabled(), user.GetStarredRepos) m.Get("/subscriptions", user.GetWatchedRepos) - }, context.UserAssignmentAPI(), checkTokenPublicOnly(), individualPermsChecker) + }, context.UserAssignmentAPI(), individualPermsChecker) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken()) // Users (requires user scope) @@ -1249,7 +1179,7 @@ func Routes() *web.Router { m.Get("", user.IsStarring) m.Put("", user.Star) m.Delete("", user.Unstar) - }, repoAssignment(), checkTokenPublicOnly()) + }, repoAssignment()) }, reqStarsEnabled(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) m.Get("/times", rejectPublicOnly(), repo.ListMyTrackedTimes) m.Get("/stopwatches", rejectPublicOnly(), repo.GetStopwatches) @@ -1274,15 +1204,16 @@ func Routes() *web.Router { m.Get("", user.CheckUserBlock) m.Put("", user.BlockUser) m.Delete("", user.UnblockUser) - }, context.UserAssignmentAPI(), checkTokenPublicOnly()) + }, context.UserAssignmentAPI()) }, rejectPublicOnly()) - }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken(), contextAuthenticatedUser(), checkTokenPublicOnly()) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken(), contextAuthenticatedUser()) // Repositories (requires repo scope, org scope) m.Post("/org/{org}/repos", // FIXME: we need org in context tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization, auth_model.AccessTokenScopeCategoryRepository), reqToken(), + rejectPublicOnly(), bind(api.CreateRepoOption{}), repo.CreateOrgRepoDeprecated) @@ -1303,12 +1234,12 @@ func Routes() *web.Router { m.Combo("").Get(reqAnyRepoReader(), repo.Get). Delete(reqToken(), reqRepoDangerZone(), repo.Delete). Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit) - m.Post("/generate", reqToken(), reqRepoReader(unit.TypeCode), bind(api.GenerateRepoOption{}), repo.Generate) + m.Post("/generate", reqToken(), rejectPublicOnly(), reqRepoReader(unit.TypeCode), bind(api.GenerateRepoOption{}), repo.Generate) m.Group("/transfer", func() { m.Post("", reqRepoDangerZone(), bind(api.TransferRepoOption{}), repo.Transfer) m.Post("/accept", repo.AcceptTransfer) m.Post("/reject", repo.RejectTransfer) - }, reqToken()) + }, reqToken(), rejectPublicOnly()) // Adds the routes for secrets/variables and runner management addActionsRoutes(m, reqRepoReader(unit.TypeActions), reqOwner(), repo.NewAction()) @@ -1607,7 +1538,7 @@ func Routes() *web.Router { }, reqAdmin(), reqToken()) m.Methods("HEAD,GET", "/{ball_type:tarball|zipball|bundle}/*", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true), repo.DownloadArchive) - }, repoAssignment(), checkTokenPublicOnly()) + }, repoAssignment()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) // Artifacts direct download endpoint authenticates via signed url @@ -1620,7 +1551,7 @@ func Routes() *web.Router { m.Combo("/notifications", reqToken()). Get(notify.ListRepoNotifications). Put(notify.ReadRepoNotifications) - }, repoAssignment(), checkTokenPublicOnly()) + }, repoAssignment()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification)) // Issue (requires issue scope) @@ -1746,7 +1677,7 @@ func Routes() *web.Router { m.Group("/projects", func() { addProjectRoutes(m, reqToken(), reqRepoWriter(unit.TypeProjects), mustNotBeArchived) }, reqRepoReader(unit.TypeProjects), mustEnableRepoProjects) - }, repoAssignment(), checkTokenPublicOnly()) + }, repoAssignment()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue)) // NOTE: these are Gitea package management API - see packages.CommonRoutes and packages.DockerContainerRoutes for endpoints that implement package manager APIs @@ -1769,15 +1700,15 @@ func Routes() *web.Router { }) m.Get("/", packages.ListPackages) - }, reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead), checkTokenPublicOnly()) + }, reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead)) // Organizations - m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), checkTokenPublicOnly(), org.ListMyOrgs) + m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs) m.Group("/users/{username}/orgs", func() { m.Get("", reqToken(), org.ListUserOrgs) m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions) - }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI(), checkTokenPublicOnly(), individualPermsChecker) - m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI(), individualPermsChecker) + m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), rejectPublicOnly(), bind(api.CreateOrgOption{}), org.Create) m.Get("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), org.GetAll) m.Group("/orgs/{org}", func() { m.Combo("").Get(org.Get). @@ -1785,7 +1716,7 @@ func Routes() *web.Router { Delete(reqToken(), reqOrgOwnership(), org.Delete) m.Post("/rename", reqToken(), reqOrgOwnership(), bind(api.RenameOrgOption{}), org.Rename) m.Combo("/repos").Get(user.ListOrgRepos). - Post(reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), bind(api.CreateRepoOption{}), repo.CreateOrgRepo). + Post(reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), rejectPublicOnly(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo). Delete(reqToken(), reqOrgOwnership(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), org.DeleteOrgRepos) m.Group("/members", func() { m.Get("", reqToken(), org.ListMembers) @@ -1840,7 +1771,7 @@ func Routes() *web.Router { m.Delete("", org.UnblockUser) }) }, reqToken(), reqOrgOwnership()) - }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly()) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true)) m.Group("/teams/{teamid}", func() { m.Combo("").Patch(reqToken(), reqOrgOwnership(), bind(api.EditTeamOption{}), org.EditTeam). Delete(reqToken(), reqOrgOwnership(), org.DeleteTeam) @@ -1866,7 +1797,7 @@ func Routes() *web.Router { Put(reqToken(), reqTeamMembership(), org.AddTeamRepository). Delete(reqToken(), reqTeamMembership(), org.RemoveTeamRepository) }) - }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), checkTokenPublicOnly()) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken()) m.Group("/admin", func() { m.Group("/cron", func() { @@ -1921,7 +1852,7 @@ func Routes() *web.Router { m.Get("/runs", admin.ListWorkflowRuns) m.Get("/jobs", admin.ListWorkflowJobs) }) - }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin()) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin(), rejectPublicOnly()) m.Group("/topics", func() { m.Get("/search", repo.TopicSearch) diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go index 11b46a05c1c..9c66fdad0ed 100644 --- a/routers/api/v1/org/member.go +++ b/routers/api/v1/org/member.go @@ -82,7 +82,7 @@ func ListMembers(ctx *context.APIContext) { err error ) - if ctx.Doer != nil { + if user_model.DoerCanViewPrivate(ctx.Doer) { isMember, err = ctx.Org.Organization.IsOrgMember(ctx, ctx.Doer.ID) if err != nil { ctx.APIErrorInternal(err) @@ -150,7 +150,7 @@ func IsMember(ctx *context.APIContext) { if ctx.Written() { return } - if ctx.Doer != nil { + if user_model.DoerCanViewPrivate(ctx.Doer) { userIsMember, err := ctx.Org.Organization.IsOrgMember(ctx, ctx.Doer.ID) if err != nil { ctx.APIErrorInternal(err) diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index ed450d21e92..7b17e12773b 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -42,7 +42,6 @@ func listUserOrgs(ctx *context.APIContext, u *user_model.User) { UserID: u.ID, IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, u), } - opts.ApplyPublicOnly(ctx.PublicOnly) orgs, maxResults, err := db.FindAndCount[organization.Organization](ctx, opts) if err != nil { ctx.APIErrorInternal(err) @@ -147,13 +146,6 @@ func GetUserOrgsPermissions(ctx *context.APIContext) { op := api.OrganizationPermissions{} - // A public-only token must not disclose membership/permission details of a - // non-public org, even for the token owner's own private orgs. - if ctx.PublicOnly && !o.Visibility.IsPublic() { - ctx.APIErrorNotFound() - return - } - if !organization.HasOrgOrUserVisible(ctx, o, ctx.Doer) { ctx.APIErrorNotFound() return @@ -225,7 +217,6 @@ func GetAll(ctx *context.APIContext) { OrderBy: db.SearchOrderByAlphabetically, Visible: vMode, } - searchOpts.ApplyPublicOnly(ctx.PublicOnly) publicOrgs, maxResults, err := user_model.SearchUsers(ctx, searchOpts) if err != nil { @@ -509,7 +500,6 @@ func ListOrgActivityFeeds(ctx *context.APIContext) { Date: ctx.FormString("date"), ListOptions: listOptions, } - opts.ApplyPublicOnly(ctx.PublicOnly) feeds, count, err := feed_service.GetFeeds(ctx, opts) if err != nil { diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index 21a8adba9bd..0dcf95003a3 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -544,13 +544,10 @@ func GetTeamRepos(ctx *context.APIContext) { team := ctx.Org.Team listOptions := utils.GetListOptions(ctx) - // A public-only token must not expose (or count) private repos, even when the - // doer owning the token otherwise has access to them, so filter them out at the - // query level to keep the returned page and the total-count header consistent. searchOpts := &repo_model.SearchTeamRepoOptions{ ListOptions: listOptions, TeamID: team.ID, - PublicOnly: ctx.PublicOnly, + Actor: ctx.Doer, } teamRepos, err := repo_model.GetTeamRepositories(ctx, searchOpts) if err != nil { @@ -619,12 +616,6 @@ func GetTeamRepo(ctx *context.APIContext) { return } - // A public-only token must not confirm the existence of a private repo. - if !ctx.TokenCanAccessRepo(repo) { - ctx.APIErrorNotFound() - return - } - if !organization.HasTeamRepo(ctx, ctx.Org.Team.OrgID, ctx.Org.Team.ID, repo.ID) { ctx.APIErrorNotFound() return @@ -881,8 +872,6 @@ func ListTeamActivityFeeds(ctx *context.APIContext) { Date: ctx.FormString("date"), ListOptions: listOptions, } - // A public-only token must not receive private activity entries. - opts.ApplyPublicOnly(ctx.PublicOnly) feeds, count, err := feed_service.GetFeeds(ctx, opts) if err != nil { diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 2b646d45837..e47f95f37cf 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -137,10 +137,9 @@ func SearchIssues(ctx *context.APIContext) { isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state")) repoIDs, allPublic, err := common.SearchIssuesRepoIDs(ctx, common.SearchIssuesRepoIDsOptions{ - Doer: ctx.Doer, - PublicOnly: ctx.PublicOnly, - OwnerName: ctx.FormString("owner"), - TeamName: ctx.FormString("team"), + Doer: ctx.Doer, + OwnerName: ctx.FormString("owner"), + TeamName: ctx.FormString("team"), }) if err != nil { if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) { diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 61b6367d3e1..d853adeef7f 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -1069,7 +1069,15 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git ctx.APIErrorInternal(err) return nil, nil } - if !ctx.TokenCanAccessRepo(headRepo) { + // user should have permission to read headRepo's codes + // TODO: could the logic be simplified if the headRepo is the same as the baseRepo? Need to think more about it. + permHead, err := access_model.GetDoerRepoPermission(ctx, headRepo, ctx.Doer) + if err != nil { + ctx.APIErrorInternal(err) + return nil, nil + } + if !permHead.CanRead(unit.TypeCode) { + log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v", ctx.Doer, headRepo, permHead) ctx.APIErrorNotFound() return nil, nil } @@ -1107,19 +1115,6 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git return nil, nil } - // user should have permission to read headRepo's codes - // TODO: could the logic be simplified if the headRepo is the same as the baseRepo? Need to think more about it. - permHead, err := access_model.GetDoerRepoPermission(ctx, headRepo, ctx.Doer) - if err != nil { - ctx.APIErrorInternal(err) - return nil, nil - } - if !permHead.CanRead(unit.TypeCode) { - log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v", ctx.Doer, headRepo, permHead) - ctx.APIErrorNotFound("Can't read headRepo UnitTypeCode") - return nil, nil - } - baseRef, err := common.ResolveRefWithSuffix(ctx, ctx.Repo.GitRepo, util.IfZero(compareReq.BaseOriRef, baseRepo.GetPullRequestTargetBranch(ctx)), compareReq.BaseOriRefSuffix) if err != nil { ctx.APIErrorAuto(err) @@ -1220,18 +1215,6 @@ func UpdatePullRequest(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } - if err = pr.LoadHeadRepo(ctx); err != nil { - ctx.APIErrorInternal(err) - return - } - - // a public-only token must not update (push into) a private head repo, - // even when the base repo named in the route is public - if !ctx.TokenCanAccessRepo(pr.HeadRepo) { - ctx.APIErrorNotFound() - return - } - // keep API back-compat: when no style is given, default to "merge" rather than the repo's DefaultUpdateStyle, // so existing API clients keep getting a merge update. rebase := repo_model.UpdateStyle(ctx.FormString("style", string(repo_model.UpdateStyleMerge))) == repo_model.UpdateStyleRebase diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go index 0448a1961b4..62c0897a470 100644 --- a/routers/api/v1/repo/release.go +++ b/routers/api/v1/repo/release.go @@ -10,6 +10,7 @@ import ( "gitea.dev/models/db" repo_model "gitea.dev/models/repo" "gitea.dev/models/unit" + user_model "gitea.dev/models/user" "gitea.dev/modules/git" api "gitea.dev/modules/structs" "gitea.dev/modules/web" @@ -23,7 +24,7 @@ func canAccessReleaseDraft(ctx *context.APIContext) bool { if !ctx.IsSigned || !ctx.Repo.Permission.CanWrite(unit.TypeReleases) { return false } - scope, hasApiTokenScope := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) + scope, hasApiTokenScope := user_model.GetDoerTokenScope(ctx.Doer) if !hasApiTokenScope { // not API token request, the request is from a user session with write access return true diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 73f93a9c3cc..6ac4de3961b 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -151,7 +151,6 @@ func Search(ctx *context.APIContext) { StarredByID: ctx.FormInt64("starredBy"), IncludeDescription: ctx.FormBool("includeDesc"), } - opts.ApplyPublicOnly(ctx.PublicOnly) if ctx.FormString("template") != "" { opts.Template = optional.Some(ctx.FormBool("template")) @@ -557,11 +556,6 @@ func GetByID(ctx *context.APIContext) { } return } - if !ctx.TokenCanAccessRepo(repo) { - ctx.APIErrorNotFound() - return - } - permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer) if err != nil { ctx.APIErrorInternal(err) @@ -1306,7 +1300,6 @@ func ListRepoActivityFeeds(ctx *context.APIContext) { Date: ctx.FormString("date"), ListOptions: listOptions, } - opts.ApplyPublicOnly(ctx.PublicOnly) feeds, count, err := feed_service.GetFeeds(ctx, opts) if err != nil { diff --git a/routers/api/v1/shared/action.go b/routers/api/v1/shared/action.go index 7d8ca3485e9..09edd3e1484 100644 --- a/routers/api/v1/shared/action.go +++ b/routers/api/v1/shared/action.go @@ -21,21 +21,8 @@ import ( "gitea.dev/routers/api/v1/utils" "gitea.dev/services/context" "gitea.dev/services/convert" - - "xorm.io/builder" ) -// actionsOwnerAccessibleRepoIDsSubQuery returns the sub-query restricting an owner-scoped actions -// listing to the repos whose actions the caller can read, or nil when no restriction applies. A bare -// org member must not be able to enumerate runs/jobs of repos they have no access to. A site admin may -// skip the access filter, but a public-only token must stay confined to public repos even for an admin. -func actionsOwnerAccessibleRepoIDsSubQuery(ctx *context.APIContext, ownerID int64) *builder.Builder { - if ownerID > 0 && (ctx.Doer == nil || !ctx.Doer.IsAdmin || ctx.PublicOnly) { - return repo_model.FindUserActionsAccessibleOwnerRepoIDsSubQuery(ownerID, ctx.Doer, ctx.PublicOnly) - } - return nil -} - // ListJobs lists jobs for api route validated ownerID and repoID // ownerID == 0 and repoID == 0 means all jobs // ownerID == 0 and repoID != 0 means all jobs for the given repo @@ -73,7 +60,7 @@ func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64, runAttemptI opts.Statuses = append(opts.Statuses, values...) } - opts.AccessibleRepoIDsSubQuery = actionsOwnerAccessibleRepoIDsSubQuery(ctx, opts.OwnerID) + opts.AccessibleRepoIDsSubQuery = repo_model.FindUserActionsAccessibleOwnerRepoIDsSubQuery(opts.OwnerID, ctx.Doer) jobs, total, err := db.FindAndCount[actions_model.ActionRunJob](ctx, opts) if err != nil { @@ -185,7 +172,7 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64, workflowID string) } excludePullRequests := ctx.FormBool("exclude_pull_requests") - opts.AccessibleRepoIDsSubQuery = actionsOwnerAccessibleRepoIDsSubQuery(ctx, opts.OwnerID) + opts.AccessibleRepoIDsSubQuery = repo_model.FindUserActionsAccessibleOwnerRepoIDsSubQuery(opts.OwnerID, ctx.Doer) runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, opts) if err != nil { diff --git a/routers/api/v1/shared/project.go b/routers/api/v1/shared/project.go index bd196eb625d..cc0c4e15d87 100644 --- a/routers/api/v1/shared/project.go +++ b/routers/api/v1/shared/project.go @@ -1686,11 +1686,10 @@ func ListProjectColumnIssues(ctx *context.APIContext) { } else { // an owner-level board spans repositories, so filter to what this doer may see issuesOpts.Owner = scope.Owner - issuesOpts.Doer = ctx.Doer - issuesOpts.AllPublic = ctx.Doer == nil - if ctx.PublicOnly { + if user_model.DoerCanViewPrivate(ctx.Doer) { + issuesOpts.Doer = ctx.Doer + } else { issuesOpts.AllPublic = true - issuesOpts.Doer = nil // a public-only token must not reach the doer's private repos } } diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go index a5df04a2cbc..6a7785f8557 100644 --- a/routers/api/v1/user/app.go +++ b/routers/api/v1/user/app.go @@ -13,6 +13,7 @@ import ( audit_model "gitea.dev/models/audit" auth_model "gitea.dev/models/auth" "gitea.dev/models/db" + user_model "gitea.dev/models/user" api "gitea.dev/modules/structs" "gitea.dev/modules/web" "gitea.dev/routers/api/v1/utils" @@ -129,7 +130,7 @@ func CreateAccessToken(ctx *context.APIContext) { t.Scope = scope // a token-authenticated request must not mint a token with a broader scope than its own - apiTokenScope, hasApiTokenScope := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) + apiTokenScope, hasApiTokenScope := user_model.GetDoerTokenScope(ctx.Doer) if hasApiTokenScope { hasScope, err := apiTokenScope.CanCreateChildScope(scope) if err != nil { diff --git a/routers/api/v1/user/repo.go b/routers/api/v1/user/repo.go index b9dbde73565..1743aa37ff0 100644 --- a/routers/api/v1/user/repo.go +++ b/routers/api/v1/user/repo.go @@ -19,15 +19,13 @@ import ( func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) { opts := utils.GetListOptions(ctx) - searchOpts := repo_model.SearchRepoOptions{ - Actor: u, + repos, count, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{ + OwnerID: u.ID, + Actor: ctx.Doer, Private: private, ListOptions: opts, OrderBy: "id ASC", - } - searchOpts.ApplyPublicOnly(ctx.PublicOnly) - - repos, count, err := repo_model.GetUserRepositories(ctx, searchOpts) + }) if err != nil { ctx.APIErrorInternal(err) return @@ -112,7 +110,6 @@ func ListMyRepos(ctx *context.APIContext) { Private: ctx.IsSigned, IncludeDescription: true, } - opts.ApplyPublicOnly(ctx.PublicOnly) repos, count, err := repo_model.SearchRepository(ctx, opts) if err != nil { diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go index 146973021a5..864dc070074 100644 --- a/routers/api/v1/user/star.go +++ b/routers/api/v1/user/star.go @@ -24,9 +24,8 @@ func getStarredRepos(ctx *context.APIContext, user *user_model.User, private boo ListOptions: utils.GetListOptions(ctx), StarrerID: user.ID, IncludePrivate: private, - Actor: user, + Actor: ctx.Doer, } - opts.ApplyPublicOnly(ctx.PublicOnly) starredRepos, err := repo_model.GetStarredRepos(ctx, opts) if err != nil { diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index 22bd2b9e494..544bbc940be 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -73,7 +73,6 @@ func Search(ctx *context.APIContext) { SearchByEmail: true, ListOptions: listOptions, } - opts.ApplyPublicOnly(ctx.PublicOnly) var err error users, maxResults, err = user_model.SearchUsers(ctx, opts) if err != nil { @@ -208,7 +207,6 @@ func ListUserActivityFeeds(ctx *context.APIContext) { Date: ctx.FormString("date"), ListOptions: listOptions, } - opts.ApplyPublicOnly(ctx.PublicOnly) feeds, count, err := feed_service.GetFeeds(ctx, opts) if err != nil { diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go index d5f3fc223ee..f552fd9f28d 100644 --- a/routers/api/v1/user/watch.go +++ b/routers/api/v1/user/watch.go @@ -22,9 +22,8 @@ func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private boo ListOptions: utils.GetListOptions(ctx), WatcherID: user.ID, IncludePrivate: private, - Actor: user, + Actor: ctx.Doer, } - opts.ApplyPublicOnly(ctx.PublicOnly) watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, opts) if err != nil { diff --git a/routers/common/issue_filter.go b/routers/common/issue_filter.go index b11d5bc9b16..445d8106f2a 100644 --- a/routers/common/issue_filter.go +++ b/routers/common/issue_filter.go @@ -31,10 +31,9 @@ func ParseIssueFilterTypeIsPull(typ string) optional.Option[bool] { } type SearchIssuesRepoIDsOptions struct { - Doer *user_model.User - PublicOnly bool - OwnerName string - TeamName string + Doer *user_model.User + OwnerName string + TeamName string } // SearchIssuesRepoIDs resolves the repository filter of an issue search. allPublic makes the indexer @@ -45,7 +44,6 @@ func SearchIssuesRepoIDs(ctx context.Context, opts SearchIssuesRepoIDsOptions) ( Collaborate: optional.None[bool](), Actor: opts.Doer, } - searchOpts.ApplyPublicOnly(opts.PublicOnly) if opts.OwnerName != "" { owner, err := user_model.GetUserByName(ctx, opts.OwnerName) if err != nil { @@ -69,7 +67,7 @@ func SearchIssuesRepoIDs(ctx context.Context, opts SearchIssuesRepoIDsOptions) ( allPublic = opts.OwnerName == "" cond := repo_model.SearchRepositoryCondition(searchOpts) if allPublic { - if !searchOpts.Private { + if !user_model.DoerCanViewPrivate(opts.Doer) { return []int64{0}, allPublic, nil // sees nothing beyond is_public, so skip the query } cond = cond.And(repo_model.NotPublicRepoUnderPublicOwnerCond()) // enumerating them scales with the instance diff --git a/routers/common/issue_filter_test.go b/routers/common/issue_filter_test.go index 6cffeef45f4..153f66b1768 100644 --- a/routers/common/issue_filter_test.go +++ b/routers/common/issue_filter_test.go @@ -16,6 +16,8 @@ import ( func TestSearchIssuesRepoIDs(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) + publicOnlyUser2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + publicOnlyUser2.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialAccessToken, 1, "public-only,read:issue") // the indexer's is_public covers repo 1 (public under a public owner) but misses repo 38 (public // under a limited org) and repo 40 (public under a private org) @@ -52,8 +54,7 @@ func TestSearchIssuesRepoIDs(t *testing.T) { }, { name: "public-only token", - doerID: 2, - opts: SearchIssuesRepoIDsOptions{PublicOnly: true}, + opts: SearchIssuesRepoIDsOptions{Doer: publicOnlyUser2}, allPublic: true, want: []int64{0}, }, diff --git a/routers/web/auth/oauth2_provider.go b/routers/web/auth/oauth2_provider.go index e969f81d0e1..edeb46e361e 100644 --- a/routers/web/auth/oauth2_provider.go +++ b/routers/web/auth/oauth2_provider.go @@ -103,7 +103,7 @@ func InfoOAuth(ctx *context.Context) { // enforce the same user scope the REST API requires before returning identity // claims; OIDC access tokens map to the "all" scope, so standard OIDC clients // are unaffected and only explicitly-restricted tokens are rejected - tokenScope, _ := ctx.Data["ApiTokenScope"].(auth.AccessTokenScope) + tokenScope, _ := user_model.GetDoerTokenScope(ctx.Doer) if allowed, err := tokenScope.HasScope(auth.AccessTokenScopeReadUser); err != nil { ctx.ServerError("HasScope", err) return @@ -121,17 +121,7 @@ func InfoOAuth(ctx *context.Context) { Picture: ctx.Doer.AvatarLink(ctx), } - var accessTokenScope auth.AccessTokenScope - if auHead := ctx.Req.Header.Get("Authorization"); auHead != "" { - if parsed, ok := httpauth.ParseAuthorizationHeader(auHead); ok && parsed.BearerToken != nil { - accessTokenScope, _, _ = auth_service.GetOAuthAccessTokenScopeAndUserID(ctx, parsed.BearerToken.Token) - } - } - - // since version 1.22 does not verify if groups should be public-only, - // onlyPublicGroups will be set only if 'public-only' is included in a valid scope - onlyPublicGroups, _ := accessTokenScope.PublicOnly() - groups, err := oauth2_provider.GetOAuthGroupsForUser(ctx, ctx.Doer, onlyPublicGroups) + groups, err := oauth2_provider.GetOAuthGroupsForUser(ctx, ctx.Doer, user_model.IsPublicOnlyDoer(ctx.Doer)) if err != nil { ctx.ServerError("Oauth groups for user", err) return diff --git a/routers/web/feed/profile.go b/routers/web/feed/profile.go index 688574140b2..14320890931 100644 --- a/routers/web/feed/profile.go +++ b/routers/web/feed/profile.go @@ -50,11 +50,6 @@ func showUserFeed(ctx *context.Context, formatType string) { includePrivate = isOrgMember } - // a public-only API token must not surface private activity, even for its own owner - if includePrivate && context.TokenIsPublicOnly(ctx) { - includePrivate = false - } - actions, _, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{ RequestedUser: ctx.ContextUser, Actor: ctx.Doer, diff --git a/routers/web/feed/render.go b/routers/web/feed/render.go index d06aa22e64c..d2af9824c4d 100644 --- a/routers/web/feed/render.go +++ b/routers/web/feed/render.go @@ -12,7 +12,7 @@ import ( // feed serves private repository content, mirroring checkDownloadTokenScope for // downloads. Returns false (and writes the response) when the token is denied. func checkRepoFeedTokenScope(ctx *context.Context) bool { - context.CheckRepoScopedToken(ctx, ctx.Repo.Repository, auth_model.Read) + context.CheckRepoScopedToken(ctx, auth_model.Read) return !ctx.Written() } diff --git a/routers/web/goget.go b/routers/web/goget.go index d4af7b3518f..f80134fc647 100644 --- a/routers/web/goget.go +++ b/routers/web/goget.go @@ -104,10 +104,6 @@ func goGetDefaultBranch(ctx *context.Context, repo *repo_model.Repository) strin if !goGetTokenCanReadRepo(ctx) { return def } - // a public-only token may only reach genuinely public resources (a public repo under a public owner) - if context.TokenIsPublicOnly(ctx) && (repo.IsPrivate || !repo.Owner.Visibility.IsPublic()) { - return def - } // the caller must be able to read the code and see the owner: a limited/private owner hides its repos // from anonymous/non-member callers even when the repo itself is public perm, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer) @@ -121,7 +117,7 @@ func goGetDefaultBranch(ctx *context.Context, repo *repo_model.Repository) strin // always may; a token request may only when its scope grants repository read, so a PAT that was never // scoped for repositories cannot disclose the branch even if its owner can read the repo. func goGetTokenCanReadRepo(ctx *context.Context) bool { - scope, hasApiTokenScope := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) + scope, hasApiTokenScope := user_model.GetDoerTokenScope(ctx.Doer) if !hasApiTokenScope { return true } diff --git a/routers/web/repo/actions/badge.go b/routers/web/repo/actions/badge.go index 01a78a7f190..34eb3f6ac3a 100644 --- a/routers/web/repo/actions/badge.go +++ b/routers/web/repo/actions/badge.go @@ -18,7 +18,7 @@ import ( ) func GetWorkflowBadge(ctx *context.Context) { - context.CheckRepoScopedToken(ctx, ctx.Repo.Repository, auth_model.Read) + context.CheckRepoScopedToken(ctx, auth_model.Read) if ctx.Written() { return } diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go index 1fa46b0c189..c4b74c0832d 100644 --- a/routers/web/repo/attachment.go +++ b/routers/web/repo/attachment.go @@ -203,7 +203,7 @@ func ServeAttachment(ctx *context.Context, uuid string) { } if requiredScope, ok := attachmentReadScope(unitType); ok { - context.CheckTokenScopes(ctx, repo, requiredScope) + context.CheckTokenScopes(ctx, requiredScope) if ctx.Written() { return } diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go index ed6de00092e..db90d6e3e60 100644 --- a/routers/web/repo/download.go +++ b/routers/web/repo/download.go @@ -20,7 +20,7 @@ import ( ) func checkDownloadTokenScope(ctx *context.Context) bool { - context.CheckRepoScopedToken(ctx, ctx.Repo.Repository, auth_model.Read) + context.CheckRepoScopedToken(ctx, auth_model.Read) return !ctx.Written() } diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index c5a042aab63..5042e8f8516 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -22,6 +22,7 @@ import ( access_model "gitea.dev/models/perm/access" repo_model "gitea.dev/models/repo" "gitea.dev/models/unit" + user_model "gitea.dev/models/user" "gitea.dev/modules/git" "gitea.dev/modules/git/gitcmd" "gitea.dev/modules/git/gitrepo" @@ -159,12 +160,12 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler { return nil } - context.CheckRepoScopedToken(ctx, repo, auth_model.GetScopeLevelFromAccessMode(accessMode)) + context.CheckRepoScopedToken(ctx, auth_model.GetScopeLevelFromAccessMode(accessMode)) if ctx.Written() { return nil } - if ctx.IsBasicAuth && ctx.Data["ApiTokenScope"] == nil && ctx.Doer.IsIndividual() { + if _, isToken := user_model.GetDoerTokenScope(ctx.Doer); ctx.IsBasicAuth && !isToken && ctx.Doer.IsIndividual() { _, err = auth_model.GetTwoFactorByUID(ctx, ctx.Doer.ID) if err == nil { // TODO: This response should be changed to "invalid credentials" for security reasons once the expectation behind it (creating an app token to authenticate) is properly documented diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index 55ef4e988b5..91f208bfb1a 100644 --- a/routers/web/repo/view_home.go +++ b/routers/web/repo/view_home.go @@ -433,7 +433,7 @@ func Home(ctx *context.Context) { // a scoped or public-only API token authenticating this web request must still satisfy // the repository read scope before private repo content is served - context.CheckRepoScopedToken(ctx, ctx.Repo.Repository, auth_model.Read) + context.CheckRepoScopedToken(ctx, auth_model.Read) if ctx.Written() { return } diff --git a/routers/web/user/home_test.go b/routers/web/user/home_test.go index a0fd73ffd9e..6ecac8dbf5f 100644 --- a/routers/web/user/home_test.go +++ b/routers/web/user/home_test.go @@ -27,7 +27,7 @@ func TestArchivedIssues(t *testing.T) { ctx.Req.Form.Set("state", "open") // Assume: User 30 has access to two Repos with Issues, one of the Repos being archived. - repos, _, _ := repo_model.GetUserRepositories(t.Context(), repo_model.SearchRepoOptions{Actor: ctx.Doer}) + repos, _, _ := repo_model.GetUserRepositories(t.Context(), repo_model.SearchRepoOptions{OwnerID: ctx.Doer.ID}) assert.Len(t, repos, 3) IsArchived := make(map[int64]bool) NumIssues := make(map[int64]int) diff --git a/routers/web/user/package.go b/routers/web/user/package.go index 185d3b6d7b8..d4791ee6a60 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -303,7 +303,7 @@ func ViewPackageVersion(ctx *context.Context) { ctx.Data["PackageVersionViewData"] = viewData ctx.Data["PackageVersionSetupManual"] = pkgSpec.RenderSetupManual(ctx, pd, viewData) - ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite || ctx.IsUserSiteAdmin() + ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite hasRepositoryAccess := false if pd.Repository != nil { @@ -417,7 +417,7 @@ func PackageSettings(ctx *context.Context) { ctx.Data["Title"] = pd.Package.Name ctx.Data["IsPackagesPage"] = true ctx.Data["PackageDescriptor"] = pd - ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite || ctx.IsUserSiteAdmin() + ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite if pd.Package.RepoID > 0 { repo, err := repo_model.GetRepositoryByID(ctx, pd.Package.RepoID) diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index a342e3b1631..2f232c24710 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -168,10 +168,6 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R date := ctx.FormString("date") pagingNum = setting.UI.FeedPagingNum showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID) - // a public-only API token must not surface private activity, even for its own owner - if showPrivate && context.TokenIsPublicOnly(ctx) { - showPrivate = false - } items, feedCount, err := feed_service.GetFeedsForDashboard(ctx, activities_model.GetFeedsOptions{ RequestedUser: ctx.ContextUser, Actor: ctx.Doer, diff --git a/routers/web/user/setting/applications.go b/routers/web/user/setting/applications.go index 5e4845c534b..60e9f46fcca 100644 --- a/routers/web/user/setting/applications.go +++ b/routers/web/user/setting/applications.go @@ -108,7 +108,7 @@ func CreateAccessToken(ctx *context.Context, owner *user_model.User) { // a token-authenticated request must not mint a token with a broader scope than its own, nor // drop the public-only restriction. Web routes accept basic-auth PATs/OAuth tokens too, so this // must mirror the REST API guard in routers/api/v1/user/app.go. - apiTokenScope, hasApiTokenScope := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) + apiTokenScope, hasApiTokenScope := user_model.GetDoerTokenScope(ctx.Doer) if hasApiTokenScope { hasScope, err := apiTokenScope.CanCreateChildScope(t.Scope) if err != nil { diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index 9b7cdb2ea9f..99cb7db5495 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -276,7 +276,7 @@ func Repos(ctx *context.Context) { } userRepos, _, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{ - Actor: ctxUser, + OwnerID: ctxUser.ID, Private: true, ListOptions: db.ListOptions{ Page: 1, @@ -300,7 +300,7 @@ func Repos(ctx *context.Context) { ctx.Data["Dirs"] = repoNames ctx.Data["ReposMap"] = repos } else { - repos, reposCount, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts}) + repos, reposCount, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{OwnerID: ctxUser.ID, Private: true, ListOptions: opts}) if err != nil { ctx.ServerError("GetUserRepositories", err) return diff --git a/routers/web/web.go b/routers/web/web.go index cfd1e769ba2..e786185b9f0 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -930,7 +930,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { reqPackageAccess := func(accessMode perm.AccessMode) func(ctx *context.Context) { return func(ctx *context.Context) { - if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() { + if ctx.Package.AccessMode < accessMode { ctx.NotFound(nil) } } diff --git a/services/actions/notifier.go b/services/actions/notifier.go index 8e21e2bfd3e..cca6283d2fc 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -88,7 +88,7 @@ func (n *actionsNotifier) notifyIssueChangeWithTitleOrContent(ctx context.Contex WithPayload(&api.PullRequestPayload{ Action: api.HookIssueEdited, Index: issue.Index, - PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), + PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, nil), Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}), Sender: convert.ToUser(ctx, doer, nil), }). @@ -120,7 +120,7 @@ func (n *actionsNotifier) IssueChangeStatus(ctx context.Context, doer *user_mode // Merge pull request calls issue.changeStatus so we need to handle separately. apiPullRequest := &api.PullRequestPayload{ Index: issue.Index, - PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), + PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, nil), Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), CommitID: commitID, @@ -240,7 +240,7 @@ func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues payload := &api.PullRequestPayload{ Action: action, Index: issue.Index, - PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), + PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, nil), Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}), Sender: convert.ToUser(ctx, doer, nil), Changes: changes, @@ -374,7 +374,7 @@ func (n *actionsNotifier) NewPullRequest(ctx context.Context, pull *issues_model WithPayload(&api.PullRequestPayload{ Action: api.HookIssueOpened, Index: pull.Issue.Index, - PullRequest: convert.ToAPIPullRequest(ctx, pull, nil), + PullRequest: convert.ToPayloadPullRequest(ctx, pull, nil), Repository: convert.ToRepo(ctx, pull.Issue.Repo, permission), Sender: convert.ToUser(ctx, pull.Issue.Poster, nil), }). @@ -454,7 +454,7 @@ func (n *actionsNotifier) PullRequestReview(ctx context.Context, pr *issues_mode WithPayload(&api.PullRequestPayload{ Action: api.HookIssueReviewed, Index: review.Issue.Index, - PullRequest: convert.ToAPIPullRequest(ctx, pr, nil), + PullRequest: convert.ToPayloadPullRequest(ctx, pr, nil), Repository: convert.ToRepo(ctx, review.Issue.Repo, permission), Sender: convert.ToUser(ctx, review.Reviewer, nil), Review: &api.ReviewPayload{ @@ -488,7 +488,7 @@ func (n *actionsNotifier) PullRequestReviewRequest(ctx context.Context, doer *us WithPayload(&api.PullRequestPayload{ Action: action, Index: issue.Index, - PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), + PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, nil), RequestedReviewer: convert.ToUser(ctx, reviewer, nil), Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), @@ -525,7 +525,7 @@ func (*actionsNotifier) MergePullRequest(ctx context.Context, doer *user_model.U // Merge pull request calls issue.changeStatus so we need to handle separately. apiPullRequest := &api.PullRequestPayload{ Index: pr.Issue.Index, - PullRequest: convert.ToAPIPullRequest(ctx, pr, nil), + PullRequest: convert.ToPayloadPullRequest(ctx, pr, nil), Repository: convert.ToRepo(ctx, pr.Issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), Action: api.HookIssueClosed, @@ -695,7 +695,7 @@ func (n *actionsNotifier) PullRequestSynchronized(ctx context.Context, doer *use Before: before, After: after, Index: pr.Issue.Index, - PullRequest: convert.ToAPIPullRequest(ctx, pr, nil), + PullRequest: convert.ToPayloadPullRequest(ctx, pr, nil), Repository: convert.ToRepo(ctx, pr.Issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}), Sender: convert.ToUser(ctx, doer, nil), }). @@ -726,7 +726,7 @@ func (n *actionsNotifier) PullRequestChangeTargetBranch(ctx context.Context, doe From: oldBranch, }, }, - PullRequest: convert.ToAPIPullRequest(ctx, pr, nil), + PullRequest: convert.ToPayloadPullRequest(ctx, pr, nil), Repository: convert.ToRepo(ctx, pr.Issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }). diff --git a/services/audit/actor.go b/services/audit/actor.go index 0abcea34128..864a3e85b03 100644 --- a/services/audit/actor.go +++ b/services/audit/actor.go @@ -50,23 +50,6 @@ func doerFromContext(ctx context.Context) *user_model.User { return nil } -// credentialFromContext returns the credential the surrounding request -// authenticated with. It is dropped when the event is recorded for someone -// other than the signed-in user, so an explicit actor is never tied to a -// credential that is not theirs. -func credentialFromContext(ctx context.Context, doer *user_model.User) string { - data := middleware.GetContextData(ctx) - if data == nil { - return "" - } - signedUser, _ := data[middleware.ContextDataKeySignedUser].(*user_model.User) - if signedUser == nil || signedUser.ID != doer.ID { - return "" - } - credential, _ := data[middleware.ContextDataKeyAuthCredential].(string) - return credential -} - // ImpersonatorFromContext resolves the admin acting as the doer, so an event // recorded during an impersonated session cannot be pinned on the impersonated // user alone. Returns nil for ordinary sessions. diff --git a/services/audit/audit_test.go b/services/audit/audit_test.go index 5dc8aa98f37..f8818d63e45 100644 --- a/services/audit/audit_test.go +++ b/services/audit/audit_test.go @@ -106,25 +106,15 @@ func TestBuildEvent(t *testing.T) { e := buildEvent(context.Background(), RecordParams{ Action: audit_model.UserCreate, Actor: actorRef(actions), - ActorCredential: actorCredential(context.Background(), actions), + ActorCredential: user_model.GetDoerCredential(actions), Scope: ScopeFromUser(u), }) assert.Equal(t, user_model.ActionsUserID, e.ActorID) assert.Equal(t, "gitea-actions:42", e.ActorCredential) key := user_model.NewDeployKeyUserWithKeyID(7) - assert.Equal(t, "deploy-key:7", actorCredential(context.Background(), key)) - assert.Empty(t, actorCredential(context.Background(), doer)) - }) - - t.Run("CredentialFromRequest", func(t *testing.T) { - ctx := newRequestContext(t, doer) - middleware.GetContextData(ctx)[middleware.ContextDataKeyAuthCredential] = "access-token:9" - assert.Equal(t, "access-token:9", actorCredential(ctx, doer)) - - // an event recorded for someone other than the signed-in user is not - // tied to the credential of that request - assert.Empty(t, actorCredential(ctx, u)) + assert.Equal(t, "deploy-key:7", user_model.GetDoerCredential(key)) + assert.Empty(t, user_model.GetDoerCredential(doer)) }) t.Run("RequestInfo", func(t *testing.T) { diff --git a/services/audit/event.go b/services/audit/event.go index 3700aa1ecca..3a237107671 100644 --- a/services/audit/event.go +++ b/services/audit/event.go @@ -120,7 +120,7 @@ func RecordAs(ctx context.Context, doer *user_model.User, action audit_model.Act writeEvent(ctx, RecordParams{ Action: action, Actor: actorRef(doer), - ActorCredential: actorCredential(ctx, doer), + ActorCredential: user_model.GetDoerCredential(doer), Impersonator: impersonatorRef(ImpersonatorFromContext(ctx), doer), Scope: scopeRef(scope), Metadata: metaPairs(metadata...), diff --git a/services/audit/refs.go b/services/audit/refs.go index f88ea20d91d..7d85fe653ee 100644 --- a/services/audit/refs.go +++ b/services/audit/refs.go @@ -25,20 +25,6 @@ func actorRef(doer *user_model.User) audit_model.EntityRef { return audit_model.EntityRef{Type: audit_model.ScopeUser, ID: doer.ID, Name: doer.Name} } -// actorCredential names what the actor acted with: the task or key behind a -// system user, otherwise the token the surrounding request authenticated with. -// An incident then traces one credential across every event it produced, -// instead of stopping at the account that owns it. -func actorCredential(ctx context.Context, doer *user_model.User) string { - if doer == nil { - return "" - } - if doer.ExtDoerData != nil { - return doer.ExtDoerData.EncodeToString() - } - return credentialFromContext(ctx, doer) -} - // impersonatorRef names the admin behind an impersonated session. It is dropped // when the actor is the admin themselves, so events an admin performs before // entering or after leaving an impersonation are not marked as impersonated. diff --git a/services/audit/scoped.go b/services/audit/scoped.go index fc8495ead06..75603f5dccc 100644 --- a/services/audit/scoped.go +++ b/services/audit/scoped.go @@ -115,7 +115,7 @@ func RecordScoped(ctx context.Context, owner *user_model.User, repo *repository_ writeEvent(ctx, RecordParams{ Action: action, Actor: actorRef(doer), - ActorCredential: actorCredential(ctx, doer), + ActorCredential: user_model.GetDoerCredential(doer), Scope: scope, Metadata: metaPairs(metadata...), }) diff --git a/services/auth/basic.go b/services/auth/basic.go index feec2e57b51..14c31e4375e 100644 --- a/services/auth/basic.go +++ b/services/auth/basic.go @@ -28,9 +28,6 @@ var ( // BasicMethodName is the constant name of the basic authentication method const ( BasicMethodName = "basic" - AccessTokenMethodName = "access_token" - OAuth2TokenMethodName = "oauth2_token" - ActionTokenMethodName = "action_token" DeployTokenMethodName = "deploy_token" ) @@ -86,9 +83,7 @@ func (b *Basic) VerifyAuthToken(req *http.Request, w http.ResponseWriter, store return nil, nil //nolint:nilnil // the auth method is not applicable } - store.GetData()["LoginMethod"] = OAuth2TokenMethodName - store.GetData()["ApiTokenScope"] = accessTokenScope - setAuthCredential(store, credentialOAuth2Grant, grantID) + u.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialOAuth2Grant, grantID, accessTokenScope) return u, nil } @@ -107,9 +102,7 @@ func (b *Basic) VerifyAuthToken(req *http.Request, w http.ResponseWriter, store log.Error("UpdateAccessToken: %v", err) } - store.GetData()["LoginMethod"] = AccessTokenMethodName - store.GetData()["ApiTokenScope"] = token.Scope - setAuthCredential(store, credentialAccessToken, token.ID) + u.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialAccessToken, token.ID, token.Scope) return u, nil } else if !errors.Is(err, util.ErrNotExist) { log.Error("GetAccessTokenBySHA: %v", err) @@ -119,7 +112,6 @@ func (b *Basic) VerifyAuthToken(req *http.Request, w http.ResponseWriter, store task, err := actions_model.GetRunningTaskByToken(req.Context(), authToken) if err == nil && task != nil { log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID) - store.GetData()["LoginMethod"] = ActionTokenMethodName return user_model.NewActionsUserWithTaskID(task.ID), nil } return nil, nil //nolint:nilnil // the auth method is not applicable @@ -194,18 +186,12 @@ func validateTOTP(req *http.Request, u *user_model.User) error { return nil } -func GetAccessScope(store DataStore) auth_model.AccessTokenScope { - if scope, hasApiTokenScope := store.GetData()["ApiTokenScope"].(auth_model.AccessTokenScope); hasApiTokenScope { +func GetAccessScope(doer *user_model.User, store DataStore) auth_model.AccessTokenScope { + if scope, ok := user_model.GetDoerTokenScope(doer); ok { return scope } - switch store.GetData()["LoginMethod"] { - case OAuth2TokenMethodName: - fallthrough - case BasicMethodName, AccessTokenMethodName: + if store.GetData()["LoginMethod"] == BasicMethodName { return auth_model.AccessTokenScopeAll - case ActionTokenMethodName: - fallthrough - default: - return "" } + return "" } diff --git a/services/auth/credential.go b/services/auth/credential.go deleted file mode 100644 index 381507e7ccc..00000000000 --- a/services/auth/credential.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2026 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package auth - -import ( - "strconv" - - "gitea.dev/modules/web/middleware" -) - -// Credential kinds naming how a request authenticated, recorded so an audit -// event can point at the token rather than only at its owner. -const ( - credentialAccessToken = "access-token" - credentialOAuth2Grant = "oauth2-grant" -) - -func setAuthCredential(store DataStore, kind string, id int64) { - store.GetData()[middleware.ContextDataKeyAuthCredential] = kind + ":" + strconv.FormatInt(id, 10) -} diff --git a/services/auth/deploy_token.go b/services/auth/deploy_token.go index bbe76e557ab..39ef7c73c47 100644 --- a/services/auth/deploy_token.go +++ b/services/auth/deploy_token.go @@ -42,6 +42,5 @@ func (d *DeployToken) Verify(req *http.Request, _ http.ResponseWriter, store Dat log.Error("UpdateDeployKeyUpdated: %v", err) } - store.GetData()["LoginMethod"] = DeployTokenMethodName return user_model.NewDeployKeyUserWithKeyID(key.ID), nil } diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go index 2fa3ba2a340..1a0881e252c 100644 --- a/services/auth/oauth2.go +++ b/services/auth/oauth2.go @@ -107,8 +107,7 @@ func parseToken(req *http.Request) (string, bool) { } // userFromToken returns the user corresponding to the OAuth token. -// It will set 'ApiTokenScope' to the scope of the access token (TODO: this behavior should be fixed, don't set ctx.Data) -func (o *OAuth2) userFromToken(ctx context.Context, tokenSHA string, store DataStore) (*user_model.User, error) { +func (o *OAuth2) userFromToken(ctx context.Context, tokenSHA string) (*user_model.User, error) { // Let's see if token is valid. if strings.Contains(tokenSHA, ".") { // First attempt to decode an actions JWT, returning the actions user @@ -124,8 +123,7 @@ func (o *OAuth2) userFromToken(ctx context.Context, tokenSHA string, store DataS if err != nil || !user.IsIndividual() { return nil, err } - store.GetData()["ApiTokenScope"] = accessTokenScope - setAuthCredential(store, credentialOAuth2Grant, grantID) + user.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialOAuth2Grant, grantID, accessTokenScope) return user, nil } t, err := auth_model.GetAccessTokenBySHA(ctx, tokenSHA) @@ -144,22 +142,25 @@ func (o *OAuth2) userFromToken(ctx context.Context, tokenSHA string, store DataS if err = auth_model.UpdateAccessToken(ctx, t); err != nil { log.Error("UpdateAccessToken: %v", err) } - store.GetData()["ApiTokenScope"] = t.Scope - setAuthCredential(store, credentialAccessToken, t.ID) - return user_model.GetUserByID(ctx, t.UID) + user, err := user_model.GetUserByID(ctx, t.UID) + if err != nil { + return nil, err + } + user.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialAccessToken, t.ID, t.Scope) + return user, nil } // Verify extracts the user ID from the OAuth token in the query parameters // or the "Authorization" header and returns the corresponding user object for that ID. // If verification is successful returns an existing user object. // Returns nil if verification fails. -func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { +func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, _ DataStore, sess SessionStore) (*user_model.User, error) { token, ok := parseToken(req) if !ok { return nil, nil //nolint:nilnil // the auth method is not applicable } - user, err := o.userFromToken(req.Context(), token, store) + user, err := o.userFromToken(req.Context(), token) if err != nil && !errors.Is(err, util.ErrNotExist) { log.Error("userFromToken: %v", err) // the callers might ignore the error, so log it here } diff --git a/services/auth/oauth2_test.go b/services/auth/oauth2_test.go index 511bee7d218..28631668fc7 100644 --- a/services/auth/oauth2_test.go +++ b/services/auth/oauth2_test.go @@ -4,11 +4,12 @@ package auth import ( + "fmt" "testing" + auth_model "gitea.dev/models/auth" "gitea.dev/models/unittest" user_model "gitea.dev/models/user" - "gitea.dev/modules/reqctx" "gitea.dev/services/actions" "github.com/stretchr/testify/assert" @@ -23,16 +24,25 @@ func TestUserIDFromToken(t *testing.T) { token, err := actions.CreateAuthorizationToken(RunningTaskID, 1, 2) assert.NoError(t, err) - ds := make(reqctx.ContextData) - o := OAuth2{} - u, err := o.userFromToken(t.Context(), token, ds) + u, err := o.userFromToken(t.Context(), token) require.NoError(t, err) assert.Equal(t, user_model.ActionsUserID, u.ID) taskID, ok := user_model.GetActionsUserTaskID(u) assert.True(t, ok) assert.Equal(t, RunningTaskID, taskID) }) + + t.Run("Access token", func(t *testing.T) { + token := &auth_model.AccessToken{UID: 2, Name: "public-only", Scope: "public-only,read:repository"} + require.NoError(t, auth_model.NewAccessToken(t.Context(), token)) + + u, err := (&OAuth2{}).userFromToken(t.Context(), token.Token) + require.NoError(t, err) + scope, _ := user_model.GetDoerTokenScope(u) + assert.Equal(t, token.Scope, scope) + assert.Equal(t, fmt.Sprintf("access-token:%d", token.ID), user_model.GetDoerCredential(u)) + }) } func TestCheckTaskIsRunning(t *testing.T) { diff --git a/services/context/api.go b/services/context/api.go index 5affd8b2778..3557f2f0b06 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -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() { diff --git a/services/context/context_response.go b/services/context/context_response.go index cbaccfc20c7..55df78d58ba 100644 --- a/services/context/context_response.go +++ b/services/context/context_response.go @@ -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 diff --git a/services/context/org.go b/services/context/org.go index b0bb03bfa70..fe7ef386b6e 100644 --- a/services/context/org.go +++ b/services/context/org.go @@ -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 diff --git a/services/context/package.go b/services/context/package.go index 977953cefb4..4951d554c44 100644 --- a/services/context/package.go +++ b/services/context/package.go @@ -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 } diff --git a/services/context/package_test.go b/services/context/package_test.go index 2b0dfd923e7..f080a92c7c9 100644 --- a/services/context/package_test.go +++ b/services/context/package_test.go @@ -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) } diff --git a/services/context/permission.go b/services/context/permission.go index 97963461685..e5d90aec169 100644 --- a/services/context/permission.go +++ b/services/context/permission.go @@ -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)...) } diff --git a/services/context/user.go b/services/context/user.go index 90b72c0470e..576a778bb48 100644 --- a/services/context/user.go +++ b/services/context/user.go @@ -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 } diff --git a/services/convert/pull.go b/services/convert/pull.go index c8d8807cb45..f1c3a5b12e9 100644 --- a/services/convert/pull.go +++ b/services/convert/pull.go @@ -22,10 +22,20 @@ import ( "gitea.dev/services/gitdiff" ) -// ToAPIPullRequest assumes following fields have been assigned with valid values: +// ToAPIPullRequest hides a head repository the doer cannot see +func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) *api.PullRequest { + return toAPIPullRequest(ctx, pr, doer, false) +} + +// ToPayloadPullRequest keeps the head repository regardless of the doer, for webhook and Actions payloads +func ToPayloadPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) *api.PullRequest { + return toAPIPullRequest(ctx, pr, doer, true) +} + +// toAPIPullRequest assumes following fields have been assigned with valid values: // Required - Issue // Optional - Merger -func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) *api.PullRequest { +func toAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, isPayload bool) *api.PullRequest { var ( baseBranch string headBranch string @@ -181,20 +191,25 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u p.AccessMode = perm.AccessModeNone } - apiPullRequest.Head.RepoID = pr.HeadRepo.ID - apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p) + // a hidden head repository is read from the base repository's PR ref + headGitRepo := gitRepo + exist = false + if isPayload || pr.HeadRepoID == pr.BaseRepoID || p.HasAnyUnitAccessOrPublicAccess() { + apiPullRequest.Head.RepoID = pr.HeadRepo.ID + apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p) - headGitRepo, err := git.OpenRepository(ctx, pr.HeadRepo) - if err != nil { - log.Error("OpenRepository[%s]: %v", pr.HeadRepo.FullName(), err) - return nil - } - defer headGitRepo.Close() + headGitRepo, err = git.OpenRepository(ctx, pr.HeadRepo) + if err != nil { + log.Error("OpenRepository[%s]: %v", pr.HeadRepo.FullName(), err) + return nil + } + defer headGitRepo.Close() - exist, err = git_model.IsBranchExist(ctx, pr.HeadRepoID, pr.HeadBranch) - if err != nil { - log.Error("GetBranch[%s]: %v", pr.HeadBranch, err) - return nil + exist, err = git_model.IsBranchExist(ctx, pr.HeadRepoID, pr.HeadBranch) + if err != nil { + log.Error("GetBranch[%s]: %v", pr.HeadBranch, err) + return nil + } } // Outer scope variables to be used in diff calculation @@ -419,7 +434,17 @@ func ToAPIPullRequests(ctx context.Context, baseRepo *repo_model.Repository, prs } // pull request head branch, both repository and branch could not exist - if pr.HeadRepo != nil { + if pr.HeadRepo != nil && pr.HeadRepoID != pr.BaseRepoID { + p, err := access_model.GetDoerRepoPermission(ctx, pr.HeadRepo, doer) + if err != nil { + log.Error("GetDoerRepoPermission[%d]: %v", pr.HeadRepoID, err) + p.AccessMode = perm.AccessModeNone + } + if p.HasAnyUnitAccessOrPublicAccess() { + apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p) + } + } + if apiPullRequest.Head.Repository != nil { apiPullRequest.Head.RepoID = pr.HeadRepo.ID exist, err := git_model.IsBranchExist(ctx, pr.HeadRepo.ID, pr.HeadBranch) if err != nil { @@ -429,14 +454,6 @@ func ToAPIPullRequests(ctx context.Context, baseRepo *repo_model.Repository, prs if exist { apiPullRequest.Head.Ref = pr.HeadBranch } - if pr.HeadRepoID != pr.BaseRepoID { - p, err := access_model.GetDoerRepoPermission(ctx, pr.HeadRepo, doer) - if err != nil { - log.Error("GetDoerRepoPermission[%d]: %v", pr.HeadRepoID, err) - p.AccessMode = perm.AccessModeNone - } - apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p) - } } if apiPullRequest.Head.Ref == "" { apiPullRequest.Head.Ref = pr.GetGitHeadRefName() diff --git a/services/lfs/locks.go b/services/lfs/locks.go index fe3f4bdbec9..3ec08de08aa 100644 --- a/services/lfs/locks.go +++ b/services/lfs/locks.go @@ -59,7 +59,7 @@ func GetListLockHandler(ctx *context.Context) { } repository.MustOwner(ctx) - context.CheckRepoScopedToken(ctx, repository, auth_model.Read) + context.CheckRepoScopedToken(ctx, auth_model.Read) if ctx.Written() { return } @@ -148,7 +148,7 @@ func PostLockHandler(ctx *context.Context) { } repository.MustOwner(ctx) - context.CheckRepoScopedToken(ctx, repository, auth_model.Write) + context.CheckRepoScopedToken(ctx, auth_model.Write) if ctx.Written() { return } @@ -213,7 +213,7 @@ func VerifyLockHandler(ctx *context.Context) { } repository.MustOwner(ctx) - context.CheckRepoScopedToken(ctx, repository, auth_model.Read) + context.CheckRepoScopedToken(ctx, auth_model.Read) if ctx.Written() { return } @@ -281,7 +281,7 @@ func UnLockHandler(ctx *context.Context) { } repository.MustOwner(ctx) - context.CheckRepoScopedToken(ctx, repository, auth_model.Write) + context.CheckRepoScopedToken(ctx, auth_model.Write) if ctx.Written() { return } diff --git a/services/lfs/server.go b/services/lfs/server.go index 6d5df20b123..1b556d6b2d1 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -473,9 +473,9 @@ func getAuthenticatedRepository(ctx *context.Context, rc *requestContext, requir } if requireWrite { - context.CheckRepoScopedToken(ctx, repository, auth_model.Write) + context.CheckRepoScopedToken(ctx, auth_model.Write) } else { - context.CheckRepoScopedToken(ctx, repository, auth_model.Read) + context.CheckRepoScopedToken(ctx, auth_model.Read) } if ctx.Written() { diff --git a/services/org/org.go b/services/org/org.go index 382cd0a6176..5281f10911d 100644 --- a/services/org/org.go +++ b/services/org/org.go @@ -171,7 +171,7 @@ func ChangeOrganizationVisibility(ctx context.Context, org *org_model.Organizati } repos, _, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{ - Actor: org.AsUser(), Private: true, ListOptions: db.ListOptionsAll, + OwnerID: org.ID, Private: true, ListOptions: db.ListOptionsAll, }) if err != nil { return err diff --git a/services/pull/update.go b/services/pull/update.go index 732a475e72b..50c04a9dcdc 100644 --- a/services/pull/update.go +++ b/services/pull/update.go @@ -145,6 +145,9 @@ func CheckUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullReques if err := pull.LoadHeadRepo(ctx); err != nil { return ret, err } + if !repo_model.DoerTokenAllowsRepo(ctx, user, pull.HeadRepo) { // whitelists and maintainer edits below skip the doer's repo permission + return ret, nil + } // 1. check whether pull request enabled. prBaseUnit, err := pull.BaseRepo.GetUnit(ctx, unit.TypePullRequests) diff --git a/services/pull/update_test.go b/services/pull/update_test.go index b340a7a5d57..f3cd27d05b2 100644 --- a/services/pull/update_test.go +++ b/services/pull/update_test.go @@ -122,6 +122,32 @@ func TestIsUserAllowedToUpdate(t *testing.T) { pr3Poster := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 12}) + t.Run("PublicOnlyTokenCannotUpdatePrivateHeadEvenWhitelisted", func(t *testing.T) { + pr3 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3}) + protectedBranch := &git_model.ProtectedBranch{ + RepoID: pr3.HeadRepoID, + RuleName: pr3.HeadBranch, + CanPush: true, + EnableWhitelist: true, + WhitelistUserIDs: []int64{pr3Poster.ID}, + } + _, err := db.GetEngine(t.Context()).Insert(protectedBranch) + require.NoError(t, err) + defer db.DeleteByBean(t.Context(), protectedBranch) + require.NoError(t, pr3.LoadHeadRepo(t.Context())) + pr3.HeadRepo.IsPrivate = true + + pushAllowed, _, _, err := checkUserAllowedToUpdate(t.Context(), pr3, pr3Poster) + assert.NoError(t, err) + assert.True(t, pushAllowed) + + publicOnlyPoster := *pr3Poster + publicOnlyPoster.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialAccessToken, 1, "public-only,write:repository") + pushAllowed, _, _, err = checkUserAllowedToUpdate(t.Context(), pr3, &publicOnlyPoster) + assert.NoError(t, err) + assert.False(t, pushAllowed) + }) + t.Run("MaintainerEditRespectsPosterPermissions", func(t *testing.T) { pr3 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3}) pr3.AllowMaintainerEdit = true diff --git a/services/repository/adopt.go b/services/repository/adopt.go index 2cf964f9ae2..f7865719f15 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -265,7 +265,7 @@ func checkUnadoptedRepositories(ctx context.Context, userName string, repoNamesT return err } repos, _, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{ - Actor: ctxUser, + OwnerID: ctxUser.ID, Private: true, ListOptions: db.ListOptions{ Page: 1, diff --git a/services/repository/delete.go b/services/repository/delete.go index cd3f4914460..e018ed3fbeb 100644 --- a/services/repository/delete.go +++ b/services/repository/delete.go @@ -398,7 +398,6 @@ func DeleteOwnerRepositoriesDirectly(ctx context.Context, owner *user_model.User }, Private: true, OwnerID: owner.ID, - Actor: owner, }) if err != nil { return fmt.Errorf("GetUserRepositories: %w", err) diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index 3a201f9cbbb..1b24d270d2e 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -65,7 +65,7 @@ func (m *webhookNotifier) IssueClearLabels(ctx context.Context, doer *user_model err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequestLabel, &api.PullRequestPayload{ Action: api.HookIssueLabelCleared, Index: issue.Index, - PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, doer), + PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, doer), Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }) @@ -179,7 +179,7 @@ func (m *webhookNotifier) IssueChangeAssignee(ctx context.Context, doer *user_mo Action: action, Changes: changes, Index: issue.Index, - PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, doer), + PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, doer), Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), } @@ -222,7 +222,7 @@ func (m *webhookNotifier) IssueChangeTitle(ctx context.Context, doer *user_model From: oldTitle, }, }, - PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, doer), + PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, doer), Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }) @@ -257,7 +257,7 @@ func (m *webhookNotifier) IssueChangeStatus(ctx context.Context, doer *user_mode // Merge pull request calls issue.changeStatus so we need to handle separately. apiPullRequest := &api.PullRequestPayload{ Index: issue.Index, - PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, doer), + PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, doer), Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), CommitID: commitID, @@ -320,7 +320,7 @@ func (m *webhookNotifier) DeleteIssue(ctx context.Context, doer *user_model.User if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequest, &api.PullRequestPayload{ Action: api.HookIssueDeleted, Index: issue.Index, - PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, doer), + PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, doer), Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }); err != nil { @@ -365,7 +365,7 @@ func (m *webhookNotifier) NewPullRequest(ctx context.Context, pull *issues_model if err := PrepareWebhooks(ctx, EventSource{Repository: pull.Issue.Repo}, webhook_module.HookEventPullRequest, &api.PullRequestPayload{ Action: api.HookIssueOpened, Index: pull.Issue.Index, - PullRequest: convert.ToAPIPullRequest(ctx, pull, pull.Issue.Poster), + PullRequest: convert.ToPayloadPullRequest(ctx, pull, pull.Issue.Poster), Repository: convert.ToRepo(ctx, pull.Issue.Repo, permission), Sender: convert.ToUser(ctx, pull.Issue.Poster, nil), }); err != nil { @@ -394,7 +394,7 @@ func (m *webhookNotifier) IssueChangeContent(ctx context.Context, doer *user_mod From: oldContent, }, }, - PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, doer), + PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, doer), Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }) @@ -436,7 +436,7 @@ func (m *webhookNotifier) UpdateComment(ctx context.Context, doer *user_model.Us var pullRequest *api.PullRequest if c.Issue.IsPull { eventType = webhook_module.HookEventPullRequestComment - pullRequest = convert.ToAPIPullRequest(ctx, c.Issue.PullRequest, doer) + pullRequest = convert.ToPayloadPullRequest(ctx, c.Issue.PullRequest, doer) } else { eventType = webhook_module.HookEventIssueComment } @@ -471,7 +471,7 @@ func (m *webhookNotifier) CreateIssueComment(ctx context.Context, doer *user_mod log.Error("LoadPullRequest: %v", err) return } - pullRequest = convert.ToAPIPullRequest(ctx, issue.PullRequest, doer) + pullRequest = convert.ToPayloadPullRequest(ctx, issue.PullRequest, doer) } else { eventType = webhook_module.HookEventIssueComment } @@ -512,7 +512,7 @@ func (m *webhookNotifier) DeleteComment(ctx context.Context, doer *user_model.Us var pullRequest *api.PullRequest if comment.Issue.IsPull { eventType = webhook_module.HookEventPullRequestComment - pullRequest = convert.ToAPIPullRequest(ctx, comment.Issue.PullRequest, doer) + pullRequest = convert.ToPayloadPullRequest(ctx, comment.Issue.PullRequest, doer) } else { eventType = webhook_module.HookEventIssueComment } @@ -597,7 +597,7 @@ func (m *webhookNotifier) IssueChangeLabels(ctx context.Context, doer *user_mode err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequestLabel, &api.PullRequestPayload{ Action: api.HookIssueLabelUpdated, Index: issue.Index, - PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, doer), + PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, doer), Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Sender: convert.ToUser(ctx, doer, nil), }) @@ -639,7 +639,7 @@ func (m *webhookNotifier) IssueChangeMilestone(ctx context.Context, doer *user_m err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequestMilestone, &api.PullRequestPayload{ Action: hookAction, Index: issue.Index, - PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, doer), + PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, doer), Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }) @@ -712,7 +712,7 @@ func (*webhookNotifier) MergePullRequest(ctx context.Context, doer *user_model.U // Merge pull request calls issue.changeStatus so we need to handle separately. apiPullRequest := &api.PullRequestPayload{ Index: pr.Issue.Index, - PullRequest: convert.ToAPIPullRequest(ctx, pr, doer), + PullRequest: convert.ToPayloadPullRequest(ctx, pr, doer), Repository: convert.ToRepo(ctx, pr.Issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), Action: api.HookIssueClosed, @@ -740,7 +740,7 @@ func (m *webhookNotifier) PullRequestChangeTargetBranch(ctx context.Context, doe From: oldBranch, }, }, - PullRequest: convert.ToAPIPullRequest(ctx, pr, doer), + PullRequest: convert.ToPayloadPullRequest(ctx, pr, doer), Repository: convert.ToRepo(ctx, issue.Repo, mode), Sender: convert.ToUser(ctx, doer, nil), }); err != nil { @@ -777,7 +777,7 @@ func (m *webhookNotifier) PullRequestReview(ctx context.Context, pr *issues_mode if err := PrepareWebhooks(ctx, EventSource{Repository: review.Issue.Repo}, reviewHookType, &api.PullRequestPayload{ Action: api.HookIssueReviewed, Index: review.Issue.Index, - PullRequest: convert.ToAPIPullRequest(ctx, pr, review.Reviewer), + PullRequest: convert.ToPayloadPullRequest(ctx, pr, review.Reviewer), RequestedReviewer: convert.ToUser(ctx, review.Reviewer, nil), Repository: convert.ToRepo(ctx, review.Issue.Repo, permission), Sender: convert.ToUser(ctx, review.Reviewer, nil), @@ -802,7 +802,7 @@ func (m *webhookNotifier) PullRequestReviewRequest(ctx context.Context, doer *us } apiPullRequest := &api.PullRequestPayload{ Index: issue.Index, - PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, doer), + PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, doer), RequestedReviewer: convert.ToUser(ctx, reviewer, nil), Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), @@ -847,7 +847,7 @@ func (m *webhookNotifier) PullRequestSynchronized(ctx context.Context, doer *use Before: before, After: after, Index: pr.Issue.Index, - PullRequest: convert.ToAPIPullRequest(ctx, pr, doer), + PullRequest: convert.ToPayloadPullRequest(ctx, pr, doer), Repository: convert.ToRepo(ctx, pr.Issue.Repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Sender: convert.ToUser(ctx, doer, nil), }); err != nil { diff --git a/tests/integration/api_packages_generic_test.go b/tests/integration/api_packages_generic_test.go index c737870dfc7..b20b18cdb6b 100644 --- a/tests/integration/api_packages_generic_test.go +++ b/tests/integration/api_packages_generic_test.go @@ -281,10 +281,9 @@ func TestPackageGenericPublicOnlyTokenLimitedOwner(t *testing.T) { // a public-only read:package token (even the owner's own) must be refused publicOnlyToken := getUserToken(t, owner.Name, auth_model.AccessTokenScopeReadPackage, auth_model.AccessTokenScopePublicOnly) req = NewRequest(t, "GET", base+"/file.bin").AddTokenAuth(publicOnlyToken) - MakeRequest(t, req, http.StatusForbidden) - // same via the v1 package API surface (checkTokenPublicOnly) + MakeRequest(t, req, http.StatusNotFound) req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/pkg/1.0.0", owner.Name)).AddTokenAuth(publicOnlyToken) - MakeRequest(t, req, http.StatusForbidden) + MakeRequest(t, req, http.StatusNotFound) // a normal read:package token still works, proving only public-only is restricted token := getUserToken(t, owner.Name, auth_model.AccessTokenScopeReadPackage) diff --git a/tests/integration/api_project_test.go b/tests/integration/api_project_test.go index 79df45a6a79..08547b67755 100644 --- a/tests/integration/api_project_test.go +++ b/tests/integration/api_project_test.go @@ -30,6 +30,7 @@ type projectScope struct { issueID int64 // a second such issue, deliberately left off the board foreignIssueID int64 + publicReader string } func TestAPIProjects(t *testing.T) { @@ -43,10 +44,12 @@ func TestAPIProjects(t *testing.T) { outsider := getUserToken(t, "user8", auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteOrganization, auth_model.AccessTokenScopeReadUser) + publicOnlyOutsider := getUserToken(t, "user8", auth_model.AccessTokenScopeReadIssue, + auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopePublicOnly) for _, scope := range []projectScope{ - {"Repository", "/api/v1/repos/user2/repo1/projects", 1, 11}, - {"Organization", "/api/v1/orgs/org3/projects", 16, 17}, - {"User", "/api/v1/user/projects", 1, 11}, + {"Repository", "/api/v1/repos/user2/repo1/projects", 1, 11, ""}, + {"Organization", "/api/v1/orgs/org3/projects", 16, 17, publicOnlyOutsider}, + {"User", "/api/v1/user/projects", 1, 11, ""}, } { t.Run(scope.name, func(t *testing.T) { testProjectLifecycle(t, scope, token) @@ -215,6 +218,10 @@ func testProjectLifecycle(t *testing.T, scope projectScope, token string) { issues := *DecodeJSON(t, MakeRequest(t, req, http.StatusOK), &[]api.Issue{}) require.Len(t, issues, 1) assert.Equal(t, scope.issueID, issues[0].ID) + if scope.publicReader != "" { + req = NewRequest(t, "GET", fmt.Sprintf("%s/columns/%d/issues", projectURL, columnIDs[1])).AddTokenAuth(scope.publicReader) + assert.Len(t, *DecodeJSON(t, MakeRequest(t, req, http.StatusOK), &[]api.Issue{}), 1) + } // the sibling column must not report the same issue req = NewRequest(t, "GET", fmt.Sprintf("%s/columns/%d/issues", projectURL, columnIDs[0])).AddTokenAuth(token) @@ -288,7 +295,7 @@ func testAPIProjectVisibility(t *testing.T, outsider string) { auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopePublicOnly) for _, url := range []string{"/api/v1/orgs/private_org35/projects", "/api/v1/users/private_org35/projects"} { - MakeRequest(t, NewRequest(t, "GET", url).AddTokenAuth(publicOnly), http.StatusForbidden) + MakeRequest(t, NewRequest(t, "GET", url).AddTokenAuth(publicOnly), http.StatusNotFound) } // a public org's board is readable by an outsider but not writable diff --git a/tests/integration/api_public_only_test.go b/tests/integration/api_public_only_test.go index c6ff027875a..3a24d2b4d20 100644 --- a/tests/integration/api_public_only_test.go +++ b/tests/integration/api_public_only_test.go @@ -44,6 +44,7 @@ func TestAPIUserReposPublicOnly(t *testing.T) { assert.False(t, repo.Private) } assert.NotContains(t, repoNames(repos), "user2/repo2") + assert.Equal(t, strconv.Itoa(len(repos)), resp.Header().Get("X-Total-Count")) } func repoNames(repos []api.Repository) []string { @@ -78,12 +79,14 @@ func TestAPIActivityFeedsPublicOnly(t *testing.T) { DecodeJSON(t, resp, &activities) assert.NotEmpty(t, activities) - publicToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopePublicOnly) - req = NewRequest(t, "GET", "/api/v1/users/user2/activities/feeds"). - AddTokenAuth(publicToken) - resp = MakeRequest(t, req, http.StatusOK) - DecodeJSON(t, resp, &activities) - assertPublicActivitiesOnly(t, activities) + for _, tokenOwner := range []string{"user2", "user1"} { + publicToken := getUserToken(t, tokenOwner, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopePublicOnly) + req = NewRequest(t, "GET", "/api/v1/users/user2/activities/feeds"). + AddTokenAuth(publicToken) + resp = MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &activities) + assertPublicActivitiesOnly(t, activities) + } orgToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization) req = NewRequest(t, "GET", "/api/v1/orgs/org3/activities/feeds"). @@ -100,7 +103,7 @@ func TestAPIActivityFeedsPublicOnly(t *testing.T) { assertPublicActivitiesOnly(t, activities) } -func TestAPIOrgPermissionsPublicOnly(t *testing.T) { +func TestAPIOrgMembershipPublicOnly(t *testing.T) { defer tests.PrepareTestEnv(t)() // user2 is a member of the private org private_org35 @@ -115,6 +118,14 @@ func TestAPIOrgPermissionsPublicOnly(t *testing.T) { publicToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopePublicOnly) req = NewRequestf(t, "GET", "/api/v1/users/user2/orgs/%s/permissions", org.Name).AddTokenAuth(publicToken) MakeRequest(t, req, http.StatusNotFound) + + var members, publicMembers []*api.User + DecodeJSON(t, MakeRequest(t, NewRequest(t, "GET", "/api/v1/orgs/org3/members").AddTokenAuth(token), http.StatusOK), &members) + DecodeJSON(t, MakeRequest(t, NewRequest(t, "GET", "/api/v1/orgs/org3/members").AddTokenAuth(publicToken), http.StatusOK), &publicMembers) + assert.Len(t, members, 3) + assert.Len(t, publicMembers, 2) + MakeRequest(t, NewRequest(t, "GET", "/api/v1/orgs/org3/members/user4").AddTokenAuth(token), http.StatusNoContent) + MakeRequest(t, NewRequest(t, "GET", "/api/v1/orgs/org3/members/user4").AddTokenAuth(publicToken), http.StatusSeeOther) } func TestAPITeamReposPublicOnly(t *testing.T) { diff --git a/tests/integration/api_pull_test.go b/tests/integration/api_pull_test.go index d6263a69f09..b5884722cf4 100644 --- a/tests/integration/api_pull_test.go +++ b/tests/integration/api_pull_test.go @@ -142,6 +142,34 @@ func TestAPIViewPulls(t *testing.T) { assert.Len(t, files, 1) })) } + + t.Run("HeadRepoHiddenWhenInaccessible", func(t *testing.T) { + session := loginUser(t, "user31") + forkToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{}).AddTokenAuth(forkToken), http.StatusAccepted) + visible := DecodeJSON(t, MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/pulls", &api.CreatePullRequestOption{ + Head: "user31:branch2", Base: "master", Title: "pull from a private user's fork", + }).AddTokenAuth(forkToken), http.StatusCreated), &api.PullRequest{}) + require.NotNil(t, visible.Head.Repository) + require.NotEmpty(t, visible.Head.Sha) + + prURL := fmt.Sprintf("/api/v1/repos/user2/repo1/pulls/%d", visible.Index) + var listed []*api.PullRequest + DecodeJSON(t, MakeRequest(t, NewRequest(t, "GET", "/api/v1/repos/user2/repo1/pulls?poster=user31"), http.StatusOK), &listed) + require.Len(t, listed, 1) + for _, hidden := range []*api.PullRequest{ + DecodeJSON(t, MakeRequest(t, NewRequest(t, "GET", prURL), http.StatusOK), &api.PullRequest{}), + listed[0], + } { + assert.Nil(t, hidden.Head.Repository) + assert.EqualValues(t, -1, hidden.Head.RepoID) + assert.Equal(t, visible.Head.Name, hidden.Head.Name) + assert.Equal(t, visible.Head.Sha, hidden.Head.Sha) + } + + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: 1, Index: visible.Index}) + assert.NotNil(t, convert.ToPayloadPullRequest(t.Context(), pr, nil).Head.Repository) + }) } func TestAPIViewPullsByBaseHead(t *testing.T) { diff --git a/tests/integration/api_user_heatmap_test.go b/tests/integration/api_user_heatmap_test.go index ebf6548a63f..370170f7875 100644 --- a/tests/integration/api_user_heatmap_test.go +++ b/tests/integration/api_user_heatmap_test.go @@ -21,7 +21,7 @@ func TestUserHeatmap(t *testing.T) { defer tests.PrepareTestEnv(t)() adminUsername := "user1" normalUsername := "user2" - token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeReadUser) + token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeAll) fakeNow := time.Date(2011, 10, 20, 0, 0, 0, 0, time.Local) timeutil.MockSet(fakeNow) diff --git a/tests/integration/api_user_public_only_test.go b/tests/integration/api_user_public_only_test.go index be315072d79..1c065c05835 100644 --- a/tests/integration/api_user_public_only_test.go +++ b/tests/integration/api_user_public_only_test.go @@ -8,6 +8,7 @@ import ( "testing" auth_model "gitea.dev/models/auth" + "gitea.dev/models/db" "gitea.dev/models/unittest" user_model "gitea.dev/models/user" api "gitea.dev/modules/structs" @@ -32,10 +33,12 @@ func TestAPIPublicOnlySelfUserRoutes(t *testing.T) { auth_model.AccessTokenScopeWriteUser, ) - t.Run("PrivateProfileForbidden", func(t *testing.T) { + t.Run("PrivateProfileUnreachable", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - MakeRequest(t, NewRequest(t, "GET", "/api/v1/users/user31").AddTokenAuth(privateReadUserToken), http.StatusForbidden) + MakeRequest(t, NewRequest(t, "GET", "/api/v1/users/user31").AddTokenAuth(privateReadUserToken), http.StatusNotFound) + require.NoError(t, db.Insert(t.Context(), &user_model.Redirect{LowerName: "old-user31", RedirectUserID: privateUser.ID})) + MakeRequest(t, NewRequest(t, "GET", "/api/v1/users/old-user31").AddTokenAuth(privateReadUserToken), http.StatusNotFound) MakeRequest(t, NewRequest(t, "GET", "/api/v1/user").AddTokenAuth(privateReadUserToken), http.StatusForbidden) }) diff --git a/tests/integration/attachment_test.go b/tests/integration/attachment_test.go index 155d182e22f..58ac34095c4 100644 --- a/tests/integration/attachment_test.go +++ b/tests/integration/attachment_test.go @@ -277,16 +277,16 @@ func TestAttachmentTokenScopes(t *testing.T) { url: "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12", readIssueStatus: http.StatusOK, readRepoStatus: http.StatusForbidden, - publicOnlyIssueStatus: http.StatusForbidden, - publicOnlyRepoStatus: http.StatusForbidden, + publicOnlyIssueStatus: http.StatusNotFound, + publicOnlyRepoStatus: http.StatusNotFound, }, { name: "RepoPrivateIssueAttachment", url: "/user2/repo2/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12", readIssueStatus: http.StatusOK, readRepoStatus: http.StatusForbidden, - publicOnlyIssueStatus: http.StatusForbidden, - publicOnlyRepoStatus: http.StatusForbidden, + publicOnlyIssueStatus: http.StatusNotFound, + publicOnlyRepoStatus: http.StatusNotFound, }, { name: "GlobalPublicReleaseAttachment", @@ -309,16 +309,16 @@ func TestAttachmentTokenScopes(t *testing.T) { url: "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22", readIssueStatus: http.StatusForbidden, readRepoStatus: http.StatusOK, - publicOnlyIssueStatus: http.StatusForbidden, - publicOnlyRepoStatus: http.StatusForbidden, + publicOnlyIssueStatus: http.StatusNotFound, + publicOnlyRepoStatus: http.StatusNotFound, }, { name: "RepoPrivateReleaseAttachment", url: "/user2/repo2/releases/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22", readIssueStatus: http.StatusForbidden, readRepoStatus: http.StatusOK, - publicOnlyIssueStatus: http.StatusForbidden, - publicOnlyRepoStatus: http.StatusForbidden, + publicOnlyIssueStatus: http.StatusNotFound, + publicOnlyRepoStatus: http.StatusNotFound, }, } diff --git a/tests/integration/download_test.go b/tests/integration/download_test.go index e97a5d76557..ce6da3a4e44 100644 --- a/tests/integration/download_test.go +++ b/tests/integration/download_test.go @@ -201,7 +201,7 @@ func TestDownloadRepoContentTokenScopes(t *testing.T) { MakeRequest(t, NewRequest(t, tc.method, tc.url).AddTokenAuth(miscToken), http.StatusForbidden) MakeRequest(t, NewRequest(t, tc.method, tc.url).AddTokenAuth(ownerReadToken), tc.withScope) - publicOnlyStatus := http.StatusForbidden + publicOnlyStatus := http.StatusNotFound if tc.publicOnlyOK { publicOnlyStatus = tc.withScope } diff --git a/tests/integration/git_smart_http_test.go b/tests/integration/git_smart_http_test.go index 9fec0cf392d..eec61f20d9d 100644 --- a/tests/integration/git_smart_http_test.go +++ b/tests/integration/git_smart_http_test.go @@ -129,7 +129,7 @@ func testGitSmartHTTPTokenScopes(t *testing.T) { t.Run("public-only scope rejects private repo", func(t *testing.T) { path := "/user2/repo2/info/refs?service=git-upload-pack" - MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(publicOnlyToken), http.StatusForbidden) + MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(publicOnlyToken), http.StatusNotFound) }) } diff --git a/tests/integration/privateactivity_test.go b/tests/integration/privateactivity_test.go index 0ce590e276c..b394e54cc72 100644 --- a/tests/integration/privateactivity_test.go +++ b/tests/integration/privateactivity_test.go @@ -123,7 +123,7 @@ func testPrivateActivityHelperHasHeatmapContentFromPublic(t *testing.T) bool { } func testPrivateActivityHelperHasHeatmapContentFromSession(t *testing.T, session *TestSession) bool { - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll) req := NewRequestf(t, "GET", "/api/v1/users/%s/heatmap", privateActivityTestUser). AddTokenAuth(token) diff --git a/tests/integration/pull_update_test.go b/tests/integration/pull_update_test.go index 393259446bc..6b4e276e5e4 100644 --- a/tests/integration/pull_update_test.go +++ b/tests/integration/pull_update_test.go @@ -61,41 +61,6 @@ func TestAPIPullUpdate(t *testing.T) { }) } -// TestAPIPullUpdatePublicOnlyToken verifies that a public-only API token cannot -// update (push into) a PR whose head repo is private, even when the base repo -// named in the route is public. -func TestAPIPullUpdatePublicOnlyToken(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - org26 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 26}) - pr := createOutdatedPR(t, user, org26) - require.NoError(t, pr.LoadBaseRepo(t.Context())) - require.NoError(t, pr.LoadHeadRepo(t.Context())) - require.NoError(t, pr.LoadIssue(t.Context())) - - // make the head repo private while the base repo stays public - require.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime(t.Context(), - &repo_model.Repository{ID: pr.HeadRepo.ID, IsPrivate: true}, "is_private")) - - // a public-only write token must be refused (404), not perform the push - publicOnlyToken := getUserToken(t, user.Name, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopePublicOnly) - req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index). - AddTokenAuth(publicOnlyToken) - MakeRequest(t, req, http.StatusNotFound) - - // the head branch must still be outdated (no push happened) - diffCount, err := git.GetDivergingCommits(t.Context(), pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName()) - require.NoError(t, err) - assert.Equal(t, 1, diffCount.Behind) - - // a normal write token still works, proving the guard is scope-specific - token := getUserToken(t, user.Name, auth_model.AccessTokenScopeWriteRepository) - req = NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index). - AddTokenAuth(token) - MakeRequest(t, req, http.StatusOK) - }) -} - func updateRepoPullRequestConfig(t *testing.T, repoID int64, update func(*repo_model.PullRequestsConfig)) { t.Helper() diff --git a/tests/integration/repo_home_token_scope_test.go b/tests/integration/repo_home_token_scope_test.go index 7404fde401a..e8891348d93 100644 --- a/tests/integration/repo_home_token_scope_test.go +++ b/tests/integration/repo_home_token_scope_test.go @@ -27,7 +27,7 @@ func TestRepoWebTokenScopes(t *testing.T) { } { t.Run(test.name, func(t *testing.T) { assertBasicAuthStatus(t, test.url, miscToken, http.StatusForbidden) - assertBasicAuthStatus(t, test.url, publicOnlyToken, http.StatusForbidden) + assertBasicAuthStatus(t, test.url, publicOnlyToken, http.StatusNotFound) assertBasicAuthStatus(t, test.url, readToken, http.StatusOK) }) } diff --git a/tests/integration/workflow_run_api_check_test.go b/tests/integration/workflow_run_api_check_test.go index 0456f71e850..33526ae4e2c 100644 --- a/tests/integration/workflow_run_api_check_test.go +++ b/tests/integration/workflow_run_api_check_test.go @@ -28,7 +28,7 @@ func TestAPIWorkflowRun(t *testing.T) { testAPIWorkflowRunBasic(t, "/api/v1/user/actions", "User2", 803, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository) }) t.Run("OrgRuns", func(t *testing.T) { - testAPIWorkflowRunBasic(t, "/api/v1/orgs/org3/actions", "User1", 802, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadRepository) + testAPIWorkflowRunBasic(t, "/api/v1/orgs/org3/actions", "User1", 802, auth_model.AccessTokenScopeAll) }) t.Run("RepoRuns", func(t *testing.T) { testAPIWorkflowRunBasic(t, "/api/v1/repos/org3/repo5/actions", "User2", 802, auth_model.AccessTokenScopeReadRepository)