Skip to content

Commit

Permalink
Merge pull request #12627 from rhatdan/passwd
Browse files Browse the repository at this point in the history
Allow users to add host user accounts to /etc/passwd
  • Loading branch information
openshift-merge-robot authored Dec 23, 2021
2 parents 5570b5b + e8c06fa commit 73a54ea
Show file tree
Hide file tree
Showing 13 changed files with 126 additions and 7 deletions.
8 changes: 8 additions & 0 deletions cmd/podman/common/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,14 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions,
"Set proxy environment variables in the container based on the host proxy vars",
)

hostUserFlagName := "hostuser"
createFlags.StringSliceVar(
&cf.HostUsers,
hostUserFlagName, []string{},
"Host user account to add to /etc/passwd within container",
)
_ = cmd.RegisterFlagCompletionFunc(hostUserFlagName, completion.AutocompleteNone)

imageVolumeFlagName := "image-volume"
createFlags.StringVar(
&cf.ImageVolume,
Expand Down
5 changes: 5 additions & 0 deletions docs/source/markdown/podman-create.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,11 @@ Container host name

Sets the container host name that is available inside the container. Can only be used with a private UTS namespace `--uts=private` (default). If `--pod` is specified and the pod shares the UTS namespace (default) the pod's hostname will be used.

#### **--hostuser**=*name*

Add a user account to /etc/passwd from the host to the container. The Username
or UID must exist on the host system.

#### **--help**

Print usage statement
Expand Down
5 changes: 5 additions & 0 deletions docs/source/markdown/podman-run.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,11 @@ The initialization time needed for a container to bootstrap. The value can be ex
The maximum time allowed to complete the healthcheck before an interval is considered failed. Like start-period, the
value can be expressed in a time format such as **1m22s**. The default value is **30s**.

#### **--hostuser**=*name*

Add a user account to /etc/passwd from the host to the container. The Username
or UID must exist on the host system.

#### **--help**

Print usage statement
Expand Down
2 changes: 2 additions & 0 deletions libpod/container_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ type ContainerSecurityConfig struct {
// Groups are additional groups to add the container's user to. These
// are resolved within the container using the container's /etc/passwd.
Groups []string `json:"groups,omitempty"`
// HostUsers are a list of host user accounts to add to /etc/passwd
HostUsers []string `json:"HostUsers,omitempty"`
// AddCurrentUserPasswdEntry indicates that Libpod should ensure that
// the container's /etc/passwd contains an entry for the user running
// Libpod - mostly used in rootless containers where the user running
Expand Down
63 changes: 56 additions & 7 deletions libpod/container_internal_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,13 +305,40 @@ func (c *Container) getUserOverrides() *lookup.Overrides {
return &overrides
}

func lookupHostUser(name string) (*runcuser.ExecUser, error) {
var execUser runcuser.ExecUser
// Lookup User on host
u, err := util.LookupUser(name)
if err != nil {
return &execUser, err
}
uid, err := strconv.ParseUint(u.Uid, 8, 32)
if err != nil {
return &execUser, err
}

gid, err := strconv.ParseUint(u.Gid, 8, 32)
if err != nil {
return &execUser, err
}
execUser.Uid = int(uid)
execUser.Gid = int(gid)
execUser.Home = u.HomeDir
return &execUser, nil
}

// Generate spec for a container
// Accepts a map of the container's dependencies
func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
overrides := c.getUserOverrides()
execUser, err := lookup.GetUserGroupInfo(c.state.Mountpoint, c.config.User, overrides)
if err != nil {
return nil, err
if util.StringInSlice(c.config.User, c.config.HostUsers) {
execUser, err = lookupHostUser(c.config.User)
}
if err != nil {
return nil, err
}
}

g := generate.NewFromSpec(c.config.Spec)
Expand Down Expand Up @@ -2348,12 +2375,25 @@ func (c *Container) generateUserGroupEntry(addedGID int) (string, int, error) {
// /etc/passwd via AddCurrentUserPasswdEntry (though this does not trigger if
// the user in question already exists in /etc/passwd) or the UID to be added
// is 0).
// 3. The user specified additional host user accounts to add the the /etc/passwd file
// Returns password entry (as a string that can be appended to /etc/passwd) and
// any error that occurred.
func (c *Container) generatePasswdEntry() (string, error) {
passwdString := ""

addedUID := 0
for _, userid := range c.config.HostUsers {
// Lookup User on host
u, err := util.LookupUser(userid)
if err != nil {
return "", err
}
entry, err := c.userPasswdEntry(u)
if err != nil {
return "", err
}
passwdString += entry
}
if c.config.AddCurrentUserPasswdEntry {
entry, uid, _, err := c.generateCurrentUserPasswdEntry()
if err != nil {
Expand Down Expand Up @@ -2386,17 +2426,25 @@ func (c *Container) generateCurrentUserPasswdEntry() (string, int, int, error) {
if err != nil {
return "", 0, 0, errors.Wrapf(err, "failed to get current user")
}
pwd, err := c.userPasswdEntry(u)
if err != nil {
return "", 0, 0, err
}

return pwd, uid, rootless.GetRootlessGID(), nil
}

func (c *Container) userPasswdEntry(u *user.User) (string, error) {
// Lookup the user to see if it exists in the container image.
_, err = lookup.GetUser(c.state.Mountpoint, u.Username)
_, err := lookup.GetUser(c.state.Mountpoint, u.Username)
if err != runcuser.ErrNoPasswdEntries {
return "", 0, 0, err
return "", err
}

// Lookup the UID to see if it exists in the container image.
_, err = lookup.GetUser(c.state.Mountpoint, u.Uid)
if err != runcuser.ErrNoPasswdEntries {
return "", 0, 0, err
return "", err
}

// If the user's actual home directory exists, or was mounted in - use
Expand Down Expand Up @@ -2430,7 +2478,7 @@ func (c *Container) generateCurrentUserPasswdEntry() (string, int, int, error) {
c.config.Spec.Process.Env = append(c.config.Spec.Process.Env, fmt.Sprintf("HOME=%s", homeDir))
}

return fmt.Sprintf("%s:*:%s:%s:%s:%s:/bin/sh\n", u.Username, u.Uid, u.Gid, u.Name, homeDir), uid, rootless.GetRootlessGID(), nil
return fmt.Sprintf("%s:*:%s:%s:%s:%s:/bin/sh\n", u.Username, u.Uid, u.Gid, u.Name, homeDir), nil
}

// generateUserPasswdEntry generates an /etc/passwd entry for the container user
Expand Down Expand Up @@ -2485,7 +2533,7 @@ func (c *Container) generateUserPasswdEntry(addedUID int) (string, int, int, err

// generatePasswdAndGroup generates container-specific passwd and group files
// iff g.config.User is a number or we are configured to make a passwd entry for
// the current user.
// the current user or the user specified HostsUsers
// Returns path to file to mount at /etc/passwd, path to file to mount at
// /etc/group, and any error that occurred. If no passwd/group file were
// required, the empty string will be returned for those path (this may occur
Expand All @@ -2496,7 +2544,8 @@ func (c *Container) generateUserPasswdEntry(addedUID int) (string, int, int, err
// with a bind mount). This is done in cases where the container is *not*
// read-only. In this case, the function will return nothing ("", "", nil).
func (c *Container) generatePasswdAndGroup() (string, string, error) {
if !c.config.AddCurrentUserPasswdEntry && c.config.User == "" {
if !c.config.AddCurrentUserPasswdEntry && c.config.User == "" &&
len(c.config.HostUsers) == 0 {
return "", "", nil
}

Expand Down
11 changes: 11 additions & 0 deletions libpod/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -1768,6 +1768,17 @@ func WithPidFile(pidFile string) CtrCreateOption {
}
}

// WithHostUsers indicates host users to add to /etc/passwd
func WithHostUsers(hostUsers []string) CtrCreateOption {
return func(ctr *Container) error {
if ctr.valid {
return define.ErrCtrFinalized
}
ctr.config.HostUsers = hostUsers
return nil
}
}

// WithInitCtrType indicates the container is a initcontainer
func WithInitCtrType(containerType string) CtrCreateOption {
return func(ctr *Container) error {
Expand Down
1 change: 1 addition & 0 deletions pkg/domain/entities/pods.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ type ContainerCreateOptions struct {
HealthTimeout string
Hostname string `json:"hostname,omitempty"`
HTTPProxy bool
HostUsers []string
ImageVolume string
Init bool
InitContainerType string
Expand Down
4 changes: 4 additions & 0 deletions pkg/specgen/generate/container_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener
return nil, nil, nil, err
}

if len(s.HostUsers) > 0 {
options = append(options, libpod.WithHostUsers(s.HostUsers))
}

command, err := makeCommand(ctx, s, imageData, rtc)
if err != nil {
return nil, nil, nil, err
Expand Down
3 changes: 3 additions & 0 deletions pkg/specgen/specgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ type ContainerBasicConfig struct {
// Conflicts with UtsNS if UtsNS is not set to private.
// Optional.
Hostname string `json:"hostname,omitempty"`
// HostUses is a list of host usernames or UIDs to add to the container
// /etc/passwd file
HostUsers []string `json:"hostusers,omitempty"`
// Sysctl sets kernel parameters for the container
Sysctl map[string]string `json:"sysctl,omitempty"`
// Remove indicates if the container should be removed once it has been started
Expand Down
1 change: 1 addition & 0 deletions pkg/specgenutil/specgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions
s.NetworkOptions = c.Net.NetworkOptions
s.UseImageHosts = c.Net.NoHosts
}
s.HostUsers = c.HostUsers
s.ImageVolumeMode = c.ImageVolume
if s.ImageVolumeMode == "bind" {
s.ImageVolumeMode = "anonymous"
Expand Down
8 changes: 8 additions & 0 deletions pkg/util/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -723,3 +723,11 @@ func SocketPath() (string, error) {
// Glue the socket path together
return filepath.Join(xdg, "podman", "podman.sock"), nil
}

func LookupUser(name string) (*user.User, error) {
// Assume UID look up first, if it fails lookup by username
if u, err := user.LookupId(name); err == nil {
return u, err
}
return user.Lookup(name)
}
12 changes: 12 additions & 0 deletions test/system/030-run.bats
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,18 @@ EOF
run_podman rmi nomtab
}

@test "podman run --hostuser tests" {
skip_if_not_rootless "test whether hostuser is successfully added"
user=$(id -un)
run_podman 1 run --rm $IMAGE grep $user /etc/passwd
run_podman run --hostuser=$user --rm $IMAGE grep $user /etc/passwd
user=$(id -u)
run_podman run --hostuser=$user --rm $IMAGE grep $user /etc/passwd
run_podman run --hostuser=$user --user $user --rm $IMAGE grep $user /etc/passwd
user=bogus
run_podman 126 run --hostuser=$user --rm $IMAGE grep $user /etc/passwd
}

@test "podman run --device-cgroup-rule tests" {
skip_if_rootless "cannot add devices in rootless mode"

Expand Down
10 changes: 10 additions & 0 deletions test/system/helpers.bash
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,16 @@ function skip_if_rootless() {
fi
}

######################
# skip_if_not_rootless # ...with an optional message
######################
function skip_if_not_rootless() {
if ! is_rootless; then
local msg=$(_add_label_if_missing "$1" "rootfull")
skip "${msg:-not applicable under rootlfull podman}"
fi
}

####################
# skip_if_remote # ...with an optional message
####################
Expand Down

0 comments on commit 73a54ea

Please sign in to comment.