mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 17:51:57 +00:00
Update commit filtering and diff view for file-level permissions to better reflect P4 behavior (#38673)
* update listing commits to show any commit modifying a file the user has access to. Filter out files in Diff() * address PR comments
This commit is contained in:
parent
4e3fdb37d5
commit
79d0c6359c
@ -18,6 +18,7 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/highlight"
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
"github.com/sourcegraph/sourcegraph/internal/authz"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
@ -238,7 +239,7 @@ func computeRepositoryComparisonDiff(cmp *RepositoryComparisonResolver) ComputeD
|
||||
Head: string(cmp.head.OID()),
|
||||
RangeType: cmp.rangeType,
|
||||
Paths: paths,
|
||||
})
|
||||
}, authz.DefaultSubRepoPermsChecker)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import (
|
||||
btypes "github.com/sourcegraph/sourcegraph/enterprise/internal/batches/types"
|
||||
"github.com/sourcegraph/sourcegraph/internal/actor"
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
"github.com/sourcegraph/sourcegraph/internal/authz"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/bitbucketcloud"
|
||||
@ -618,7 +619,7 @@ func computeDiffStat(ctx context.Context, db database.DB, c *btypes.Changeset, r
|
||||
Repo: repo,
|
||||
Base: c.SyncState.BaseRefOid,
|
||||
Head: c.SyncState.HeadRefOid,
|
||||
})
|
||||
}, authz.DefaultSubRepoPermsChecker)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -57,7 +57,7 @@ type DiffOptions struct {
|
||||
// Diff returns an iterator that can be used to access the diff between two
|
||||
// commits on a per-file basis. The iterator must be closed with Close when no
|
||||
// longer required.
|
||||
func (c *ClientImplementor) Diff(ctx context.Context, opts DiffOptions) (*DiffFileIterator, error) {
|
||||
func (c *ClientImplementor) Diff(ctx context.Context, opts DiffOptions, checker authz.SubRepoPermissionChecker) (*DiffFileIterator, error) {
|
||||
// Rare case: the base is the empty tree, in which case we must use ..
|
||||
// instead of ... as the latter only works for commits.
|
||||
if opts.Base == DevNullSHA {
|
||||
@ -91,14 +91,31 @@ func (c *ClientImplementor) Diff(ctx context.Context, opts DiffOptions) (*DiffFi
|
||||
}
|
||||
|
||||
return &DiffFileIterator{
|
||||
rdr: rdr,
|
||||
mfdr: diff.NewMultiFileDiffReader(rdr),
|
||||
rdr: rdr,
|
||||
mfdr: diff.NewMultiFileDiffReader(rdr),
|
||||
fileFilterFunc: getFilterFunc(ctx, checker, opts.Repo),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type DiffFileIterator struct {
|
||||
rdr io.ReadCloser
|
||||
mfdr *diff.MultiFileDiffReader
|
||||
rdr io.ReadCloser
|
||||
mfdr *diff.MultiFileDiffReader
|
||||
fileFilterFunc diffFileIteratorFilter
|
||||
}
|
||||
|
||||
type diffFileIteratorFilter func(fileName string) (bool, error)
|
||||
|
||||
func getFilterFunc(ctx context.Context, checker authz.SubRepoPermissionChecker, repo api.RepoName) diffFileIteratorFilter {
|
||||
if !authz.SubRepoEnabled(checker) {
|
||||
return nil
|
||||
}
|
||||
return func(fileName string) (bool, error) {
|
||||
shouldFilter, err := authz.FilterActorPath(ctx, checker, actor.FromContext(ctx), repo, fileName)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return shouldFilter, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (i *DiffFileIterator) Close() error {
|
||||
@ -108,7 +125,19 @@ func (i *DiffFileIterator) Close() error {
|
||||
// Next returns the next file diff. If no more diffs are available, the diff
|
||||
// will be nil and the error will be io.EOF.
|
||||
func (i *DiffFileIterator) Next() (*diff.FileDiff, error) {
|
||||
return i.mfdr.ReadFile()
|
||||
fd, err := i.mfdr.ReadFile()
|
||||
if err != nil {
|
||||
return fd, err
|
||||
}
|
||||
if i.fileFilterFunc != nil {
|
||||
if canRead, err := i.fileFilterFunc(fd.NewName); err != nil {
|
||||
return nil, err
|
||||
} else if !canRead {
|
||||
// go to next
|
||||
return i.Next()
|
||||
}
|
||||
}
|
||||
return fd, err
|
||||
}
|
||||
|
||||
// ShortLogOptions contains options for (Repository).ShortLog.
|
||||
@ -1591,11 +1620,12 @@ func hasAccessToCommit(ctx context.Context, commit *wrappedCommit, repoName api.
|
||||
for _, fileName := range commit.files {
|
||||
if hasAccess, err := authz.FilterActorPath(ctx, checker, a, repoName, fileName); err != nil {
|
||||
return false, err
|
||||
} else if !hasAccess {
|
||||
return false, nil
|
||||
} else if hasAccess {
|
||||
// if the user has access to one file modified in the commit, they have access to view the commit
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// CommitsUniqueToBranch returns a map from commits that exist on a particular
|
||||
|
||||
@ -15,6 +15,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sourcegraph/go-diff/diff"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@ -86,6 +88,111 @@ func TestParseShortLog(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiffWithSubRepoFiltering(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx = actor.WithActor(ctx, &actor.Actor{
|
||||
UID: 1,
|
||||
})
|
||||
db := database.NewMockDB()
|
||||
|
||||
ClientMocks.LocalGitserver = true
|
||||
defer ResetClientMocks()
|
||||
|
||||
cmds := getGitCommandsWithFileLists([]string{"file0"}, []string{"file1", "file1.1"}, []string{"file2"}, []string{"file3", "file3.3"})
|
||||
checker := getTestSubRepoPermsChecker("file1.1", "file2")
|
||||
testCases := []struct {
|
||||
label string
|
||||
extraGitCommands []string
|
||||
expectedDiffFiles []string
|
||||
expectedFileStat *diff.Stat
|
||||
rangeOverAllCommits bool
|
||||
}{
|
||||
{
|
||||
label: "adding files",
|
||||
expectedDiffFiles: []string{"file1", "file3", "file3.3"},
|
||||
expectedFileStat: &diff.Stat{Added: 3},
|
||||
rangeOverAllCommits: true,
|
||||
},
|
||||
{
|
||||
label: "changing filename",
|
||||
extraGitCommands: []string{
|
||||
"mv file1.1 file_can_access",
|
||||
"git add file_can_access",
|
||||
makeGitCommit("rename", 7),
|
||||
},
|
||||
expectedDiffFiles: []string{"file_can_access"},
|
||||
expectedFileStat: &diff.Stat{Added: 1},
|
||||
},
|
||||
{
|
||||
label: "file modified",
|
||||
extraGitCommands: []string{
|
||||
"echo new_file_content > file2",
|
||||
"echo more_new_file_content > file1",
|
||||
"git add file2",
|
||||
"git add file1",
|
||||
makeGitCommit("edit_files", 7),
|
||||
},
|
||||
expectedDiffFiles: []string{"file1"}, // file2 is updated but user doesn't have access
|
||||
expectedFileStat: &diff.Stat{Changed: 1},
|
||||
},
|
||||
{
|
||||
label: "diff for commit w/ no access returns empty result",
|
||||
extraGitCommands: []string{
|
||||
"echo new_file_content > file2",
|
||||
"git add file2",
|
||||
makeGitCommit("no_access", 7),
|
||||
},
|
||||
expectedDiffFiles: []string{},
|
||||
expectedFileStat: &diff.Stat{},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.label, func(t *testing.T) {
|
||||
repo := MakeGitRepository(t, append(cmds, tc.extraGitCommands...)...)
|
||||
c := NewClient(db)
|
||||
commits, err := c.Commits(ctx, repo, CommitsOptions{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err fetching commits: %s", err)
|
||||
}
|
||||
baseCommit := commits[1]
|
||||
headCommit := commits[0]
|
||||
if tc.rangeOverAllCommits {
|
||||
baseCommit = commits[len(commits)-1]
|
||||
}
|
||||
|
||||
iter, err := c.Diff(ctx, DiffOptions{Base: string(baseCommit.ID), Head: string(headCommit.ID), Repo: repo}, checker)
|
||||
if err != nil {
|
||||
t.Fatalf("error fetching diff: %s", err)
|
||||
}
|
||||
defer iter.Close()
|
||||
|
||||
stat := &diff.Stat{}
|
||||
fileNames := make([]string, 0, 3)
|
||||
for {
|
||||
file, err := iter.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
fileNames = append(fileNames, file.NewName)
|
||||
|
||||
fs := file.Stat()
|
||||
stat.Added += fs.Added
|
||||
stat.Changed += fs.Changed
|
||||
stat.Deleted += fs.Deleted
|
||||
}
|
||||
if diff := cmp.Diff(fileNames, tc.expectedDiffFiles); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
if diff := cmp.Diff(stat, tc.expectedFileStat); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiff(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := database.NewMockDB()
|
||||
@ -97,7 +204,7 @@ func TestDiff(t *testing.T) {
|
||||
".foo",
|
||||
} {
|
||||
t.Run("invalid base: "+input, func(t *testing.T) {
|
||||
i, err := NewClient(db).Diff(ctx, DiffOptions{Base: input})
|
||||
i, err := NewClient(db).Diff(ctx, DiffOptions{Base: input}, nil)
|
||||
if i != nil {
|
||||
t.Errorf("unexpected non-nil iterator: %+v", i)
|
||||
}
|
||||
@ -125,7 +232,7 @@ func TestDiff(t *testing.T) {
|
||||
return nil, nil
|
||||
}
|
||||
t.Cleanup(ResetMocks)
|
||||
_, _ = c.Diff(ctx, tc.opts)
|
||||
_, _ = c.Diff(ctx, tc.opts, nil)
|
||||
})
|
||||
}
|
||||
})
|
||||
@ -137,7 +244,7 @@ func TestDiff(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(ResetMocks)
|
||||
|
||||
i, err := c.Diff(ctx, DiffOptions{Base: "foo", Head: "bar"})
|
||||
i, err := c.Diff(ctx, DiffOptions{Base: "foo", Head: "bar"}, nil)
|
||||
if i != nil {
|
||||
t.Errorf("unexpected non-nil iterator: %+v", i)
|
||||
}
|
||||
@ -218,7 +325,7 @@ index 9bd8209..d2acfa9 100644
|
||||
}
|
||||
t.Cleanup(ResetMocks)
|
||||
|
||||
i, err := c.Diff(ctx, DiffOptions{Base: "foo", Head: "bar"})
|
||||
i, err := c.Diff(ctx, DiffOptions{Base: "foo", Head: "bar"}, nil)
|
||||
if i == nil {
|
||||
t.Error("unexpected nil iterator")
|
||||
}
|
||||
@ -1621,6 +1728,7 @@ func TestCommits_SubRepoPerms(t *testing.T) {
|
||||
"git add file3",
|
||||
"GIT_COMMITTER_NAME=c GIT_COMMITTER_EMAIL=c@c.com GIT_COMMITTER_DATE=2006-01-02T15:04:07Z git commit -m commit3 --author='a <a@a.com>' --date 2006-01-02T15:04:07Z",
|
||||
}
|
||||
repo := MakeGitRepository(t, gitCommands...)
|
||||
|
||||
tests := map[string]struct {
|
||||
wantCommits []*gitdomain.Commit
|
||||
@ -1628,9 +1736,16 @@ func TestCommits_SubRepoPerms(t *testing.T) {
|
||||
wantTotal uint
|
||||
noAccessPaths []string
|
||||
}{
|
||||
"if no read perms on file should filter out commit": {
|
||||
wantTotal: 1,
|
||||
"if no read perms on at least one file in the commit should filter out commit": {
|
||||
wantTotal: 2,
|
||||
wantCommits: []*gitdomain.Commit{
|
||||
{
|
||||
ID: "b96d097108fa49e339ca88bc97ab07f833e62131",
|
||||
Author: gitdomain.Signature{Name: "a", Email: "a@a.com", Date: MustParseTime(time.RFC3339, "2006-01-02T15:04:06Z")},
|
||||
Committer: &gitdomain.Signature{Name: "c", Email: "c@c.com", Date: MustParseTime(time.RFC3339, "2006-01-02T15:04:07Z")},
|
||||
Message: "commit2",
|
||||
Parents: []api.CommitID{"d38233a79e037d2ab8170b0d0bc0aa438473e6da"},
|
||||
},
|
||||
{
|
||||
ID: "d38233a79e037d2ab8170b0d0bc0aa438473e6da",
|
||||
Author: gitdomain.Signature{Name: "a", Email: "a@a.com", Date: MustParseTime(time.RFC3339, "2006-01-02T15:04:05Z")},
|
||||
@ -1667,7 +1782,6 @@ func TestCommits_SubRepoPerms(t *testing.T) {
|
||||
|
||||
for label, test := range tests {
|
||||
t.Run(label, func(t *testing.T) {
|
||||
repo := MakeGitRepository(t, gitCommands...)
|
||||
checker := getTestSubRepoPermsChecker(test.noAccessPaths...)
|
||||
commits, err := NewClient(database.NewMockDB()).Commits(ctx, repo, test.opt, checker)
|
||||
if err != nil {
|
||||
@ -1679,7 +1793,7 @@ func TestCommits_SubRepoPerms(t *testing.T) {
|
||||
t.Errorf("%s: got %d commits, want %d", label, len(commits), len(test.wantCommits))
|
||||
}
|
||||
|
||||
checkCommits(t, label, commits, test.wantCommits)
|
||||
checkCommits(t, commits, test.wantCommits)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -2299,10 +2413,10 @@ func testCommits(ctx context.Context, label string, repo api.RepoName, opt Commi
|
||||
if len(commits) != len(wantCommits) {
|
||||
t.Errorf("%s: got %d commits, want %d", label, len(commits), len(wantCommits))
|
||||
}
|
||||
checkCommits(t, label, commits, wantCommits)
|
||||
checkCommits(t, commits, wantCommits)
|
||||
}
|
||||
|
||||
func checkCommits(t *testing.T, label string, commits, wantCommits []*gitdomain.Commit) {
|
||||
func checkCommits(t *testing.T, commits, wantCommits []*gitdomain.Commit) {
|
||||
t.Helper()
|
||||
for i := 0; i < len(commits) || i < len(wantCommits); i++ {
|
||||
var gotC, wantC *gitdomain.Commit
|
||||
@ -2312,8 +2426,8 @@ func checkCommits(t *testing.T, label string, commits, wantCommits []*gitdomain.
|
||||
if i < len(wantCommits) {
|
||||
wantC = wantCommits[i]
|
||||
}
|
||||
if !CommitsEqual(gotC, wantC) {
|
||||
t.Errorf("%s: got commit %d == %+v, want %+v", label, i, gotC, wantC)
|
||||
if diff := cmp.Diff(gotC, wantC); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2335,6 +2449,25 @@ func getTestSubRepoPermsChecker(noAccessPaths ...string) authz.SubRepoPermission
|
||||
return checker
|
||||
}
|
||||
|
||||
func getGitCommandsWithFileLists(filenamesPerCommit ...[]string) []string {
|
||||
cmds := make([]string, 0, len(filenamesPerCommit)*3)
|
||||
for i, filenames := range filenamesPerCommit {
|
||||
for _, fn := range filenames {
|
||||
cmds = append(cmds,
|
||||
fmt.Sprintf("touch %s", fn),
|
||||
fmt.Sprintf("echo my_content_%d > %s", i, fn),
|
||||
fmt.Sprintf("git add %s", fn))
|
||||
}
|
||||
cmds = append(cmds,
|
||||
fmt.Sprintf("GIT_COMMITTER_NAME=a GIT_COMMITTER_EMAIL=a@a.com GIT_COMMITTER_DATE=2006-01-02T15:04:05=%dZ git commit -m commit%d --author='a <a@a.com>' --date 2006-01-02T15:04:0%dZ", i, i, i))
|
||||
}
|
||||
return cmds
|
||||
}
|
||||
|
||||
func makeGitCommit(commitMessage string, seconds int) string {
|
||||
return fmt.Sprintf("GIT_COMMITTER_NAME=a GIT_COMMITTER_EMAIL=a@a.com GIT_COMMITTER_DATE=2006-01-02T15:04:05=%dZ git commit -m %s --author='a <a@a.com>' --date 2006-01-02T15:04:0%dZ", seconds, commitMessage, seconds)
|
||||
}
|
||||
|
||||
func getGitCommandsWithFiles(fileName1, fileName2 string) []string {
|
||||
return []string{
|
||||
fmt.Sprintf("touch %s", fileName1),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user