Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: getUIDandGID is able to resolve non-existing users and groups #2106

Merged
merged 24 commits into from
Jul 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ minikube-setup:
test: out/executor
@ ./scripts/test.sh

test-with-coverage: test
go tool cover -html=out/coverage.out


.PHONY: integration-test
integration-test:
@ ./scripts/integration-test.sh
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2018 Google, Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

FROM debian:9.11
RUN echo "hey" > /tmp/foo

FROM debian:9.11
RUN groupadd --gid 5000 testgroup
COPY --from=0 --chown=1001:1001 /tmp/foo /tmp/baz
# with existing group
COPY --from=0 --chown=1001:testgroup /tmp/foo /tmp/baz
25 changes: 25 additions & 0 deletions integration/dockerfiles/Dockerfile_test_user_nonexisting
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2018 Google, Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

FROM debian:9.11
USER 1001:1001
RUN echo "hey2" > /tmp/foo
USER 1001
RUN echo "hello" > /tmp/foobar

# With existing group
USER root
RUN groupadd testgroup
USER 1001:testgroup
RUN echo "hello" > /tmp/foobar
2 changes: 1 addition & 1 deletion integration/k8s_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func TestK8s(t *testing.T) {
t.Logf("Waiting for K8s kaniko build job to finish: %s\n",
"job/kaniko-test-"+job.Name)

kubeWaitCmd := exec.Command("kubectl", "wait", "--for=condition=complete", "--timeout=1m",
kubeWaitCmd := exec.Command("kubectl", "wait", "--for=condition=complete", "--timeout=2m",
"job/kaniko-test-"+job.Name)
if out, errR := RunCommandWithoutTest(kubeWaitCmd); errR != nil {
t.Log(kubeWaitCmd.Args)
Expand Down
1 change: 1 addition & 0 deletions pkg/commands/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func (c *CopyCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bu

replacementEnvs := buildArgs.ReplacementEnvs(config.Env)
uid, gid, err := getUserGroup(c.cmd.Chown, replacementEnvs)
logrus.Debugf("found uid %v and gid %v for chown string %v", uid, gid, c.cmd.Chown)
if err != nil {
return errors.Wrap(err, "getting user group from chown")
}
Expand Down
11 changes: 3 additions & 8 deletions pkg/commands/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"fmt"
"os"
"os/exec"
"os/user"
"strings"
"syscall"

Expand All @@ -41,8 +40,7 @@ type RunCommand struct {

// for testing
var (
userLookup = user.Lookup
userLookupID = user.LookupId
userLookup = util.LookupUser
)

func (r *RunCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
Expand Down Expand Up @@ -151,11 +149,7 @@ func addDefaultHOME(u string, envs []string) ([]string, error) {
// Otherwise the user is set to uid and HOME is /
userObj, err := userLookup(u)
if err != nil {
if uo, e := userLookupID(u); e == nil {
userObj = uo
} else {
return nil, err
}
return nil, fmt.Errorf("lookup user %v: %w", u, err)
}

return append(envs, fmt.Sprintf("%s=%s", constants.HOME, userObj.HomeDir)), nil
Expand Down Expand Up @@ -256,6 +250,7 @@ func (cr *CachingRunCommand) MetadataOnly() bool {
return false
}

// todo: this should create the workdir if it doesn't exist, atleast this is what docker does
func setWorkDirIfExists(workdir string) string {
if _, err := os.Lstat(workdir); err == nil {
return workdir
Expand Down
20 changes: 8 additions & 12 deletions pkg/commands/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package commands
import (
"archive/tar"
"bytes"
"errors"
"io"
"io/ioutil"
"log"
Expand All @@ -38,7 +37,6 @@ func Test_addDefaultHOME(t *testing.T) {
user string
mockUser *user.User
lookupError error
mockUserID *user.User
initial []string
expected []string
}{
Expand Down Expand Up @@ -81,19 +79,18 @@ func Test_addDefaultHOME(t *testing.T) {
},
},
{
name: "USER is set using the UID",
user: "newuser",
lookupError: errors.New("User not found"),
mockUserID: &user.User{
Username: "user",
HomeDir: "/home/user",
name: "USER is set using the UID",
user: "1000",
mockUser: &user.User{
Username: "1000",
HomeDir: "/",
},
initial: []string{
"PATH=/something/else",
},
expected: []string{
"PATH=/something/else",
"HOME=/home/user",
"HOME=/",
},
},
{
Expand All @@ -113,11 +110,10 @@ func Test_addDefaultHOME(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
original := userLookup
userLookup = func(username string) (*user.User, error) { return test.mockUser, test.lookupError }
userLookupID = func(username string) (*user.User, error) { return test.mockUserID, nil }
defer func() {
userLookup = user.Lookup
userLookupID = user.LookupId
userLookup = original
}()
actual, err := addDefaultHOME(test.user, test.initial)
testutil.CheckErrorAndDeepEqual(t, false, err, test.expected, actual)
Expand Down
5 changes: 0 additions & 5 deletions pkg/commands/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,6 @@ import (
"github.com/sirupsen/logrus"
)

// for testing
var (
Lookup = util.Lookup
hown3d marked this conversation as resolved.
Show resolved Hide resolved
)

type UserCommand struct {
BaseCommand
cmd *instructions.UserCommand
Expand Down
9 changes: 0 additions & 9 deletions pkg/commands/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,10 @@ limitations under the License.
package commands

import (
"fmt"
"os/user"
"testing"

"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
"github.com/GoogleContainerTools/kaniko/pkg/util"

"github.com/GoogleContainerTools/kaniko/testutil"
v1 "github.com/google/go-containerregistry/pkg/v1"
Expand Down Expand Up @@ -109,13 +107,6 @@ func TestUpdateUser(t *testing.T) {
User: test.user,
},
}
Lookup = func(_ string) (*user.User, error) {
if test.userObj != nil {
return test.userObj, nil
}
return nil, fmt.Errorf("error while looking up user")
}
defer func() { Lookup = util.Lookup }()
buildArgs := dockerfile.NewBuildArgs([]string{})
err := cmd.ExecuteCommand(cfg, buildArgs)
testutil.CheckErrorAndDeepEqual(t, false, err, test.expectedUID, cfg.User)
Expand Down
117 changes: 71 additions & 46 deletions pkg/util/command_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"os"
"os/user"
"path/filepath"
reflect "reflect"
"strconv"
"strings"

Expand All @@ -39,7 +38,7 @@ import (

// for testing
var (
getUIDAndGID = GetUIDAndGIDFromString
getUIDAndGIDFunc = getUIDAndGID
)

const (
Expand Down Expand Up @@ -353,7 +352,7 @@ func GetUserGroup(chownStr string, env []string) (int64, int64, error) {
return -1, -1, err
}

uid32, gid32, err := getUIDAndGID(chown, true)
uid32, gid32, err := getUIDAndGIDFromString(chown, true)
if err != nil {
return -1, -1, err
}
Expand All @@ -364,86 +363,112 @@ func GetUserGroup(chownStr string, env []string) (int64, int64, error) {
// Extract user and group id from a string formatted 'user:group'.
// If fallbackToUID is set, the gid is equal to uid if the group is not specified
// otherwise gid is set to zero.
func GetUIDAndGIDFromString(userGroupString string, fallbackToUID bool) (uint32, uint32, error) {
// UserID and GroupID don't need to be present on the system.
func getUIDAndGIDFromString(userGroupString string, fallbackToUID bool) (uint32, uint32, error) {
userAndGroup := strings.Split(userGroupString, ":")
userStr := userAndGroup[0]
var groupStr string
if len(userAndGroup) > 1 {
groupStr = userAndGroup[1]
}
return getUIDAndGIDFunc(userStr, groupStr, fallbackToUID)
}

if reflect.TypeOf(userStr).String() == "int" {
return 0, 0, nil
}
hown3d marked this conversation as resolved.
Show resolved Hide resolved

uidStr, gidStr, err := GetUserFromUsername(userStr, groupStr, fallbackToUID)
func getUIDAndGID(userStr string, groupStr string, fallbackToUID bool) (uint32, uint32, error) {
user, err := LookupUser(userStr)
if err != nil {
return 0, 0, err
}

// uid and gid need to be fit into uint32
uid64, err := strconv.ParseUint(uidStr, 10, 32)
uid32, err := getUID(user.Uid)
if err != nil {
return 0, 0, err
}

gid64, err := strconv.ParseUint(gidStr, 10, 32)
gid, err := getGIDFromName(groupStr, fallbackToUID)
if err != nil {
if errors.Is(err, fallbackToUIDError) {
return uid32, uid32, nil
}
return 0, 0, err
}

return uint32(uid64), uint32(gid64), nil
return uid32, gid, nil
}

func GetUserFromUsername(userStr string, groupStr string, fallbackToUID bool) (string, string, error) {
// Lookup by username
userObj, err := Lookup(userStr)
// getGID tries to parse the gid or falls back to getGroupFromName if it's not an id
func getGID(groupStr string, fallbackToUID bool) (uint32, error) {
gid, err := strconv.ParseUint(groupStr, 10, 32)
if err != nil {
return "", "", err
return 0, fallbackToUIDOrError(err, fallbackToUID)
}
return uint32(gid), nil
}

// Same dance with groups
var group *user.Group
if groupStr != "" {
group, err = user.LookupGroup(groupStr)
// getGIDFromName tries to parse the groupStr into an existing group.
// if the group doesn't exist, fallback to getGID to parse non-existing valid GIDs.
func getGIDFromName(groupStr string, fallbackToUID bool) (uint32, error) {
group, err := user.LookupGroup(groupStr)
if err != nil {
// unknown group error could relate to a non existing group
var groupErr *user.UnknownGroupError
if errors.Is(err, groupErr) {
return getGID(groupStr, fallbackToUID)
}
group, err = user.LookupGroupId(groupStr)
if err != nil {
if _, ok := err.(user.UnknownGroupError); !ok {
return "", "", err
}
group, err = user.LookupGroupId(groupStr)
if err != nil {
return "", "", err
}
return getGID(groupStr, fallbackToUID)
}
}
return getGID(group.Gid, fallbackToUID)
}

var fallbackToUIDError = new(fallbackToUIDErrorType)

type fallbackToUIDErrorType struct{}

uid := userObj.Uid
gid := "0"
func (e fallbackToUIDErrorType) Error() string {
return "fallback to uid"
}

func fallbackToUIDOrError(err error, fallbackToUID bool) error {
if fallbackToUID {
gid = userObj.Gid
return fallbackToUIDError
}
if group != nil {
gid = group.Gid
}

return uid, gid, nil
return err
}

func Lookup(userStr string) (*user.User, error) {
// LookupUser will try to lookup the userStr inside the passwd file.
// If the user does not exists, the function will fallback to parsing the userStr as an uid.
func LookupUser(userStr string) (*user.User, error) {
imjasonh marked this conversation as resolved.
Show resolved Hide resolved
userObj, err := user.Lookup(userStr)
if err != nil {
if _, ok := err.(user.UnknownUserError); !ok {
unknownUserErr := new(user.UnknownUserError)
// only return if it's not an unknown user error
if !errors.As(err, unknownUserErr) {
return nil, err
}

// Lookup by id
u, e := user.LookupId(userStr)
if e != nil {
return nil, err
userObj, err = user.LookupId(userStr)
if err != nil {
uid, err := getUID(userStr)
if err != nil {
// at this point, the user does not exist and the userStr is not a valid number.
return nil, fmt.Errorf("user %v is not a uid and does not exist on the system", userStr)
}
userObj = &user.User{
Uid: fmt.Sprint(uid),
HomeDir: "/",
}
}

userObj = u
}

return userObj, nil
}

func getUID(userStr string) (uint32, error) {
// checkif userStr is a valid id
uid, err := strconv.ParseUint(userStr, 10, 32)
if err != nil {
return 0, err
}
return uint32(uid), nil
}
Loading