gitserver: Replace P4Exec endpoint with properly typed and individually tested RPC calls (#57247)

This exposes a proper API that we exactly know the surface of and know what to test for. If there are any issues with this endpoint, it will be very clear what that is, vs. a user-error calling p4exec with invalid arguments or so.
Also, this reduces the risk of accidentally exposing a p4 command that should not be exposed.
This commit is contained in:
Erik Seliger 2023-10-09 15:06:49 +02:00 committed by GitHub
parent c955a571e0
commit bff2e222b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 9412 additions and 2665 deletions

View File

@ -14,6 +14,7 @@ import (
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/database/dbtest"
"github.com/sourcegraph/sourcegraph/internal/gitserver"
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
)
func TestGitserverResolver(t *testing.T) {
@ -31,7 +32,7 @@ func TestGitserverResolver(t *testing.T) {
userCtx := actor.WithActor(ctx, actor.FromUser(user.ID))
adminCtx := actor.WithActor(ctx, actor.FromUser(admin.ID))
gitserverInstances := []gitserver.SystemInfo{
gitserverInstances := []protocol.SystemInfo{
{
Address: "127.0.0.1:3501",
FreeSpace: 10240,

View File

@ -11,6 +11,7 @@ go_library(
"list_gitolite.go",
"lock.go",
"observability.go",
"p4exec.go",
"patch.go",
"repo_info.go",
"search.go",
@ -94,6 +95,7 @@ go_test(
"cleanup_test.go",
"list_gitolite_test.go",
"main_test.go",
"p4exec_test.go",
"server_test.go",
"serverutil_test.go",
],

View File

@ -216,10 +216,10 @@ func TestClient_ArchiveReader(t *testing.T) {
base := proto.NewGitserverServiceClient(cc)
return base.RepoUpdate(ctx, in, opts...)
}
return &gitserver.MockGRPCClient{
MockArchive: mockArchive,
MockRepoUpdate: mockRepoUpdate,
}
cli := gitserver.NewMockGitserverServiceClient()
cli.ArchiveFunc.SetDefaultHook(mockArchive)
cli.RepoUpdateFunc.SetDefaultHook(mockRepoUpdate)
return cli
}
})
@ -260,7 +260,9 @@ func TestClient_ArchiveReader(t *testing.T) {
base := proto.NewGitserverServiceClient(cc)
return base.Archive(ctx, in, opts...)
}
return &gitserver.MockGRPCClient{MockArchive: mockArchive}
cli := gitserver.NewMockGitserverServiceClient()
cli.ArchiveFunc.SetDefaultHook(mockArchive)
return cli
}
})

View File

@ -0,0 +1,346 @@
package internal
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"strconv"
"strings"
"time"
"github.com/sourcegraph/log"
"go.opentelemetry.io/otel/attribute"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/accesslog"
"github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/executil"
"github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/gitserverfs"
"github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/perforce"
"github.com/sourcegraph/sourcegraph/internal/actor"
"github.com/sourcegraph/sourcegraph/internal/conf"
proto "github.com/sourcegraph/sourcegraph/internal/gitserver/v1"
"github.com/sourcegraph/sourcegraph/internal/grpc/streamio"
"github.com/sourcegraph/sourcegraph/internal/honey"
"github.com/sourcegraph/sourcegraph/internal/trace"
)
func (gs *GRPCServer) P4Exec(req *proto.P4ExecRequest, ss proto.GitserverService_P4ExecServer) error {
arguments := byteSlicesToStrings(req.GetArgs()) //nolint:staticcheck
if len(arguments) < 1 {
return status.Error(codes.InvalidArgument, "args must be greater than or equal to 1")
}
subCommand := arguments[0]
// Make sure the subcommand is explicitly allowed
allowlist := []string{"protects", "groups", "users", "group", "changes"}
allowed := false
for _, c := range allowlist {
if subCommand == c {
allowed = true
break
}
}
if !allowed {
return status.Error(codes.InvalidArgument, fmt.Sprintf("subcommand %q is not allowed", subCommand))
}
p4home, err := gitserverfs.MakeP4HomeDir(gs.Server.ReposDir)
if err != nil {
return status.Error(codes.Internal, err.Error())
}
// Log which actor is accessing p4-exec.
//
// p4-exec is currently only used for fetching user based permissions information
// so, we don't have a repo name.
accesslog.Record(ss.Context(), "<no-repo>",
log.String("p4user", req.GetP4User()), //nolint:staticcheck
log.String("p4port", req.GetP4Port()), //nolint:staticcheck
log.Strings("args", arguments),
)
// Make sure credentials are valid before heavier operation
err = perforce.P4TestWithTrust(ss.Context(), p4home, req.GetP4Port(), req.GetP4User(), req.GetP4Passwd()) //nolint:staticcheck
if err != nil {
if ctxErr := ss.Context().Err(); ctxErr != nil {
return status.FromContextError(ctxErr).Err()
}
return status.Error(codes.InvalidArgument, err.Error())
}
w := streamio.NewWriter(func(p []byte) error {
return ss.Send(&proto.P4ExecResponse{
Data: p,
})
})
var r p4ExecRequest
r.FromProto(req)
return gs.doP4Exec(ss.Context(), gs.Server.Logger, &r, "unknown-grpc-client", w)
}
func (gs *GRPCServer) doP4Exec(ctx context.Context, logger log.Logger, req *p4ExecRequest, userAgent string, w io.Writer) error {
execStatus := gs.Server.p4Exec(ctx, logger, req, userAgent, w)
if execStatus.ExitStatus != 0 || execStatus.Err != nil {
if ctxErr := ctx.Err(); ctxErr != nil {
return status.FromContextError(ctxErr).Err()
}
gRPCStatus := codes.Unknown
if strings.Contains(execStatus.Err.Error(), "signal: killed") {
gRPCStatus = codes.Aborted
}
s, err := status.New(gRPCStatus, execStatus.Err.Error()).WithDetails(&proto.ExecStatusPayload{
StatusCode: int32(execStatus.ExitStatus),
Stderr: execStatus.Stderr,
})
if err != nil {
gs.Server.Logger.Error("failed to marshal status", log.Error(err))
return err
}
return s.Err()
}
return nil
}
func (s *Server) handleP4Exec(w http.ResponseWriter, r *http.Request) {
var req p4ExecRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if len(req.Args) < 1 {
http.Error(w, "args must be greater than or equal to 1", http.StatusBadRequest)
return
}
// Make sure the subcommand is explicitly allowed
allowlist := []string{"protects", "groups", "users", "group", "changes"}
allowed := false
for _, arg := range allowlist {
if req.Args[0] == arg {
allowed = true
break
}
}
if !allowed {
http.Error(w, fmt.Sprintf("subcommand %q is not allowed", req.Args[0]), http.StatusBadRequest)
return
}
p4home, err := gitserverfs.MakeP4HomeDir(s.ReposDir)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Log which actor is accessing p4-exec.
//
// p4-exec is currently only used for fetching user based permissions information
// so, we don't have a repo name.
accesslog.Record(r.Context(), "<no-repo>",
log.String("p4user", req.P4User),
log.String("p4port", req.P4Port),
log.Strings("args", req.Args),
)
// Make sure credentials are valid before heavier operation
err = perforce.P4TestWithTrust(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s.p4execHTTP(w, r, &req)
}
func (s *Server) p4execHTTP(w http.ResponseWriter, r *http.Request, req *p4ExecRequest) {
logger := s.Logger.Scoped("p4exec", "")
// Flush writes more aggressively than standard net/http so that clients
// with a context deadline see as much partial response body as possible.
if fw := newFlushingResponseWriter(logger, w); fw != nil {
w = fw
defer fw.Close()
}
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
defer cancel()
w.Header().Set("Trailer", "X-Exec-Error")
w.Header().Add("Trailer", "X-Exec-Exit-Status")
w.Header().Add("Trailer", "X-Exec-Stderr")
w.WriteHeader(http.StatusOK)
execStatus := s.p4Exec(ctx, logger, req, r.UserAgent(), w)
w.Header().Set("X-Exec-Error", errorString(execStatus.Err))
w.Header().Set("X-Exec-Exit-Status", strconv.Itoa(execStatus.ExitStatus))
w.Header().Set("X-Exec-Stderr", execStatus.Stderr)
}
func (s *Server) p4Exec(ctx context.Context, logger log.Logger, req *p4ExecRequest, userAgent string, w io.Writer) execStatus {
start := time.Now()
var cmdStart time.Time // set once we have ensured commit
exitStatus := executil.UnsetExitStatus
var stdoutN, stderrN int64
var status string
var execErr error
// Instrumentation
{
cmd := ""
if len(req.Args) > 0 {
cmd = req.Args[0]
}
args := strings.Join(req.Args, " ")
var tr trace.Trace
tr, ctx = trace.New(ctx, "p4exec."+cmd, attribute.String("port", req.P4Port))
tr.SetAttributes(attribute.String("args", args))
logger = logger.WithTrace(trace.Context(ctx))
execRunning.WithLabelValues(cmd).Inc()
defer func() {
tr.AddEvent("done",
attribute.String("status", status),
attribute.Int64("stdout", stdoutN),
attribute.Int64("stderr", stderrN),
)
tr.SetError(execErr)
tr.End()
duration := time.Since(start)
execRunning.WithLabelValues(cmd).Dec()
execDuration.WithLabelValues(cmd, status).Observe(duration.Seconds())
var cmdDuration time.Duration
if !cmdStart.IsZero() {
cmdDuration = time.Since(cmdStart)
}
isSlow := cmdDuration > 30*time.Second
if honey.Enabled() || traceLogs || isSlow {
act := actor.FromContext(ctx)
ev := honey.NewEvent("gitserver-p4exec")
ev.SetSampleRate(honeySampleRate(cmd, act))
ev.AddField("p4port", req.P4Port)
ev.AddField("cmd", cmd)
ev.AddField("args", args)
ev.AddField("actor", act.UIDString())
ev.AddField("client", userAgent)
ev.AddField("duration_ms", duration.Milliseconds())
ev.AddField("stdout_size", stdoutN)
ev.AddField("stderr_size", stderrN)
ev.AddField("exit_status", exitStatus)
ev.AddField("status", status)
if execErr != nil {
ev.AddField("error", execErr.Error())
}
if !cmdStart.IsZero() {
ev.AddField("cmd_duration_ms", cmdDuration.Milliseconds())
}
if traceID := trace.ID(ctx); traceID != "" {
ev.AddField("traceID", traceID)
ev.AddField("trace", trace.URL(traceID, conf.DefaultClient()))
}
_ = ev.Send()
if traceLogs {
logger.Debug("TRACE gitserver p4exec", log.Object("ev.Fields", mapToLoggerField(ev.Fields())...))
}
if isSlow {
logger.Warn("Long p4exec request", log.Object("ev.Fields", mapToLoggerField(ev.Fields())...))
}
}
}()
}
p4home, err := gitserverfs.MakeP4HomeDir(s.ReposDir)
if err != nil {
return execStatus{ExitStatus: -1, Err: err}
}
var stderrBuf bytes.Buffer
stdoutW := &writeCounter{w: w}
stderrW := &writeCounter{w: &limitWriter{W: &stderrBuf, N: 1024}}
cmdStart = time.Now()
cmd := exec.CommandContext(ctx, "p4", req.Args...)
cmd.Env = append(os.Environ(),
"P4PORT="+req.P4Port,
"P4USER="+req.P4User,
"P4PASSWD="+req.P4Passwd,
"HOME="+p4home,
)
cmd.Stdout = stdoutW
cmd.Stderr = stderrW
exitStatus, execErr = executil.RunCommand(ctx, s.RecordingCommandFactory.Wrap(ctx, s.Logger, cmd))
status = strconv.Itoa(exitStatus)
stdoutN = stdoutW.n
stderrN = stderrW.n
stderr := stderrBuf.String()
return execStatus{
ExitStatus: exitStatus,
Stderr: stderr,
Err: execErr,
}
}
// p4ExecRequest is a request to execute a p4 command with given arguments.
//
// Note that this request is deserialized by both gitserver and the frontend's
// internal proxy route and any major change to this structure will need to be
// reconciled in both places.
type p4ExecRequest struct {
P4Port string `json:"p4port"`
P4User string `json:"p4user"`
P4Passwd string `json:"p4passwd"`
Args []string `json:"args"`
}
func (r *p4ExecRequest) ToProto() *proto.P4ExecRequest {
return &proto.P4ExecRequest{
P4Port: r.P4Port,
P4User: r.P4User,
P4Passwd: r.P4Passwd,
Args: stringsToByteSlices(r.Args),
}
}
func (r *p4ExecRequest) FromProto(p *proto.P4ExecRequest) {
*r = p4ExecRequest{
P4Port: p.GetP4Port(), //nolint:staticcheck
P4User: p.GetP4User(), //nolint:staticcheck
P4Passwd: p.GetP4Passwd(), //nolint:staticcheck
Args: byteSlicesToStrings(p.GetArgs()), //nolint:staticcheck
}
}
func stringsToByteSlices(in []string) [][]byte {
res := make([][]byte, len(in))
for i, s := range in {
res[i] = []byte(s)
}
return res
}

View File

@ -0,0 +1,260 @@
package internal
import (
"bytes"
"context"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os/exec"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/sourcegraph/log/logtest"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/executil"
"github.com/sourcegraph/sourcegraph/internal/database/dbmocks"
proto "github.com/sourcegraph/sourcegraph/internal/gitserver/v1"
"github.com/sourcegraph/sourcegraph/internal/grpc"
"github.com/sourcegraph/sourcegraph/internal/grpc/defaults"
"github.com/sourcegraph/sourcegraph/internal/observation"
"github.com/sourcegraph/sourcegraph/internal/wrexec"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
func TestServer_handleP4Exec(t *testing.T) {
defaultMockRunCommand := func(ctx context.Context, cmd *exec.Cmd) (int, error) {
switch cmd.Args[1] {
case "users":
_, _ = cmd.Stdout.Write([]byte("admin <admin@joe-perforce-server> (admin) accessed 2021/01/31"))
_, _ = cmd.Stderr.Write([]byte("teststderr"))
return 42, errors.New("the answer to life the universe and everything")
}
return 0, nil
}
t.Cleanup(func() {
executil.UpdateRunCommandMock(nil)
})
startServer := func(t *testing.T) (handler http.Handler, client proto.GitserverServiceClient, cleanup func()) {
t.Helper()
logger := logtest.Scoped(t)
s := &Server{
Logger: logger,
ReposDir: t.TempDir(),
ObservationCtx: observation.TestContextTB(t),
skipCloneForTests: true,
DB: dbmocks.NewMockDB(),
RecordingCommandFactory: wrexec.NewNoOpRecordingCommandFactory(),
Locker: NewRepositoryLocker(),
}
server := defaults.NewServer(logger)
proto.RegisterGitserverServiceServer(server, &GRPCServer{Server: s})
handler = grpc.MultiplexHandlers(server, s.Handler())
srv := httptest.NewServer(handler)
u, _ := url.Parse(srv.URL)
conn, err := defaults.Dial(u.Host, logger.Scoped("gRPC client", ""))
if err != nil {
t.Fatalf("failed to dial: %v", err)
}
client = proto.NewGitserverServiceClient(conn)
return handler, client, func() {
srv.Close()
conn.Close()
server.Stop()
}
}
t.Run("gRPC", func(t *testing.T) {
readAll := func(execClient proto.GitserverService_P4ExecClient) ([]byte, error) {
var buf bytes.Buffer
for {
resp, err := execClient.Recv()
if errors.Is(err, io.EOF) {
return buf.Bytes(), nil
}
if err != nil {
return buf.Bytes(), err
}
_, err = buf.Write(resp.GetData())
if err != nil {
t.Fatalf("failed to write data: %v", err)
}
}
}
t.Run("users", func(t *testing.T) {
executil.UpdateRunCommandMock(defaultMockRunCommand)
_, client, closeFunc := startServer(t)
t.Cleanup(closeFunc)
stream, err := client.P4Exec(context.Background(), &proto.P4ExecRequest{
Args: [][]byte{[]byte("users")},
})
if err != nil {
t.Fatalf("failed to call P4Exec: %v", err)
}
data, err := readAll(stream)
s, ok := status.FromError(err)
if !ok {
t.Fatal("received non-status error from p4exec call")
}
if diff := cmp.Diff("the answer to life the universe and everything", s.Message()); diff != "" {
t.Fatalf("unexpected error in stream (-want +got):\n%s", diff)
}
expectedData := []byte("admin <admin@joe-perforce-server> (admin) accessed 2021/01/31")
if diff := cmp.Diff(expectedData, data); diff != "" {
t.Fatalf("unexpected data (-want +got):\n%s", diff)
}
})
t.Run("empty request", func(t *testing.T) {
executil.UpdateRunCommandMock(defaultMockRunCommand)
_, client, closeFunc := startServer(t)
t.Cleanup(closeFunc)
stream, err := client.P4Exec(context.Background(), &proto.P4ExecRequest{})
if err != nil {
t.Fatalf("failed to call P4Exec: %v", err)
}
_, err = readAll(stream)
if status.Code(err) != codes.InvalidArgument {
t.Fatalf("expected InvalidArgument error, got %v", err)
}
})
t.Run("disallowed command", func(t *testing.T) {
executil.UpdateRunCommandMock(defaultMockRunCommand)
_, client, closeFunc := startServer(t)
t.Cleanup(closeFunc)
stream, err := client.P4Exec(context.Background(), &proto.P4ExecRequest{
Args: [][]byte{[]byte("bad_command")},
})
if err != nil {
t.Fatalf("failed to call P4Exec: %v", err)
}
_, err = readAll(stream)
if status.Code(err) != codes.InvalidArgument {
t.Fatalf("expected InvalidArgument error, got %v", err)
}
})
t.Run("context cancelled", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
executil.UpdateRunCommandMock(func(ctx context.Context, _ *exec.Cmd) (int, error) {
// fake a context cancellation that occurs while the process is running
cancel()
return 0, ctx.Err()
})
_, client, closeFunc := startServer(t)
t.Cleanup(closeFunc)
stream, err := client.P4Exec(ctx, &proto.P4ExecRequest{
Args: [][]byte{[]byte("users")},
})
if err != nil {
t.Fatalf("failed to call P4Exec: %v", err)
}
_, err = readAll(stream)
if !(errors.Is(err, context.Canceled) || status.Code(err) == codes.Canceled) {
t.Fatalf("expected context cancelation error, got %v", err)
}
})
})
t.Run("HTTP", func(t *testing.T) {
tests := []Test{
{
Name: "Command",
Request: newRequest("POST", "/p4-exec", strings.NewReader(`{"args": ["users"]}`)),
ExpectedCode: http.StatusOK,
ExpectedBody: "admin <admin@joe-perforce-server> (admin) accessed 2021/01/31",
ExpectedTrailers: http.Header{
"X-Exec-Error": {"the answer to life the universe and everything"},
"X-Exec-Exit-Status": {"42"},
"X-Exec-Stderr": {"teststderr"},
},
},
{
Name: "Error",
Request: newRequest("POST", "/p4-exec", strings.NewReader(`{"args": ["bad_command"]}`)),
ExpectedCode: http.StatusBadRequest,
ExpectedBody: "subcommand \"bad_command\" is not allowed",
},
{
Name: "EmptyBody",
Request: newRequest("POST", "/p4-exec", nil),
ExpectedCode: http.StatusBadRequest,
ExpectedBody: `EOF`,
},
{
Name: "EmptyInput",
Request: newRequest("POST", "/p4-exec", strings.NewReader("{}")),
ExpectedCode: http.StatusBadRequest,
ExpectedBody: `args must be greater than or equal to 1`,
},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
executil.UpdateRunCommandMock(defaultMockRunCommand)
handler, _, closeFunc := startServer(t)
t.Cleanup(closeFunc)
w := httptest.ResponseRecorder{Body: new(bytes.Buffer)}
handler.ServeHTTP(&w, test.Request)
res := w.Result()
if res.StatusCode != test.ExpectedCode {
t.Errorf("wrong status: expected %d, got %d", test.ExpectedCode, w.Code)
}
body, err := io.ReadAll(res.Body)
if err != nil {
t.Fatal(err)
}
if strings.TrimSpace(string(body)) != test.ExpectedBody {
t.Errorf("wrong body: expected %q, got %q", test.ExpectedBody, string(body))
}
for k, v := range test.ExpectedTrailers {
if got := res.Trailer.Get(k); got != v[0] {
t.Errorf("wrong trailer %q: expected %q, got %q", k, v[0], got)
}
}
})
}
})
}

View File

