diff --git a/resources/exec.go b/resources/exec.go index 9915862e..ce12271e 100644 --- a/resources/exec.go +++ b/resources/exec.go @@ -23,6 +23,7 @@ import ( "fmt" "log" "os/exec" + "os/user" "strings" "sync" "syscall" @@ -46,6 +47,8 @@ type ExecRes struct { WatchShell string `yaml:"watchshell"` // the (optional) shell to use to run the watch cmd IfCmd string `yaml:"ifcmd"` // the if command to run IfShell string `yaml:"ifshell"` // the (optional) shell to use to run the if cmd + User string `yaml:"user"` // the (optional) user to use to execute the command + Group string `yaml:"group"` // the (optional) group to use to execute the command Output *string // all cmd output, read only, do not set! Stdout *string // the cmd stdout, read only, do not set! Stderr *string // the cmd stderr, read only, do not set! @@ -66,6 +69,17 @@ func (obj *ExecRes) Validate() error { return fmt.Errorf("command can't be empty") } + // check that, if an user or a group is set, we're running as root + if obj.User != "" || obj.Group != "" { + currentUser, err := user.Current() + if err != nil { + return errwrap.Wrapf(err, "error looking up current user") + } + if currentUser.Uid != "0" { + return errwrap.Errorf("running as root is required if you want to use exec with a different user/group") + } + } + return obj.BaseRes.Validate() } @@ -121,6 +135,12 @@ func (obj *ExecRes) Watch() error { Pgid: 0, } + // if we have a user and group, use them + var err error + if cmd.SysProcAttr.Credential, err = obj.getCredential(); err != nil { + return errwrap.Wrapf(err, "error while setting credential") + } + cmdReader, err := cmd.StdoutPipe() if err != nil { return errwrap.Wrapf(err, "error creating StdoutPipe for Cmd") @@ -208,6 +228,13 @@ func (obj *ExecRes) CheckApply(apply bool) (bool, error) { Setpgid: true, Pgid: 0, } + + // if we have an user and group, use them + var err error + if cmd.SysProcAttr.Credential, err = obj.getCredential(); err != nil { + return false, errwrap.Wrapf(err, "error while setting credential") + } + if err := cmd.Run(); err != nil { // TODO: check exit value return true, nil // don't run @@ -245,6 +272,12 @@ func (obj *ExecRes) CheckApply(apply bool) (bool, error) { Pgid: 0, } + // if we have a user and group, use them + var err error + if cmd.SysProcAttr.Credential, err = obj.getCredential(); err != nil { + return false, errwrap.Wrapf(err, "error while setting credential") + } + var out splitWriter out.Init() // from the docs: "If Stdout and Stderr are the same writer, at most one @@ -263,7 +296,6 @@ func (obj *ExecRes) CheckApply(apply bool) (bool, error) { done := make(chan error) go func() { done <- cmd.Wait() }() - var err error // error returned by cmd select { case e := <-done: err = e // store @@ -422,6 +454,12 @@ func (obj *ExecRes) Compare(r Res) bool { if obj.IfShell != res.IfShell { return false } + if obj.User != res.User { + return false + } + if obj.Group != res.Group { + return false + } return true } @@ -446,6 +484,37 @@ func (obj *ExecRes) UnmarshalYAML(unmarshal func(interface{}) error) error { return nil } +// getCredential returns the correct *syscall.Credential if an User and Group +// are set. +func (obj *ExecRes) getCredential() (*syscall.Credential, error) { + var uid, gid int + var err error + var currentUser *user.User + if currentUser, err = user.Current(); err != nil { + return nil, errwrap.Wrapf(err, "error looking up current user") + } + if currentUser.Uid != "0" { + // since we're not root, we've got nothing to do + return nil, nil + } + + if obj.Group != "" { + gid, err = GetGID(obj.Group) + if err != nil { + return nil, errwrap.Wrapf(err, "error looking up gid for %s", obj.Group) + } + } + + if obj.User != "" { + uid, err = GetUID(obj.User) + if err != nil { + return nil, errwrap.Wrapf(err, "error looking up uid for %s", obj.User) + } + } + + return &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}, nil +} + // splitWriter mimics what the ssh.CombinedOutput command does, but stores the // the stdout and stderr separately. This is slightly tricky because we don't // want the combined output to be interleaved incorrectly. It creates sub writer diff --git a/test/shell/exec-usergroup.sh b/test/shell/exec-usergroup.sh new file mode 100755 index 00000000..578c81cd --- /dev/null +++ b/test/shell/exec-usergroup.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +set -x +set -o pipefail + +if ! timeout 1s sudo -A true; then + echo "sudo disabled: not checking exec user and group" + exit +fi + +BASE_PATH="/tmp/mgmt/" +BASE_PATH_TEST="${BASE_PATH}test-exec-usergroup/" +# on Fedora, it's nobody while on ubuntu it's nogroup +GROUP="nogroup" +if grep -q nobody /etc/group; then + GROUP="nobody" +fi + +function setup { + mkdir -p "${BASE_PATH_TEST}" + sudo -A chown nobody:${GROUP} "${BASE_PATH_TEST}" + sudo -A chmod ug=rwx,o=rx "${BASE_PATH_TEST}" +} + +function cleanup { + sudo -A rm -rf "${BASE_PATH_TEST}" +} + +# run_test will run each test. It takes 3 parameters: +# - $1: graph (e.g. exec-usergroup-nobody.yaml) +# - $2: user to be tested (e.g. nobody or "") +# - $3: group to be tested (e.g. nobody or "") +function run_usergroup_test() { + graph=$1 + user=$2 + group=$3 + + setup + + # run till completion + sudo -A timeout --kill-after=30s 25s ./mgmt run --yaml ./exec-usergroup/${graph} --converged-timeout=5 --no-watch --tmp-prefix & + pid=$! + wait $pid # get exit status + e=$? + + # tests + test -e "${BASE_PATH_TEST}/result-exec-usergroup" + if [ $? != 0 ]; then + echo "${BASE_PATH_TEST}result-exec-usergroup has not been created" + exit 1 + fi + if [ "${user}" != "" ]; then + test $(stat -c%U "${BASE_PATH_TEST}/result-exec-usergroup") = $user + if [ $? != 0 ]; then + echo "${BASE_PATH_TEST}result-exec-usergroup owner is not ${user}" + exit 1 + fi + fi + if [ "${group}" != "" ]; then + test $(stat -c%G "${BASE_PATH_TEST}/result-exec-usergroup") = $group + if [ $? != 0 ]; then + echo "${BASE_PATH_TEST}result-exec-usergroup group is not ${group}" + exit 1 + fi + fi + + cleanup +} + +# ensure the workspace is clean +cleanup + +# run_usergroup_test +run_usergroup_test "exec-usergroup-${GROUP}.yaml" "nobody" "${GROUP}" +run_usergroup_test "exec-usergroup-user.yaml" "nobody" "" +run_usergroup_test "exec-usergroup-group-${GROUP}.yaml" "" "${GROUP}" diff --git a/test/shell/exec-usergroup/exec-usergroup-group-nobody.yaml b/test/shell/exec-usergroup/exec-usergroup-group-nobody.yaml new file mode 100644 index 00000000..53a2f723 --- /dev/null +++ b/test/shell/exec-usergroup/exec-usergroup-group-nobody.yaml @@ -0,0 +1,29 @@ +--- +graph: mygraph +resources: + exec: + - name: test.sh + cmd: /tmp/mgmt/test-exec-usergroup/test-exec-usergroup.sh + shell: /bin/bash + group: nobody + meta: + autoedge: true + file: + - name: file1 + meta: + autoedge: true + path: "/tmp/mgmt/test-exec-usergroup/test-exec-usergroup.sh" + content: | + # this is an mgmt test + id + echo "this is a test" > /tmp/mgmt/test-exec-usergroup/result-exec-usergroup + state: exists + mode: "0777" +edges: +- name: e1 + from: + kind: file + name: file1 + to: + kind: exec + name: test.sh diff --git a/test/shell/exec-usergroup/exec-usergroup-group-nogroup.yaml b/test/shell/exec-usergroup/exec-usergroup-group-nogroup.yaml new file mode 100644 index 00000000..de2140f2 --- /dev/null +++ b/test/shell/exec-usergroup/exec-usergroup-group-nogroup.yaml @@ -0,0 +1,29 @@ +--- +graph: mygraph +resources: + exec: + - name: test.sh + cmd: /tmp/mgmt/test-exec-usergroup/test-exec-usergroup.sh + shell: /bin/bash + group: nogroup + meta: + autoedge: true + file: + - name: file1 + meta: + autoedge: true + path: "/tmp/mgmt/test-exec-usergroup/test-exec-usergroup.sh" + content: | + # this is an mgmt test + id + echo "this is a test" > /tmp/mgmt/test-exec-usergroup/result-exec-usergroup + state: exists + mode: "0777" +edges: +- name: e1 + from: + kind: file + name: file1 + to: + kind: exec + name: test.sh diff --git a/test/shell/exec-usergroup/exec-usergroup-nobody.yaml b/test/shell/exec-usergroup/exec-usergroup-nobody.yaml new file mode 100644 index 00000000..0d156ac2 --- /dev/null +++ b/test/shell/exec-usergroup/exec-usergroup-nobody.yaml @@ -0,0 +1,30 @@ +--- +graph: mygraph +resources: + exec: + - name: test.sh + cmd: /tmp/mgmt/test-exec-usergroup/test-exec-usergroup.sh + shell: /bin/bash + user: nobody + group: nobody + meta: + autoedge: true + file: + - name: file1 + meta: + autoedge: true + path: "/tmp/mgmt/test-exec-usergroup/test-exec-usergroup.sh" + content: | + # this is an mgmt test + id + echo "this is a test" > /tmp/mgmt/test-exec-usergroup/result-exec-usergroup + state: exists + mode: "0777" +edges: +- name: e1 + from: + kind: file + name: file1 + to: + kind: exec + name: test.sh diff --git a/test/shell/exec-usergroup/exec-usergroup-nogroup.yaml b/test/shell/exec-usergroup/exec-usergroup-nogroup.yaml new file mode 100644 index 00000000..74de7e90 --- /dev/null +++ b/test/shell/exec-usergroup/exec-usergroup-nogroup.yaml @@ -0,0 +1,30 @@ +--- +graph: mygraph +resources: + exec: + - name: test.sh + cmd: /tmp/mgmt/test-exec-usergroup/test-exec-usergroup.sh + shell: /bin/bash + user: nobody + group: nogroup + meta: + autoedge: true + file: + - name: file1 + meta: + autoedge: true + path: "/tmp/mgmt/test-exec-usergroup/test-exec-usergroup.sh" + content: | + # this is an mgmt test + id + echo "this is a test" > /tmp/mgmt/test-exec-usergroup/result-exec-usergroup + state: exists + mode: "0777" +edges: +- name: e1 + from: + kind: file + name: file1 + to: + kind: exec + name: test.sh diff --git a/test/shell/exec-usergroup/exec-usergroup-user.yaml b/test/shell/exec-usergroup/exec-usergroup-user.yaml new file mode 100644 index 00000000..fc674ade --- /dev/null +++ b/test/shell/exec-usergroup/exec-usergroup-user.yaml @@ -0,0 +1,29 @@ +--- +graph: mygraph +resources: + exec: + - name: test.sh + cmd: /tmp/mgmt/test-exec-usergroup/test-exec-usergroup.sh + shell: /bin/bash + user: nobody + meta: + autoedge: true + file: + - name: file1 + meta: + autoedge: true + path: "/tmp/mgmt/test-exec-usergroup/test-exec-usergroup.sh" + content: | + # this is an mgmt test + id + echo "this is a test" > /tmp/mgmt/test-exec-usergroup/result-exec-usergroup + state: exists + mode: "0777" +edges: +- name: e1 + from: + kind: file + name: file1 + to: + kind: exec + name: test.sh