From 04a537756d9b7b526759c02b5b5d68c135b210ea Mon Sep 17 00:00:00 2001
From: Daniel J Walsh <dwalsh@redhat.com>
Date: Tue, 9 Oct 2018 07:54:37 -0400
Subject: Generate a passwd file for users not in container

If someone runs podman as a user (uid) that is not defined in the container
we want generate a passwd file so that getpwuid() will work inside of container.

Signed-off-by: Daniel J Walsh <dwalsh@redhat.com>
---
 libpod/container.go          |  5 ++++
 libpod/container_internal.go | 66 ++++++++++++++++++++++++++++++++++++++++++++
 pkg/chrootuser/user.go       |  7 +++++
 pkg/chrootuser/user_basic.go |  4 +++
 pkg/chrootuser/user_linux.go | 26 +++++++++++++++++
 test/e2e/run_passwd_test.go  | 60 ++++++++++++++++++++++++++++++++++++++++
 test/e2e/run_test.go         |  2 +-
 7 files changed, 169 insertions(+), 1 deletion(-)
 create mode 100644 test/e2e/run_passwd_test.go

diff --git a/libpod/container.go b/libpod/container.go
index 5997c0b66..4e17b1102 100644
--- a/libpod/container.go
+++ b/libpod/container.go
@@ -642,6 +642,11 @@ func (c *Container) Hostname() string {
 	return c.ID()[:12]
 }
 
+// WorkingDir returns the containers working dir
+func (c *Container) WorkingDir() string {
+	return c.config.Spec.Process.Cwd
+}
+
 // State Accessors
 // Require locking
 
diff --git a/libpod/container_internal.go b/libpod/container_internal.go
index 77bba9e85..ab79aa790 100644
--- a/libpod/container_internal.go
+++ b/libpod/container_internal.go
@@ -9,6 +9,7 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"strconv"
 	"strings"
 	"syscall"
 
@@ -946,6 +947,19 @@ func (c *Container) makeBindMounts() error {
 	}
 	c.state.BindMounts["/etc/resolv.conf"] = newResolv
 
+	newPasswd, err := c.generatePasswd()
+	if err != nil {
+		return errors.Wrapf(err, "error creating temporary passwd file for container %s", c.ID())
+	}
+	if newPasswd != "" {
+		// Make /etc/passwd
+		if _, ok := c.state.BindMounts["/etc/passwd"]; ok {
+			// If it already exists, delete so we can recreate
+			delete(c.state.BindMounts, "/etc/passwd")
+		}
+		logrus.Debugf("adding entry to /etc/passwd for non existent default user")
+		c.state.BindMounts["/etc/passwd"] = newPasswd
+	}
 	// Make /etc/hosts
 	if _, ok := c.state.BindMounts["/etc/hosts"]; ok {
 		// If it already exists, delete so we can recreate
@@ -1017,6 +1031,58 @@ func (c *Container) writeStringToRundir(destFile, output string) (string, error)
 	return filepath.Join(c.state.DestinationRunDir, destFile), nil
 }
 
+// generatePasswd generates a container specific passwd file,
+// iff g.config.User is a number
+func (c *Container) generatePasswd() (string, error) {
+	var (
+		groupspec string
+		gid       uint32
+	)
+	if c.config.User == "" {
+		return "", nil
+	}
+	spec := strings.SplitN(c.config.User, ":", 2)
+	userspec := spec[0]
+	if len(spec) > 1 {
+		groupspec = spec[1]
+	}
+	// If a non numeric User, then don't generate passwd
+	uid, err := strconv.ParseUint(userspec, 10, 32)
+	if err != nil {
+		return "", nil
+	}
+	// if UID exists inside of container rootfs /etc/passwd then
+	// don't generate passwd
+	if _, _, err := chrootuser.LookupUIDInContainer(c.state.Mountpoint, uid); err == nil {
+		return "", nil
+	}
+	if err == nil && groupspec != "" {
+		if !c.state.Mounted {
+			return "", errors.Wrapf(ErrCtrStateInvalid, "container %s must be mounted in order to translate group field for passwd record", c.ID())
+		}
+		gid, err = chrootuser.GetGroup(c.state.Mountpoint, groupspec)
+		if err != nil {
+			return "", errors.Wrapf(err, "unable to get gid from %s formporary passwd file")
+		}
+	}
+
+	originPasswdFile := filepath.Join(c.state.Mountpoint, "/etc/passwd")
+	orig, err := ioutil.ReadFile(originPasswdFile)
+	if err != nil {
+		return "", errors.Wrapf(err, "unable to read passwd file %s", originPasswdFile)
+	}
+
+	pwd := fmt.Sprintf("%s%d:x:%d:%d:container user:%s:/bin/sh\n", orig, uid, uid, gid, c.WorkingDir())
+	passwdFile, err := c.writeStringToRundir("passwd", pwd)
+	if err != nil {
+		return "", errors.Wrapf(err, "failed to create temporary passwd fileo")
+	}
+	if os.Chmod(passwdFile, 0644); err != nil {
+		return "", err
+	}
+	return passwdFile, nil
+}
+
 // generateResolvConf generates a containers resolv.conf
 func (c *Container) generateResolvConf() (string, error) {
 	// Determine the endpoint for resolv.conf in case it is a symlink
diff --git a/pkg/chrootuser/user.go b/pkg/chrootuser/user.go
index 3de138b86..c83dcc230 100644
--- a/pkg/chrootuser/user.go
+++ b/pkg/chrootuser/user.go
@@ -99,3 +99,10 @@ func GetAdditionalGroupsForUser(rootdir string, userid uint64) ([]uint32, error)
 	}
 	return gids, nil
 }
