Skip to content

Commit

Permalink
Add way to specify user for exec command.
Browse files Browse the repository at this point in the history
  • Loading branch information
sfc-gh-mwalas committed Nov 14, 2024
1 parent a76925d commit 820a343
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 71 deletions.
9 changes: 7 additions & 2 deletions services/exec/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func (p *execCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfac

type runCmd struct {
streaming bool
user string

// returnCode internally keeps track of the final status to return
returnCode subcommands.ExitStatus
Expand All @@ -75,7 +76,7 @@ type runCmd struct {
func (*runCmd) Name() string { return "run" }
func (*runCmd) Synopsis() string { return "Run provided command and return a response." }
func (*runCmd) Usage() string {
return `run [--stream] <command> [<args>...]:
return `run [--stream] [--user user] <command> [<args>...]:
Run a command remotely and return the response
Note: This is not optimized for large output or long running commands. If
Expand All @@ -84,11 +85,15 @@ func (*runCmd) Usage() string {
The --stream flag can be used to stream back command output as the command
runs. It doesn't affect the timeout.
--user flag allows to specify a user for running command, equivalent of
sudo -u <user> <command> ...
`
}

func (p *runCmd) SetFlags(f *flag.FlagSet) {
f.BoolVar(&p.streaming, "stream", DefaultStreaming, "If true, stream back stdout and stdin during the command instead of sending it all at the end.")
f.StringVar(&p.user, "user", "", "If specified, allows to run a command as a specified user. Equivalent of sudo -u <user> <command> ... .")
}

func (p *runCmd) printCommandOutput(state *util.ExecuteState, idx int, resp *pb.ExecResponse, err error) {
Expand Down Expand Up @@ -121,7 +126,7 @@ func (p *runCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface
}

c := pb.NewExecClientProxy(state.Conn)
req := &pb.ExecRequest{Command: f.Args()[0], Args: f.Args()[1:]}
req := &pb.ExecRequest{Command: f.Args()[0], Args: f.Args()[1:], User: p.user}

if p.streaming {
resp, err := c.StreamingRunOneMany(ctx, req)
Expand Down
58 changes: 34 additions & 24 deletions services/exec/exec.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions services/exec/exec.proto
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ service Exec {
message ExecRequest {
string command = 1;
repeated string args = 2;
// User to execute command as, equivalent of `sudo -u <user> <command>`.
string user = 3;
}

// ExecResponse describes output of execution
Expand Down
81 changes: 37 additions & 44 deletions services/exec/exec_grpc.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 22 additions & 1 deletion services/exec/server/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import (
"context"
"io"
"os/exec"
"os/user"
"path/filepath"
"strconv"

"google.golang.org/grpc"
"google.golang.org/grpc/codes"
Expand All @@ -45,7 +47,26 @@ type server struct{}
// Run executes command and returns result
func (s *server) Run(ctx context.Context, req *pb.ExecRequest) (res *pb.ExecResponse, err error) {
recorder := metrics.RecorderFromContextOrNoop(ctx)
run, err := util.RunCommand(ctx, req.Command, req.Args)

var opts []util.Option
if req.User != "" {
u, err := user.Lookup(req.User)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "user '%s' not found:\n%v", req.User, err)
}
// This will work only on POSIX (Windows has non-decimal uids) yet these are our targets.
uid, err := strconv.Atoi(u.Uid)
if err != nil {
return nil, status.Errorf(codes.Internal, "'%s' user's uid %s failed to convert to numeric value:\n%v", req.User, u.Uid, err)
}
gid, err := strconv.Atoi(u.Gid)
if err != nil {
return nil, status.Errorf(codes.Internal, "'%s' user's gid %s failed to convert to numeric value:\n%v", req.User, u.Gid, err)
}
opts = append(opts, util.CommandUser(uint32(uid)))
opts = append(opts, util.CommandGroup(uint32(gid)))
}
run, err := util.RunCommand(ctx, req.Command, req.Args, opts...)
if err != nil {
recorder.CounterOrLog(ctx, execRunFailureCounter, 1)
return nil, err
Expand Down
9 changes: 9 additions & 0 deletions services/exec/server/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ func TestExec(t *testing.T) {
name string
bin string
args []string
user string
wantErr bool
returnCodeNonZero bool
stdout string
Expand All @@ -111,13 +112,21 @@ func TestExec(t *testing.T) {
bin: "foo",
wantErr: true,
},
{
name: "user specified -- fails as it can't setuid",
bin: testutil.ResolvePath(t, "echo"),
args: []string{"hello world"},
user: "nobody",
wantErr: true,
},
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {
// Test a normal exec.
resp, err := client.Run(ctx, &pb.ExecRequest{
Command: tc.bin,
Args: tc.args,
User: tc.user,
})
t.Logf("%s: resp: %+v", tc.name, resp)
t.Logf("%s: err: %v", tc.name, err)
Expand Down

0 comments on commit 820a343

Please sign in to comment.