mirror of
https://github.com/go-gitea/gitea.git
synced 2025-04-03 05:39:55 +08:00
Give organisation members access to organisation feeds (#33508)
Currently the organisation feed only includes items for public repositories (for non-administrators). This pull requests adds notifications from private repositories to the organisation-feed (for accounts that have access to the organisation). Feed-items only get shown for repositories where the users team(s) should have access to, this filtering seems to get done by some existing code. Needs some tests, but am unsure where/how to add them. Before:  After:  --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
3e2e7bf4e5
commit
30b13942f0
models/unittest
routers/web/feed
tests/integration
@ -12,13 +12,17 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
"xorm.io/xorm/schemas"
|
"xorm.io/xorm/schemas"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fixtureItem struct {
|
type FixtureItem struct {
|
||||||
tableName string
|
fileFullPath string
|
||||||
|
tableName string
|
||||||
|
|
||||||
tableNameQuoted string
|
tableNameQuoted string
|
||||||
sqlInserts []string
|
sqlInserts []string
|
||||||
sqlInsertArgs [][]any
|
sqlInsertArgs [][]any
|
||||||
@ -27,10 +31,11 @@ type fixtureItem struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type fixturesLoaderInternal struct {
|
type fixturesLoaderInternal struct {
|
||||||
|
xormEngine *xorm.Engine
|
||||||
|
xormTableNames map[string]bool
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
dbType schemas.DBType
|
dbType schemas.DBType
|
||||||
files []string
|
fixtures map[string]*FixtureItem
|
||||||
fixtures map[string]*fixtureItem
|
|
||||||
quoteObject func(string) string
|
quoteObject func(string) string
|
||||||
paramPlaceholder func(idx int) string
|
paramPlaceholder func(idx int) string
|
||||||
}
|
}
|
||||||
@ -59,29 +64,27 @@ func (f *fixturesLoaderInternal) preprocessFixtureRow(row []map[string]any) (err
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fixturesLoaderInternal) prepareFixtureItem(file string) (_ *fixtureItem, err error) {
|
func (f *fixturesLoaderInternal) prepareFixtureItem(fixture *FixtureItem) (err error) {
|
||||||
fixture := &fixtureItem{}
|
|
||||||
fixture.tableName, _, _ = strings.Cut(filepath.Base(file), ".")
|
|
||||||
fixture.tableNameQuoted = f.quoteObject(fixture.tableName)
|
fixture.tableNameQuoted = f.quoteObject(fixture.tableName)
|
||||||
|
|
||||||
if f.dbType == schemas.MSSQL {
|
if f.dbType == schemas.MSSQL {
|
||||||
fixture.mssqlHasIdentityColumn, err = f.mssqlTableHasIdentityColumn(f.db, fixture.tableName)
|
fixture.mssqlHasIdentityColumn, err = f.mssqlTableHasIdentityColumn(f.db, fixture.tableName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := os.ReadFile(file)
|
data, err := os.ReadFile(fixture.fileFullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read file %q: %w", file, err)
|
return fmt.Errorf("failed to read file %q: %w", fixture.fileFullPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var rows []map[string]any
|
var rows []map[string]any
|
||||||
if err = yaml.Unmarshal(data, &rows); err != nil {
|
if err = yaml.Unmarshal(data, &rows); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal yaml data from %q: %w", file, err)
|
return fmt.Errorf("failed to unmarshal yaml data from %q: %w", fixture.fileFullPath, err)
|
||||||
}
|
}
|
||||||
if err = f.preprocessFixtureRow(rows); err != nil {
|
if err = f.preprocessFixtureRow(rows); err != nil {
|
||||||
return nil, fmt.Errorf("failed to preprocess fixture rows from %q: %w", file, err)
|
return fmt.Errorf("failed to preprocess fixture rows from %q: %w", fixture.fileFullPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var sqlBuf []byte
|
var sqlBuf []byte
|
||||||
@ -107,16 +110,14 @@ func (f *fixturesLoaderInternal) prepareFixtureItem(file string) (_ *fixtureItem
|
|||||||
sqlBuf = sqlBuf[:0]
|
sqlBuf = sqlBuf[:0]
|
||||||
sqlArguments = sqlArguments[:0]
|
sqlArguments = sqlArguments[:0]
|
||||||
}
|
}
|
||||||
return fixture, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fixturesLoaderInternal) loadFixtures(tx *sql.Tx, file string) (err error) {
|
func (f *fixturesLoaderInternal) loadFixtures(tx *sql.Tx, fixture *FixtureItem) (err error) {
|
||||||
fixture := f.fixtures[file]
|
if fixture.tableNameQuoted == "" {
|
||||||
if fixture == nil {
|
if err = f.prepareFixtureItem(fixture); err != nil {
|
||||||
if fixture, err = f.prepareFixtureItem(file); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
f.fixtures[file] = fixture
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tx.Exec(fmt.Sprintf("DELETE FROM %s", fixture.tableNameQuoted)) // sqlite3 doesn't support truncate
|
_, err = tx.Exec(fmt.Sprintf("DELETE FROM %s", fixture.tableNameQuoted)) // sqlite3 doesn't support truncate
|
||||||
@ -147,15 +148,26 @@ func (f *fixturesLoaderInternal) Load() error {
|
|||||||
}
|
}
|
||||||
defer func() { _ = tx.Rollback() }()
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
for _, file := range f.files {
|
for _, fixture := range f.fixtures {
|
||||||
if err := f.loadFixtures(tx, file); err != nil {
|
if !f.xormTableNames[fixture.tableName] {
|
||||||
return fmt.Errorf("failed to load fixtures from %s: %w", file, err)
|
continue
|
||||||
|
}
|
||||||
|
if err := f.loadFixtures(tx, fixture); err != nil {
|
||||||
|
return fmt.Errorf("failed to load fixtures from %s: %w", fixture.fileFullPath, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tx.Commit()
|
if err = tx.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for xormTableName := range f.xormTableNames {
|
||||||
|
if f.fixtures[xormTableName] == nil {
|
||||||
|
_, _ = f.xormEngine.Exec("DELETE FROM `" + xormTableName + "`")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FixturesFileFullPaths(dir string, files []string) ([]string, error) {
|
func FixturesFileFullPaths(dir string, files []string) (map[string]*FixtureItem, error) {
|
||||||
if files != nil && len(files) == 0 {
|
if files != nil && len(files) == 0 {
|
||||||
return nil, nil // load nothing
|
return nil, nil // load nothing
|
||||||
}
|
}
|
||||||
@ -169,20 +181,25 @@ func FixturesFileFullPaths(dir string, files []string) ([]string, error) {
|
|||||||
files = append(files, e.Name())
|
files = append(files, e.Name())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i, file := range files {
|
fixtureItems := map[string]*FixtureItem{}
|
||||||
if !filepath.IsAbs(file) {
|
for _, file := range files {
|
||||||
files[i] = filepath.Join(dir, file)
|
fileFillPath := file
|
||||||
|
if !filepath.IsAbs(fileFillPath) {
|
||||||
|
fileFillPath = filepath.Join(dir, file)
|
||||||
}
|
}
|
||||||
|
tableName, _, _ := strings.Cut(filepath.Base(file), ".")
|
||||||
|
fixtureItems[tableName] = &FixtureItem{fileFullPath: fileFillPath, tableName: tableName}
|
||||||
}
|
}
|
||||||
return files, nil
|
return fixtureItems, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFixturesLoader(x *xorm.Engine, opts FixturesOptions) (FixturesLoader, error) {
|
func NewFixturesLoader(x *xorm.Engine, opts FixturesOptions) (FixturesLoader, error) {
|
||||||
files, err := FixturesFileFullPaths(opts.Dir, opts.Files)
|
fixtureItems, err := FixturesFileFullPaths(opts.Dir, opts.Files)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get fixtures files: %w", err)
|
return nil, fmt.Errorf("failed to get fixtures files: %w", err)
|
||||||
}
|
}
|
||||||
f := &fixturesLoaderInternal{db: x.DB().DB, dbType: x.Dialect().URI().DBType, files: files, fixtures: map[string]*fixtureItem{}}
|
|
||||||
|
f := &fixturesLoaderInternal{xormEngine: x, db: x.DB().DB, dbType: x.Dialect().URI().DBType, fixtures: fixtureItems}
|
||||||
switch f.dbType {
|
switch f.dbType {
|
||||||
case schemas.SQLITE:
|
case schemas.SQLITE:
|
||||||
f.quoteObject = func(s string) string { return fmt.Sprintf(`"%s"`, s) }
|
f.quoteObject = func(s string) string { return fmt.Sprintf(`"%s"`, s) }
|
||||||
@ -197,5 +214,12 @@ func NewFixturesLoader(x *xorm.Engine, opts FixturesOptions) (FixturesLoader, er
|
|||||||
f.quoteObject = func(s string) string { return fmt.Sprintf("[%s]", s) }
|
f.quoteObject = func(s string) string { return fmt.Sprintf("[%s]", s) }
|
||||||
f.paramPlaceholder = func(idx int) string { return "?" }
|
f.paramPlaceholder = func(idx int) string { return "?" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
xormBeans, _ := db.NamesToBean()
|
||||||
|
f.xormTableNames = map[string]bool{}
|
||||||
|
for _, bean := range xormBeans {
|
||||||
|
f.xormTableNames[db.TableName(bean)] = true
|
||||||
|
}
|
||||||
|
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
activities_model "code.gitea.io/gitea/models/activities"
|
activities_model "code.gitea.io/gitea/models/activities"
|
||||||
|
"code.gitea.io/gitea/models/organization"
|
||||||
"code.gitea.io/gitea/models/renderhelper"
|
"code.gitea.io/gitea/models/renderhelper"
|
||||||
"code.gitea.io/gitea/modules/markup/markdown"
|
"code.gitea.io/gitea/modules/markup/markdown"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
@ -28,12 +29,23 @@ func ShowUserFeedAtom(ctx *context.Context) {
|
|||||||
// showUserFeed show user activity as RSS / Atom feed
|
// showUserFeed show user activity as RSS / Atom feed
|
||||||
func showUserFeed(ctx *context.Context, formatType string) {
|
func showUserFeed(ctx *context.Context, formatType string) {
|
||||||
includePrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
|
includePrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
|
||||||
|
isOrganisation := ctx.ContextUser.IsOrganization()
|
||||||
|
if ctx.IsSigned && isOrganisation && !includePrivate {
|
||||||
|
// When feed is requested by a member of the organization,
|
||||||
|
// include the private repo's the member has access to.
|
||||||
|
isOrgMember, err := organization.IsOrganizationMember(ctx, ctx.ContextUser.ID, ctx.Doer.ID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("IsOrganizationMember", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
includePrivate = isOrgMember
|
||||||
|
}
|
||||||
|
|
||||||
actions, _, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{
|
actions, _, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{
|
||||||
RequestedUser: ctx.ContextUser,
|
RequestedUser: ctx.ContextUser,
|
||||||
Actor: ctx.Doer,
|
Actor: ctx.Doer,
|
||||||
IncludePrivate: includePrivate,
|
IncludePrivate: includePrivate,
|
||||||
OnlyPerformedBy: !ctx.ContextUser.IsOrganization(),
|
OnlyPerformedBy: !isOrganisation,
|
||||||
IncludeDeleted: false,
|
IncludeDeleted: false,
|
||||||
Date: ctx.FormString("date"),
|
Date: ctx.FormString("date"),
|
||||||
})
|
})
|
||||||
|
38
routers/web/feed/profile_test.go
Normal file
38
routers/web/feed/profile_test.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
package feed_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/routers/web/feed"
|
||||||
|
"code.gitea.io/gitea/services/contexttest"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
unittest.MainTest(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckGetOrgFeedsAsOrgMember(t *testing.T) {
|
||||||
|
unittest.PrepareTestEnv(t)
|
||||||
|
t.Run("OrgMember", func(t *testing.T) {
|
||||||
|
ctx, resp := contexttest.MockContext(t, "org3.atom")
|
||||||
|
ctx.ContextUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
||||||
|
contexttest.LoadUser(t, ctx, 2)
|
||||||
|
ctx.IsSigned = true
|
||||||
|
feed.ShowUserFeedAtom(ctx)
|
||||||
|
assert.Contains(t, resp.Body.String(), "<entry>") // Should contain 1 private entry
|
||||||
|
})
|
||||||
|
t.Run("NonOrgMember", func(t *testing.T) {
|
||||||
|
ctx, resp := contexttest.MockContext(t, "org3.atom")
|
||||||
|
ctx.ContextUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
||||||
|
contexttest.LoadUser(t, ctx, 5)
|
||||||
|
ctx.IsSigned = true
|
||||||
|
feed.ShowUserFeedAtom(ctx)
|
||||||
|
assert.NotContains(t, resp.Body.String(), "<entry>") // Should not contain any entries
|
||||||
|
})
|
||||||
|
}
|
@ -166,9 +166,6 @@ jobs:
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
|
||||||
doAPIDeleteRepository(httpContext)(t)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,9 +345,6 @@ jobs:
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
|
||||||
doAPIDeleteRepository(httpContext)(t)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -434,8 +428,6 @@ jobs:
|
|||||||
assert.Equal(t, setting.Actions.DefaultActionsURL.URL(), gtCtx["gitea_default_actions_url"].GetStringValue())
|
assert.Equal(t, setting.Actions.DefaultActionsURL.URL(), gtCtx["gitea_default_actions_url"].GetStringValue())
|
||||||
token := gtCtx["token"].GetStringValue()
|
token := gtCtx["token"].GetStringValue()
|
||||||
assert.Equal(t, actionTask.TokenLastEight, token[len(token)-8:])
|
assert.Equal(t, actionTask.TokenLastEight, token[len(token)-8:])
|
||||||
|
|
||||||
doAPIDeleteRepository(user2APICtx)(t)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -543,12 +535,14 @@ jobs:
|
|||||||
err = actions_service.CleanupEphemeralRunners(t.Context())
|
err = actions_service.CleanupEphemeralRunners(t.Context())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
runner.client.runnerServiceClient.UpdateTask(t.Context(), connect.NewRequest(&runnerv1.UpdateTaskRequest{
|
_, err = runner.client.runnerServiceClient.UpdateTask(t.Context(), connect.NewRequest(&runnerv1.UpdateTaskRequest{
|
||||||
State: &runnerv1.TaskState{
|
State: &runnerv1.TaskState{
|
||||||
Id: actionTask.ID,
|
Id: actionTask.ID,
|
||||||
Result: runnerv1.Result_RESULT_SUCCESS,
|
Result: runnerv1.Result_RESULT_SUCCESS,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
resp, err = runner.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{
|
resp, err = runner.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{
|
||||||
TasksVersion: 0,
|
TasksVersion: 0,
|
||||||
}))
|
}))
|
||||||
@ -561,7 +555,7 @@ jobs:
|
|||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Nil(t, resp)
|
assert.Nil(t, resp)
|
||||||
|
|
||||||
// create an runner that picks a job and get force cancelled
|
// create a runner that picks a job and get force cancelled
|
||||||
runnerToBeRemoved := newMockRunner()
|
runnerToBeRemoved := newMockRunner()
|
||||||
runnerToBeRemoved.registerAsRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-runner-to-be-removed", []string{"ubuntu-latest"}, true)
|
runnerToBeRemoved.registerAsRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-runner-to-be-removed", []string{"ubuntu-latest"}, true)
|
||||||
|
|
||||||
@ -583,9 +577,6 @@ jobs:
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
unittest.AssertNotExistsBean(t, &actions_model.ActionRunner{ID: runnerToRemove.ID})
|
unittest.AssertNotExistsBean(t, &actions_model.ActionRunner{ID: runnerToRemove.ID})
|
||||||
|
|
||||||
// this cleanup is required to allow further tests to pass
|
|
||||||
doAPIDeleteRepository(user2APICtx)(t)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ func TestDownloadTaskLogs(t *testing.T) {
|
|||||||
{
|
{
|
||||||
treePath: ".gitea/workflows/download-task-logs-zstd.yml",
|
treePath: ".gitea/workflows/download-task-logs-zstd.yml",
|
||||||
fileContent: `name: download-task-logs-zstd
|
fileContent: `name: download-task-logs-zstd
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- '.gitea/workflows/download-task-logs-zstd.yml'
|
- '.gitea/workflows/download-task-logs-zstd.yml'
|
||||||
@ -67,7 +67,7 @@ jobs:
|
|||||||
{
|
{
|
||||||
treePath: ".gitea/workflows/download-task-logs-no-zstd.yml",
|
treePath: ".gitea/workflows/download-task-logs-no-zstd.yml",
|
||||||
fileContent: `name: download-task-logs-no-zstd
|
fileContent: `name: download-task-logs-no-zstd
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- '.gitea/workflows/download-task-logs-no-zstd.yml'
|
- '.gitea/workflows/download-task-logs-no-zstd.yml'
|
||||||
@ -152,8 +152,5 @@ jobs:
|
|||||||
resetFunc()
|
resetFunc()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository)
|
|
||||||
doAPIDeleteRepository(httpContext)(t)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -44,8 +44,6 @@ func TestAPIGetRawFileOrLFS(t *testing.T) {
|
|||||||
reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/media/"+lfs)
|
reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/media/"+lfs)
|
||||||
respLFS := MakeRequestNilResponseRecorder(t, reqLFS, http.StatusOK)
|
respLFS := MakeRequestNilResponseRecorder(t, reqLFS, http.StatusOK)
|
||||||
assert.Equal(t, testFileSizeSmall, respLFS.Length)
|
assert.Equal(t, testFileSizeSmall, respLFS.Length)
|
||||||
|
|
||||||
doAPIDeleteRepository(httpContext)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user