+
+// LookupUIDInContainer returns username and gid associated with a UID in a container
+// it will use the /etc/passwd files inside of the rootdir
+// to return this information.
+func LookupUIDInContainer(rootdir string, uid uint64) (user string, gid uint64, err error) {
+	return lookupUIDInContainer(rootdir, uid)
+}
diff --git a/pkg/chrootuser/user_basic.go b/pkg/chrootuser/user_basic.go
index 4ed7918e9..79b0b24b5 100644
--- a/pkg/chrootuser/user_basic.go
+++ b/pkg/chrootuser/user_basic.go
@@ -21,3 +21,7 @@ func lookupGroupForUIDInContainer(rootdir string, userid uint64) (string, uint64
 func lookupAdditionalGroupsForUIDInContainer(rootdir string, userid uint64) (gid []uint32, err error) {
 	return nil, errors.New("supplemental groups list lookup by uid not supported")
 }
+
+func lookupUIDInContainer(rootdir string, uid uint64) (string, uint64, error) {
+	return "", 0, errors.New("UID lookup not supported")
+}
diff --git a/pkg/chrootuser/user_linux.go b/pkg/chrootuser/user_linux.go
index acd0af822..583eca569 100644
--- a/pkg/chrootuser/user_linux.go
+++ b/pkg/chrootuser/user_linux.go
@@ -265,3 +265,29 @@ func lookupGroupInContainer(rootdir, groupname string) (gid uint64, err error) {
 
 	return 0, user.UnknownGroupError(fmt.Sprintf("error looking up group %q", groupname))
 }
+
+func lookupUIDInContainer(rootdir string, uid uint64) (string, uint64, error) {
+	cmd, f, err := openChrootedFile(rootdir, "/etc/passwd")
+	if err != nil {
+		return "", 0, err
+	}
+	defer func() {
+		_ = cmd.Wait()
+	}()
+	rc := bufio.NewReader(f)
+	defer f.Close()
+
+	lookupUser.Lock()
+	defer lookupUser.Unlock()
+
+	pwd := parseNextPasswd(rc)
+	for pwd != nil {
+		if pwd.uid != uid {
+			pwd = parseNextPasswd(rc)
+			continue
+		}
+		return pwd.name, pwd.gid, nil
+	}
+
+	return "", 0, user.UnknownUserError(fmt.Sprintf("error looking up uid %q", uid))
+}
diff --git a/test/e2e/run_passwd_test.go b/test/e2e/run_passwd_test.go
new file mode 100644
index 000000000..cea457ae4
--- /dev/null
+++ b/test/e2e/run_passwd_test.go
@@ -0,0 +1,60 @@
+package integration
+
+import (
+	"os"
+
+	"fmt"
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("Podman run passwd", func() {
+	var (
+		tempdir    string
+		err        error
+		podmanTest PodmanTest
+	)
+
+	BeforeEach(func() {
+		tempdir, err = CreateTempDirInTempDir()
+		if err != nil {
+			os.Exit(1)
+		}
+		podmanTest = PodmanCreate(tempdir)
+		podmanTest.RestoreAllArtifacts()
+	})
+
+	AfterEach(func() {
+		podmanTest.Cleanup()
+		f := CurrentGinkgoTestDescription()
+		timedResult := fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds())
+		GinkgoWriter.Write([]byte(timedResult))
+	})
+
+	It("podman run no user specified ", func() {
+		session := podmanTest.Podman([]string{"run", ALPINE, "mount"})
+		session.WaitWithDefaultTimeout()
+		Expect(session.ExitCode()).To(Equal(0))
+		Expect(session.LineInOutputContains("passwd")).To(BeFalse())
+	})
+	It("podman run user specified in container", func() {
+		session := podmanTest.Podman([]string{"run", "-u", "bin", ALPINE, "mount"})
+		session.WaitWithDefaultTimeout()
+		Expect(session.ExitCode()).To(Equal(0))
+		Expect(session.LineInOutputContains("passwd")).To(BeFalse())
+	})
+
+	It("podman run UID specified in container", func() {
+		session := podmanTest.Podman([]string{"run", "-u", "2:1", ALPINE, "mount"})
+		session.WaitWithDefaultTimeout()
+		Expect(session.ExitCode()).To(Equal(0))
+		Expect(session.LineInOutputContains("passwd")).To(BeFalse())
+	})
+
+	It("podman run UID not specified in container", func() {
+		session := podmanTest.Podman([]string{"run", "-u", "20001:1", ALPINE, "mount"})
+		session.WaitWithDefaultTimeout()
+		Expect(session.ExitCode()).To(Equal(0))
+		Expect(session.LineInOutputContains("passwd")).To(BeTrue())
+	})
+})
diff --git a/test/e2e/run_test.go b/test/e2e/run_test.go
index a443d4ca5..271651056 100644
--- a/test/e2e/run_test.go
+++ b/test/e2e/run_test.go
@@ -401,7 +401,7 @@ var _ = Describe("Podman run", func() {
 		session := podmanTest.Podman([]string{"run", "--rm", "--user=1234", ALPINE, "id"})
 		session.WaitWithDefaultTimeout()
 		Expect(session.ExitCode()).To(Equal(0))
-		Expect(session.OutputToString()).To(Equal("uid=1234 gid=0(root)"))
+		Expect(session.OutputToString()).To(Equal("uid=1234(1234) gid=0(root)"))
 	})
 
 	It("podman run with user (integer, in /etc/passwd)", func() {
-- 
cgit v1.2.3-54-g00ecf