@ -469,9 +469,9 @@ func (s *Server) shelveChangelist(ctx context.Context, req protocol.CreateCommit
}
// check to see if there's a changelist for this target branch already
cid, err := p4Cmd.changeListIDFromClientSpecName(p4client)
if err == nil && cid != "" {
return cid, nil
cl, err := perforce.GetChangelistByClient(ctx, p4port, p4user, p4passwd, tmpClientDir, p4client)
if err == nil && cl.ID != "" {
return cl.ID, nil
}
// extract the base changelist id from the base commit
@ -584,7 +584,7 @@ func (s *Server) shelveChangelist(ctx context.Context, req protocol.CreateCommit
// feed the changelist form into `p4 shelve`
// capture the output to parse for a changelist id
cid, err = p4Cmd.shelveChangelist(changeForm)
cid, err := p4Cmd.shelveChangelist(changeForm)
if err != nil {
errorMessage := "failed shelving the changelist"
logger.Error(errorMessage, log.String("output", digErrorMessage(err)), log.Error(errors.New(errorMessage)))
@ -656,25 +656,6 @@ func (p p4Command) commandContext(args ...string) *exec.Cmd {
return cmd
}
// Uses `p4 changes` to see if there is a changelist already associated with the given client spec
func (p p4Command) changeListIDFromClientSpecName(p4client string) (string, error) {
cmd := p.commandContext("changes",
"-r", // list in reverse order, which means that the given changelist id will be the first one listed
"-m", "1", // limit output to one record, so that the given changelist is the only one listed
"-l", // use a long listing, which includes the whole commit message
"-c", p4client,
)
out, err := cmd.CombinedOutput()
if err != nil {
return "", errors.Wrap(err, string(out))
}
pcl, err := internalperforce.ParseChangelistOutput(string(out))
if err != nil {
return "", errors.Wrap(err, string(out))
}
return pcl.ID, nil
}
const clientSpecForm = `Client: %s
Owner: %s
Description:

View File

@ -4,11 +4,15 @@ load("//dev:go_defs.bzl", "go_test")
go_library(
name = "perforce",
srcs = [
"changelist.go",
"cloneable.go",
"depots.go",
"groups.go",
"login.go",
"perforce.go",
"protects.go",
"url.go",
"users.go",
"util.go",
],
importpath = "github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/perforce",
@ -17,6 +21,7 @@ go_library(
"//cmd/gitserver/internal/common",
"//cmd/gitserver/internal/executil",
"//internal/api",
"//internal/byteutils",
"//internal/conf",
"//internal/database",
"//internal/extsvc",
@ -34,7 +39,10 @@ go_library(
go_test(
name = "perforce_test",
srcs = [
"changelist_test.go",
"groups_test.go",
"perforce_test.go",
"protects_test.go",
"url_test.go",
"util_test.go",
],
@ -48,6 +56,7 @@ go_test(
"//internal/extsvc",
"//internal/gitserver",
"//internal/observation",
"//internal/perforce",
"//internal/types",
"//internal/vcs",
"//schema",

View File

@ -0,0 +1,166 @@
package perforce
import (
"bytes"
"context"
"encoding/json"
"os"
"os/exec"
"strconv"
"strings"
"time"
"github.com/sourcegraph/log"
"github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/executil"
p4types "github.com/sourcegraph/sourcegraph/internal/perforce"
"github.com/sourcegraph/sourcegraph/internal/wrexec"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
func GetChangelistByID(ctx context.Context, p4home, p4port, p4user, p4passwd, changelistID string) (*p4types.Changelist, error) {
cmd := exec.CommandContext(
ctx,
"p4",
"-Mj",
"-z", "tag",
"changes",
"-r", // list in reverse order, which means that the given changelist id will be the first one listed
"-m", "1", // limit output to one record, so that the given changelist is the only one listed
"-l", // use a long listing, which includes the whole commit message
"-e", changelistID, // start from this changelist and go up
)
cmd.Env = append(os.Environ(),
"P4PORT="+p4port,
"P4USER="+p4user,
"P4PASSWD="+p4passwd,
"HOME="+p4home,
)
out, err := executil.RunCommandCombinedOutput(ctx, wrexec.Wrap(ctx, log.NoOp(), cmd))
if err != nil {
if ctxerr := ctx.Err(); ctxerr != nil {
err = errors.Wrap(ctxerr, "p4 changes context error")
}
if len(out) > 0 {
err = errors.Wrapf(err, `failed to run command "p4 changes" (output follows)\n\n%s`, specifyCommandInErrorMessage(string(out), cmd))
}
return nil, err
}
output := bytes.TrimSpace(out)
if len(output) == 0 {
return nil, errors.New("invalid changelist " + changelistID)
}
pcl, err := parseChangelistOutput(output)
if err != nil {
return nil, errors.Wrap(err, "unable to parse change output")
}
return pcl, nil
}
func GetChangelistByClient(ctx context.Context, p4port, p4user, p4passwd, workDir, client string) (*p4types.Changelist, error) {
cmd := exec.CommandContext(
ctx,
"p4",
"-Mj",
"-z", "tag",
"changes",
"-r", // list in reverse order, which means that the given changelist id will be the first one listed
"-m", "1", // limit output to one record, so that the given changelist is the only one listed
"-l", // use a long listing, which includes the whole commit message
"-c", client,
)
cmd.Env = append(os.Environ(),
"P4PORT="+p4port,
"P4USER="+p4user,
"P4PASSWD="+p4passwd,
"P4CLIENT="+client,
)
cmd.Dir = workDir
out, err := executil.RunCommandCombinedOutput(ctx, wrexec.Wrap(ctx, log.NoOp(), cmd))
if err != nil {
if ctxerr := ctx.Err(); ctxerr != nil {
err = errors.Wrap(ctxerr, "p4 changes context error")
}
if len(out) > 0 {
err = errors.Wrapf(err, `failed to run command "p4 changes" (output follows)\n\n%s`, specifyCommandInErrorMessage(string(out), cmd))
}
return nil, err
}
output := bytes.TrimSpace(out)
if len(output) == 0 {
return nil, errors.New("no changelist found for client " + client)
}
pcl, err := parseChangelistOutput(output)
if err != nil {
return nil, errors.Wrap(err, "unable to parse change output")
}
return pcl, nil
}
type changelistJson struct {
// Change is the number of the changelist.
Change string `json:"change"`
ChangeType string `json:"changeType"`
Client string `json:"client"`
Desc string `json:"desc"`
Path string `json:"path"`
Status string `json:"status"`
Time string `json:"time"`
User string `json:"user"`
}
// parseChangelistOutput parses one JSON line of p4 changes output.
// output should be whitespace-trimmed and not empty.
func parseChangelistOutput(output []byte) (*p4types.Changelist, error) {
var cidj changelistJson
err := json.Unmarshal(output, &cidj)
if err != nil {
return nil, errors.Wrap(err, "unable to unmarshal change output")
}
state, err := parseChangelistState(cidj.Status)
if err != nil {
return nil, errors.Wrap(err, "unable to parse changelist state")
}
intTime, err := strconv.Atoi(cidj.Time)
if err != nil {
return nil, errors.Wrap(err, "invalid time: "+cidj.Time)
}
creationDate := time.Unix(int64(intTime), 0)
return &p4types.Changelist{
ID: cidj.Change,
State: state,
Author: cidj.User,
CreationDate: creationDate,
Title: cidj.Client,
Message: strings.TrimSpace(cidj.Desc),
}, nil
}
func parseChangelistState(state string) (p4types.ChangelistState, error) {
switch strings.ToLower(strings.TrimSpace(state)) {
case "submitted":
return p4types.ChangelistStateSubmitted, nil
case "pending":
return p4types.ChangelistStatePending, nil
case "shelved":
return p4types.ChangelistStateShelved, nil
case "closed":
return p4types.ChangelistStateClosed, nil
default:
return "", errors.Newf("invalid Perforce changelist state: %s", state)
}
}

View File

@ -0,0 +1,59 @@
package perforce
import (
"testing"
"time"
"github.com/google/go-cmp/cmp"
p4types "github.com/sourcegraph/sourcegraph/internal/perforce"
)
func TestParseChangelistOutput(t *testing.T) {
created := time.Unix(1629179137, 0)
testCases := []struct {
output string
expectedChangelist *p4types.Changelist
shouldError bool
}{
{
output: `{"change":"10","changeType":"public","client":"tester","desc":"test-first-one\nAppend still another line to all SECOND.md files\nHere's a second line of message\n","path":"//go/src/...","status":"pending","time":"1629179137","user":"admin"}`,
expectedChangelist: &p4types.Changelist{
ID: "10",
CreationDate: created,
Author: "admin",
Title: "tester",
State: p4types.ChangelistStatePending,
Message: "test-first-one\nAppend still another line to all SECOND.md files\nHere's a second line of message",
},
},
{
output: `{"change":123}`,
shouldError: true,
},
{
output: `definitelynotjson`,
shouldError: true,
},
{
output: `{"change":"1188","changeType":"public","client":"tester","desc":"test","path":"//go/src/...","status":"INVALID","time":"1629179137","user":"admin"}`,
shouldError: true,
},
}
for _, testCase := range testCases {
changelist, err := parseChangelistOutput([]byte(testCase.output))
if testCase.shouldError {
if err == nil {
t.Errorf("expected error but got nil")
}
continue
}
if err != nil {
t.Errorf("unexpected error: %s", err)
}
if diff := cmp.Diff(testCase.expectedChangelist, changelist); diff != "" {
t.Errorf("parsed changelist did not match expected (-want +got):\n%s", diff)
}
}
}

View File

@ -16,35 +16,51 @@ import (
"github.com/sourcegraph/sourcegraph/lib/errors"
)
type PerforceDepotType string
type perforceDepotType string
func (t perforceDepotType) Valid() bool {
switch t {
case perforceDepotTypeLocal,
perforceDepotTypeRemote,
perforceDepotTypeStream,
perforceDepotTypeSpec,
perforceDepotTypeUnload,
perforceDepotTypeArchive,
perforceDepotTypeTangent,
perforceDepotTypeGraph:
return true
default:
return false
}
}
const (
Local PerforceDepotType = "local"
Remote PerforceDepotType = "remote"
Stream PerforceDepotType = "stream"
Spec PerforceDepotType = "spec"
Unload PerforceDepotType = "unload"
Archive PerforceDepotType = "archive"
Tangent PerforceDepotType = "tangent"
Graph PerforceDepotType = "graph"
perforceDepotTypeLocal perforceDepotType = "local"
perforceDepotTypeRemote perforceDepotType = "remote"
perforceDepotTypeStream perforceDepotType = "stream"
perforceDepotTypeSpec perforceDepotType = "spec"
perforceDepotTypeUnload perforceDepotType = "unload"
perforceDepotTypeArchive perforceDepotType = "archive"
perforceDepotTypeTangent perforceDepotType = "tangent"
perforceDepotTypeGraph perforceDepotType = "graph"
)
// PerforceDepot is a definiton of a depot that matches the format
// perforceDepot is a definiton of a depot that matches the format
// returned from `p4 -Mj -ztag depots`
type PerforceDepot struct {
type perforceDepot struct {
Desc string `json:"desc,omitempty"`
Map string `json:"map,omitempty"`
Name string `json:"name,omitempty"`
// Time is seconds since the Epoch, but p4 quotes it in the output, so it's a string
Time string `json:"time,omitempty"`
// Type is local, remote, stream, spec, unload, archive, tangent, graph
Type PerforceDepotType `json:"type,omitempty"`
Type perforceDepotType `json:"type,omitempty"`
}
// P4Depots returns all of the depots to which the user has access on the host
// and whose names match the given nameFilter, which can contain asterisks (*) for wildcards
// if nameFilter is blank, return all depots
func P4Depots(ctx context.Context, p4home, p4port, p4user, p4passwd, nameFilter string) ([]PerforceDepot, error) {
// if nameFilter is blank, return all depots.
func P4Depots(ctx context.Context, p4home, p4port, p4user, p4passwd, nameFilter string) ([]perforceDepot, error) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
@ -71,12 +87,12 @@ func P4Depots(ctx context.Context, p4home, p4port, p4user, p4passwd, nameFilter
}
return nil, err
}
depots := make([]PerforceDepot, 0)
depots := make([]perforceDepot, 0)
if len(out) > 0 {
// the output of `p4 -Mj -ztag depots` is a series of JSON-formatted depot definitions, one per line
buf := bufio.NewScanner(bytes.NewBuffer(out))
for buf.Scan() {
depot := PerforceDepot{}
depot := perforceDepot{}
err := json.Unmarshal(buf.Bytes(), &depot)
if err != nil {
return nil, errors.Wrap(err, "malformed output from p4 depots")

View File

@ -0,0 +1,71 @@
package perforce
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"github.com/sourcegraph/log"
"github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/executil"
"github.com/sourcegraph/sourcegraph/internal/wrexec"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
// P4GroupMembers returns all usernames that are members of the given group.
func P4GroupMembers(ctx context.Context, p4home, p4port, p4user, p4passwd, group string) ([]string, error) {
cmd := exec.CommandContext(ctx, "p4", "-Mj", "-ztag", "group", "-o", group)
cmd.Env = append(os.Environ(),
"P4PORT="+p4port,
"P4USER="+p4user,
"P4PASSWD="+p4passwd,
"HOME="+p4home,
)
out, err := executil.RunCommandCombinedOutput(ctx, wrexec.Wrap(ctx, log.NoOp(), cmd))
if err != nil {
if ctxerr := ctx.Err(); ctxerr != nil {
err = errors.Wrap(ctxerr, "p4 group context error")
}
if len(out) > 0 {
err = errors.Wrapf(err, `failed to run command "p4 group" (output follows)\n\n%s`, specifyCommandInErrorMessage(string(out), cmd))
}
return nil, err
}
if len(out) == 0 {
// no error, but also no members. Maybe the group doesn't have any members?
return nil, nil
}
return parseP4GroupMembers(out)
}
func parseP4GroupMembers(out []byte) ([]string, error) {
var jsonGroup map[string]any
err := json.Unmarshal(out, &jsonGroup)
if err != nil {
return nil, errors.Wrap(err, "malformed output from p4 group")
}
users := make([]string, 0)
currentUserIdx := 0
for {
user, ok := jsonGroup[fmt.Sprintf("Users%d", currentUserIdx)]
currentUserIdx++
if !ok {
break
}
username, ok := user.(string)
if !ok {
continue
}
users = append(users, username)
}
return users, nil
}

View File

@ -0,0 +1,16 @@
package perforce
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestParseP4GroupMembers(t *testing.T) {
groupOut := []byte(`{"Group":"all","MaxLockTime":"unset","MaxOpenFiles":"unset","MaxResults":"unset","MaxScanRows":"unset","Owners0":"admin","PasswordTimeout":"unset","Timeout":"43200","Users0":"admin","Users1":"alice","Users2":"bob","Users3":"buildkite","Users4":"test-perforce"}`)
users, err := parseP4GroupMembers(groupOut)
require.NoError(t, err)
require.Equal(t, []string{"admin", "alice", "bob", "buildkite", "test-perforce"}, users)
}

View File

@ -40,6 +40,46 @@ func P4TestWithTrust(ctx context.Context, p4home, p4port, p4user, p4passwd strin
return err
}
// P4UserIsSuperUser checks if the given credentials are for a super level user.
// If the user is a super user, no error is returned. If not, ErrIsNotSuperUser
// is returned.
// Other errors may occur.
func P4UserIsSuperUser(ctx context.Context, p4home, p4port, p4user, p4passwd string) error {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
// Validate the user has "super" access with "-u" option, see https://www.perforce.com/perforce/r12.1/manuals/cmdref/protects.html
cmd := exec.CommandContext(ctx, "p4", "protects", "-u", p4user)
cmd.Env = append(os.Environ(),
"P4PORT="+p4port,
"P4USER="+p4user,
"P4PASSWD="+p4passwd,
"HOME="+p4home,
)
out, err := executil.RunCommandCombinedOutput(ctx, wrexec.Wrap(ctx, log.NoOp(), cmd))
if err != nil {
if ctxerr := ctx.Err(); ctxerr != nil {
err = ctxerr
}
if strings.Contains(err.Error(), "You don't have permission for this operation.") {
return ErrIsNotSuperUser
}
if len(out) > 0 {
err = errors.Errorf("%s (output follows)\n\n%s", err, out)
}
return err
}
return nil
}
var ErrIsNotSuperUser = errors.New("the user does not have super access")
// P4Trust blindly accepts fingerprint of the Perforce server.
func P4Trust(ctx context.Context, p4home, host string) error {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)

View File

@ -221,6 +221,7 @@ func TestServicePipeline(t *testing.T) {
db := dbmocks.NewMockDB()
db.ReposFunc.SetDefaultReturn(repos)
db.RepoCommitsChangelistsFunc.SetDefaultReturn(dbmocks.NewMockRepoCommitsChangelistsStore())
logger := logtest.NoOp(t)
svc := NewService(ctx, observation.NewContext(logger), logger, db, list.New())

View File

@ -0,0 +1,126 @@
package perforce
import (
"bytes"
"context"
"encoding/json"
"os"
"os/exec"
"github.com/sourcegraph/log"
"github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/executil"
"github.com/sourcegraph/sourcegraph/internal/byteutils"
p4types "github.com/sourcegraph/sourcegraph/internal/perforce"
"github.com/sourcegraph/sourcegraph/internal/wrexec"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
// P4ProtectsForUser returns all protect definitions that apply to the given username.
func P4ProtectsForUser(ctx context.Context, p4home, p4port, p4user, p4passwd, username string) ([]*p4types.Protect, error) {
// -u User : Displays protection lines that apply to the named user. This option
// requires super access.
cmd := exec.CommandContext(ctx, "p4", "-Mj", "-ztag", "protects", "-u", username)
cmd.Env = append(os.Environ(),
"P4PORT="+p4port,
"P4USER="+p4user,
"P4PASSWD="+p4passwd,
"HOME="+p4home,
)
out, err := executil.RunCommandCombinedOutput(ctx, wrexec.Wrap(ctx, log.NoOp(), cmd))
if err != nil {
if ctxerr := ctx.Err(); ctxerr != nil {
err = errors.Wrap(ctxerr, "p4 protects context error")
}
if len(out) > 0 {
err = errors.Wrapf(err, `failed to run command "p4 protects" (output follows)\n\n%s`, specifyCommandInErrorMessage(string(out), cmd))
}
return nil, err
}
if len(out) == 0 {
// no error, but also no protects.
return nil, nil
}
return parseP4Protects(out)
}
// P4ProtectsForUser returns all protect definitions that apply to the given depot.
func P4ProtectsForDepot(ctx context.Context, p4home, p4port, p4user, p4passwd, depot string) ([]*p4types.Protect, error) {
// -a : Displays protection lines for all users. This option requires super
// access.
cmd := exec.CommandContext(ctx, "p4", "-Mj", "-ztag", "protects", "-a", depot)
cmd.Env = append(os.Environ(),
"P4PORT="+p4port,
"P4USER="+p4user,
"P4PASSWD="+p4passwd,
"HOME="+p4home,
)
out, err := executil.RunCommandCombinedOutput(ctx, wrexec.Wrap(ctx, log.NoOp(), cmd))
if err != nil {
if ctxerr := ctx.Err(); ctxerr != nil {
err = errors.Wrap(ctxerr, "p4 protects context error")
}
if len(out) > 0 {
err = errors.Wrapf(err, `failed to run command "p4 protects" (output follows)\n\n%s`, specifyCommandInErrorMessage(string(out), cmd))
}
return nil, err
}
if len(out) == 0 {
// no error, but also no protects.
return nil, nil
}
return parseP4Protects(out)
}
type perforceJSONProtect struct {
DepotFile string `json:"depotFile"`
Host string `json:"host"`
Line string `json:"line"`
Perm string `json:"perm"`
IsGroup *string `json:"isgroup,omitempty"`
Unmap *string `json:"unmap,omitempty"`
User string `json:"user"`
}
func parseP4Protects(out []byte) ([]*p4types.Protect, error) {
protects := make([]*p4types.Protect, 0)
lr := byteutils.NewLineReader(out)
for lr.Scan() {
line := lr.Line()
// Trim whitespace
line = bytes.TrimSpace(line)
var parsedLine perforceJSONProtect
if err := json.Unmarshal(line, &parsedLine); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal protect line")
}
entityType := "user"
if parsedLine.IsGroup != nil {
entityType = "group"
}
protects = append(protects, &p4types.Protect{
Host: parsedLine.Host,
EntityType: entityType,
EntityName: parsedLine.User,
Match: parsedLine.DepotFile,
IsExclusion: parsedLine.Unmap != nil,
Level: parsedLine.Perm,
})
}
return protects, nil
}

View File

@ -0,0 +1,84 @@
package perforce
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/sourcegraph/sourcegraph/internal/perforce"
)
func TestParseP4Protects(t *testing.T) {
protectsOut := []byte(`{"depotFile":"//...","host":"*","line":"1","perm":"list","unmap":"","user":"*"}
{"depotFile":"//test-perms/Frontend/...","host":"*","isgroup":"","line":"6","perm":"write","user":"Frontend"}
{"depotFile":"//integration-test-depot/...","host":"*","isgroup":"","line":"14","perm":"=read","unmap":"","user":"all"}
{"depotFile":"//go/...","host":"*","line":"24","perm":"read","user":"bob"}
{"depotFile":"//go/api/...","host":"*","line":"25","perm":"=read","unmap":"","user":"bob"}
{"depotFile":"//go/*/except.txt","host":"*","isgroup":"","line":"26","perm":"read","user":"Frontend"}
{"depotFile":"//go/...","host":"192.168.10.1/24","line":"27","perm":"=read","unmap":"","user":"bob"}
`)
protects, err := parseP4Protects(protectsOut)
require.NoError(t, err)
want := []*perforce.Protect{
{
Level: "list",
EntityType: "user",
EntityName: "*",
Match: "//...",
IsExclusion: true,
Host: "*",
},
{
Level: "write",
EntityType: "group",
EntityName: "Frontend",
Match: "//test-perms/Frontend/...",
IsExclusion: false,
Host: "*",
},
{
Level: "=read",
EntityType: "group",
EntityName: "all",
Match: "//integration-test-depot/...",
IsExclusion: true,
Host: "*",
},
{
Level: "read",
EntityType: "user",
EntityName: "bob",
Match: "//go/...",
IsExclusion: false,
Host: "*",
},
{
Level: "=read",
EntityType: "user",
EntityName: "bob",
Match: "//go/api/...",
IsExclusion: true,
Host: "*",
},
{
Level: "read",
EntityType: "group",
EntityName: "Frontend",
Match: "//go/*/except.txt",
IsExclusion: false,
Host: "*",
},
{
Level: "=read",
EntityType: "user",
EntityName: "bob",
Match: "//go/...",
IsExclusion: true,
Host: "192.168.10.1/24",
},
}
require.Equal(t, want, protects)
}

View File

@ -0,0 +1,93 @@
package perforce
import (
"context"
"encoding/json"
"os"
"os/exec"
"github.com/sourcegraph/log"
"github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/executil"
"github.com/sourcegraph/sourcegraph/internal/byteutils"
"github.com/sourcegraph/sourcegraph/internal/perforce"
"github.com/sourcegraph/sourcegraph/internal/wrexec"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
// P4Users returns all of users known to the Perforce server.
func P4Users(ctx context.Context, p4home, p4port, p4user, p4passwd string) ([]perforce.User, error) {
cmd := exec.CommandContext(ctx, "p4", "-Mj", "-ztag", "users")
cmd.Env = append(os.Environ(),
"P4PORT="+p4port,
"P4USER="+p4user,
"P4PASSWD="+p4passwd,
"HOME="+p4home,
)
out, err := executil.RunCommandCombinedOutput(ctx, wrexec.Wrap(ctx, log.NoOp(), cmd))
if err != nil {
if ctxerr := ctx.Err(); ctxerr != nil {
err = errors.Wrap(ctxerr, "p4 users context error")
}
if len(out) > 0 {
err = errors.Wrapf(err, `failed to run command "p4 users" (output follows)\n\n%s`, specifyCommandInErrorMessage(string(out), cmd))
}
return nil, err
}
if len(out) == 0 {
// no error, but also no users. Maybe the user doesn't have access to any users?
return nil, nil
}
users := make([]perforce.User, 0)
lr := byteutils.NewLineReader(out)
for lr.Scan() {
line := lr.Line()
// the output of `p4 -Mj -ztag users` is a series of JSON-formatted user definitions, one per line.
var user perforceSJONUser
err := json.Unmarshal(line, &user)
if err != nil {
return nil, errors.Wrap(err, "malformed output from p4 users")
}
users = append(users, perforce.User{
Username: user.User,
Email: user.Email,
})
}
return users, nil
}
type perforceUserType string
func (t perforceUserType) Valid() bool {
switch t {
case perforceUserTypeStandard,
perforceUserTypeOperator,
perforceUserTypeService:
return true
default:
return false
}
}
const (
perforceUserTypeStandard perforceUserType = "standard"
perforceUserTypeOperator perforceUserType = "operator"
perforceUserTypeService perforceUserType = "service"
)
// perforceSJONUser is a definition of a user that matches the format returned from
// `p4 -Mj -ztag users`.
type perforceSJONUser struct {
Email string `json:"Email,omitempty"`
User string `json:"User,omitempty"`
Password string `json:"Password,omitempty"`
FullName string `json:"FullName,omitempty"`
// Access is seconds since the Epoch, but p4 quotes it in the output, so it's a string
Access string `json:"Access,omitempty"`
Update string `json:"Update,omitempty"`
Type perforceUserType `json:"Type,omitempty"`
}

View File

@ -307,6 +307,12 @@ func (s *Server) Handler() http.Handler {
mux.HandleFunc("/is-perforce-path-cloneable", trace.WithRouteName("is-perforce-path-cloneable", s.handleIsPerforcePathCloneable))
mux.HandleFunc("/check-perforce-credentials", trace.WithRouteName("check-perforce-credentials", s.handleCheckPerforceCredentials))
mux.HandleFunc("/commands/get-object", trace.WithRouteName("commands/get-object", s.handleGetObject))
mux.HandleFunc("/perforce-users", trace.WithRouteName("perforce-users", s.handlePerforceUsers))
mux.HandleFunc("/perforce-protects-for-user", trace.WithRouteName("perforce-protects-for-user", s.handlePerforceProtectsForUser))
mux.HandleFunc("/perforce-protects-for-depot", trace.WithRouteName("perforce-protects-for-depot", s.handlePerforceProtectsForDepot))
mux.HandleFunc("/perforce-group-members", trace.WithRouteName("perforce-group-members", s.handlePerforceGroupMembers))
mux.HandleFunc("/is-perforce-super-user", trace.WithRouteName("is-perforce-super-user", s.handleIsPerforceSuperUser))
mux.HandleFunc("/perforce-get-changelist", trace.WithRouteName("perforce-get-changelist", s.handlePerforceGetChangelist))
mux.HandleFunc("/ping", trace.WithRouteName("ping", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
@ -1216,197 +1222,6 @@ func (s *Server) execHTTP(w http.ResponseWriter, r *http.Request, req *protocol.
}
}
func (s *Server) handleP4Exec(w http.ResponseWriter, r *http.Request) {
var req protocol.P4ExecRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if len(req.Args) < 1 {
http.Error(w, "args must be greater than or equal to 1", http.StatusBadRequest)
return
}
// Make sure the subcommand is explicitly allowed
allowlist := []string{"protects", "groups", "users", "group", "changes"}
allowed := false
for _, arg := range allowlist {
if req.Args[0] == arg {
allowed = true
break
}
}
if !allowed {
http.Error(w, fmt.Sprintf("subcommand %q is not allowed", req.Args[0]), http.StatusBadRequest)
return
}
p4home, err := gitserverfs.MakeP4HomeDir(s.ReposDir)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Log which actor is accessing p4-exec.
//
// p4-exec is currently only used for fetching user based permissions information
// so, we don't have a repo name.
accesslog.Record(r.Context(), "<no-repo>",
log.String("p4user", req.P4User),
log.String("p4port", req.P4Port),
log.Strings("args", req.Args),
)
// Make sure credentials are valid before heavier operation
err = perforce.P4TestWithTrust(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s.p4execHTTP(w, r, &req)
}
func (s *Server) p4execHTTP(w http.ResponseWriter, r *http.Request, req *protocol.P4ExecRequest) {
logger := s.Logger.Scoped("p4exec", "")
// Flush writes more aggressively than standard net/http so that clients
// with a context deadline see as much partial response body as possible.
if fw := newFlushingResponseWriter(logger, w); fw != nil {
w = fw
defer fw.Close()
}
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
defer cancel()
w.Header().Set("Trailer", "X-Exec-Error")
w.Header().Add("Trailer", "X-Exec-Exit-Status")
w.Header().Add("Trailer", "X-Exec-Stderr")
w.WriteHeader(http.StatusOK)
execStatus := s.p4Exec(ctx, logger, req, r.UserAgent(), w)
w.Header().Set("X-Exec-Error", errorString(execStatus.Err))
w.Header().Set("X-Exec-Exit-Status", strconv.Itoa(execStatus.ExitStatus))
w.Header().Set("X-Exec-Stderr", execStatus.Stderr)
}
func (s *Server) p4Exec(ctx context.Context, logger log.Logger, req *protocol.P4ExecRequest, userAgent string, w io.Writer) execStatus {
start := time.Now()
var cmdStart time.Time // set once we have ensured commit
exitStatus := executil.UnsetExitStatus
var stdoutN, stderrN int64
var status string
var execErr error
// Instrumentation
{
cmd := ""
if len(req.Args) > 0 {
cmd = req.Args[0]
}
args := strings.Join(req.Args, " ")
var tr trace.Trace
tr, ctx = trace.New(ctx, "p4exec."+cmd, attribute.String("port", req.P4Port))
tr.SetAttributes(attribute.String("args", args))
logger = logger.WithTrace(trace.Context(ctx))
execRunning.WithLabelValues(cmd).Inc()
defer func() {
tr.AddEvent("done",
attribute.String("status", status),
attribute.Int64("stdout", stdoutN),
attribute.Int64("stderr", stderrN),
)
tr.SetError(execErr)
tr.End()
duration := time.Since(start)
execRunning.WithLabelValues(cmd).Dec()
execDuration.WithLabelValues(cmd, status).Observe(duration.Seconds())
var cmdDuration time.Duration
if !cmdStart.IsZero() {
cmdDuration = time.Since(cmdStart)
}
isSlow := cmdDuration > 30*time.Second
if honey.Enabled() || traceLogs || isSlow {
act := actor.FromContext(ctx)
ev := honey.NewEvent("gitserver-p4exec")
ev.SetSampleRate(honeySampleRate(cmd, act))
ev.AddField("p4port", req.P4Port)
ev.AddField("cmd", cmd)
ev.AddField("args", args)
ev.AddField("actor", act.UIDString())
ev.AddField("client", userAgent)
ev.AddField("duration_ms", duration.Milliseconds())
ev.AddField("stdout_size", stdoutN)
ev.AddField("stderr_size", stderrN)
ev.AddField("exit_status", exitStatus)
ev.AddField("status", status)
if execErr != nil {
ev.AddField("error", execErr.Error())
}
if !cmdStart.IsZero() {
ev.AddField("cmd_duration_ms", cmdDuration.Milliseconds())
}
if traceID := trace.ID(ctx); traceID != "" {
ev.AddField("traceID", traceID)
ev.AddField("trace", trace.URL(traceID, conf.DefaultClient()))
}
_ = ev.Send()
if traceLogs {
logger.Debug("TRACE gitserver p4exec", log.Object("ev.Fields", mapToLoggerField(ev.Fields())...))
}
if isSlow {
logger.Warn("Long p4exec request", log.Object("ev.Fields", mapToLoggerField(ev.Fields())...))
}
}
}()
}
p4home, err := gitserverfs.MakeP4HomeDir(s.ReposDir)
if err != nil {
return execStatus{ExitStatus: -1, Err: err}
}
var stderrBuf bytes.Buffer
stdoutW := &writeCounter{w: w}
stderrW := &writeCounter{w: &limitWriter{W: &stderrBuf, N: 1024}}
cmdStart = time.Now()
cmd := exec.CommandContext(ctx, "p4", req.Args...)
cmd.Env = append(os.Environ(),
"P4PORT="+req.P4Port,
"P4USER="+req.P4User,
"P4PASSWD="+req.P4Passwd,
"HOME="+p4home,
)
cmd.Stdout = stdoutW
cmd.Stderr = stderrW
exitStatus, execErr = executil.RunCommand(ctx, s.RecordingCommandFactory.Wrap(ctx, s.Logger, cmd))
status = strconv.Itoa(exitStatus)
stdoutN = stdoutW.n
stderrN = stderrW.n
stderr := stderrBuf.String()
return execStatus{
ExitStatus: exitStatus,
Stderr: stderr,
Err: execErr,
}
}
func setLastFetched(ctx context.Context, db database.DB, shardID string, dir common.GitDir, name api.RepoName) error {
lastFetched, err := repoLastFetched(dir)
if err != nil {
@ -2263,3 +2078,297 @@ func (s *Server) handleGetObject(w http.ResponseWriter, r *http.Request) {
return
}
}
func (s *Server) handlePerforceUsers(w http.ResponseWriter, r *http.Request) {
var req protocol.PerforceUsersRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
p4home, err := gitserverfs.MakeP4HomeDir(s.ReposDir)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = perforce.P4TestWithTrust(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
accesslog.Record(
r.Context(),
"<no-repo>",
log.String("p4user", req.P4User),
log.String("p4port", req.P4Port),
)
users, err := perforce.P4Users(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp := &protocol.PerforceUsersResponse{
Users: make([]protocol.PerforceUser, 0, len(users)),
}
for _, user := range users {
resp.Users = append(resp.Users, protocol.PerforceUser{
Username: user.Username,
Email: user.Email,
})
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (s *Server) handlePerforceProtectsForUser(w http.ResponseWriter, r *http.Request) {
var req protocol.PerforceProtectsForUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
p4home, err := gitserverfs.MakeP4HomeDir(s.ReposDir)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = perforce.P4TestWithTrust(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
accesslog.Record(
r.Context(),
"<no-repo>",
log.String("p4user", req.P4User),
log.String("p4port", req.P4Port),
)
protects, err := perforce.P4ProtectsForUser(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd, req.Username)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
jsonProtects := make([]protocol.PerforceProtect, len(protects))
for i, p := range protects {
jsonProtects[i] = protocol.PerforceProtect{
Level: p.Level,
EntityType: p.EntityType,
EntityName: p.EntityName,
Match: p.Match,
IsExclusion: p.IsExclusion,
Host: p.Host,
}
}
resp := &protocol.PerforceProtectsForUserResponse{
Protects: jsonProtects,
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (s *Server) handlePerforceProtectsForDepot(w http.ResponseWriter, r *http.Request) {
var req protocol.PerforceProtectsForDepotRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
p4home, err := gitserverfs.MakeP4HomeDir(s.ReposDir)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = perforce.P4TestWithTrust(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
accesslog.Record(
r.Context(),
"<no-repo>",
log.String("p4user", req.P4User),
log.String("p4port", req.P4Port),
)
protects, err := perforce.P4ProtectsForDepot(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd, req.Depot)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
jsonProtects := make([]protocol.PerforceProtect, len(protects))
for i, p := range protects {
jsonProtects[i] = protocol.PerforceProtect{
Level: p.Level,
EntityType: p.EntityType,
EntityName: p.EntityName,
Match: p.Match,
IsExclusion: p.IsExclusion,
Host: p.Host,
}
}
resp := &protocol.PerforceProtectsForDepotResponse{
Protects: jsonProtects,
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (s *Server) handlePerforceGroupMembers(w http.ResponseWriter, r *http.Request) {
var req protocol.PerforceGroupMembersRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
p4home, err := gitserverfs.MakeP4HomeDir(s.ReposDir)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = perforce.P4TestWithTrust(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
accesslog.Record(
r.Context(),
"<no-repo>",
log.String("p4user", req.P4User),
log.String("p4port", req.P4Port),
)
members, err := perforce.P4GroupMembers(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd, req.Group)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp := &protocol.PerforceGroupMembersResponse{
Usernames: members,
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (s *Server) handleIsPerforceSuperUser(w http.ResponseWriter, r *http.Request) {
var req protocol.IsPerforceSuperUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
p4home, err := gitserverfs.MakeP4HomeDir(s.ReposDir)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = perforce.P4TestWithTrust(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
accesslog.Record(
r.Context(),
"<no-repo>",
log.String("p4user", req.P4User),
log.String("p4port", req.P4Port),
)
err = perforce.P4UserIsSuperUser(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd)
if err != nil {
if err == perforce.ErrIsNotSuperUser {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp := &protocol.IsPerforceSuperUserResponse{}
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (s *Server) handlePerforceGetChangelist(w http.ResponseWriter, r *http.Request) {
var req protocol.PerforceGetChangelistRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
p4home, err := gitserverfs.MakeP4HomeDir(s.ReposDir)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = perforce.P4TestWithTrust(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
accesslog.Record(
r.Context(),
"<no-repo>",
log.String("p4user", req.P4User),
log.String("p4port", req.P4Port),
)
changelist, err := perforce.GetChangelistByID(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd, req.ChangelistID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp := &protocol.PerforceGetChangelistResponse{
Changelist: protocol.PerforceChangelist{
ID: changelist.ID,
CreationDate: changelist.CreationDate,
State: string(changelist.State),
Author: changelist.Author,
Title: changelist.Title,
Message: changelist.Message,
},
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}

View File

@ -2,7 +2,6 @@ package internal
import (
"context"
"fmt"
"io"
"strings"
@ -30,6 +29,8 @@ type GRPCServer struct {
proto.UnimplementedGitserverServiceServer
}
var _ proto.GitserverServiceServer = &GRPCServer{}
func (gs *GRPCServer) BatchLog(ctx context.Context, req *proto.BatchLogRequest) (*proto.BatchLogResponse, error) {
gs.Server.operations = gs.Server.ensureOperations()
@ -251,92 +252,6 @@ func (gs *GRPCServer) GetObject(ctx context.Context, req *proto.GetObjectRequest
return resp.ToProto(), nil
}
func (gs *GRPCServer) P4Exec(req *proto.P4ExecRequest, ss proto.GitserverService_P4ExecServer) error {
arguments := byteSlicesToStrings(req.GetArgs())
if len(arguments) < 1 {
return status.Error(codes.InvalidArgument, "args must be greater than or equal to 1")
}
subCommand := arguments[0]
// Make sure the subcommand is explicitly allowed
allowlist := []string{"protects", "groups", "users", "group", "changes"}
allowed := false
for _, c := range allowlist {
if subCommand == c {
allowed = true
break
}
}
if !allowed {
return status.Error(codes.InvalidArgument, fmt.Sprintf("subcommand %q is not allowed", subCommand))
}
p4home, err := gitserverfs.MakeP4HomeDir(gs.Server.ReposDir)
if err != nil {
return status.Error(codes.Internal, err.Error())
}
// Log which actor is accessing p4-exec.
//
// p4-exec is currently only used for fetching user based permissions information
// so, we don't have a repo name.
accesslog.Record(ss.Context(), "<no-repo>",
log.String("p4user", req.GetP4User()),
log.String("p4port", req.GetP4Port()),
log.Strings("args", arguments),
)
// Make sure credentials are valid before heavier operation
err = perforce.P4TestWithTrust(ss.Context(), p4home, req.GetP4Port(), req.GetP4User(), req.GetP4Passwd())
if err != nil {
if ctxErr := ss.Context().Err(); ctxErr != nil {
return status.FromContextError(ctxErr).Err()
}
return status.Error(codes.InvalidArgument, err.Error())
}
w := streamio.NewWriter(func(p []byte) error {
return ss.Send(&proto.P4ExecResponse{
Data: p,
})
})
var r protocol.P4ExecRequest
r.FromProto(req)
return gs.doP4Exec(ss.Context(), gs.Server.Logger, &r, "unknown-grpc-client", w)
}
func (gs *GRPCServer) doP4Exec(ctx context.Context, logger log.Logger, req *protocol.P4ExecRequest, userAgent string, w io.Writer) error {
execStatus := gs.Server.p4Exec(ctx, logger, req, userAgent, w)
if execStatus.ExitStatus != 0 || execStatus.Err != nil {
if ctxErr := ctx.Err(); ctxErr != nil {
return status.FromContextError(ctxErr).Err()
}
gRPCStatus := codes.Unknown
if strings.Contains(execStatus.Err.Error(), "signal: killed") {
gRPCStatus = codes.Aborted
}
s, err := status.New(gRPCStatus, execStatus.Err.Error()).WithDetails(&proto.ExecStatusPayload{
StatusCode: int32(execStatus.ExitStatus),
Stderr: execStatus.Stderr,
})
if err != nil {
gs.Server.Logger.Error("failed to marshal status", log.Error(err))
return err
}
return s.Err()
}
return nil
}
func (gs *GRPCServer) ListGitolite(ctx context.Context, req *proto.ListGitoliteRequest) (*proto.ListGitoliteResponse, error) {
host := req.GetGitoliteHost()
repos, err := defaultGitolite.listRepos(ctx, host)
@ -459,7 +374,8 @@ func (gs *GRPCServer) IsPerforcePathCloneable(ctx context.Context, req *proto.Is
return nil, status.Error(codes.Internal, err.Error())
}
err = perforce.IsDepotPathCloneable(ctx, p4home, req.GetP4Port(), req.GetP4User(), req.GetP4Passwd(), req.GetDepotPath())
conn := req.GetConnectionDetails()
err = perforce.IsDepotPathCloneable(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd(), req.DepotPath)
if err != nil {
return nil, status.Error(codes.NotFound, err.Error())
}
@ -473,14 +389,232 @@ func (gs *GRPCServer) CheckPerforceCredentials(ctx context.Context, req *proto.C
return nil, status.Error(codes.Internal, err.Error())
}
err = perforce.P4TestWithTrust(ctx, p4home, req.GetP4Port(), req.GetP4User(), req.GetP4Passwd())
conn := req.GetConnectionDetails()
err = perforce.P4TestWithTrust(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd())
if err != nil {
if ctxErr := ctx.Err(); ctxErr != nil {
return nil, status.FromContextError(ctxErr).Err()
}
return nil, status.Error(codes.InvalidArgument, err.Error())
}
return &proto.CheckPerforceCredentialsResponse{}, nil
}
func (gs *GRPCServer) PerforceUsers(ctx context.Context, req *proto.PerforceUsersRequest) (*proto.PerforceUsersResponse, error) {
p4home, err := gitserverfs.MakeP4HomeDir(gs.Server.ReposDir)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
conn := req.GetConnectionDetails()
err = perforce.P4TestWithTrust(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd())
if err != nil {
if ctxErr := ctx.Err(); ctxErr != nil {
return nil, status.FromContextError(ctxErr).Err()
}
return nil, status.Error(codes.InvalidArgument, err.Error())
}
accesslog.Record(
ctx,
"<no-repo>",
log.String("p4user", conn.GetP4User()),
log.String("p4port", conn.GetP4Port()),
)
users, err := perforce.P4Users(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd())
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
resp := &proto.PerforceUsersResponse{
Users: make([]*proto.PerforceUser, 0, len(users)),
}
for _, user := range users {
resp.Users = append(resp.Users, user.ToProto())
}
return resp, nil
}
func (gs *GRPCServer) PerforceProtectsForUser(ctx context.Context, req *proto.PerforceProtectsForUserRequest) (*proto.PerforceProtectsForUserResponse, error) {
p4home, err := gitserverfs.MakeP4HomeDir(gs.Server.ReposDir)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
conn := req.GetConnectionDetails()
err = perforce.P4TestWithTrust(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd())
if err != nil {
if ctxErr := ctx.Err(); ctxErr != nil {
return nil, status.FromContextError(ctxErr).Err()
}
return nil, status.Error(codes.InvalidArgument, err.Error())
}
accesslog.Record(
ctx,
"<no-repo>",
log.String("p4user", conn.GetP4User()),
log.String("p4port", conn.GetP4Port()),
)
protects, err := perforce.P4ProtectsForUser(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd(), req.GetUsername())
if err != nil {
return nil, err
}
protoProtects := make([]*proto.PerforceProtect, len(protects))
for i, p := range protects {
protoProtects[i] = p.ToProto()
}
return &proto.PerforceProtectsForUserResponse{
Protects: protoProtects,
}, nil
}
func (gs *GRPCServer) PerforceProtectsForDepot(ctx context.Context, req *proto.PerforceProtectsForDepotRequest) (*proto.PerforceProtectsForDepotResponse, error) {
p4home, err := gitserverfs.MakeP4HomeDir(gs.Server.ReposDir)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
conn := req.GetConnectionDetails()
err = perforce.P4TestWithTrust(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd())
if err != nil {
if ctxErr := ctx.Err(); ctxErr != nil {
return nil, status.FromContextError(ctxErr).Err()
}
return nil, status.Error(codes.InvalidArgument, err.Error())
}
accesslog.Record(
ctx,
"<no-repo>",
log.String("p4user", conn.GetP4User()),
log.String("p4port", conn.GetP4Port()),
)
protects, err := perforce.P4ProtectsForDepot(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd(), req.GetDepot())
if err != nil {
return nil, err
}
protoProtects := make([]*proto.PerforceProtect, len(protects))
for i, p := range protects {
protoProtects[i] = p.ToProto()
}
return &proto.PerforceProtectsForDepotResponse{
Protects: protoProtects,
}, nil
}
func (gs *GRPCServer) PerforceGroupMembers(ctx context.Context, req *proto.PerforceGroupMembersRequest) (*proto.PerforceGroupMembersResponse, error) {
p4home, err := gitserverfs.MakeP4HomeDir(gs.Server.ReposDir)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
conn := req.GetConnectionDetails()
err = perforce.P4TestWithTrust(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd())
if err != nil {
if ctxErr := ctx.Err(); ctxErr != nil {
return nil, status.FromContextError(ctxErr).Err()
}
return nil, status.Error(codes.InvalidArgument, err.Error())
}
accesslog.Record(
ctx,
"<no-repo>",
log.String("p4user", conn.GetP4User()),
log.String("p4port", conn.GetP4Port()),
)
members, err := perforce.P4GroupMembers(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd(), req.GetGroup())
if err != nil {
return nil, err
}
return &proto.PerforceGroupMembersResponse{
Usernames: members,
}, nil
}
func (gs *GRPCServer) IsPerforceSuperUser(ctx context.Context, req *proto.IsPerforceSuperUserRequest) (*proto.IsPerforceSuperUserResponse, error) {
p4home, err := gitserverfs.MakeP4HomeDir(gs.Server.ReposDir)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
conn := req.GetConnectionDetails()
err = perforce.P4TestWithTrust(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd())
if err != nil {
if ctxErr := ctx.Err(); ctxErr != nil {
return nil, status.FromContextError(ctxErr).Err()
}
return nil, status.Error(codes.InvalidArgument, err.Error())
}
err = perforce.P4UserIsSuperUser(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd())
if err != nil {
if ctxErr := ctx.Err(); ctxErr != nil {
return nil, status.FromContextError(ctxErr).Err()
}
if err == perforce.ErrIsNotSuperUser {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
return nil, status.Error(codes.Internal, err.Error())
}
return &proto.IsPerforceSuperUserResponse{}, nil
}
func (gs *GRPCServer) PerforceGetChangelist(ctx context.Context, req *proto.PerforceGetChangelistRequest) (*proto.PerforceGetChangelistResponse, error) {
p4home, err := gitserverfs.MakeP4HomeDir(gs.Server.ReposDir)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
conn := req.GetConnectionDetails()
err = perforce.P4TestWithTrust(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd())
if err != nil {
if ctxErr := ctx.Err(); ctxErr != nil {
return nil, status.FromContextError(ctxErr).Err()
}
return nil, status.Error(codes.InvalidArgument, err.Error())
}
accesslog.Record(
ctx,
"<no-repo>",
log.String("p4user", conn.GetP4User()),
log.String("p4port", conn.GetP4Port()),
)
changelist, err := perforce.GetChangelistByID(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd(), req.GetChangelistId())
if err != nil {
return nil, err
}
return &proto.PerforceGetChangelistResponse{
Changelist: changelist.ToProto(),
}, nil
}
func byteSlicesToStrings(in [][]byte) []string {
res := make([]string, len(in))
for i, b := range in {

View File

@ -9,7 +9,6 @@ import (
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"os/exec"
"path/filepath"
@ -23,8 +22,6 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/sync/semaphore"
"golang.org/x/time/rate"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/sourcegraph/log/logtest"
@ -39,9 +36,6 @@ import (
"github.com/sourcegraph/sourcegraph/internal/database/dbtest"
"github.com/sourcegraph/sourcegraph/internal/gitserver"
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
proto "github.com/sourcegraph/sourcegraph/internal/gitserver/v1"
"github.com/sourcegraph/sourcegraph/internal/grpc"
"github.com/sourcegraph/sourcegraph/internal/grpc/defaults"
"github.com/sourcegraph/sourcegraph/internal/limiter"
"github.com/sourcegraph/sourcegraph/internal/observation"
"github.com/sourcegraph/sourcegraph/internal/ratelimit"
@ -267,239 +261,6 @@ func TestExecRequest(t *testing.T) {
}
}
func TestServer_handleP4Exec(t *testing.T) {
defaultMockRunCommand := func(ctx context.Context, cmd *exec.Cmd) (int, error) {
switch cmd.Args[1] {
case "users":
_, _ = cmd.Stdout.Write([]byte("admin <admin@joe-perforce-server> (admin) accessed 2021/01/31"))
_, _ = cmd.Stderr.Write([]byte("teststderr"))
return 42, errors.New("the answer to life the universe and everything")
}
return 0, nil
}
t.Cleanup(func() {
executil.UpdateRunCommandMock(nil)
})
startServer := func(t *testing.T) (handler http.Handler, client proto.GitserverServiceClient, cleanup func()) {
t.Helper()
logger := logtest.Scoped(t)
s := &Server{
Logger: logger,
ReposDir: t.TempDir(),
ObservationCtx: observation.TestContextTB(t),
skipCloneForTests: true,
DB: dbmocks.NewMockDB(),
RecordingCommandFactory: wrexec.NewNoOpRecordingCommandFactory(),
Locker: NewRepositoryLocker(),
}
server := defaults.NewServer(logger)
proto.RegisterGitserverServiceServer(server, &GRPCServer{Server: s})
handler = grpc.MultiplexHandlers(server, s.Handler())
srv := httptest.NewServer(handler)
u, _ := url.Parse(srv.URL)
conn, err := defaults.Dial(u.Host, logger.Scoped("gRPC client", ""))
if err != nil {
t.Fatalf("failed to dial: %v", err)
}
client = proto.NewGitserverServiceClient(conn)
return handler, client, func() {
srv.Close()
conn.Close()
server.Stop()
}
}
t.Run("gRPC", func(t *testing.T) {
readAll := func(execClient proto.GitserverService_P4ExecClient) ([]byte, error) {
var buf bytes.Buffer
for {
resp, err := execClient.Recv()
if errors.Is(err, io.EOF) {
return buf.Bytes(), nil
}
if err != nil {
return buf.Bytes(), err
}
_, err = buf.Write(resp.GetData())
if err != nil {
t.Fatalf("failed to write data: %v", err)
}
}
}
t.Run("users", func(t *testing.T) {
executil.UpdateRunCommandMock(defaultMockRunCommand)
_, client, closeFunc := startServer(t)
t.Cleanup(closeFunc)
stream, err := client.P4Exec(context.Background(), &proto.P4ExecRequest{
Args: [][]byte{[]byte("users")},
})
if err != nil {
t.Fatalf("failed to call P4Exec: %v", err)
}
data, err := readAll(stream)
s, ok := status.FromError(err)
if !ok {
t.Fatal("received non-status error from p4exec call")
}
if diff := cmp.Diff("the answer to life the universe and everything", s.Message()); diff != "" {
t.Fatalf("unexpected error in stream (-want +got):\n%s", diff)
}
expectedData := []byte("admin <admin@joe-perforce-server> (admin) accessed 2021/01/31")
if diff := cmp.Diff(expectedData, data); diff != "" {
t.Fatalf("unexpected data (-want +got):\n%s", diff)
}
})
t.Run("empty request", func(t *testing.T) {
executil.UpdateRunCommandMock(defaultMockRunCommand)
_, client, closeFunc := startServer(t)
t.Cleanup(closeFunc)
stream, err := client.P4Exec(context.Background(), &proto.P4ExecRequest{})
if err != nil {
t.Fatalf("failed to call P4Exec: %v", err)
}
_, err = readAll(stream)
if status.Code(err) != codes.InvalidArgument {
t.Fatalf("expected InvalidArgument error, got %v", err)
}
})
t.Run("disallowed command", func(t *testing.T) {
executil.UpdateRunCommandMock(defaultMockRunCommand)
_, client, closeFunc := startServer(t)
t.Cleanup(closeFunc)
stream, err := client.P4Exec(context.Background(), &proto.P4ExecRequest{
Args: [][]byte{[]byte("bad_command")},
})
if err != nil {
t.Fatalf("failed to call P4Exec: %v", err)
}
_, err = readAll(stream)
if status.Code(err) != codes.InvalidArgument {
t.Fatalf("expected InvalidArgument error, got %v", err)
}
})
t.Run("context cancelled", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
executil.UpdateRunCommandMock(func(ctx context.Context, _ *exec.Cmd) (int, error) {
// fake a context cancellation that occurs while the process is running
cancel()
return 0, ctx.Err()
})
_, client, closeFunc := startServer(t)
t.Cleanup(closeFunc)
stream, err := client.P4Exec(ctx, &proto.P4ExecRequest{
Args: [][]byte{[]byte("users")},
})
if err != nil {
t.Fatalf("failed to call P4Exec: %v", err)
}
_, err = readAll(stream)
if !(errors.Is(err, context.Canceled) || status.Code(err) == codes.Canceled) {
t.Fatalf("expected context cancelation error, got %v", err)
}
})
})
t.Run("HTTP", func(t *testing.T) {
tests := []Test{
{
Name: "Command",
Request: newRequest("POST", "/p4-exec", strings.NewReader(`{"args": ["users"]}`)),
ExpectedCode: http.StatusOK,
ExpectedBody: "admin <admin@joe-perforce-server> (admin) accessed 2021/01/31",
ExpectedTrailers: http.Header{
"X-Exec-Error": {"the answer to life the universe and everything"},
"X-Exec-Exit-Status": {"42"},
"X-Exec-Stderr": {"teststderr"},
},
},
{
Name: "Error",
Request: newRequest("POST", "/p4-exec", strings.NewReader(`{"args": ["bad_command"]}`)),
ExpectedCode: http.StatusBadRequest,
ExpectedBody: "subcommand \"bad_command\" is not allowed",
},
{
Name: "EmptyBody",
Request: newRequest("POST", "/p4-exec", nil),
ExpectedCode: http.StatusBadRequest,
ExpectedBody: `EOF`,
},
{
Name: "EmptyInput",
Request: newRequest("POST", "/p4-exec", strings.NewReader("{}")),
ExpectedCode: http.StatusBadRequest,
ExpectedBody: `args must be greater than or equal to 1`,
},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
executil.UpdateRunCommandMock(defaultMockRunCommand)
handler, _, closeFunc := startServer(t)
t.Cleanup(closeFunc)
w := httptest.ResponseRecorder{Body: new(bytes.Buffer)}
handler.ServeHTTP(&w, test.Request)
res := w.Result()
if res.StatusCode != test.ExpectedCode {
t.Errorf("wrong status: expected %d, got %d", test.ExpectedCode, w.Code)
}
body, err := io.ReadAll(res.Body)
if err != nil {
t.Fatal(err)
}
if strings.TrimSpace(string(body)) != test.ExpectedBody {
t.Errorf("wrong body: expected %q, got %q", test.ExpectedBody, string(body))
}
for k, v := range test.ExpectedTrailers {
if got := res.Trailer.Get(k); got != v[0] {
t.Errorf("wrong trailer %q: expected %q, got %q", k, v[0], got)
}
}
})
}
})
}
func staticGetRemoteURL(remote string) func(context.Context, api.RepoName) (string, error) {
return func(context.Context, api.RepoName) (string, error) {
return remote, nil

View File

@ -60,3 +60,9 @@ and in its place the `gitserver client` exposes an API supporting specific Git o
| `Stat` | returns a FileInfo describing the named file at commit | | maps output from `git ls-tree` into `FileInfo` structs, with the object hash in the `Sys_` member |
| `IsPerforcePathCloneable` | check if a given depot/depot path is cloneable with the given credentials | calls gitserver's `/is-perforce-path-cloneable` endpoint | |
| `CheckPerforceCredentials` | check if a given Perforce credential is valid | calls gitserver's `/check-perforce-credentials` endpoint | |
| `PerforceUsers` | list all perforce users | calls gitserver's `/perforce-users` endpoint | |
| `PerforceProtectsForUser` | list all protections that apply to a user | calls gitserver's `/perforce-protects-for-user` endpoint | |
| `PerforceProtectsForDepot` | list all protections that apply to a depot | calls gitserver's `/perforce-protects-for-depot` endpoint | |
| `PerforceGroupMembers` | list all members of a Perforce group | calls gitserver's `/perforce-group-members` endpoint | |
| `IsPerforceSuperUser` | check if a given ticket is for a super level user | calls gitserver's `/is-perforce-super-user` endpoint | |
| `PerforceGetChangelist` | get details about a perforce changelist | calls gitserver's `/perforce-get-changelist` endpoint | |

View File

@ -5,6 +5,7 @@ go_library(
name = "perforce",
srcs = [
"authz.go",
"debug.go",
"perforce.go",
"protects.go",
],
@ -16,7 +17,9 @@ go_library(
"//internal/extsvc",
"//internal/extsvc/perforce",
"//internal/gitserver",
"//internal/gitserver/protocol",
"//internal/licensing",
"//internal/perforce",
"//internal/trace",
"//internal/types",
"//lib/errors",
@ -48,12 +51,15 @@ go_test(
"//internal/extsvc",
"//internal/extsvc/perforce",
"//internal/gitserver",
"//internal/gitserver/protocol",
"//internal/perforce",
"//internal/types",
"//lib/errors",
"//schema",
"@com_github_google_go_cmp//cmp",
"@com_github_inconshreveable_log15//:log15",
"@com_github_json_iterator_go//:go",
"@com_github_sourcegraph_log//:log",
"@com_github_sourcegraph_log//logtest",
"@com_github_stretchr_testify//require",
],
)

View File

@ -24,9 +24,6 @@ func TestPerformDebugScan(t *testing.T) {
run(logger, "//depot/main/", input, false)
logged := exporter()
// For now we'll just check that the count as well as first and last lines are
// what we expect
assert.Len(t, logged, 444)
assert.Equal(t, "Converted depot to glob", logged[0].Message) // fails without error
assert.Equal(t, "Include rule", logged[len(logged)-1].Message)
}

View File

@ -0,0 +1,80 @@
package perforce
import (
"bufio"
"io"
"strings"
"github.com/sourcegraph/log"
"github.com/sourcegraph/sourcegraph/internal/authz"
"github.com/sourcegraph/sourcegraph/internal/extsvc"
p4types "github.com/sourcegraph/sourcegraph/internal/perforce"
)
// PerformDebugScan will scan protections rules from r and log detailed
// information about how each line was parsed.
func PerformDebugScan(logger log.Logger, r io.Reader, depot extsvc.RepoID, ignoreRulesWithHost bool) (*authz.ExternalUserPermissions, error) {
perms := &authz.ExternalUserPermissions{
SubRepoPermissions: make(map[extsvc.RepoID]*authz.SubRepoPermissions),
}
pr, err := parseP4ProtectsRaw(r)
if err != nil {
return perms, err
}
scanner := fullRepoPermsScanner(logger, perms, []extsvc.RepoID{depot})
err = scanProtects(logger, pr, scanner, ignoreRulesWithHost)
return perms, err
}
func parseP4ProtectsRaw(rc io.Reader) ([]*p4types.Protect, error) {
protects := make([]*p4types.Protect, 0)
scanner := bufio.NewScanner(rc)
for scanner.Scan() {
line := scanner.Text()
// Trim whitespace
line = strings.TrimSpace(line)
// Skip comments and blank lines
if strings.HasPrefix(line, "##") || line == "" {
continue
}
// Trim trailing comments
if i := strings.Index(line, "##"); i > -1 {
line = line[:i]
}
// Split into fields
fields := strings.Fields(line)
if len(fields) < 5 {
continue
}
parsedLine := p4ProtectLine{
level: fields[0],
entityType: fields[1],
name: fields[2],
match: fields[4],
}
if strings.HasPrefix(parsedLine.match, "-") {
parsedLine.isExclusion = true // is an exclusion
parsedLine.match = strings.TrimPrefix(parsedLine.match, "-") // trim leading -
}
protects = append(protects, &p4types.Protect{
Level: parsedLine.level,
EntityType: parsedLine.entityType,
EntityName: parsedLine.name,
Host: fields[3],
Match: parsedLine.match,
IsExclusion: parsedLine.isExclusion,
})
}
return protects, scanner.Err()
}

View File

@ -1,12 +1,8 @@
package perforce
import (
"bufio"
"context"
"io"
"net/http"
"net/url"
"strings"
"sync"
"time"
@ -17,6 +13,8 @@ import (
"github.com/sourcegraph/sourcegraph/internal/authz"
"github.com/sourcegraph/sourcegraph/internal/extsvc"
"github.com/sourcegraph/sourcegraph/internal/extsvc/perforce"
"github.com/sourcegraph/sourcegraph/internal/gitserver"
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
"github.com/sourcegraph/sourcegraph/internal/trace"
"github.com/sourcegraph/sourcegraph/internal/types"
"github.com/sourcegraph/sourcegraph/lib/errors"
@ -38,7 +36,7 @@ type Provider struct {
user string
password string
p4Execer p4Execer
gitserverClient gitserver.Client
emailsCacheMutex sync.RWMutex
cachedAllUserEmails map[string]string // username -> email
@ -54,15 +52,11 @@ func cacheIsUpToDate(lastUpdate time.Time) bool {
return time.Since(lastUpdate) < cacheTTL
}
type p4Execer interface {
P4Exec(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error)
}
// NewProvider returns a new Perforce authorization provider that uses the given
// host, user and password to talk to a Perforce Server that is the source of
// truth for permissions. It assumes emails of Sourcegraph accounts match 1-1
// with emails of Perforce Server users.
func NewProvider(logger log.Logger, p4Execer p4Execer, urn, host, user, password string, depots []extsvc.RepoID, ignoreRulesWithHost bool) *Provider {
func NewProvider(logger log.Logger, gitserverClient gitserver.Client, urn, host, user, password string, depots []extsvc.RepoID, ignoreRulesWithHost bool) *Provider {
baseURL, _ := url.Parse(host)
return &Provider{
logger: logger,
@ -72,7 +66,7 @@ func NewProvider(logger log.Logger, p4Execer p4Execer, urn, host, user, password
host: host,
user: user,
password: password,
p4Execer: p4Execer,
gitserverClient: gitserverClient,
cachedGroupMembers: make(map[string][]string),
ignoreRulesWithHost: ignoreRulesWithHost,
}
@ -104,24 +98,25 @@ func (p *Provider) FetchAccount(ctx context.Context, user *types.User, _ []*exts
emailSet[email] = struct{}{}
}
rc, _, err := p.p4Execer.P4Exec(ctx, p.host, p.user, p.password, "users")
users, err := p.gitserverClient.PerforceUsers(ctx, protocol.PerforceConnectionDetails{
P4Port: p.host,
P4User: p.user,
P4Passwd: p.password,
})
if err != nil {
return nil, errors.Wrap(err, "list users")
}
defer func() { _ = rc.Close() }()
scanner := bufio.NewScanner(rc)
for scanner.Scan() {
username, email, ok := scanEmail(scanner)
if !ok {
for _, p4User := range users {
if p4User.Email == "" || p4User.Username == "" {
continue
}
if _, ok := emailSet[email]; ok {
if _, ok := emailSet[p4User.Email]; ok {
accountData, err := jsoniter.Marshal(
perforce.AccountData{
Username: username,
Email: email,
Username: p4User.Username,
Email: p4User.Email,
},
)
if err != nil {
@ -133,7 +128,7 @@ func (p *Provider) FetchAccount(ctx context.Context, user *types.User, _ []*exts
AccountSpec: extsvc.AccountSpec{
ServiceType: p.codeHost.ServiceType,
ServiceID: p.codeHost.ServiceID,
AccountID: email,
AccountID: p4User.Email,
},
AccountData: extsvc.AccountData{
Data: extsvc.NewUnencryptedData(accountData),
@ -141,12 +136,7 @@ func (p *Provider) FetchAccount(ctx context.Context, user *types.User, _ []*exts
}, nil
}
}
if err = scanner.Err(); err != nil {
return nil, errors.Wrap(err, "scanner.Err")
}
// Drain remaining body
_, _ = io.Copy(io.Discard, rc)
return nil, nil
}
@ -167,22 +157,23 @@ func (p *Provider) FetchUserPerms(ctx context.Context, account *extsvc.Account,
return nil, errors.New("no user found in the external account data")
}
// -u User : Displays protection lines that apply to the named user. This option
// requires super access.
rc, _, err := p.p4Execer.P4Exec(ctx, p.host, p.user, p.password, "protects", "-u", user.Username)
protects, err := p.gitserverClient.PerforceProtectsForUser(ctx, protocol.PerforceConnectionDetails{
P4Port: p.host,
P4User: p.user,
P4Passwd: p.password,
}, user.Username)
if err != nil {
return nil, errors.Wrap(err, "list ACLs by user")
}
defer func() { _ = rc.Close() }()
// Pull permissions from protects file.
perms := &authz.ExternalUserPermissions{}
if len(p.depots) == 0 {
err = errors.Wrap(scanProtects(p.logger, rc, repoIncludesExcludesScanner(perms), p.ignoreRulesWithHost), "repoIncludesExcludesScanner")
err = errors.Wrap(scanProtects(p.logger, protects, repoIncludesExcludesScanner(perms), p.ignoreRulesWithHost), "repoIncludesExcludesScanner")
} else {
// SubRepoPermissions-enabled code path
perms.SubRepoPermissions = make(map[extsvc.RepoID]*authz.SubRepoPermissions, len(p.depots))
err = errors.Wrap(scanProtects(p.logger, rc, fullRepoPermsScanner(p.logger, perms, p.depots), p.ignoreRulesWithHost), "fullRepoPermsScanner")
err = errors.Wrap(scanProtects(p.logger, protects, fullRepoPermsScanner(p.logger, perms, p.depots), p.ignoreRulesWithHost), "fullRepoPermsScanner")
}
// As per interface definition for this method, implementation should return
@ -197,22 +188,20 @@ func (p *Provider) getAllUserEmails(ctx context.Context) (map[string]string, err
}
userEmails := make(map[string]string)
rc, _, err := p.p4Execer.P4Exec(ctx, p.host, p.user, p.password, "users")
users, err := p.gitserverClient.PerforceUsers(ctx, protocol.PerforceConnectionDetails{
P4Port: p.host,
P4User: p.user,
P4Passwd: p.password,
})
if err != nil {
return nil, errors.Wrap(err, "list users")
}
defer func() { _ = rc.Close() }()
scanner := bufio.NewScanner(rc)
for scanner.Scan() {
username, email, ok := scanEmail(scanner)
if !ok {
for _, p4User := range users {
if p4User.Username == "" || p4User.Email == "" {
continue
}
userEmails[username] = email
}
if err = scanner.Err(); err != nil {
return nil, errors.Wrap(err, "scanner.Err")
userEmails[p4User.Username] = p4User.Email
}
p.emailsCacheMutex.Lock()
@ -248,39 +237,19 @@ func (p *Provider) getGroupMembers(ctx context.Context, group string) ([]string,
p.groupsCacheMutex.Lock()
defer p.groupsCacheMutex.Unlock()
rc, _, err := p.p4Execer.P4Exec(ctx, p.host, p.user, p.password, "group", "-o", group)
members, err := p.gitserverClient.PerforceGroupMembers(
ctx,
protocol.PerforceConnectionDetails{
P4Port: p.host,
P4User: p.user,
P4Passwd: p.password,
},
group,
)
if err != nil {
return nil, errors.Wrap(err, "list group members")
}
defer func() { _ = rc.Close() }()
var members []string
startScan := false
scanner := bufio.NewScanner(rc)
for scanner.Scan() {
line := scanner.Text()
// Only start scan when we encounter the "Users:" line
if !startScan {
if strings.HasPrefix(line, "Users:") {
startScan = true
}
continue
}
// Lines for users always start with a tab "\t"
if !strings.HasPrefix(line, "\t") {
break
}
members = append(members, strings.TrimSpace(line))
}
if err = scanner.Err(); err != nil {
return nil, errors.Wrap(err, "scanner.Err")
}
// Drain remaining body
_, _ = io.Copy(io.Discard, rc)
p.cachedGroupMembers[group] = members
p.groupsCacheLastUpdate = time.Now()
@ -334,16 +303,21 @@ func (p *Provider) FetchRepoPerms(ctx context.Context, repo *extsvc.Repository,
return nil, &authz.ErrUnimplemented{Feature: "perforce.FetchRepoPerms for sub-repo permissions"}
}
// -a : Displays protection lines for all users. This option requires super
// access.
rc, _, err := p.p4Execer.P4Exec(ctx, p.host, p.user, p.password, "protects", "-a", repo.ID)
protects, err := p.gitserverClient.PerforceProtectsForDepot(
ctx,
protocol.PerforceConnectionDetails{
P4Port: p.host,
P4User: p.user,
P4Passwd: p.password,
},
repo.ID,
)
if err != nil {
return nil, errors.Wrap(err, "list ACLs by depot")
}
defer func() { _ = rc.Close() }()
users := make(map[string]struct{})
if err := scanProtects(p.logger, rc, allUsersScanner(ctx, p, users), p.ignoreRulesWithHost); err != nil {
if err := scanProtects(p.logger, protects, allUsersScanner(ctx, p, users), p.ignoreRulesWithHost); err != nil {
return nil, errors.Wrap(err, "scanning protects")
}
@ -379,25 +353,9 @@ func (p *Provider) URN() string {
}
func (p *Provider) ValidateConnection(ctx context.Context) error {
// Validate the user has "super" access with "-u" option, see https://www.perforce.com/perforce/r12.1/manuals/cmdref/protects.html
rc, _, err := p.p4Execer.P4Exec(ctx, p.host, p.user, p.password, "protects", "-u", p.user)
if err == nil {
_ = rc.Close()
return nil
}
if strings.Contains(err.Error(), "You don't have permission for this operation.") {
return errors.New("the user does not have super access")
}
return errors.Wrap(err, "invalid user access level")
}
func scanEmail(s *bufio.Scanner) (string, string, bool) {
fields := strings.Fields(s.Text())
if len(fields) < 2 {
return "", "", false
}
username := fields[0] // e.g. alice
email := strings.Trim(fields[1], "<>") // e.g. alice@example.com
return username, email, true
return p.gitserverClient.IsPerforceSuperUser(ctx, protocol.PerforceConnectionDetails{
P4Port: p.host,
P4User: p.user,
P4Passwd: p.password,
})
}

View File

@ -3,14 +3,11 @@ package perforce
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
jsoniter "github.com/json-iterator/go"
"github.com/sourcegraph/log"
"github.com/sourcegraph/log/logtest"
"github.com/sourcegraph/sourcegraph/internal/api"
@ -19,7 +16,10 @@ import (
"github.com/sourcegraph/sourcegraph/internal/extsvc"
"github.com/sourcegraph/sourcegraph/internal/extsvc/perforce"
"github.com/sourcegraph/sourcegraph/internal/gitserver"
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
p4types "github.com/sourcegraph/sourcegraph/internal/perforce"
"github.com/sourcegraph/sourcegraph/internal/types"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
func TestProvider_FetchAccount(t *testing.T) {
@ -30,16 +30,14 @@ func TestProvider_FetchAccount(t *testing.T) {
Username: "alice",
}
execer := p4ExecFunc(func(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error) {
data := `
alice <alice@example.com> (Alice) accessed 2020/12/04
cindy <cindy@example.com> (Cindy) accessed 2020/12/04
`
return io.NopCloser(strings.NewReader(data)), nil, nil
})
gitserverClient := gitserver.NewStrictMockClient()
gitserverClient.PerforceUsersFunc.SetDefaultReturn([]*p4types.User{
{Username: "alice", Email: "alice@example.com"},
{Username: "cindy", Email: "cindy@example.com"},
}, nil)
t.Run("no matching account", func(t *testing.T) {
p := NewTestProvider(logger, "", "ssl:111.222.333.444:1666", "admin", "password", execer)
p := NewProvider(logger, gitserverClient, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
got, err := p.FetchAccount(ctx, user, nil, []string{"bob@example.com"})
if err != nil {
t.Fatal(err)
@ -51,7 +49,7 @@ cindy <cindy@example.com> (Cindy) accessed 2020/12/04
})
t.Run("found matching account", func(t *testing.T) {
p := NewTestProvider(logger, "", "ssl:111.222.333.444:1666", "admin", "password", execer)
p := NewProvider(logger, gitserverClient, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
got, err := p.FetchAccount(ctx, user, nil, []string{"alice@example.com"})
if err != nil {
t.Fatal(err)
@ -89,7 +87,7 @@ func TestProvider_FetchUserPerms(t *testing.T) {
t.Run("nil account", func(t *testing.T) {
logger := logtest.Scoped(t)
p := NewProvider(logger, gitserver.NewClient(), "", "ssl:111.222.333.444:1666", "admin", "password", nil, false)
p := NewProvider(logger, gitserver.NewClient(), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
_, err := p.FetchUserPerms(ctx, nil, authz.FetchPermsOptions{})
want := "no account provided"
got := fmt.Sprintf("%v", err)
@ -149,12 +147,12 @@ func TestProvider_FetchUserPerms(t *testing.T) {
tests := []struct {
name string
response string
protects []*p4types.Protect
wantPerms *authz.ExternalUserPermissions
}{
{
name: "include only",
response: `
protects: testParseP4ProtectsRaw(t, strings.NewReader(`
list user alice * //Sourcegraph/Security/... ## "list" can't grant read access
read user alice * //Sourcegraph/Engineering/...
owner user alice * //Sourcegraph/Engineering/Backend/...
@ -162,7 +160,7 @@ open user alice * //Sourcegraph/Engineering/Frontend/...
review user alice * //Sourcegraph/Handbook/...
review user alice * //Sourcegraph/*/Handbook/...
review user alice * //Sourcegraph/.../Handbook/...
`,
`)),
wantPerms: &authz.ExternalUserPermissions{
IncludeContains: []extsvc.RepoID{
"//Sourcegraph/Engineering/%",
@ -176,7 +174,7 @@ review user alice * //Sourcegraph/.../Handbook/...
},
{
name: "exclude only",
response: `
protects: testParseP4ProtectsRaw(t, strings.NewReader(`
list user alice * -//Sourcegraph/Security/...
read user alice * -//Sourcegraph/Engineering/...
owner user alice * -//Sourcegraph/Engineering/Backend/...
@ -184,8 +182,7 @@ open user alice * -//Sourcegraph/Engineering/Frontend/...
review user alice * -//Sourcegraph/Handbook/...
review user alice * -//Sourcegraph/*/Handbook/...
review user alice * -//Sourcegraph/.../Handbook/...
`,
wantPerms: &authz.ExternalUserPermissions{
`)), wantPerms: &authz.ExternalUserPermissions{
ExcludeContains: []extsvc.RepoID{
"//Sourcegraph/[^/]+/Handbook/%",
"//Sourcegraph/%/Handbook/%",
@ -194,7 +191,7 @@ review user alice * -//Sourcegraph/.../Handbook/...
},
{
name: "include and exclude",
response: `
protects: testParseP4ProtectsRaw(t, strings.NewReader(`
read user alice * //Sourcegraph/Security/...
read user alice * //Sourcegraph/Engineering/...
owner user alice * //Sourcegraph/Engineering/Backend/...
@ -208,7 +205,7 @@ list user alice * -//Sourcegraph/Security/... ## "list" c
open user alice * -//Sourcegraph/Engineering/Backend/Credentials/... ## sub-match of a previous include
open user alice * -//Sourcegraph/Engineering/*/Frontend/Folder/... ## sub-match of a previous include
open user alice * -//Sourcegraph/*/Handbook/... ## sub-match of wildcard A include
`,
`)),
wantPerms: &authz.ExternalUserPermissions{
IncludeContains: []extsvc.RepoID{
"//Sourcegraph/Engineering/%",
@ -228,7 +225,7 @@ open user alice * -//Sourcegraph/*/Handbook/... ## sub-matc
},
{
name: "include and exclude, then include again",
response: `
protects: testParseP4ProtectsRaw(t, strings.NewReader(`
read user alice * //Sourcegraph/Security/...
read user alice * //Sourcegraph/Engineering/...
owner user alice * //Sourcegraph/Engineering/Backend/...
@ -244,7 +241,7 @@ open user alice * -//Sourcegraph/Engineering/*/Frontend/Folder/... ## sub-matc
open user alice * -//Sourcegraph/*/Handbook/... ## sub-match of wildcard A include
read user alice * //Sourcegraph/Security/... ## give access to alice again after revoking
`,
`)),
wantPerms: &authz.ExternalUserPermissions{
IncludeContains: []extsvc.RepoID{
"//Sourcegraph/Engineering/%",
@ -267,11 +264,10 @@ read user alice * //Sourcegraph/Security/... ## give access to alice agai
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
logger := logtest.Scoped(t)
execer := p4ExecFunc(func(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error) {
return io.NopCloser(strings.NewReader(test.response)), nil, nil
})
gc := gitserver.NewStrictMockClient()
gc.PerforceProtectsForUserFunc.SetDefaultReturn(test.protects, nil)
p := NewTestProvider(logger, "", "ssl:111.222.333.444:1666", "admin", "password", execer)
p := NewProvider(logger, gc, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
got, err := p.FetchUserPerms(ctx,
&extsvc.Account{
AccountSpec: extsvc.AccountSpec{
@ -297,13 +293,16 @@ read user alice * //Sourcegraph/Security/... ## give access to alice agai
// Specific behaviour is tested in TestScanFullRepoPermissions
t.Run("SubRepoPermissions", func(t *testing.T) {
logger := logtest.Scoped(t)
execer := p4ExecFunc(func(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error) {
return io.NopCloser(strings.NewReader(`
read user alice * //Sourcegraph/Engineering/...
read user alice * -//Sourcegraph/Security/...
`)), nil, nil
})
p := NewTestProvider(logger, "", "ssl:111.222.333.444:1666", "admin", "password", execer)
gitserverClient := gitserver.NewStrictMockClient()
ps := []*p4types.Protect{
{Level: "read", EntityType: "user", EntityName: "alice", Host: "*", Match: "//Sourcegraph/Engineering/..."},
{Level: "read", EntityType: "user", EntityName: "alice", Host: "*", Match: "//Sourcegraph/Security/...", IsExclusion: true},
}
gitserverClient.PerforceProtectsForDepotFunc.SetDefaultReturn(ps, nil)
gitserverClient.PerforceProtectsForUserFunc.SetDefaultReturn(ps, nil)
p := NewProvider(logger, gitserverClient, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
p.depots = append(p.depots, "//Sourcegraph/")
got, err := p.FetchUserPerms(ctx,
@ -370,54 +369,36 @@ func TestProvider_FetchRepoPerms(t *testing.T) {
t.Fatalf("err: want %q but got %q", want, got)
}
})
execer := p4ExecFunc(func(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error) {
var data string
switch args[0] {
case "protects":
data = `
## The actual depot prefix does not matter, the "-" sign does
list user * * -//...
write user alice * //Sourcegraph/...
write user bob * //Sourcegraph/...
admin group Backend * //Sourcegraph/... ## includes "alice" and "cindy"
admin group Frontend * -//Sourcegraph/... ## excludes "bob", "david" and "frank"
read user cindy * -//Sourcegraph/...
list user david * //Sourcegraph/... ## "list" can't grant read access
`
case "users":
data = `
alice <alice@example.com> (Alice) accessed 2020/12/04
bob <bob@example.com> (Bob) accessed 2020/12/04
cindy <cindy@example.com> (Cindy) accessed 2020/12/04
david <david@example.com> (David) accessed 2020/12/04
frank <frank@example.com> (Frank) accessed 2020/12/04
`
case "group":
switch args[2] {
case "Backend":
data = `
Users:
alice
cindy
`
case "Frontend":
data = `
Users:
bob
david
frank
`
}
gitserverClient := gitserver.NewStrictMockClient()
gitserverClient.PerforceUsersFunc.SetDefaultReturn([]*p4types.User{
{Username: "alice", Email: "alice@example.com"},
{Username: "bob", Email: "bob@example.com"},
{Username: "cindy", Email: "cindy@example.com"},
{Username: "david", Email: "david@example.com"},
{Username: "frank", Email: "frank@example.com"},
}, nil)
gitserverClient.PerforceGroupMembersFunc.SetDefaultHook(func(ctx context.Context, conn protocol.PerforceConnectionDetails, group string) ([]string, error) {
switch group {
case "Backend":
return []string{"alice", "cindy"}, nil
case "Frontend":
return []string{"bob", "david", "frank"}, nil
default:
return nil, errors.New("invalid group")
}
return io.NopCloser(strings.NewReader(data)), nil, nil
})
gitserverClient.PerforceProtectsForDepotFunc.SetDefaultReturn([]*p4types.Protect{
{Level: "list", EntityType: "user", EntityName: "*", Host: "*", Match: "//...", IsExclusion: true},
{Level: "write", EntityType: "user", EntityName: "alice", Host: "*", Match: "//Sourcegraph/..."},
{Level: "write", EntityType: "user", EntityName: "bob", Host: "*", Match: "//Sourcegraph/..."},
{Level: "admin", EntityType: "group", EntityName: "Backend", Host: "*", Match: "//Sourcegraph/..."}, // includes "alice" and "cindy"
{Level: "admin", EntityType: "group", EntityName: "Frontend", Host: "*", Match: "//Sourcegraph/...", IsExclusion: true}, // excludes "bob", "david" and "frank"
{Level: "read", EntityType: "user", EntityName: "cindy", Host: "*", Match: "//Sourcegraph/...", IsExclusion: true},
{Level: "list", EntityType: "user", EntityName: "david", Host: "*", Match: "//Sourcegraph/..."}, // "list" can't grant read access
}, nil)
p := NewTestProvider(logger, "", "ssl:111.222.333.444:1666", "admin", "password", execer)
p := NewProvider(logger, gitserverClient, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
got, err := p.FetchRepoPerms(ctx,
&extsvc.Repository{
URI: "gitlab.com/user/repo",
@ -437,15 +418,3 @@ Users:
t.Fatalf("Mismatch (-want +got):\n%s", diff)
}
}
func NewTestProvider(logger log.Logger, urn, host, user, password string, execer p4Execer) *Provider {
p := NewProvider(logger, gitserver.NewClient(), urn, host, user, password, []extsvc.RepoID{}, false)
p.p4Execer = execer
return p
}
type p4ExecFunc func(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error)
func (p p4ExecFunc) P4Exec(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error) {
return p(ctx, host, user, password, args...)
}

View File

@ -1,10 +1,8 @@
package perforce
import (
"bufio"
"context"
"fmt"
"io"
"strings"
"github.com/gobwas/glob"
@ -12,6 +10,7 @@ import (
"github.com/sourcegraph/sourcegraph/internal/authz"
"github.com/sourcegraph/sourcegraph/internal/extsvc"
"github.com/sourcegraph/sourcegraph/internal/perforce"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
@ -183,17 +182,6 @@ func matchesAgainstDepot(match globMatch, depot string) bool {
return false
}
// PerformDebugScan will scan protections rules from r and log detailed
// information about how each line was parsed.
func PerformDebugScan(logger log.Logger, r io.Reader, depot extsvc.RepoID, ignoreRulesWithHost bool) (*authz.ExternalUserPermissions, error) {
perms := &authz.ExternalUserPermissions{
SubRepoPermissions: make(map[extsvc.RepoID]*authz.SubRepoPermissions),
}
scanner := fullRepoPermsScanner(logger, perms, []extsvc.RepoID{depot})
err := scanProtects(logger, r, scanner, ignoreRulesWithHost)
return perms, err
}
// protectsScanner provides callbacks for scanning the output of `p4 protects`.
type protectsScanner struct {
// Called on the parsed contents of each `p4 protects` line.
@ -205,55 +193,26 @@ type protectsScanner struct {
// scanProtects is a utility function for processing values from `p4 protects`.
// It handles skipping comments, cleaning whitespace, parsing relevant fields, and
// skipping entries that do not affect read access.
func scanProtects(logger log.Logger, rc io.Reader, s *protectsScanner, ignoreRulesWithHost bool) error {
func scanProtects(logger log.Logger, protects []*perforce.Protect, s *protectsScanner, ignoreRulesWithHost bool) error {
logger = logger.Scoped("scanProtects", "")
scanner := bufio.NewScanner(rc)
for scanner.Scan() {
line := scanner.Text()
// Trim whitespace
line = strings.TrimSpace(line)
// Skip comments and blank lines
if strings.HasPrefix(line, "##") || line == "" {
continue
}
// Trim trailing comments
if i := strings.Index(line, "##"); i > -1 {
line = line[:i]
}
logger.Debug("Scanning protects line", log.String("line", line))
// Split into fields
fields := strings.Fields(line)
if len(fields) < 5 {
logger.Debug("Line has less than 5 fields, discarding")
continue
}
for _, protect := range protects {
// skip any rule that relies on particular client IP addresses or hostnames
// this is the initial approach to address wrong behaviors
// that are causing clients to need to disable sub-repo permissions
// GitHub issue: https://github.com/sourcegraph/sourcegraph/issues/53374
// Subsequent approaches will need to add more sophisticated handling of hosts
// perhaps even capturing the browser IP address and comparing it to the host field.
if ignoreRulesWithHost && fields[3] != "*" {
logger.Debug("Skipping host-specific rule", log.String("line", line))
if ignoreRulesWithHost && protect.Host != "*" {
logger.Debug("Skipping host-specific rule", log.String("protect", fmt.Sprintf("%#v", protect)))
continue
}
// Parse line
parsedLine := p4ProtectLine{
level: fields[0],
entityType: fields[1],
name: fields[2],
match: fields[4],
}
if strings.HasPrefix(parsedLine.match, "-") {
parsedLine.isExclusion = true // is an exclusion
parsedLine.match = strings.TrimPrefix(parsedLine.match, "-") // trim leading -
level: protect.Level,
entityType: protect.EntityType,
name: protect.EntityName,
match: protect.Match,
isExclusion: protect.IsExclusion,
}
// We only care about read access. If the permission doesn't change read access,
@ -269,12 +228,12 @@ func scanProtects(logger log.Logger, rc io.Reader, s *protectsScanner, ignoreRul
return err
}
}
var finalizeErr error
if s.finalize != nil {
finalizeErr = s.finalize()
return s.finalize()
}
scanErr := scanner.Err()
return errors.CombineErrors(scanErr, finalizeErr)
return nil
}
// scanRepoIncludesExcludes converts `p4 protects` to Postgres SIMILAR TO-compatible

View File

@ -5,13 +5,13 @@ import (
"context"
"fmt"
"io"
"net/http"
"os"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/sourcegraph/log/logtest"
"github.com/stretchr/testify/require"
"github.com/sourcegraph/sourcegraph/internal/actor"
"github.com/sourcegraph/sourcegraph/internal/api"
@ -19,6 +19,8 @@ import (
srp "github.com/sourcegraph/sourcegraph/internal/authz/subrepoperms"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/internal/extsvc"
"github.com/sourcegraph/sourcegraph/internal/gitserver"
p4types "github.com/sourcegraph/sourcegraph/internal/perforce"
"github.com/sourcegraph/sourcegraph/schema"
)
@ -229,11 +231,7 @@ func TestScanFullRepoPermissions(t *testing.T) {
rc := io.NopCloser(bytes.NewReader(data))
execer := p4ExecFunc(func(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error) {
return rc, nil, nil
})
p := NewTestProvider(logger, "", "ssl:111.222.333.444:1666", "admin", "password", execer)
p := NewProvider(logger, gitserver.NewStrictMockClient(), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
p.depots = []extsvc.RepoID{
"//depot/main/",
"//depot/training/",
@ -244,7 +242,7 @@ func TestScanFullRepoPermissions(t *testing.T) {
perms := &authz.ExternalUserPermissions{
SubRepoPermissions: make(map[extsvc.RepoID]*authz.SubRepoPermissions),
}
if err := scanProtects(logger, rc, fullRepoPermsScanner(logger, perms, p.depots), false); err != nil {
if err := scanProtects(logger, testParseP4ProtectsRaw(t, rc), fullRepoPermsScanner(logger, perms, p.depots), false); err != nil {
t.Fatal(err)
}
@ -314,18 +312,14 @@ func TestScanFullRepoPermissionsWithWildcardMatchingDepot(t *testing.T) {
rc := io.NopCloser(bytes.NewReader(data))
execer := p4ExecFunc(func(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error) {
return rc, nil, nil
})
p := NewTestProvider(logger, "", "ssl:111.222.333.444:1666", "admin", "password", execer)
p := NewProvider(logger, gitserver.NewStrictMockClient(), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
p.depots = []extsvc.RepoID{
"//depot/main/base/",
}
perms := &authz.ExternalUserPermissions{
SubRepoPermissions: make(map[extsvc.RepoID]*authz.SubRepoPermissions),
}
if err := scanProtects(logger, rc, fullRepoPermsScanner(logger, perms, p.depots), false); err != nil {
if err := scanProtects(logger, testParseP4ProtectsRaw(t, rc), fullRepoPermsScanner(logger, perms, p.depots), false); err != nil {
t.Fatal(err)
}
@ -614,17 +608,15 @@ read group Dev1 * //depot/main/.../*.go
t.Fatal(err)
}
})
execer := p4ExecFunc(func(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error) {
return rc, nil, nil
})
p := NewTestProvider(logger, "", "ssl:111.222.333.444:1666", "admin", "password", execer)
p := NewProvider(logger, gitserver.NewStrictMockClient(), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
p.depots = []extsvc.RepoID{
extsvc.RepoID(tc.depot),
}
perms := &authz.ExternalUserPermissions{
SubRepoPermissions: make(map[extsvc.RepoID]*authz.SubRepoPermissions),
}
if err := scanProtects(logger, rc, fullRepoPermsScanner(logger, perms, p.depots), true); err != nil {
if err := scanProtects(logger, testParseP4ProtectsRaw(t, rc), fullRepoPermsScanner(logger, perms, p.depots), true); err != nil {
t.Fatal(err)
}
rules, ok := perms.SubRepoPermissions[extsvc.RepoID(tc.depot)]
@ -680,18 +672,14 @@ func TestFullScanWildcardDepotMatching(t *testing.T) {
rc := io.NopCloser(bytes.NewReader(data))
execer := p4ExecFunc(func(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error) {
return rc, nil, nil
})
p := NewTestProvider(logger, "", "ssl:111.222.333.444:1666", "admin", "password", execer)
p := NewProvider(logger, gitserver.NewStrictMockClient(), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
p.depots = []extsvc.RepoID{
"//depot/654/deploy/base/",
}
perms := &authz.ExternalUserPermissions{
SubRepoPermissions: make(map[extsvc.RepoID]*authz.SubRepoPermissions),
}
if err := scanProtects(logger, rc, fullRepoPermsScanner(logger, perms, p.depots), false); err != nil {
if err := scanProtects(logger, testParseP4ProtectsRaw(t, rc), fullRepoPermsScanner(logger, perms, p.depots), false); err != nil {
t.Fatal(err)
}
@ -829,12 +817,9 @@ func TestScanAllUsers(t *testing.T) {
}
rc := io.NopCloser(bytes.NewReader(data))
execer := p4ExecFunc(func(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error) {
return rc, nil, nil
})
p := NewTestProvider(logger, "", "ssl:111.222.333.444:1666", "admin", "password", execer)
gc := gitserver.NewStrictMockClient()
gc.PerforceGroupMembersFunc.SetDefaultReturn(nil, nil)
p := NewProvider(logger, gc, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
p.cachedGroupMembers = map[string][]string{
"dev": {"user1", "user2"},
}
@ -844,7 +829,7 @@ func TestScanAllUsers(t *testing.T) {
}
users := make(map[string]struct{})
if err := scanProtects(logger, rc, allUsersScanner(ctx, p, users), false); err != nil {
if err := scanProtects(logger, testParseP4ProtectsRaw(t, rc), allUsersScanner(ctx, p, users), false); err != nil {
t.Fatal(err)
}
want := map[string]struct{}{
@ -855,3 +840,9 @@ func TestScanAllUsers(t *testing.T) {
t.Fatal(diff)
}
}
func testParseP4ProtectsRaw(t *testing.T, rc io.Reader) []*p4types.Protect {
protects, err := parseP4ProtectsRaw(rc)
require.NoError(t, err)
return protects
}

View File

@ -45,6 +45,7 @@ go_library(
"//internal/gitserver/protocol",
"//internal/httpcli",
"//internal/jsonc",
"//internal/perforce",
"//internal/types",
"//internal/vcs",
"//lib/errors",
@ -104,6 +105,7 @@ go_test(
"//internal/gitserver/protocol",
"//internal/httpcli",
"//internal/httptestutil",
"//internal/perforce",
"//internal/ratelimit",
"//internal/rcache",
"//internal/testutil",

File diff suppressed because it is too large Load Diff

View File

@ -11,15 +11,16 @@ import (
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
"github.com/sourcegraph/sourcegraph/internal/httpcli"
"github.com/sourcegraph/sourcegraph/internal/jsonc"
"github.com/sourcegraph/sourcegraph/internal/perforce"
"github.com/sourcegraph/sourcegraph/internal/types"
"github.com/sourcegraph/sourcegraph/lib/errors"
"github.com/sourcegraph/sourcegraph/schema"
)
type PerforceSource struct {
server schema.PerforceConnection
conn schema.PerforceConnection
gitServerClient gitserver.Client
perforceCreds *gitserver.PerforceCredentials
perforceCreds *protocol.PerforceConnectionDetails
}
func NewPerforceSource(ctx context.Context, gitserverClient gitserver.Client, svc *types.ExternalService, _ *httpcli.Factory) (*PerforceSource, error) {
@ -33,13 +34,17 @@ func NewPerforceSource(ctx context.Context, gitserverClient gitserver.Client, sv
}
return &PerforceSource{
server: c,
conn: c,
gitServerClient: gitserverClient,
}, nil
}
// GitserverPushConfig returns an authenticated push config used for pushing commits to the code host.
func (s PerforceSource) GitserverPushConfig(repo *types.Repo) (*protocol.PushConfig, error) {
if s.perforceCreds == nil {
return nil, errors.New("no credentials set for Perforce Source")
}
// Return a PushConfig with a crafted URL that includes the Perforce scheme and the credentials
// The perforce scheme will tell `createCommitFromPatch` that this repo is a Perforce repo
// so it can handle it differently from Git repos.
@ -49,7 +54,7 @@ func (s PerforceSource) GitserverPushConfig(repo *types.Repo) (*protocol.PushCon
if err == nil {
depot = "//" + u.Path + "/"
}
remoteURL := fmt.Sprintf("perforce://%s:%s@%s%s", s.perforceCreds.Username, s.perforceCreds.Password, s.server.P4Port, depot)
remoteURL := fmt.Sprintf("perforce://%s:%s@%s%s", s.perforceCreds.P4User, s.perforceCreds.P4Passwd, s.conn.P4Port, depot)
return &protocol.PushConfig{
RemoteURL: remoteURL,
}, nil
@ -61,16 +66,16 @@ func (s PerforceSource) GitserverPushConfig(repo *types.Repo) (*protocol.PushCon
func (s PerforceSource) WithAuthenticator(a auth.Authenticator) (ChangesetSource, error) {
switch av := a.(type) {
case *auth.BasicAuthWithSSH:
s.perforceCreds = &gitserver.PerforceCredentials{
Username: av.Username,
Password: av.Password,
Host: s.server.P4Port,
s.perforceCreds = &protocol.PerforceConnectionDetails{
P4Port: s.conn.P4Port,
P4User: av.Username,
P4Passwd: av.Password,
}
case *auth.BasicAuth:
s.perforceCreds = &gitserver.PerforceCredentials{
Username: av.Username,
Password: av.Password,
Host: s.server.P4Port,
s.perforceCreds = &protocol.PerforceConnectionDetails{
P4Port: s.conn.P4Port,
P4User: av.Username,
P4Passwd: av.Password,
}
default:
return s, errors.New("unexpected auther type for Perforce Source")
@ -85,7 +90,7 @@ func (s PerforceSource) ValidateAuthenticator(ctx context.Context) error {
if s.perforceCreds == nil {
return errors.New("no credentials set for Perforce Source")
}
return s.gitServerClient.CheckPerforceCredentials(ctx, s.perforceCreds.Host, s.perforceCreds.Username, s.perforceCreds.Password)
return s.gitServerClient.CheckPerforceCredentials(ctx, *s.perforceCreds)
}
// LoadChangeset loads the given Changeset from the source and updates it. If
@ -95,10 +100,11 @@ func (s PerforceSource) LoadChangeset(ctx context.Context, cs *Changeset) error
if s.perforceCreds == nil {
return errors.New("no credentials set for Perforce Source")
}
cl, err := s.gitServerClient.P4GetChangelist(ctx, cs.ExternalID, *s.perforceCreds)
cl, err := s.gitServerClient.PerforceGetChangelist(ctx, *s.perforceCreds, cs.ExternalID)
if err != nil {
return errors.Wrap(err, "getting changelist")
}
return errors.Wrap(s.setChangesetMetadata(cl, cs), "setting perforce changeset metadata")
}
@ -114,7 +120,7 @@ func (s PerforceSource) CreateDraftChangeset(_ context.Context, _ *Changeset) (b
return false, errors.New("not implemented")
}
func (s PerforceSource) setChangesetMetadata(cl *protocol.PerforceChangelist, cs *Changeset) error {
func (s PerforceSource) setChangesetMetadata(cl *perforce.Changelist, cs *Changeset) error {
if err := cs.SetMetadata(cl); err != nil {
return errors.Wrap(err, "setting changeset metadata")
}

View File

@ -2,13 +2,12 @@ package sources
import (
"context"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/sourcegraph/sourcegraph/internal/gitserver"
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
"github.com/sourcegraph/sourcegraph/internal/perforce"
"github.com/sourcegraph/sourcegraph/schema"
btypes "github.com/sourcegraph/sourcegraph/internal/batches/types"
@ -18,7 +17,7 @@ import (
var (
testPerforceChangeID = "111"
testPerforceCredentials = gitserver.PerforceCredentials{Username: "user", Password: "pass", Host: "https://perforce.sgdev.org:1666"}
testPerforceCredentials = protocol.PerforceConnectionDetails{P4User: "user", P4Passwd: "pass", P4Port: "perforce.sgdev.org:1666"}
)
func TestPerforceSource_ValidateAuthenticator(t *testing.T) {
@ -43,10 +42,10 @@ func TestPerforceSource_LoadChangeset(t *testing.T) {
cs, _ := mockPerforceChangeset()
s, client := mockPerforceSource()
want := errors.New("error")
client.P4GetChangelistFunc.SetDefaultHook(func(ctx context.Context, changeID string, credentials gitserver.PerforceCredentials) (*protocol.PerforceChangelist, error) {
client.PerforceGetChangelistFunc.SetDefaultHook(func(ctx context.Context, credentials protocol.PerforceConnectionDetails, changeID string) (*perforce.Changelist, error) {
assert.Equal(t, changeID, testPerforceChangeID)
assert.Equal(t, testPerforceCredentials, credentials)
return new(protocol.PerforceChangelist), want
return new(perforce.Changelist), want
})
err := s.LoadChangeset(ctx, cs)
@ -59,7 +58,7 @@ func TestPerforceSource_LoadChangeset(t *testing.T) {
s, client := mockPerforceSource()
change := mockPerforceChange()
client.P4GetChangelistFunc.SetDefaultHook(func(ctx context.Context, changeID string, credentials gitserver.PerforceCredentials) (*protocol.PerforceChangelist, error) {
client.PerforceGetChangelistFunc.SetDefaultHook(func(ctx context.Context, credentials protocol.PerforceConnectionDetails, changeID string) (*perforce.Changelist, error) {
assert.Equal(t, changeID, testPerforceChangeID)
assert.Equal(t, testPerforceCredentials, credentials)
return change, nil
@ -77,10 +76,10 @@ func TestPerforceSource_CreateChangeset(t *testing.T) {
cs, _ := mockPerforceChangeset()
s, client := mockPerforceSource()
want := errors.New("error")
client.P4GetChangelistFunc.SetDefaultHook(func(ctx context.Context, changeID string, credentials gitserver.PerforceCredentials) (*protocol.PerforceChangelist, error) {
assert.Equal(t, changeID, testPerforceChangeID)
assert.Equal(t, testPerforceCredentials, credentials)
return new(protocol.PerforceChangelist), want
client.PerforceGetChangelistFunc.SetDefaultHook(func(ctx context.Context, conn protocol.PerforceConnectionDetails, changelistID string) (*perforce.Changelist, error) {
assert.Equal(t, changelistID, testPerforceChangeID)
assert.Equal(t, testPerforceCredentials, conn)
return new(perforce.Changelist), want
})
b, err := s.CreateChangeset(ctx, cs)
@ -94,9 +93,9 @@ func TestPerforceSource_CreateChangeset(t *testing.T) {
s, client := mockPerforceSource()
change := mockPerforceChange()
client.P4GetChangelistFunc.SetDefaultHook(func(ctx context.Context, changeID string, credentials gitserver.PerforceCredentials) (*protocol.PerforceChangelist, error) {
assert.Equal(t, changeID, testPerforceChangeID)
assert.Equal(t, testPerforceCredentials, credentials)
client.PerforceGetChangelistFunc.SetDefaultHook(func(ctx context.Context, conn protocol.PerforceConnectionDetails, changelistID string) (*perforce.Changelist, error) {
assert.Equal(t, changelistID, testPerforceChangeID)
assert.Equal(t, testPerforceCredentials, conn)
return change, nil
})
@ -126,11 +125,11 @@ func mockPerforceChangeset() (*Changeset, *types.Repo) {
// mockPerforceChange returns a plausible changelist that would be
// returned from Perforce.
func mockPerforceChange() *protocol.PerforceChangelist {
return &protocol.PerforceChangelist{
func mockPerforceChange() *perforce.Changelist {
return &perforce.Changelist{
ID: testPerforceChangeID,
Author: "Peter Guy",
State: protocol.PerforceChangelistStatePending,
State: perforce.ChangelistStatePending,
}
}
@ -138,12 +137,6 @@ func mockPerforceSource() (*PerforceSource, *MockGitserverClient) {
client := NewStrictMockGitserverClient()
// Cred checks should pass by default.
client.CheckPerforceCredentialsFunc.SetDefaultReturn(nil)
s := &PerforceSource{gitServerClient: client, perforceCreds: &testPerforceCredentials, server: schema.PerforceConnection{P4Port: "https://perforce.sgdev.org:1666"}}
s := &PerforceSource{gitServerClient: client, perforceCreds: &testPerforceCredentials, conn: schema.PerforceConnection{P4Port: "perforce.sgdev.org:1666"}}
return s, client
}
type fakeCloser struct {
io.Reader
}
func (fakeCloser) Close() error { return nil }

View File

@ -28,7 +28,7 @@ go_library(
"//internal/extsvc/github",
"//internal/extsvc/gitlab",
"//internal/gitserver",
"//internal/gitserver/protocol",
"//internal/perforce",
"//internal/types",
"//lib/errors",
"@com_github_inconshreveable_log15//:log15",
@ -54,7 +54,7 @@ go_test(
"//internal/extsvc/bitbucketserver",
"//internal/extsvc/github",
"//internal/extsvc/gitlab",
"//internal/gitserver/protocol",
"//internal/perforce",
"//internal/timeutil",
"//internal/types",
"//lib/errors",

View File

@ -13,7 +13,7 @@ import (
"github.com/sourcegraph/sourcegraph/internal/extsvc"
adobatches "github.com/sourcegraph/sourcegraph/internal/extsvc/azuredevops"
"github.com/sourcegraph/sourcegraph/internal/extsvc/gerrit"
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
"github.com/sourcegraph/sourcegraph/internal/perforce"
"github.com/sourcegraph/go-diff/diff"
@ -124,7 +124,7 @@ func computeCheckState(c *btypes.Changeset, events ChangesetEvents) btypes.Chang
return computeAzureDevOpsBuildState(m)
case *gerritbatches.AnnotatedChange:
return computeGerritBuildState(m)
case *protocol.PerforceChangelistState:
case *perforce.ChangelistState:
// Perforce doesn't have builds built-in, its better to be explicit by still
// including this case for clarity.
return btypes.ChangesetCheckStateUnknown
@ -639,13 +639,13 @@ func computeSingleChangesetExternalState(c *btypes.Changeset) (s btypes.Changese
default:
return "", errors.Errorf("unknown Gerrit Change state: %s", m.Change.Status)
}
case *protocol.PerforceChangelist:
case *perforce.Changelist:
switch m.State {
case protocol.PerforceChangelistStateClosed:
case perforce.ChangelistStateClosed:
s = btypes.ChangesetExternalStateClosed
case protocol.PerforceChangelistStateSubmitted:
case perforce.ChangelistStateSubmitted:
s = btypes.ChangesetExternalStateMerged
case protocol.PerforceChangelistStatePending, protocol.PerforceChangelistStateShelved:
case perforce.ChangelistStatePending, perforce.ChangelistStateShelved:
s = btypes.ChangesetExternalStateOpen
default:
return "", errors.Errorf("unknown Perforce Change state: %s", m.State)
@ -758,7 +758,7 @@ func computeSingleChangesetReviewState(c *btypes.Changeset) (s btypes.ChangesetR
}
}
case *protocol.PerforceChangelist:
case *perforce.Changelist:
states[btypes.ChangesetReviewStatePending] = true
default:
return "", errors.New("unknown changeset type")

View File

@ -11,7 +11,7 @@ import (
azuredevops2 "github.com/sourcegraph/sourcegraph/internal/batches/sources/azuredevops"
"github.com/sourcegraph/sourcegraph/internal/extsvc/azuredevops"
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
"github.com/sourcegraph/sourcegraph/internal/perforce"
"github.com/sourcegraph/sourcegraph/lib/errors"
btypes "github.com/sourcegraph/sourcegraph/internal/batches/types"
@ -866,25 +866,25 @@ func TestComputeExternalState(t *testing.T) {
},
{
name: "perforce closed - no events",
changeset: perforceChangeset(daysAgo(10), protocol.PerforceChangelistStateClosed),
changeset: perforceChangeset(daysAgo(10), perforce.ChangelistStateClosed),
history: []changesetStatesAtTime{},
want: btypes.ChangesetExternalStateClosed,
},
{
name: "perforce submitted - no events",
changeset: perforceChangeset(daysAgo(10), protocol.PerforceChangelistStateSubmitted),
changeset: perforceChangeset(daysAgo(10), perforce.ChangelistStateSubmitted),
history: []changesetStatesAtTime{},
want: btypes.ChangesetExternalStateMerged,
},
{
name: "perforce pending - no events",
changeset: perforceChangeset(daysAgo(10), protocol.PerforceChangelistStatePending),
changeset: perforceChangeset(daysAgo(10), perforce.ChangelistStatePending),
history: []changesetStatesAtTime{},
want: btypes.ChangesetExternalStateOpen,
},
{
name: "perforce shelved - no events",
changeset: perforceChangeset(daysAgo(10), protocol.PerforceChangelistStateShelved),
changeset: perforceChangeset(daysAgo(10), perforce.ChangelistStateShelved),
history: []changesetStatesAtTime{},
want: btypes.ChangesetExternalStateOpen,
},
@ -1051,11 +1051,11 @@ func gitLabChangeset(updatedAt time.Time, state gitlab.MergeRequestState, notes
}
}
func perforceChangeset(updatedAt time.Time, state protocol.PerforceChangelistState) *btypes.Changeset {
func perforceChangeset(updatedAt time.Time, state perforce.ChangelistState) *btypes.Changeset {
return &btypes.Changeset{
ExternalServiceType: extsvc.TypePerforce,
UpdatedAt: updatedAt,
Metadata: &protocol.PerforceChangelist{
Metadata: &perforce.Changelist{
State: state,
},
}

View File

@ -53,9 +53,9 @@ go_library(
"//internal/extsvc/gitlab",
"//internal/featureflag",
"//internal/github_apps/store",
"//internal/gitserver/protocol",
"//internal/metrics",
"//internal/observation",
"//internal/perforce",
"//internal/timeutil",
"//internal/workerutil/dbworker/store",
"//lib/batches",

View File

@ -16,7 +16,7 @@ import (
gerritbatches "github.com/sourcegraph/sourcegraph/internal/batches/sources/gerrit"
"github.com/sourcegraph/sourcegraph/internal/extsvc/azuredevops"
"github.com/sourcegraph/sourcegraph/internal/extsvc/gerrit"
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
"github.com/sourcegraph/sourcegraph/internal/perforce"
"github.com/keegancsmith/sqlf"
"github.com/lib/pq"
@ -1527,7 +1527,7 @@ func ScanChangeset(t *btypes.Changeset, s dbutil.Scanner) error {
m.Change = &gerrit.Change{}
t.Metadata = m
case extsvc.TypePerforce:
t.Metadata = new(protocol.PerforceChangelist)
t.Metadata = new(perforce.Changelist)
case extsvc.TypeGerrit:
t.Metadata = new(gerrit.Change)
default:

View File

@ -44,7 +44,7 @@ go_library(
"//internal/extsvc/gitlab",
"//internal/extsvc/gitlab/webhooks",
"//internal/gitserver/gitdomain",
"//internal/gitserver/protocol",
"//internal/perforce",
"//internal/timeutil",
"//internal/types",
"//lib/batches",

View File

@ -11,21 +11,19 @@ import (
"github.com/inconshreveable/log15"
"github.com/sourcegraph/go-diff/diff"
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
gerritbatches "github.com/sourcegraph/sourcegraph/internal/batches/sources/gerrit"
"github.com/sourcegraph/sourcegraph/internal/extsvc/azuredevops"
"github.com/sourcegraph/sourcegraph/internal/api"
adobatches "github.com/sourcegraph/sourcegraph/internal/batches/sources/azuredevops"
bbcs "github.com/sourcegraph/sourcegraph/internal/batches/sources/bitbucketcloud"
gerritbatches "github.com/sourcegraph/sourcegraph/internal/batches/sources/gerrit"
"github.com/sourcegraph/sourcegraph/internal/extsvc"
"github.com/sourcegraph/sourcegraph/internal/extsvc/azuredevops"
"github.com/sourcegraph/sourcegraph/internal/extsvc/bitbucketcloud"
"github.com/sourcegraph/sourcegraph/internal/extsvc/bitbucketserver"
"github.com/sourcegraph/sourcegraph/internal/extsvc/gerrit"
"github.com/sourcegraph/sourcegraph/internal/extsvc/github"
"github.com/sourcegraph/sourcegraph/internal/extsvc/gitlab"
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
"github.com/sourcegraph/sourcegraph/internal/perforce"
"github.com/sourcegraph/sourcegraph/internal/timeutil"
"github.com/sourcegraph/sourcegraph/lib/batches"
"github.com/sourcegraph/sourcegraph/lib/errors"
@ -470,7 +468,7 @@ func (c *Changeset) SetMetadata(meta any) error {
c.ExternalServiceType = extsvc.TypeGerrit
c.ExternalBranch = gitdomain.EnsureRefPrefix(pr.Change.Branch)
c.ExternalUpdatedAt = pr.Change.Updated
case *protocol.PerforceChangelist:
case *perforce.Changelist:
c.Metadata = pr
c.ExternalID = pr.ID
c.ExternalServiceType = extsvc.TypePerforce
@ -510,7 +508,7 @@ func (c *Changeset) Title() (string, error) {
// Remove extra quotes added by the commit message
title = strings.TrimPrefix(strings.TrimSuffix(title, "\""), "\"")
return title, nil
case *protocol.PerforceChangelist:
case *perforce.Changelist:
return m.Title, nil
default:
return "", errors.New("title unknown changeset type")
@ -537,7 +535,7 @@ func (c *Changeset) AuthorName() (string, error) {
return m.CreatedBy.UniqueName, nil
case *gerritbatches.AnnotatedChange:
return m.Change.Owner.Name, nil
case *protocol.PerforceChangelist:
case *perforce.Changelist:
return m.Author, nil
default:
return "", errors.New("authorname unknown changeset type")
@ -572,7 +570,7 @@ func (c *Changeset) AuthorEmail() (string, error) {
return m.CreatedBy.UniqueName, nil
case *gerritbatches.AnnotatedChange:
return m.Change.Owner.Email, nil
case *protocol.PerforceChangelist:
case *perforce.Changelist:
return "", nil
default:
return "", errors.New("author email unknown changeset type")
@ -596,7 +594,7 @@ func (c *Changeset) ExternalCreatedAt() time.Time {
return m.CreationDate
case *gerritbatches.AnnotatedChange:
return m.Change.Created
case *protocol.PerforceChangelist:
case *perforce.Changelist:
return m.CreationDate
default:
return time.Time{}
@ -619,7 +617,7 @@ func (c *Changeset) Body() (string, error) {
case *gerritbatches.AnnotatedChange:
// Gerrit doesn't really differentiate between title/description.
return m.Change.Subject, nil
case *protocol.PerforceChangelist:
case *perforce.Changelist:
return "", nil
default:
return "", errors.New("body unknown changeset type")
@ -687,7 +685,7 @@ func (c *Changeset) URL() (s string, err error) {
return returnURL.String(), nil
case *gerritbatches.AnnotatedChange:
return m.CodeHostURL.JoinPath("c", url.PathEscape(m.Change.Project), "+", url.PathEscape(strconv.Itoa(m.Change.ChangeNumber))).String(), nil
case *protocol.PerforceChangelist:
case *perforce.Changelist:
return "", nil
default:
return "", errors.New("url unknown changeset type")
@ -907,7 +905,7 @@ func (c *Changeset) Events() (events []*ChangesetEvent, err error) {
Metadata: reviewer,
})
}
case *protocol.PerforceChangelist:
case *perforce.Changelist:
// We don't have any events we care about right now
break
}
@ -932,7 +930,7 @@ func (c *Changeset) HeadRefOid() (string, error) {
return "", nil
case *gerritbatches.AnnotatedChange:
return "", nil
case *protocol.PerforceChangelist:
case *perforce.Changelist:
return "", nil
default:
return "", errors.New("head ref oid unknown changeset type")
@ -955,7 +953,7 @@ func (c *Changeset) HeadRef() (string, error) {
return m.SourceRefName, nil
case *gerritbatches.AnnotatedChange:
return "", nil
case *protocol.PerforceChangelist:
case *perforce.Changelist:
return "", nil
default:
return "", errors.New("headref unknown changeset type")
@ -979,7 +977,7 @@ func (c *Changeset) BaseRefOid() (string, error) {
return "", nil
case *gerritbatches.AnnotatedChange:
return "", nil
case *protocol.PerforceChangelist:
case *perforce.Changelist:
return "", nil
default:
return "", errors.New("base ref oid unknown changeset type")
@ -1002,7 +1000,7 @@ func (c *Changeset) BaseRef() (string, error) {
return m.TargetRefName, nil
case *gerritbatches.AnnotatedChange:
return "refs/heads/" + m.Change.Branch, nil
case *protocol.PerforceChangelist:
case *perforce.Changelist:
// TODO: @peterguy we may need to change this to something.
return "", nil
default:

View File

@ -97,8 +97,6 @@ go_test(
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_google_grpc//codes",
"@org_golang_google_grpc//status",
"@org_golang_google_protobuf//encoding/protojson",
],
)

View File

@ -40,7 +40,7 @@ import (
"github.com/sourcegraph/sourcegraph/internal/lazyregexp"
"github.com/sourcegraph/sourcegraph/internal/limiter"
"github.com/sourcegraph/sourcegraph/internal/observation"
p4tools "github.com/sourcegraph/sourcegraph/internal/perforce"
"github.com/sourcegraph/sourcegraph/internal/perforce"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
@ -279,12 +279,6 @@ type Client interface {
// MergeBase returns the merge base commit for the specified commits.
MergeBase(ctx context.Context, repo api.RepoName, a, b api.CommitID) (api.CommitID, error)
// P4Exec sends a p4 command with given arguments and returns an io.ReadCloser for the output.
P4Exec(_ context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error)
// P4GetChangelist gets the changelist specified by changelistID.
P4GetChangelist(_ context.Context, changelistID string, creds PerforceCredentials) (*protocol.PerforceChangelist, error)
// Remove removes the repository clone from gitserver.
Remove(context.Context, api.RepoName) error
@ -440,39 +434,50 @@ type Client interface {
Addrs() []string
// SystemsInfo returns information about all gitserver instances associated with a Sourcegraph instance.
SystemsInfo(ctx context.Context) ([]SystemInfo, error)
SystemsInfo(ctx context.Context) ([]protocol.SystemInfo, error)
// SystemInfo returns information about the gitserver instance at the given address.
SystemInfo(ctx context.Context, addr string) (SystemInfo, error)
SystemInfo(ctx context.Context, addr string) (protocol.SystemInfo, error)
// IsPerforcePathCloneable checks if the given Perforce depot path is cloneable by
// checking if it is a valid depot and the given user has permission to access it.
IsPerforcePathCloneable(ctx context.Context, p4port, p4user, p4passwd, depotPath string) error
IsPerforcePathCloneable(ctx context.Context, conn protocol.PerforceConnectionDetails, depotPath string) error
// CheckPerforceCredentials checks if the given Perforce credentials are valid
CheckPerforceCredentials(ctx context.Context, p4port, p4user, p4passwd string) error
CheckPerforceCredentials(ctx context.Context, conn protocol.PerforceConnectionDetails) error
// PerforceUsers lists all the users known to the given Perforce server.
PerforceUsers(ctx context.Context, conn protocol.PerforceConnectionDetails) ([]*perforce.User, error)
// PerforceProtectsForUser returns all protects that apply to the given Perforce user.
PerforceProtectsForUser(ctx context.Context, conn protocol.PerforceConnectionDetails, username string) ([]*perforce.Protect, error)
// PerforceProtectsForDepot returns all protects that apply to the given Perforce depot.
PerforceProtectsForDepot(ctx context.Context, conn protocol.PerforceConnectionDetails, depot string) ([]*perforce.Protect, error)
// PerforceGroupMembers returns the members of the given Perforce group.
PerforceGroupMembers(ctx context.Context, conn protocol.PerforceConnectionDetails, group string) ([]string, error)
// IsPerforceSuperUser checks if the given Perforce user is a super user, and otherwise returns an error.
IsPerforceSuperUser(ctx context.Context, conn protocol.PerforceConnectionDetails) error
// PerforceGetChangelist gets the perforce changelist details for the given changelist ID.
PerforceGetChangelist(ctx context.Context, conn protocol.PerforceConnectionDetails, changelist string) (*perforce.Changelist, error)
}
type SystemInfo struct {
Address string
FreeSpace uint64
TotalSpace uint64
PercentUsed float32
}
func (c *clientImplementor) SystemsInfo(ctx context.Context) ([]SystemInfo, error) {
func (c *clientImplementor) SystemsInfo(ctx context.Context) ([]protocol.SystemInfo, error) {
addresses := c.clientSource.Addresses()
wg := pool.NewWithResults[SystemInfo]().WithErrors().WithContext(ctx)
wg := pool.NewWithResults[protocol.SystemInfo]().WithErrors().WithContext(ctx)
for _, addr := range addresses {
addr := addr // capture addr
wg.Go(func(ctx context.Context) (SystemInfo, error) {
wg.Go(func(ctx context.Context) (protocol.SystemInfo, error) {
response, err := c.getDiskInfo(ctx, addr)
if err != nil {
return SystemInfo{}, err
return protocol.SystemInfo{}, err
}
return SystemInfo{
return protocol.SystemInfo{
Address: addr.Address(),
FreeSpace: response.GetFreeSpace(),
TotalSpace: response.GetTotalSpace(),
@ -484,18 +489,18 @@ func (c *clientImplementor) SystemsInfo(ctx context.Context) ([]SystemInfo, erro
return wg.Wait()
}
func (c *clientImplementor) SystemInfo(ctx context.Context, addr string) (SystemInfo, error) {
func (c *clientImplementor) SystemInfo(ctx context.Context, addr string) (protocol.SystemInfo, error) {
ac := c.clientSource.GetAddressWithClient(addr)
if ac == nil {
return SystemInfo{}, errors.Newf("no client for address: %s", addr)
return protocol.SystemInfo{}, errors.Newf("no client for address: %s", addr)
}
response, err := c.getDiskInfo(ctx, ac)
if err != nil {
return SystemInfo{}, nil
return protocol.SystemInfo{}, err
}
return SystemInfo{
return protocol.SystemInfo{
Address: ac.Address(),
FreeSpace: response.FreeSpace,
TotalSpace: response.TotalSpace,
@ -951,140 +956,11 @@ func convertGitserverError(err error) error {
return err
}
func (c *clientImplementor) P4Exec(ctx context.Context, host, user, password string, args ...string) (_ io.ReadCloser, _ http.Header, err error) {
ctx, _, endObservation := c.operations.p4Exec.With(ctx, &err, observation.Args{Attrs: []attribute.KeyValue{
attribute.String("host", host),
attribute.StringSlice("args", args),
}})
defer endObservation(1, observation.Args{})
// Check that ctx is not expired.
if err := ctx.Err(); err != nil {
deadlineExceededCounter.Inc()
return nil, nil, err
}
req := &protocol.P4ExecRequest{
P4Port: host,
P4User: user,
P4Passwd: password,
Args: args,
}
if conf.IsGRPCEnabled(ctx) {
client, err := c.ClientForRepo(ctx, "")
if err != nil {
return nil, nil, err
}
ctx, cancel := context.WithCancel(ctx)
stream, err := client.P4Exec(ctx, req.ToProto())
if err != nil {
cancel()
return nil, nil, err
}
// We need to check the first message from the gRPC errors to see if we get an argument or permisison related
// error before continuing to read the rest of the stream. If the first message is an error, we cancel the stream and
// forward the error.
//
// This is necessary to provide parity between the REST and gRPC implementations of
// P4Exec. Users of cli.P4Exec may assume error handling occurs immediately,
// as is the case with the HTTP implementation where these kinds of errors are returned as soon as the
// function returns. gRPC is asynchronous, so we have to start consuming messages from
// the stream to see any errors from the server. Reading the first message ensures we
// handle any errors synchronously, similar to the HTTP implementation.
firstMessage, firstError := stream.Recv()
switch status.Code(firstError) {
case codes.InvalidArgument, codes.PermissionDenied:
cancel()
return nil, nil, convertGitserverError(firstError)
}
firstMessageRead := false
r := streamio.NewReader(func() ([]byte, error) {
// Check if we've read the first message yet. If not, read it and return.
if !firstMessageRead {
firstMessageRead = true
if firstError != nil {
return nil, firstError
}
return firstMessage.GetData(), nil
}
msg, err := stream.Recv()
if err != nil {
if status.Code(err) == codes.Canceled {
return nil, context.Canceled
}
if status.Code(err) == codes.DeadlineExceeded {
return nil, context.DeadlineExceeded
}
return nil, err
}
return msg.GetData(), nil
})
return &readCloseWrapper{r: r, closeFn: cancel}, nil, nil
} else {
resp, err := c.httpPost(ctx, "", "p4-exec", req)
if err != nil {
return nil, nil, err
}
if resp.StatusCode != http.StatusOK {
defer resp.Body.Close()
return nil, nil, errors.Errorf("unexpected status code: %d - %s", resp.StatusCode, readResponseBody(resp.Body))
}
return resp.Body, resp.Trailer, nil
}
}
var deadlineExceededCounter = promauto.NewCounter(prometheus.CounterOpts{
Name: "src_gitserver_client_deadline_exceeded",
Help: "Times that Client.sendExec() returned context.DeadlineExceeded",
})
func (c *clientImplementor) P4GetChangelist(ctx context.Context, changelistID string, creds PerforceCredentials) (*protocol.PerforceChangelist, error) {
reader, _, err := c.P4Exec(ctx, creds.Host, creds.Username, creds.Password,
"changes",
"-r", // list in reverse order, which means that the given changelist id will be the first one listed
"-m", "1", // limit output to one record, so that the given changelist is the only one listed
"-l", // use a long listing, which includes the whole commit message
"-e", changelistID, // start from this changelist and go up
)
if err != nil {
return nil, err
}
body, err := io.ReadAll(reader)
if err != nil {
return nil, errors.Wrap(err, "failed to read the output of p4 changes")
}
output := strings.TrimSpace(string(body))
if output == "" {
return nil, errors.New("invalid changelist " + changelistID)
}
pcl, err := p4tools.ParseChangelistOutput(output)
if err != nil {
return nil, errors.Wrap(err, "unable to parse change output")
}
return pcl, nil
}
type PerforceCredentials struct {
Host string
Username string
Password string
}
// BatchLog invokes the given callback with the `git log` output for a batch of repository
// and commit pairs. If the invoked callback returns a non-nil error, the operation will begin
// to abort processing further results.
@ -1613,7 +1489,7 @@ func (c *clientImplementor) removeFrom(ctx context.Context, repo api.RepoName, f
return nil
}
func (c *clientImplementor) IsPerforcePathCloneable(ctx context.Context, p4port, p4user, p4passwd, depotPath string) error {
func (c *clientImplementor) IsPerforcePathCloneable(ctx context.Context, conn protocol.PerforceConnectionDetails, depotPath string) error {
if conf.IsGRPCEnabled(ctx) {
// depotPath is not actually a repo name, but it will spread the load of isPerforcePathCloneable
// a bit over the different gitserver instances. It's really just used as a consistent hashing
@ -1623,10 +1499,8 @@ func (c *clientImplementor) IsPerforcePathCloneable(ctx context.Context, p4port,
return err
}
_, err = client.IsPerforcePathCloneable(ctx, &proto.IsPerforcePathCloneableRequest{
P4Port: p4port,
P4User: p4user,
P4Passwd: p4passwd,
DepotPath: depotPath,
ConnectionDetails: conn.ToProto(),
DepotPath: depotPath,
})
if err != nil {
// Unwrap proto errors for nicer error messages.
@ -1641,9 +1515,9 @@ func (c *clientImplementor) IsPerforcePathCloneable(ctx context.Context, p4port,
addr := c.AddrForRepo(ctx, api.RepoName(depotPath))
b, err := json.Marshal(&protocol.IsPerforcePathCloneableRequest{
P4Port: p4port,
P4User: p4user,
P4Passwd: p4passwd,
P4Port: conn.P4Port,
P4User: conn.P4User,
P4Passwd: conn.P4Passwd,
DepotPath: depotPath,
})
if err != nil {
@ -1668,19 +1542,17 @@ func (c *clientImplementor) IsPerforcePathCloneable(ctx context.Context, p4port,
return nil
}
func (c *clientImplementor) CheckPerforceCredentials(ctx context.Context, p4port, p4user, p4passwd string) error {
func (c *clientImplementor) CheckPerforceCredentials(ctx context.Context, conn protocol.PerforceConnectionDetails) error {
if conf.IsGRPCEnabled(ctx) {
// p4port is not actually a repo name, but it will spread the load of CheckPerforceCredentials
// a bit over the different gitserver instances. It's really just used as a consistent hashing
// key here.
client, err := c.ClientForRepo(ctx, api.RepoName(p4port))
client, err := c.ClientForRepo(ctx, api.RepoName(conn.P4Port))
if err != nil {
return err
}
_, err = client.CheckPerforceCredentials(ctx, &proto.CheckPerforceCredentialsRequest{
P4Port: p4port,
P4User: p4user,
P4Passwd: p4passwd,
ConnectionDetails: conn.ToProto(),
})
if err != nil {
// Unwrap proto errors for nicer error messages.
@ -1693,11 +1565,11 @@ func (c *clientImplementor) CheckPerforceCredentials(ctx context.Context, p4port
return nil
}
addr := c.AddrForRepo(ctx, api.RepoName(p4port))
addr := c.AddrForRepo(ctx, api.RepoName(conn.P4Port))
b, err := json.Marshal(&protocol.CheckPerforceCredentialsRequest{
P4Port: p4port,
P4User: p4user,
P4Passwd: p4passwd,
P4Port: conn.P4Port,
P4User: conn.P4User,
P4Passwd: conn.P4Passwd,
})
if err != nil {
return err
@ -1721,6 +1593,370 @@ func (c *clientImplementor) CheckPerforceCredentials(ctx context.Context, p4port
return nil
}
func (c *clientImplementor) PerforceUsers(ctx context.Context, conn protocol.PerforceConnectionDetails) ([]*perforce.User, error) {
if conf.IsGRPCEnabled(ctx) {
// p4port is not actually a repo name, but it will spread the load of CheckPerforceCredentials
// a bit over the different gitserver instances. It's really just used as a consistent hashing
// key here.
client, err := c.ClientForRepo(ctx, api.RepoName(conn.P4Port))
if err != nil {
return nil, err
}
resp, err := client.PerforceUsers(ctx, &proto.PerforceUsersRequest{
ConnectionDetails: conn.ToProto(),
})
if err != nil {
return nil, err
}
users := make([]*perforce.User, len(resp.GetUsers()))
for i, u := range resp.GetUsers() {
users[i] = perforce.UserFromProto(u)
}
return users, nil
}
addr := c.AddrForRepo(ctx, api.RepoName(conn.P4Port))
b, err := json.Marshal(&protocol.PerforceUsersRequest{
P4Port: conn.P4Port,
P4User: conn.P4User,
P4Passwd: conn.P4Passwd,
})
if err != nil {
return nil, err
}
uri := "http://" + addr + "/perforce-users"
resp, err := c.do(ctx, "perforce-users", uri, b)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, &url.Error{
URL: resp.Request.URL.String(),
Op: "PerforceUsers",
Err: errors.Errorf("PerforceUsers: http status %d: %s", resp.StatusCode, readResponseBody(io.LimitReader(resp.Body, 200))),
}
}
var payload protocol.PerforceUsersResponse
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, err
}
users := make([]*perforce.User, len(payload.Users))
for i, u := range payload.Users {
users[i] = &perforce.User{
Username: u.Username,
Email: u.Email,
}
}
return users, nil
}
func (c *clientImplementor) PerforceProtectsForUser(ctx context.Context, conn protocol.PerforceConnectionDetails, username string) ([]*perforce.Protect, error) {
if conf.IsGRPCEnabled(ctx) {
// p4port is not actually a repo name, but it will spread the load of CheckPerforceCredentials
// a bit over the different gitserver instances. It's really just used as a consistent hashing
// key here.
client, err := c.ClientForRepo(ctx, api.RepoName(conn.P4Port))
if err != nil {
return nil, err
}
resp, err := client.PerforceProtectsForUser(ctx, &proto.PerforceProtectsForUserRequest{
ConnectionDetails: conn.ToProto(),
Username: username,
})
if err != nil {
return nil, err
}
protects := make([]*perforce.Protect, len(resp.GetProtects()))
for i, p := range resp.GetProtects() {
protects[i] = perforce.ProtectFromProto(p)
}
return protects, nil
}
addr := c.AddrForRepo(ctx, api.RepoName(conn.P4Port))
b, err := json.Marshal(&protocol.PerforceProtectsForUserRequest{
P4Port: conn.P4Port,
P4User: conn.P4User,
P4Passwd: conn.P4Passwd,
Username: username,
})
if err != nil {
return nil, err
}
uri := "http://" + addr + "/perforce-protects-for-user"
resp, err := c.do(ctx, "perforce-protects-for-user", uri, b)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, &url.Error{
URL: resp.Request.URL.String(),
Op: "PerforceProtectsForUser",
Err: errors.Errorf("PerforceProtectsForUser: http status %d: %s", resp.StatusCode, readResponseBody(io.LimitReader(resp.Body, 200))),
}
}
var payload protocol.PerforceProtectsForUserResponse
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, err
}
protects := make([]*perforce.Protect, len(payload.Protects))
for i, p := range payload.Protects {
protects[i] = &perforce.Protect{
Level: p.Level,
EntityType: p.EntityType,
EntityName: p.EntityName,
Match: p.Match,
IsExclusion: p.IsExclusion,
Host: p.Host,
}
}
return protects, nil
}
func (c *clientImplementor) PerforceProtectsForDepot(ctx context.Context, conn protocol.PerforceConnectionDetails, depot string) ([]*perforce.Protect, error) {
if conf.IsGRPCEnabled(ctx) {
// p4port is not actually a repo name, but it will spread the load of CheckPerforceCredentials
// a bit over the different gitserver instances. It's really just used as a consistent hashing
// key here.
client, err := c.ClientForRepo(ctx, api.RepoName(conn.P4Port))
if err != nil {
return nil, err
}
resp, err := client.PerforceProtectsForDepot(ctx, &proto.PerforceProtectsForDepotRequest{
ConnectionDetails: conn.ToProto(),
Depot: depot,
})
if err != nil {
return nil, err
}
protects := make([]*perforce.Protect, len(resp.GetProtects()))
for i, p := range resp.GetProtects() {
protects[i] = perforce.ProtectFromProto(p)
}
return protects, nil
}
addr := c.AddrForRepo(ctx, api.RepoName(conn.P4Port))
b, err := json.Marshal(&protocol.PerforceProtectsForDepotRequest{
P4Port: conn.P4Port,
P4User: conn.P4User,
P4Passwd: conn.P4Passwd,
Depot: depot,
})
if err != nil {
return nil, err
}
uri := "http://" + addr + "/perforce-protects-for-depot"
resp, err := c.do(ctx, "perforce-protects-for-depot", uri, b)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, &url.Error{
URL: resp.Request.URL.String(),
Op: "PerforceProtectsForDepot",
Err: errors.Errorf("PerforceProtectsForDepot: http status %d: %s", resp.StatusCode, readResponseBody(io.LimitReader(resp.Body, 200))),
}
}
var payload protocol.PerforceProtectsForDepotResponse
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, err
}
protects := make([]*perforce.Protect, len(payload.Protects))
for i, p := range payload.Protects {
protects[i] = &perforce.Protect{
Level: p.Level,
EntityType: p.EntityType,
EntityName: p.EntityName,
Match: p.Match,
IsExclusion: p.IsExclusion,
Host: p.Host,
}
}
return protects, nil
}
func (c *clientImplementor) PerforceGroupMembers(ctx context.Context, conn protocol.PerforceConnectionDetails, group string) ([]string, error) {
if conf.IsGRPCEnabled(ctx) {
// p4port is not actually a repo name, but it will spread the load of CheckPerforceCredentials
// a bit over the different gitserver instances. It's really just used as a consistent hashing
// key here.
client, err := c.ClientForRepo(ctx, api.RepoName(conn.P4Port))
if err != nil {
return nil, err
}
resp, err := client.PerforceGroupMembers(ctx, &proto.PerforceGroupMembersRequest{
ConnectionDetails: conn.ToProto(),
Group: group,
})
if err != nil {
return nil, err
}
return resp.GetUsernames(), nil
}
addr := c.AddrForRepo(ctx, api.RepoName(conn.P4Port))
b, err := json.Marshal(&protocol.PerforceGroupMembersRequest{
P4Port: conn.P4Port,
P4User: conn.P4User,
P4Passwd: conn.P4Passwd,
Group: group,
})
if err != nil {
return nil, err
}
uri := "http://" + addr + "/perforce-group-members"
resp, err := c.do(ctx, "perforce-group-members", uri, b)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, &url.Error{
URL: resp.Request.URL.String(),
Op: "PerforceGroupMembers",
Err: errors.Errorf("PerforceGroupMembers: http status %d: %s", resp.StatusCode, readResponseBody(io.LimitReader(resp.Body, 200))),
}
}
var payload protocol.PerforceGroupMembersResponse
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, err
}
return payload.Usernames, nil
}
func (c *clientImplementor) IsPerforceSuperUser(ctx context.Context, conn protocol.PerforceConnectionDetails) error {
if conf.IsGRPCEnabled(ctx) {
// p4port is not actually a repo name, but it will spread the load of CheckPerforceCredentials
// a bit over the different gitserver instances. It's really just used as a consistent hashing
// key here.
client, err := c.ClientForRepo(ctx, api.RepoName(conn.P4Port))
if err != nil {
return err
}
_, err = client.IsPerforceSuperUser(ctx, &proto.IsPerforceSuperUserRequest{
ConnectionDetails: conn.ToProto(),
})
return err
}
addr := c.AddrForRepo(ctx, api.RepoName(conn.P4Port))
b, err := json.Marshal(&protocol.IsPerforceSuperUserRequest{
P4Port: conn.P4Port,
P4User: conn.P4User,
P4Passwd: conn.P4Passwd,
})
if err != nil {
return err
}
uri := "http://" + addr + "/is-perforce-super-user"
resp, err := c.do(ctx, "is-perforce-super-user", uri, b)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return &url.Error{
URL: resp.Request.URL.String(),
Op: "IsPerforceSuperUser",
Err: errors.Errorf("IsPerforceSuperUser: http status %d: %s", resp.StatusCode, readResponseBody(io.LimitReader(resp.Body, 200))),
}
}
return nil
}
func (c *clientImplementor) PerforceGetChangelist(ctx context.Context, conn protocol.PerforceConnectionDetails, changelist string) (*perforce.Changelist, error) {
if conf.IsGRPCEnabled(ctx) {
// p4port is not actually a repo name, but it will spread the load of CheckPerforceCredentials
// a bit over the different gitserver instances. It's really just used as a consistent hashing
// key here.
client, err := c.ClientForRepo(ctx, api.RepoName(conn.P4Port))
if err != nil {
return nil, err
}
resp, err := client.PerforceGetChangelist(ctx, &proto.PerforceGetChangelistRequest{
ConnectionDetails: conn.ToProto(),
ChangelistId: changelist,
})
if err != nil {
return nil, err
}
return perforce.ChangelistFromProto(resp.GetChangelist()), nil
}
addr := c.AddrForRepo(ctx, api.RepoName(conn.P4Port))
b, err := json.Marshal(&protocol.PerforceGetChangelistRequest{
P4Port: conn.P4Port,
P4User: conn.P4User,
P4Passwd: conn.P4Passwd,
ChangelistID: changelist,
})
if err != nil {
return nil, err
}
uri := "http://" + addr + "/perforce-get-changelist"
resp, err := c.do(ctx, "perforce-get-changelist", uri, b)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, &url.Error{
URL: resp.Request.URL.String(),
Op: "PerforceGetChangelist",
Err: errors.Errorf("PerforceGetChangelist: http status %d: %s", resp.StatusCode, readResponseBody(io.LimitReader(resp.Body, 200))),
}
}
var payload protocol.PerforceGetChangelistResponse
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, err
}
cl := &perforce.Changelist{
ID: payload.Changelist.ID,
CreationDate: payload.Changelist.CreationDate,
State: perforce.ChangelistState(payload.Changelist.State),
Author: payload.Changelist.Author,
Title: payload.Changelist.Title,
Message: payload.Changelist.Message,
}
return cl, nil
}
// httpPost will apply the MD5 hashing scheme on the repo name to determine the gitserver instance
// to which the HTTP POST request is sent.
func (c *clientImplementor) httpPost(ctx context.Context, repo api.RepoName, op string, payload any) (resp *http.Response, err error) {

View File

@ -8,8 +8,6 @@ import (
"io"
"math/rand"
"net/http"
"net/http/httptest"
"net/url"
"os/exec"
"path/filepath"
"reflect"
@ -23,8 +21,6 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/protojson"
"github.com/sourcegraph/sourcegraph/internal/api"
@ -256,25 +252,6 @@ func TestClient_RepoCloneProgress_ProtoRoundTrip(t *testing.T) {
}
}
func TestClient_P4ExecRequest_ProtoRoundTrip(t *testing.T) {
var diff string
fn := func(original protocol.P4ExecRequest) bool {
var converted protocol.P4ExecRequest
converted.FromProto(original.ToProto())
if diff = cmp.Diff(original, converted); diff != "" {
return false
}
return true
}
if err := quick.Check(fn, nil); err != nil {
t.Errorf("P4ExecRequest proto roundtrip failed (-want +got):\n%s", diff)
}
}
func TestClient_RepoClone_ProtoRoundTrip(t *testing.T) {
var diff string
@ -326,9 +303,10 @@ func TestClient_Remove(t *testing.T) {
*called = true
return nil, nil
}
return &gitserver.MockGRPCClient{
MockRepoDelete: mockRepoDelete,
}
cli := gitserver.NewStrictMockGitserverServiceClient()
cli.RepoDeleteFunc.SetDefaultHook(mockRepoDelete)
return cli
}
})
cli := gitserver.NewTestClient(
@ -393,277 +371,6 @@ func TestClient_Remove(t *testing.T) {
}
type mockP4ExecClient struct {
isEndOfStream bool
Err error
grpc.ClientStream
}
func (m *mockP4ExecClient) Recv() (*proto.P4ExecResponse, error) {
if m.isEndOfStream {
return nil, io.EOF
}
if m.Err != nil {
s, _ := status.FromError(m.Err)
return nil, s.Err()
}
response := &proto.P4ExecResponse{
Data: []byte("example output"),
}
// Set the end-of-stream condition
m.isEndOfStream = true
return response, nil
}
func TestClient_P4ExecGRPC(t *testing.T) {
_ = gitserver.CreateRepoDir(t)
type test struct {
name string
host string
user string
password string
args []string
mockErr error
wantBody string
wantReaderConstructionError string
wantReaderError string
}
tests := []test{
{
name: "check request body",
host: "ssl:111.222.333.444:1666",
user: "admin",
password: "pa$$word",
args: []string{"protects"},
wantBody: "example output",
wantReaderConstructionError: "<nil>",
wantReaderError: "<nil>",
},
{
name: "error response",
mockErr: errors.New("example error"),
wantReaderConstructionError: "<nil>",
wantReaderError: "rpc error: code = Unknown desc = example error",
},
{
name: "context cancellation",
mockErr: status.New(codes.Canceled, context.Canceled.Error()).Err(),
wantReaderConstructionError: "<nil>",
wantReaderError: context.Canceled.Error(),
},
{
name: "context expiration",
mockErr: status.New(codes.DeadlineExceeded, context.DeadlineExceeded.Error()).Err(),
wantReaderConstructionError: "<nil>",
wantReaderError: context.DeadlineExceeded.Error(),
},
{
name: "invalid credentials - reported on reader instantiation",
mockErr: status.New(codes.InvalidArgument, "that is totally wrong").Err(),
wantReaderConstructionError: status.New(codes.InvalidArgument, "that is totally wrong").Err().Error(),
wantReaderError: status.New(codes.InvalidArgument, "that is totally wrong").Err().Error(),
},
{
name: "permission denied - reported on reader instantiation",
mockErr: status.New(codes.PermissionDenied, "you can't do this").Err(),
wantReaderConstructionError: status.New(codes.PermissionDenied, "you can't do this").Err().Error(),
wantReaderError: status.New(codes.PermissionDenied, "you can't do this").Err().Error(),
},
}
ctx := context.Background()
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
conf.Mock(&conf.Unified{
SiteConfiguration: schema.SiteConfiguration{
ExperimentalFeatures: &schema.ExperimentalFeatures{
EnableGRPC: boolPointer(true),
},
},
})
t.Cleanup(func() {
conf.Mock(nil)
})
const gitserverAddr = "172.16.8.1:8080"
addrs := []string{gitserverAddr}
called := false
source := gitserver.NewTestClientSource(t, addrs, func(o *gitserver.TestClientSourceOptions) {
o.ClientFunc = func(cc *grpc.ClientConn) proto.GitserverServiceClient {
mockP4Exec := func(ctx context.Context, in *proto.P4ExecRequest, opts ...grpc.CallOption) (proto.GitserverService_P4ExecClient, error) {
called = true
return &mockP4ExecClient{
Err: test.mockErr,
}, nil
}
return &gitserver.MockGRPCClient{MockP4Exec: mockP4Exec}
}
})
cli := gitserver.NewTestClient(&http.Client{}, source)
rc, _, err := cli.P4Exec(ctx, test.host, test.user, test.password, test.args...)
if diff := cmp.Diff(test.wantReaderConstructionError, fmt.Sprintf("%v", err)); diff != "" {
t.Errorf("error when creating reader mismatch (-want +got):\n%s", diff)
}
var body []byte
if rc != nil {
t.Cleanup(func() {
_ = rc.Close()
})
body, err = io.ReadAll(rc)
if err != nil {
if diff := cmp.Diff(test.wantReaderError, fmt.Sprintf("%v", err)); diff != "" {
t.Errorf("Mismatch (-want +got):\n%s", diff)
}
}
}
if diff := cmp.Diff(test.wantBody, string(body)); diff != "" {
t.Fatalf("Mismatch (-want +got):\n%s", diff)
}
if !called {
t.Fatal("GRPC should be called")
}
})
}
}
func TestClient_P4Exec(t *testing.T) {
_ = gitserver.CreateRepoDir(t)
type test struct {
name string
host string
user string
password string
args []string
handler http.HandlerFunc
wantBody string
wantErr string
}
tests := []test{
{
name: "check request body",
host: "ssl:111.222.333.444:1666",
user: "admin",
password: "pa$$word",
args: []string{"protects"},
handler: func(w http.ResponseWriter, r *http.Request) {
if r.ProtoMajor == 2 {
// Ignore attempted gRPC connections
w.WriteHeader(http.StatusNotImplemented)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatal(err)
}
wantBody := `{"p4port":"ssl:111.222.333.444:1666","p4user":"admin","p4passwd":"pa$$word","args":["protects"]}`
if diff := cmp.Diff(wantBody, string(body)); diff != "" {
t.Fatalf("Mismatch (-want +got):\n%s", diff)
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("example output"))
},
wantBody: "example output",
wantErr: "<nil>",
},
{
name: "error response",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.ProtoMajor == 2 {
// Ignore attempted gRPC connections
w.WriteHeader(http.StatusNotImplemented)
return
}
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte("example error"))
},
wantErr: "unexpected status code: 400 - example error",
},
}
ctx := context.Background()
runTest := func(t *testing.T, test test, cli gitserver.Client, called bool) {
t.Run(test.name, func(t *testing.T) {
t.Log(test.name)
rc, _, err := cli.P4Exec(ctx, test.host, test.user, test.password, test.args...)
if diff := cmp.Diff(test.wantErr, fmt.Sprintf("%v", err)); diff != "" {
t.Fatalf("Mismatch (-want +got):\n%s", diff)
}
var body []byte
if rc != nil {
defer func() { _ = rc.Close() }()
body, err = io.ReadAll(rc)
if err != nil {
t.Fatal(err)
}
}
if diff := cmp.Diff(test.wantBody, string(body)); diff != "" {
t.Fatalf("Mismatch (-want +got):\n%s", diff)
}
})
}
t.Run("HTTP", func(t *testing.T) {
for _, test := range tests {
conf.Mock(&conf.Unified{
SiteConfiguration: schema.SiteConfiguration{
ExperimentalFeatures: &schema.ExperimentalFeatures{
EnableGRPC: boolPointer(false),
},
},
})
t.Cleanup(func() {
conf.Mock(nil)
})
testServer := httptest.NewServer(test.handler)
defer testServer.Close()
u, _ := url.Parse(testServer.URL)
addrs := []string{u.Host}
source := gitserver.NewTestClientSource(t, addrs)
called := false
cli := gitserver.NewTestClient(&http.Client{}, source)
runTest(t, test, cli, called)
if called {
t.Fatal("handler shoulde be called")
}
}
})
}
func TestClient_BatchLogGRPC(t *testing.T) {
conf.Mock(&conf.Unified{
SiteConfiguration: schema.SiteConfiguration{
@ -703,7 +410,9 @@ func TestClient_BatchLogGRPC(t *testing.T) {
return resp.ToProto(), nil
}
return &gitserver.MockGRPCClient{MockBatchLog: mockBatchLog}
cli := gitserver.NewStrictMockGitserverServiceClient()
cli.BatchLogFunc.SetDefaultHook(mockBatchLog)
return cli
}
})
@ -784,9 +493,9 @@ func TestClient_BatchLog(t *testing.T) {
}, nil
}
return &gitserver.MockGRPCClient{
MockBatchLog: mockBatchLog,
}
cli := gitserver.NewStrictMockGitserverServiceClient()
cli.BatchLogFunc.SetDefaultHook(mockBatchLog)
return cli
}
})
@ -984,7 +693,9 @@ func TestClient_IsRepoCloneableGRPC(t *testing.T) {
}
return tc.mockResponse.ToProto(), nil
}
return &gitserver.MockGRPCClient{MockIsRepoCloneable: mockIsRepoCloneable}
cli := gitserver.NewStrictMockGitserverServiceClient()
cli.IsRepoCloneableFunc.SetDefaultHook(mockIsRepoCloneable)
return cli
}
})
@ -1021,7 +732,9 @@ func TestClient_IsRepoCloneableGRPC(t *testing.T) {
}
return tc.mockResponse.ToProto(), nil
}
return &gitserver.MockGRPCClient{MockIsRepoCloneable: mockIsRepoCloneable}
cli := gitserver.NewStrictMockGitserverServiceClient()
cli.IsRepoCloneableFunc.SetDefaultHook(mockIsRepoCloneable)
return cli
}
})
@ -1056,7 +769,7 @@ func TestClient_SystemsInfo(t *testing.T) {
gitserverAddr2 = "172.16.8.2:8080"
)
expectedResponses := []gitserver.SystemInfo{
expectedResponses := []protocol.SystemInfo{
{
Address: gitserverAddr1,
FreeSpace: 102400,
@ -1123,7 +836,9 @@ func TestClient_SystemsInfo(t *testing.T) {
called = true
return response, nil
}
return &gitserver.MockGRPCClient{MockDiskInfo: mockDiskInfo}
cli := gitserver.NewStrictMockGitserverServiceClient()
cli.DiskInfoFunc.SetDefaultHook(mockDiskInfo)
return cli
}
})
@ -1154,7 +869,9 @@ func TestClient_SystemsInfo(t *testing.T) {
called = true
return nil, nil
}
return &gitserver.MockGRPCClient{MockDiskInfo: mockDiskInfo}
cli := gitserver.NewStrictMockGitserverServiceClient()
cli.DiskInfoFunc.SetDefaultHook(mockDiskInfo)
return cli
}
})
@ -1227,7 +944,9 @@ func TestClient_SystemInfo(t *testing.T) {
called = true
return mockResponse, nil
}
return &gitserver.MockGRPCClient{MockDiskInfo: mockDiskInfo}
cli := gitserver.NewStrictMockGitserverServiceClient()
cli.DiskInfoFunc.SetDefaultHook(mockDiskInfo)
return cli
}
})
@ -1259,7 +978,9 @@ func TestClient_SystemInfo(t *testing.T) {
called = true
return mockResponse, nil
}
return &gitserver.MockGRPCClient{MockDiskInfo: mockDiskInfo}
cli := gitserver.NewStrictMockGitserverServiceClient()
cli.DiskInfoFunc.SetDefaultHook(mockDiskInfo)
return cli
}
})
@ -1287,8 +1008,6 @@ func TestClient_SystemInfo(t *testing.T) {
})
}
var _ proto.GitserverService_P4ExecClient = &mockP4ExecClient{}
type fuzzTime time.Time
func (fuzzTime) Generate(rand *rand.Rand, _ int) reflect.Value {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -5,11 +5,11 @@ import (
"sync"
"github.com/sourcegraph/log"
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
"github.com/sourcegraph/sourcegraph/lib/errors"
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
"github.com/sourcegraph/sourcegraph/internal/metrics"
"github.com/sourcegraph/sourcegraph/internal/observation"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
type operations struct {
@ -32,7 +32,6 @@ type operations struct {
lstat *observation.Operation
mergeBase *observation.Operation
newFileReader *observation.Operation
p4Exec *observation.Operation
readDir *observation.Operation
readFile *observation.Operation
resolveRevision *observation.Operation
@ -107,7 +106,6 @@ func newOperations(observationCtx *observation.Context) *operations {
lstat: subOp("lStat"),
mergeBase: op("MergeBase"),
newFileReader: op("NewFileReader"),
p4Exec: op("P4Exec"),
readDir: op("ReadDir"),
readFile: op("ReadFile"),
resolveRevision: resolveRevisionOperation,

View File

@ -2,7 +2,6 @@ package protocol
import (
"encoding/json"
"strings"
"time"
"go.opentelemetry.io/otel/attribute"
@ -364,36 +363,6 @@ func (bl *BatchLogResult) FromProto(p *proto.BatchLogResult) {
}
}
// P4ExecRequest is a request to execute a p4 command with given arguments.
//
// Note that this request is deserialized by both gitserver and the frontend's
// internal proxy route and any major change to this structure will need to be
// reconciled in both places.
type P4ExecRequest struct {
P4Port string `json:"p4port"`
P4User string `json:"p4user"`
P4Passwd string `json:"p4passwd"`
Args []string `json:"args"`
}
func (r *P4ExecRequest) ToProto() *proto.P4ExecRequest {
return &proto.P4ExecRequest{
P4Port: r.P4Port,
P4User: r.P4User,
P4Passwd: r.P4Passwd,
Args: stringsToByteSlices(r.Args),
}
}
func (r *P4ExecRequest) FromProto(p *proto.P4ExecRequest) {
*r = P4ExecRequest{
P4Port: p.GetP4Port(),
P4User: p.GetP4User(),
P4Passwd: p.GetP4Passwd(),
Args: byteSlicesToStrings(p.GetArgs()),
}
}
// RepoUpdateRequest is a request to update the contents of a given repo, or clone it if it doesn't exist.
type RepoUpdateRequest struct {
// Repo identifies URL for repo.
@ -863,56 +832,6 @@ func (r *GetObjectResponse) FromProto(p *proto.GetObjectResponse) {
}
type PerforceChangelist struct {
ID string
CreationDate time.Time
State PerforceChangelistState
Author string
Title string
Message string
}
type PerforceChangelistState string
const (
PerforceChangelistStateSubmitted PerforceChangelistState = "submitted"
PerforceChangelistStatePending PerforceChangelistState = "pending"
PerforceChangelistStateShelved PerforceChangelistState = "shelved"
// Perforce doesn't actually return a state for closed changelists, so this is one we use to indicate the changelist is closed.
PerforceChangelistStateClosed PerforceChangelistState = "closed"
)
func ParsePerforceChangelistState(state string) (PerforceChangelistState, error) {
switch strings.ToLower(strings.TrimSpace(state)) {
case "submitted":
return PerforceChangelistStateSubmitted, nil
case "pending":
return PerforceChangelistStatePending, nil
case "shelved":
return PerforceChangelistStateShelved, nil
case "closed":
return PerforceChangelistStateClosed, nil
default:
return "", errors.Newf("invalid Perforce changelist state: %s", state)
}
}
func stringsToByteSlices(in []string) [][]byte {
res := make([][]byte, len(in))
for i, s := range in {
res[i] = []byte(s)
}
return res
}
func byteSlicesToStrings(in [][]byte) []string {
res := make([]string, len(in))
for i, s := range in {
res[i] = string(s)
}
return res
}
// IsPerforcePathCloneableRequest is the request to check if a Perforce path is cloneable.
type IsPerforcePathCloneableRequest struct {
P4Port string `json:"p4port"`
@ -933,3 +852,112 @@ type CheckPerforceCredentialsRequest struct {
// IsPerforcePathCloneableResponse is the response from checking if given Perforce credentials are valid.
type CheckPerforceCredentialsResponse struct{}
// PerforceConnectionDetails holds all the details required to talk to a Perforce server.
type PerforceConnectionDetails struct {
P4Port string
P4User string
P4Passwd string
}
func (c PerforceConnectionDetails) ToProto() *proto.PerforceConnectionDetails {
return &proto.PerforceConnectionDetails{
P4Port: c.P4Port,
P4User: c.P4User,
P4Passwd: c.P4Passwd,
}
}
// SystemInfo holds info on a Gitserver instance.
type SystemInfo struct {
Address string
FreeSpace uint64
TotalSpace uint64
PercentUsed float32
}
type PerforceUsersRequest struct {
P4Port string `json:"p4port"`
P4User string `json:"p4user"`
P4Passwd string `json:"p4passwd"`
}
type PerforceUsersResponse struct {
Users []PerforceUser `json:"users"`
}
type PerforceUser struct {
Username string `json:"username"`
Email string `json:"email"`
}
type PerforceProtectsForUserRequest struct {
P4Port string `json:"p4port"`
P4User string `json:"p4user"`
P4Passwd string `json:"p4passwd"`
Username string `json:"username"`
}
type PerforceProtectsForUserResponse struct {
Protects []PerforceProtect `json:"protects"`
}
type PerforceProtect struct {
Level string `json:"level"`
EntityType string `json:"entityType"`
EntityName string `json:"entityName"`
Match string `json:"match"`
IsExclusion bool `json:"isExclusion"`
Host string `json:"host"`
}
type PerforceProtectsForDepotRequest struct {
P4Port string `json:"p4port"`
P4User string `json:"p4user"`
P4Passwd string `json:"p4passwd"`
Depot string `json:"depot"`
}
type PerforceProtectsForDepotResponse struct {
Protects []PerforceProtect `json:"protects"`
}
type PerforceGroupMembersRequest struct {
P4Port string `json:"p4port"`
P4User string `json:"p4user"`
P4Passwd string `json:"p4passwd"`
Group string `json:"group"`
}
type PerforceGroupMembersResponse struct {
Usernames []string `json:"usernames"`
}
type IsPerforceSuperUserRequest struct {
P4Port string `json:"p4port"`
P4User string `json:"p4user"`
P4Passwd string `json:"p4passwd"`
}
type IsPerforceSuperUserResponse struct {
}
type PerforceGetChangelistRequest struct {
P4Port string `json:"p4port"`
P4User string `json:"p4user"`
P4Passwd string `json:"p4passwd"`
ChangelistID string `json:"changelistID"`
}
type PerforceGetChangelistResponse struct {
Changelist PerforceChangelist `json:"changelist"`
}
type PerforceChangelist struct {
ID string `json:"id"`
CreationDate time.Time `json:"creationDate"`
State string `json:"state"`
Author string `json:"author"`
Title string `json:"title"`
Message string `json:"message"`
}

View File

@ -39,6 +39,7 @@ go_library(
"//internal/gitserver:__pkg__",
"//internal/gitserver/gitdomain:__pkg__",
"//internal/gitserver/protocol:__pkg__",
"//internal/perforce:__pkg__",
],
deps = [
"//lib/errors",

File diff suppressed because it is too large Load Diff

View File

@ -17,13 +17,21 @@ service GitserverService {
rpc ListGitolite(ListGitoliteRequest) returns (ListGitoliteResponse) {}
rpc Search(SearchRequest) returns (stream SearchResponse) {}
rpc Archive(ArchiveRequest) returns (stream ArchiveResponse) {}
rpc P4Exec(P4ExecRequest) returns (stream P4ExecResponse) {}
rpc P4Exec(P4ExecRequest) returns (stream P4ExecResponse) {
option deprecated = true;
}
rpc RepoClone(RepoCloneRequest) returns (RepoCloneResponse) {}
rpc RepoCloneProgress(RepoCloneProgressRequest) returns (RepoCloneProgressResponse) {}
rpc RepoDelete(RepoDeleteRequest) returns (RepoDeleteResponse) {}
rpc RepoUpdate(RepoUpdateRequest) returns (RepoUpdateResponse) {}
rpc IsPerforcePathCloneable(IsPerforcePathCloneableRequest) returns (IsPerforcePathCloneableResponse) {}
rpc CheckPerforceCredentials(CheckPerforceCredentialsRequest) returns (CheckPerforceCredentialsResponse) {}
rpc PerforceUsers(PerforceUsersRequest) returns (PerforceUsersResponse) {}
rpc PerforceProtectsForUser(PerforceProtectsForUserRequest) returns (PerforceProtectsForUserResponse) {}
rpc PerforceProtectsForDepot(PerforceProtectsForDepotRequest) returns (PerforceProtectsForDepotResponse) {}
rpc PerforceGroupMembers(PerforceGroupMembersRequest) returns (PerforceGroupMembersResponse) {}
rpc IsPerforceSuperUser(IsPerforceSuperUserRequest) returns (IsPerforceSuperUserResponse) {}
rpc PerforceGetChangelist(PerforceGetChangelistRequest) returns (PerforceGetChangelistResponse) {}
}
// DiskInfoRequest is a empty request for the DiskInfo RPC.
@ -447,15 +455,17 @@ message RepoUpdateResponse {
string error = 3;
}
// Do not use: The P4Exec method has been deprecated and will disappear soon!
message P4ExecRequest {
string p4port = 1;
string p4user = 2;
string p4passwd = 3;
repeated bytes args = 4;
string p4port = 1 [deprecated = true];
string p4user = 2 [deprecated = true];
string p4passwd = 3 [deprecated = true];
repeated bytes args = 4 [deprecated = true];
}
// Do not use: The P4Exec method has been deprecated and will disappear soon!
message P4ExecResponse {
bytes data = 1;
bytes data = 1 [deprecated = true];
}
// ListGitoliteRequest is a request to list all repositories in gitolite.
@ -509,10 +519,8 @@ message GitObject {
// IsPerforcePathCloneableRequest is the request to check if a Perforce path is cloneable.
message IsPerforcePathCloneableRequest {
string p4port = 1;
string p4user = 2;
string p4passwd = 3;
string depot_path = 4;
PerforceConnectionDetails connection_details = 1;
string depot_path = 2;
}
// IsPerforcePathCloneableResponse is the response from checking if a Perforce path is cloneable.
@ -520,10 +528,121 @@ message IsPerforcePathCloneableResponse {}
// CheckPerforceCredentialsRequest is the request to check if given Perforce credentials are valid.
message CheckPerforceCredentialsRequest {
PerforceConnectionDetails connection_details = 1;
}
// IsPerforcePathCloneableResponse is the response from checking if given Perforce credentials are valid.
message CheckPerforceCredentialsResponse {}
// PerforceConnectionDetails holds all the details required to talk to a Perforce server.
message PerforceConnectionDetails {
string p4port = 1;
string p4user = 2;
string p4passwd = 3;
}
// IsPerforcePathCloneableResponse is the response from checking if given Perforce credentials are valid.
message CheckPerforceCredentialsResponse {}
// PerforceGetChangelistRequest is used to retrieve information about a specific
// Perforce changelist.
message PerforceGetChangelistRequest {
PerforceConnectionDetails connection_details = 1;
string changelist_id = 2;
}
// PerforceGetChangelistResponse returns information about the requested changelist.
message PerforceGetChangelistResponse {
PerforceChangelist changelist = 1;
}
// PerforceChangelist represents a changelist in Perforce.
message PerforceChangelist {
// PerforceChangelistState is the valid state values of a Perforce changelist.
enum PerforceChangelistState {
PERFORCE_CHANGELIST_STATE_UNSPECIFIED = 0;
PERFORCE_CHANGELIST_STATE_SUBMITTED = 1;
PERFORCE_CHANGELIST_STATE_PENDING = 2;
PERFORCE_CHANGELIST_STATE_SHELVED = 3;
// Perforce doesn't actually return a state for closed changelists, so this is
// one we use to indicate the changelist is closed.
PERFORCE_CHANGELIST_STATE_CLOSED = 4;
}
string id = 1;
google.protobuf.Timestamp creation_date = 2;
PerforceChangelistState state = 3;
string author = 4;
string title = 5;
string message = 6;
}
// IsPerforceSuperUserRequest can be used to check if a given Perforce user is a
// super user.
message IsPerforceSuperUserRequest {
PerforceConnectionDetails connection_details = 1;
}
// IsPerforceSuperUserResponse is the response from checking if a given Perforce
// user is a super user.
// No fields here, returning an error means "no".
message IsPerforceSuperUserResponse {}
// PerforceProtectsForDepotRequest requests all the protections that apply to the
// given depot.
message PerforceProtectsForDepotRequest {
PerforceConnectionDetails connection_details = 1;
string depot = 2;
}
// PerforceProtectsForDepotResponse returns all the protections that apply to the
// given depot.
message PerforceProtectsForDepotResponse {
repeated PerforceProtect protects = 1;
}
// PerforceProtectsForUserRequest requests all the protections that apply to the
// given user.
message PerforceProtectsForUserRequest {
PerforceConnectionDetails connection_details = 1;
string username = 2;
}
// PerforceProtectsForUserResponse returns all the protections that apply to the
// given user.
message PerforceProtectsForUserResponse {
repeated PerforceProtect protects = 1;
}
// PerforceProtect is a single line definition of a protection in Perforce.
message PerforceProtect {
string level = 1;
string entity_type = 2;
string entity_name = 3;
string match = 4;
bool is_exclusion = 5;
string host = 6;
}
// PerforceGroupMembersRequest requests the members of the given Perforce group.
message PerforceGroupMembersRequest {
PerforceConnectionDetails connection_details = 1;
string group = 2;
}
// PerforceGroupMembersResponse returns the members of the requested Perforce group.
message PerforceGroupMembersResponse {
repeated string usernames = 1;
}
// PerforceUsersRequest lists all the users known to the Perforce server.
message PerforceUsersRequest {
PerforceConnectionDetails connection_details = 1;
}
// PerforceUsersResponse contains the list of users known by the server.
message PerforceUsersResponse {
repeated PerforceUser users = 1;
}
// PerforceUser is a representation of a user account in Perforce.
message PerforceUser {
string username = 1;
string email = 2;
}

View File

@ -35,6 +35,12 @@ const (
GitserverService_RepoUpdate_FullMethodName = "/gitserver.v1.GitserverService/RepoUpdate"
GitserverService_IsPerforcePathCloneable_FullMethodName = "/gitserver.v1.GitserverService/IsPerforcePathCloneable"
GitserverService_CheckPerforceCredentials_FullMethodName = "/gitserver.v1.GitserverService/CheckPerforceCredentials"
GitserverService_PerforceUsers_FullMethodName = "/gitserver.v1.GitserverService/PerforceUsers"
GitserverService_PerforceProtectsForUser_FullMethodName = "/gitserver.v1.GitserverService/PerforceProtectsForUser"
GitserverService_PerforceProtectsForDepot_FullMethodName = "/gitserver.v1.GitserverService/PerforceProtectsForDepot"
GitserverService_PerforceGroupMembers_FullMethodName = "/gitserver.v1.GitserverService/PerforceGroupMembers"
GitserverService_IsPerforceSuperUser_FullMethodName = "/gitserver.v1.GitserverService/IsPerforceSuperUser"
GitserverService_PerforceGetChangelist_FullMethodName = "/gitserver.v1.GitserverService/PerforceGetChangelist"
)
// GitserverServiceClient is the client API for GitserverService service.
@ -50,6 +56,7 @@ type GitserverServiceClient interface {
ListGitolite(ctx context.Context, in *ListGitoliteRequest, opts ...grpc.CallOption) (*ListGitoliteResponse, error)
Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (GitserverService_SearchClient, error)
Archive(ctx context.Context, in *ArchiveRequest, opts ...grpc.CallOption) (GitserverService_ArchiveClient, error)
// Deprecated: Do not use.
P4Exec(ctx context.Context, in *P4ExecRequest, opts ...grpc.CallOption) (GitserverService_P4ExecClient, error)
RepoClone(ctx context.Context, in *RepoCloneRequest, opts ...grpc.CallOption) (*RepoCloneResponse, error)
RepoCloneProgress(ctx context.Context, in *RepoCloneProgressRequest, opts ...grpc.CallOption) (*RepoCloneProgressResponse, error)
@ -57,6 +64,12 @@ type GitserverServiceClient interface {
RepoUpdate(ctx context.Context, in *RepoUpdateRequest, opts ...grpc.CallOption) (*RepoUpdateResponse, error)
IsPerforcePathCloneable(ctx context.Context, in *IsPerforcePathCloneableRequest, opts ...grpc.CallOption) (*IsPerforcePathCloneableResponse, error)
CheckPerforceCredentials(ctx context.Context, in *CheckPerforceCredentialsRequest, opts ...grpc.CallOption) (*CheckPerforceCredentialsResponse, error)
PerforceUsers(ctx context.Context, in *PerforceUsersRequest, opts ...grpc.CallOption) (*PerforceUsersResponse, error)
PerforceProtectsForUser(ctx context.Context, in *PerforceProtectsForUserRequest, opts ...grpc.CallOption) (*PerforceProtectsForUserResponse, error)
PerforceProtectsForDepot(ctx context.Context, in *PerforceProtectsForDepotRequest, opts ...grpc.CallOption) (*PerforceProtectsForDepotResponse, error)
PerforceGroupMembers(ctx context.Context, in *PerforceGroupMembersRequest, opts ...grpc.CallOption) (*PerforceGroupMembersResponse, error)
IsPerforceSuperUser(ctx context.Context, in *IsPerforceSuperUserRequest, opts ...grpc.CallOption) (*IsPerforceSuperUserResponse, error)
PerforceGetChangelist(ctx context.Context, in *PerforceGetChangelistRequest, opts ...grpc.CallOption) (*PerforceGetChangelistResponse, error)
}
type gitserverServiceClient struct {
@ -242,6 +255,7 @@ func (x *gitserverServiceArchiveClient) Recv() (*ArchiveResponse, error) {
return m, nil
}
// Deprecated: Do not use.
func (c *gitserverServiceClient) P4Exec(ctx context.Context, in *P4ExecRequest, opts ...grpc.CallOption) (GitserverService_P4ExecClient, error) {
stream, err := c.cc.NewStream(ctx, &GitserverService_ServiceDesc.Streams[4], GitserverService_P4Exec_FullMethodName, opts...)
if err != nil {
@ -328,6 +342,60 @@ func (c *gitserverServiceClient) CheckPerforceCredentials(ctx context.Context, i
return out, nil
}
func (c *gitserverServiceClient) PerforceUsers(ctx context.Context, in *PerforceUsersRequest, opts ...grpc.CallOption) (*PerforceUsersResponse, error) {
out := new(PerforceUsersResponse)
err := c.cc.Invoke(ctx, GitserverService_PerforceUsers_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *gitserverServiceClient) PerforceProtectsForUser(ctx context.Context, in *PerforceProtectsForUserRequest, opts ...grpc.CallOption) (*PerforceProtectsForUserResponse, error) {
out := new(PerforceProtectsForUserResponse)
err := c.cc.Invoke(ctx, GitserverService_PerforceProtectsForUser_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *gitserverServiceClient) PerforceProtectsForDepot(ctx context.Context, in *PerforceProtectsForDepotRequest, opts ...grpc.CallOption) (*PerforceProtectsForDepotResponse, error) {
out := new(PerforceProtectsForDepotResponse)
err := c.cc.Invoke(ctx, GitserverService_PerforceProtectsForDepot_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *gitserverServiceClient) PerforceGroupMembers(ctx context.Context, in *PerforceGroupMembersRequest, opts ...grpc.CallOption) (*PerforceGroupMembersResponse, error) {
out := new(PerforceGroupMembersResponse)
err := c.cc.Invoke(ctx, GitserverService_PerforceGroupMembers_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *gitserverServiceClient) IsPerforceSuperUser(ctx context.Context, in *IsPerforceSuperUserRequest, opts ...grpc.CallOption) (*IsPerforceSuperUserResponse, error) {
out := new(IsPerforceSuperUserResponse)
err := c.cc.Invoke(ctx, GitserverService_IsPerforceSuperUser_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *gitserverServiceClient) PerforceGetChangelist(ctx context.Context, in *PerforceGetChangelistRequest, opts ...grpc.CallOption) (*PerforceGetChangelistResponse, error) {
out := new(PerforceGetChangelistResponse)
err := c.cc.Invoke(ctx, GitserverService_PerforceGetChangelist_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// GitserverServiceServer is the server API for GitserverService service.
// All implementations must embed UnimplementedGitserverServiceServer
// for forward compatibility
@ -341,6 +409,7 @@ type GitserverServiceServer interface {
ListGitolite(context.Context, *ListGitoliteRequest) (*ListGitoliteResponse, error)
Search(*SearchRequest, GitserverService_SearchServer) error
Archive(*ArchiveRequest, GitserverService_ArchiveServer) error
// Deprecated: Do not use.
P4Exec(*P4ExecRequest, GitserverService_P4ExecServer) error
RepoClone(context.Context, *RepoCloneRequest) (*RepoCloneResponse, error)
RepoCloneProgress(context.Context, *RepoCloneProgressRequest) (*RepoCloneProgressResponse, error)
@ -348,6 +417,12 @@ type GitserverServiceServer interface {
RepoUpdate(context.Context, *RepoUpdateRequest) (*RepoUpdateResponse, error)
IsPerforcePathCloneable(context.Context, *IsPerforcePathCloneableRequest) (*IsPerforcePathCloneableResponse, error)
CheckPerforceCredentials(context.Context, *CheckPerforceCredentialsRequest) (*CheckPerforceCredentialsResponse, error)
PerforceUsers(context.Context, *PerforceUsersRequest) (*PerforceUsersResponse, error)
PerforceProtectsForUser(context.Context, *PerforceProtectsForUserRequest) (*PerforceProtectsForUserResponse, error)
PerforceProtectsForDepot(context.Context, *PerforceProtectsForDepotRequest) (*PerforceProtectsForDepotResponse, error)
PerforceGroupMembers(context.Context, *PerforceGroupMembersRequest) (*PerforceGroupMembersResponse, error)
IsPerforceSuperUser(context.Context, *IsPerforceSuperUserRequest) (*IsPerforceSuperUserResponse, error)
PerforceGetChangelist(context.Context, *PerforceGetChangelistRequest) (*PerforceGetChangelistResponse, error)
mustEmbedUnimplementedGitserverServiceServer()
}
@ -403,6 +478,24 @@ func (UnimplementedGitserverServiceServer) IsPerforcePathCloneable(context.Conte
func (UnimplementedGitserverServiceServer) CheckPerforceCredentials(context.Context, *CheckPerforceCredentialsRequest) (*CheckPerforceCredentialsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CheckPerforceCredentials not implemented")
}
func (UnimplementedGitserverServiceServer) PerforceUsers(context.Context, *PerforceUsersRequest) (*PerforceUsersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method PerforceUsers not implemented")
}
func (UnimplementedGitserverServiceServer) PerforceProtectsForUser(context.Context, *PerforceProtectsForUserRequest) (*PerforceProtectsForUserResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method PerforceProtectsForUser not implemented")
}
func (UnimplementedGitserverServiceServer) PerforceProtectsForDepot(context.Context, *PerforceProtectsForDepotRequest) (*PerforceProtectsForDepotResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method PerforceProtectsForDepot not implemented")
}
func (UnimplementedGitserverServiceServer) PerforceGroupMembers(context.Context, *PerforceGroupMembersRequest) (*PerforceGroupMembersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method PerforceGroupMembers not implemented")
}
func (UnimplementedGitserverServiceServer) IsPerforceSuperUser(context.Context, *IsPerforceSuperUserRequest) (*IsPerforceSuperUserResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method IsPerforceSuperUser not implemented")
}
func (UnimplementedGitserverServiceServer) PerforceGetChangelist(context.Context, *PerforceGetChangelistRequest) (*PerforceGetChangelistResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method PerforceGetChangelist not implemented")
}
func (UnimplementedGitserverServiceServer) mustEmbedUnimplementedGitserverServiceServer() {}
// UnsafeGitserverServiceServer may be embedded to opt out of forward compatibility for this service.
@ -724,6 +817,114 @@ func _GitserverService_CheckPerforceCredentials_Handler(srv interface{}, ctx con
return interceptor(ctx, in, info, handler)
}
func _GitserverService_PerforceUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PerforceUsersRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GitserverServiceServer).PerforceUsers(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: GitserverService_PerforceUsers_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GitserverServiceServer).PerforceUsers(ctx, req.(*PerforceUsersRequest))
}
return interceptor(ctx, in, info, handler)
}
func _GitserverService_PerforceProtectsForUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PerforceProtectsForUserRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GitserverServiceServer).PerforceProtectsForUser(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: GitserverService_PerforceProtectsForUser_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GitserverServiceServer).PerforceProtectsForUser(ctx, req.(*PerforceProtectsForUserRequest))
}
return interceptor(ctx, in, info, handler)
}
func _GitserverService_PerforceProtectsForDepot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PerforceProtectsForDepotRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GitserverServiceServer).PerforceProtectsForDepot(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: GitserverService_PerforceProtectsForDepot_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GitserverServiceServer).PerforceProtectsForDepot(ctx, req.(*PerforceProtectsForDepotRequest))
}
return interceptor(ctx, in, info, handler)
}
func _GitserverService_PerforceGroupMembers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PerforceGroupMembersRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GitserverServiceServer).PerforceGroupMembers(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: GitserverService_PerforceGroupMembers_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GitserverServiceServer).PerforceGroupMembers(ctx, req.(*PerforceGroupMembersRequest))
}
return interceptor(ctx, in, info, handler)
}
func _GitserverService_IsPerforceSuperUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(IsPerforceSuperUserRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GitserverServiceServer).IsPerforceSuperUser(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: GitserverService_IsPerforceSuperUser_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GitserverServiceServer).IsPerforceSuperUser(ctx, req.(*IsPerforceSuperUserRequest))
}
return interceptor(ctx, in, info, handler)
}
func _GitserverService_PerforceGetChangelist_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PerforceGetChangelistRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GitserverServiceServer).PerforceGetChangelist(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: GitserverService_PerforceGetChangelist_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GitserverServiceServer).PerforceGetChangelist(ctx, req.(*PerforceGetChangelistRequest))
}
return interceptor(ctx, in, info, handler)
}
// GitserverService_ServiceDesc is the grpc.ServiceDesc for GitserverService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@ -775,6 +976,30 @@ var GitserverService_ServiceDesc = grpc.ServiceDesc{
MethodName: "CheckPerforceCredentials",
Handler: _GitserverService_CheckPerforceCredentials_Handler,
},
{
MethodName: "PerforceUsers",
Handler: _GitserverService_PerforceUsers_Handler,
},
{
MethodName: "PerforceProtectsForUser",
Handler: _GitserverService_PerforceProtectsForUser_Handler,
},
{
MethodName: "PerforceProtectsForDepot",
Handler: _GitserverService_PerforceProtectsForDepot_Handler,
},
{
MethodName: "PerforceGroupMembers",
Handler: _GitserverService_PerforceGroupMembers_Handler,
},
{
MethodName: "IsPerforceSuperUser",
Handler: _GitserverService_IsPerforceSuperUser_Handler,
},
{
MethodName: "PerforceGetChangelist",
Handler: _GitserverService_PerforceGetChangelist_Handler,
},
},
Streams: []grpc.StreamDesc{
{

View File

@ -1,16 +1,20 @@
load("//dev:go_defs.bzl", "go_test")
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("//dev:go_defs.bzl", "go_test")
go_library(
name = "perforce",
srcs = ["changelist.go"],
srcs = [
"changelist.go",
"types.go",
],
importpath = "github.com/sourcegraph/sourcegraph/internal/perforce",
visibility = ["//:__subpackages__"],
deps = [
"//internal/api",
"//internal/gitserver/protocol",
"//internal/gitserver/v1:gitserver",
"//internal/lazyregexp",
"//lib/errors",
"@org_golang_google_protobuf//types/known/timestamppb",
],
)
@ -18,9 +22,5 @@ go_test(
name = "perforce_test",
srcs = ["changelist_test.go"],
embed = [":perforce"],
deps = [
"//internal/gitserver/protocol",
"@com_github_google_go_cmp//cmp",
"@com_github_stretchr_testify//require",
],
deps = ["@com_github_stretchr_testify//require"],
)

View File

@ -3,17 +3,73 @@ package perforce
import (
"fmt"
"encoding/json"
"strings"
"time"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
v1 "github.com/sourcegraph/sourcegraph/internal/gitserver/v1"
"github.com/sourcegraph/sourcegraph/internal/lazyregexp"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
type Changelist struct {
ID string
CreationDate time.Time
State ChangelistState
Author string
Title string
Message string
}
func ChangelistFromProto(proto *v1.PerforceChangelist) *Changelist {
return &Changelist{
ID: proto.GetId(),
CreationDate: proto.GetCreationDate().AsTime(),
State: "help",
Author: proto.GetAuthor(),
Title: proto.GetTitle(),
Message: proto.GetMessage(),
}
}
func (c *Changelist) ToProto() *v1.PerforceChangelist {
return &v1.PerforceChangelist{
Id: c.ID,
CreationDate: timestamppb.New(c.CreationDate),
State: c.State.ToProto(),
Author: c.Author,
Title: c.Title,
Message: c.Message,
}
}
type ChangelistState string
func (s ChangelistState) ToProto() v1.PerforceChangelist_PerforceChangelistState {
switch s {
case ChangelistStateSubmitted:
return v1.PerforceChangelist_PERFORCE_CHANGELIST_STATE_SUBMITTED
case ChangelistStatePending:
return v1.PerforceChangelist_PERFORCE_CHANGELIST_STATE_PENDING
case ChangelistStateShelved:
return v1.PerforceChangelist_PERFORCE_CHANGELIST_STATE_SHELVED
case ChangelistStateClosed:
return v1.PerforceChangelist_PERFORCE_CHANGELIST_STATE_CLOSED
default:
return v1.PerforceChangelist_PERFORCE_CHANGELIST_STATE_UNSPECIFIED
}
}
const (
ChangelistStateSubmitted ChangelistState = "submitted"
ChangelistStatePending ChangelistState = "pending"
ChangelistStateShelved ChangelistState = "shelved"
// Perforce doesn't actually return a state for closed changelists, so this is one we use to indicate the changelist is closed.
ChangelistStateClosed ChangelistState = "closed"
)
// Either git-p4 or p4-fusion could have been used to convert a perforce depot to a git repo. In
// which case the which case the commit message would look like:
//
@ -45,89 +101,3 @@ func (e *ChangelistNotFoundError) NotFound() bool { return true }
func (e *ChangelistNotFoundError) Error() string {
return fmt.Sprintf("changelist ID not found. repo=%d, changelist id=%d", e.RepoID, e.ID)
}
type BadChangelistError struct {
CID string
Repo api.RepoName
}
func (e *BadChangelistError) Error() string {
return fmt.Sprintf("invalid changelist ID %q for repo %q", e.Repo, e.CID)
}
// Example changelist info output in "long" format
// (from `p4 changes -l ...`)
// Change 1188 on 2023/06/09 by admin@yet-moar-lines *pending*
//
// Append still another line to all SECOND.md files
//
// "admin@yet-moar-lines" is the username @ the client spec name, which in this case is the branch name from the batch change
// the final field - "*pending*" in this example - is optional and not present when the changelist has been submitted ("merged", in Git parlance)
// Example changelist info in json format
// (from `p4 -ztags -Mj changes -l ...`)
// {"data":"Change 1178 on 2023/06/01 by admin@hello-third-world *pending*\n\n\tAppend Hello World to all THIRD.md files\n","level":0}
var changelistInfoPattern = lazyregexp.New(`^Change (\d+) on (\d{4}/\d{2}/\d{2}) by ([^ ]+)@([^ ]+)(?: [*](pending|submitted|shelved)[*])?(?: '(.+)')?$`)
type changelistJson struct {
Data string `json:"data"`
Level int `json:"level"`
}
// Parses the output of `p4 changes`
// Handles one changelist only
// Accepts any format: standard, long, json standard, json long
func ParseChangelistOutput(output string) (*protocol.PerforceChangelist, error) {
// output will be whitespace-trimmed and not empty
// if the given text is json format, extract the Data portion
// so that it will have the same format as the standard output
cidj := new(changelistJson)
err := json.Unmarshal([]byte(output), cidj)
if err == nil {
output = strings.TrimSpace(cidj.Data)
}
lines := strings.Split(output, "\n")
// the first line contains the changelist information
matches := changelistInfoPattern.FindStringSubmatch(lines[0])
if matches == nil || len(matches) < 5 {
return nil, errors.New("invalid changelist output")
}
pcl := new(protocol.PerforceChangelist)
pcl.ID = matches[1]
time, err := time.Parse("2006/01/02", matches[2])
if err != nil {
return nil, errors.Wrap(err, "invalid date: "+matches[2])
}
pcl.CreationDate = time
pcl.Author = matches[3]
pcl.Title = matches[4]
status := "submitted"
if len(matches) > 5 && matches[5] != "" {
status = matches[5]
}
cls, err := protocol.ParsePerforceChangelistState(status)
if err != nil {
return nil, err
}
pcl.State = cls
if len(matches) > 6 && matches[6] != "" {
// the commit message is inline with the info
pcl.Message = strings.TrimSpace(matches[6])
} else {
// the commit message is in subsequent lines of the output
var builder strings.Builder
for i := 2; i < len(lines); i++ {
if i > 2 {
builder.WriteString("\n")
}
builder.WriteString(strings.TrimSpace(lines[i]))
}
pcl.Message = builder.String()
}
return pcl, nil
}

View File

@ -3,12 +3,8 @@ package perforce
import (
"reflect"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
)
func TestGetP4ChangelistID(t *testing.T) {
@ -55,131 +51,3 @@ func TestGetP4ChangelistID(t *testing.T) {
}
}
}
func TestParseChangelistOutput(t *testing.T) {
testCases := []struct {
output string
expectedChangelist *protocol.PerforceChangelist
shouldError bool
}{
{
output: `Change 1188 on 2023/06/09 by admin@test-first-one *pending*
Append still another line to all SECOND.md files
Here's a second line of message`,
expectedChangelist: &protocol.PerforceChangelist{
ID: "1188",
CreationDate: time.Date(2023, 6, 9, 0, 0, 0, 0, time.UTC),
Author: "admin",
Title: "test-first-one",
State: protocol.PerforceChangelistStatePending,
Message: "Append still another line to all SECOND.md files\nHere's a second line of message",
},
},
{
output: `Change 1234567 on 2023/04/09 by someone@dsobsdfoibsdv
Append still another line to all SECOND.md files
Here's a second line of message`,
expectedChangelist: &protocol.PerforceChangelist{
ID: "1234567",
CreationDate: time.Date(2023, 4, 9, 0, 0, 0, 0, time.UTC),
Author: "someone",
Title: "dsobsdfoibsdv",
State: protocol.PerforceChangelistStateSubmitted,
Message: "Append still another line to all SECOND.md files\nHere's a second line of message",
},
},
{
output: `{"data":"Change 1188 on 2023/06/09 by admin@json-with-status *pending*\n\n\tAppend still another line to all SECOND.md files\n\tand another line here\n","level":0}`,
expectedChangelist: &protocol.PerforceChangelist{
ID: "1188",
CreationDate: time.Date(2023, 6, 9, 0, 0, 0, 0, time.UTC),
Author: "admin",
Title: "json-with-status",
State: protocol.PerforceChangelistStatePending,
Message: "Append still another line to all SECOND.md files\nand another line here",
},
},
{
output: `{"data":"Change 1188 on 2023/06/09 by admin@json-no-status\n\n\tAppend still another line to all SECOND.md files\n\tand another line here\n","level":0}`,
expectedChangelist: &protocol.PerforceChangelist{
ID: "1188",
CreationDate: time.Date(2023, 6, 9, 0, 0, 0, 0, time.UTC),
Author: "admin",
Title: "json-no-status",
State: protocol.PerforceChangelistStateSubmitted,
Message: "Append still another line to all SECOND.md files\nand another line here",
},
},
{
output: `{"data":"Change 27 on 2023/05/03 by admin@buttercup\n\n\t generated change at 2023-05-02 17:44:59.012487 -0700 PDT m=+7.371337167\n","level":0}`,
expectedChangelist: &protocol.PerforceChangelist{
ID: "27",
CreationDate: time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC),
Author: "admin",
Title: "buttercup",
State: protocol.PerforceChangelistStateSubmitted,
Message: `generated change at 2023-05-02 17:44:59.012487 -0700 PDT m=+7.371337167`,
},
},
{
output: `Change 27 on 2023/05/03 by admin@buttercup`,
expectedChangelist: &protocol.PerforceChangelist{
ID: "27",
CreationDate: time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC),
Author: "admin",
Title: "buttercup",
State: protocol.PerforceChangelistStateSubmitted,
Message: "",
},
},
{
output: `{"data":"Change 55 on 2023/05/03 by admin@test5 ' generated change at 2023-05-'","level":0}`,
expectedChangelist: &protocol.PerforceChangelist{
ID: "55",
CreationDate: time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC),
Author: "admin",
Title: "test5",
State: protocol.PerforceChangelistStateSubmitted,
Message: `generated change at 2023-05-`,
},
},
{
output: `{"data":"Change 55 on 2023/56/42 by admin@buttercup ' generated change at 2023-05-'","level":0}`,
shouldError: true,
},
{
output: `Change 55 by admin@buttercup 'generated change at 2023-05-'`,
shouldError: true,
},
{
output: `{"data":"Change 1185 on 2023/06/09 by admin@yet-moar-lines *INVALID* 'Append still another line to al'","level":0}`,
shouldError: true,
},
{
output: `Change INVALID on 2023/06/09 by admin@yet-moar-lines *pending*
Append still another line to all SECOND.md files
`,
shouldError: true,
},
}
for _, testCase := range testCases {
changelist, err := ParseChangelistOutput(testCase.output)
if testCase.shouldError {
if err == nil {
t.Errorf("expected error but got nil")
}
continue
}
if err != nil {
t.Errorf("unexpected error: %s", err)
}
if diff := cmp.Diff(testCase.expectedChangelist, changelist); diff != "" {
t.Errorf("parsed changelist did not match expected (-want +got):\n%s", diff)
}
}
}

View File

@ -0,0 +1,55 @@
package perforce
import (
v1 "github.com/sourcegraph/sourcegraph/internal/gitserver/v1"
)
type User struct {
Username string
Email string
}
func UserFromProto(proto *v1.PerforceUser) *User {
return &User{
Username: proto.GetUsername(),
Email: proto.GetEmail(),
}
}
func (u *User) ToProto() *v1.PerforceUser {
return &v1.PerforceUser{
Username: u.Username,
Email: u.Email,
}
}
type Protect struct {
Level string
EntityType string
EntityName string
Match string
IsExclusion bool
Host string
}
func ProtectFromProto(proto *v1.PerforceProtect) *Protect {
return &Protect{
Level: proto.GetLevel(),
EntityType: proto.GetEntityType(),
EntityName: proto.GetEntityName(),
Match: proto.GetMatch(),
IsExclusion: proto.GetIsExclusion(),
Host: proto.GetHost(),
}
}
func (p *Protect) ToProto() *v1.PerforceProtect {
return &v1.PerforceProtect{
Level: p.Level,
EntityType: p.EntityType,
EntityName: p.EntityName,
Match: p.Match,
IsExclusion: p.IsExclusion,
Host: p.Host,
}
}

View File

@ -77,6 +77,7 @@ go_library(
"//internal/extsvc/pypi",
"//internal/extsvc/rubygems",
"//internal/gitserver",
"//internal/gitserver/protocol",
"//internal/goroutine",
"//internal/httpcli",
"//internal/httptestutil",
@ -175,6 +176,7 @@ go_test(
"//internal/extsvc/phabricator",
"//internal/github_apps/types",
"//internal/gitserver",
"//internal/gitserver/protocol",
"//internal/goroutine",
"//internal/httpcli",
"//internal/httptestutil",

View File

@ -10,6 +10,7 @@ import (
"github.com/sourcegraph/sourcegraph/internal/extsvc"
"github.com/sourcegraph/sourcegraph/internal/extsvc/perforce"
"github.com/sourcegraph/sourcegraph/internal/gitserver"
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
"github.com/sourcegraph/sourcegraph/internal/jsonc"
"github.com/sourcegraph/sourcegraph/internal/types"
"github.com/sourcegraph/sourcegraph/lib/errors"
@ -51,7 +52,12 @@ func newPerforceSource(gitserverClient gitserver.Client, svc *types.ExternalServ
// from the code host configuration.
func (s PerforceSource) CheckConnection(ctx context.Context) error {
gclient := gitserver.NewClient()
err := gclient.CheckPerforceCredentials(ctx, s.config.P4Port, s.config.P4User, s.config.P4Passwd)
conn := protocol.PerforceConnectionDetails{
P4Port: s.config.P4Port,
P4User: s.config.P4User,
P4Passwd: s.config.P4Passwd,
}
err := gclient.CheckPerforceCredentials(ctx, conn)
if err != nil {
return errors.Wrap(err, "Unable to connect to the Perforce server")
}
@ -69,7 +75,12 @@ func (s PerforceSource) ListRepos(ctx context.Context, results chan SourceResult
return
}
err := s.gitserverClient.IsPerforcePathCloneable(ctx, s.config.P4Port, s.config.P4User, s.config.P4Passwd, depot)
conn := protocol.PerforceConnectionDetails{
P4Port: s.config.P4Port,
P4User: s.config.P4User,
P4Passwd: s.config.P4Passwd,
}
err := s.gitserverClient.IsPerforcePathCloneable(ctx, conn, depot)
if err != nil {
results <- SourceResult{Source: s, Err: errors.Wrap(err, "checking if perforce path is cloneable")}
continue

View File

@ -10,6 +10,7 @@ import (
"github.com/sourcegraph/sourcegraph/internal/extsvc"
"github.com/sourcegraph/sourcegraph/internal/gitserver"
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
"github.com/sourcegraph/sourcegraph/internal/testutil"
"github.com/sourcegraph/sourcegraph/internal/types"
"github.com/sourcegraph/sourcegraph/internal/types/typestest"
@ -83,7 +84,7 @@ func TestPerforceSource_ListRepos(t *testing.T) {
}
gc := gitserver.NewMockClient()
gc.IsPerforcePathCloneableFunc.SetDefaultHook(func(ctx context.Context, _, _, _, depotPath string) error {
gc.IsPerforcePathCloneableFunc.SetDefaultHook(func(ctx context.Context, _ protocol.PerforceConnectionDetails, depotPath string) error {
if depotPath == "//Sourcegraph" || depotPath == "//Engineering/Cloud" {
return nil
}

View File

@ -22,6 +22,7 @@ import (
"github.com/sourcegraph/sourcegraph/internal/database/dbtest"
"github.com/sourcegraph/sourcegraph/internal/extsvc"
"github.com/sourcegraph/sourcegraph/internal/gitserver"
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
"github.com/sourcegraph/sourcegraph/internal/observation"
"github.com/sourcegraph/sourcegraph/internal/timeutil"
"github.com/sourcegraph/sourcegraph/internal/types"
@ -235,7 +236,7 @@ func TestStatusMessages(t *testing.T) {
},
})
mockGitserverClient.SystemsInfoFunc.SetDefaultReturn([]gitserver.SystemInfo{
mockGitserverClient.SystemsInfoFunc.SetDefaultReturn([]protocol.SystemInfo{
{
Address: "gitserver-0",
PercentUsed: 75.10345,
@ -257,7 +258,7 @@ func TestStatusMessages(t *testing.T) {
},
{
testSetup: func() {
mockGitserverClient.SystemsInfoFunc.SetDefaultReturn([]gitserver.SystemInfo{
mockGitserverClient.SystemsInfoFunc.SetDefaultReturn([]protocol.SystemInfo{
{
Address: "gitserver-0",
PercentUsed: 95.10345,

View File

@ -130,3 +130,7 @@
path: github.com/sourcegraph/sourcegraph/internal/embeddings/background/repo
interfaces:
- RepoEmbeddingJobsStore
- filename: internal/gitserver/mock.go
path: github.com/sourcegraph/sourcegraph/internal/gitserver/v1
interfaces:
- GitserverServiceClient

View File

@ -10,7 +10,8 @@
"docker-images/*": "ignore all code under docker-images",
"main.go": "ignore main",
".*_test\\.go$": "ignore test code",
"internal/batches/testing/*": "ignore batches testing code"
"internal/batches/testing/*": "ignore batches testing code",
"internal/gitserver/mock.go": "ignore deprecation warning of P4Exec"
}
},
"bodyclose": {