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

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

A common pattern in dockerfiles is to provide a plain uid and gid number, which doesn't neccesarily exist inside the os.

Signed-off-by: Höhl, Lukas <lukas.hoehl@accso.de>

* test: add chown dockerfile

Signed-off-by: Höhl, Lukas <lukas.hoehl@accso.de>

* chore: format

Signed-off-by: Höhl, Lukas <lukas.hoehl@accso.de>

* chore: add comment

Signed-off-by: Höhl, Lukas <lukas.hoehl@accso.de>

* tests: fix chown dockerfile

Signed-off-by: Höhl, Lukas <lukas.hoehl@accso.de>

* refactor: split up getIdsFromUsernameAndGroup func

Signed-off-by: Höhl, Lukas <lukas.hoehl@accso.de>

* fix: implement raw uid logic for LookupUser

Signed-off-by: Höhl, Lukas <lukas.hoehl@accso.de>

* test: add dockerfiles for integration test

* fix: lookup user error message

* test: add dockerfiles for non-existing user testcase

* fix: forgot error check

* tests: fix syscall credentials test

* chore: add debug output for copy command

* tests: set specific gid for integration dockerfile

* tests: fix syscall credentials test

github runner had the exact uid that i was testing on, so the groups were not empty

Signed-off-by: Höhl, Lukas <lukas.hoehl@accso.de>

* tests: fix test script

Signed-off-by: Höhl, Lukas <lukas.hoehl@accso.de>

* chore: apply golangci lint checks

Signed-off-by: Höhl, Lukas <lukas.hoehl@accso.de>

* fix: reset file ownership in createFile if not root owned

Signed-off-by: Höhl, Lukas <lukas.hoehl@accso.de>

* chore: logrus.Debugf missed format variable

Signed-off-by: Höhl, Lukas <lukas.hoehl@accso.de>

* chore(test-script): remove go html coverage

Signed-off-by: Höhl, Lukas <lukas.hoehl@accso.de>

* test(k8s): increase wait timeout

Signed-off-by: Höhl, Lukas <lukas.hoehl@accso.de>
This commit is contained in:
Lukas 2022-07-12 16:21:37 +02:00 committed by GitHub
parent 8710ce3311
commit aad03dc285
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 542 additions and 150 deletions

View File

@ -66,6 +66,10 @@ minikube-setup:
test: out/executor test: out/executor
@ ./scripts/test.sh @ ./scripts/test.sh
test-with-coverage: test
go tool cover -html=out/coverage.out
.PHONY: integration-test .PHONY: integration-test
integration-test: integration-test:
@ ./scripts/integration-test.sh @ ./scripts/integration-test.sh

View File

@ -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

View File

@ -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

View File

@ -89,7 +89,7 @@ func TestK8s(t *testing.T) {
t.Logf("Waiting for K8s kaniko build job to finish: %s\n", t.Logf("Waiting for K8s kaniko build job to finish: %s\n",
"job/kaniko-test-"+job.Name) "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) "job/kaniko-test-"+job.Name)
if out, errR := RunCommandWithoutTest(kubeWaitCmd); errR != nil { if out, errR := RunCommandWithoutTest(kubeWaitCmd); errR != nil {
t.Log(kubeWaitCmd.Args) t.Log(kubeWaitCmd.Args)

View File

@ -53,6 +53,7 @@ func (c *CopyCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bu
replacementEnvs := buildArgs.ReplacementEnvs(config.Env) replacementEnvs := buildArgs.ReplacementEnvs(config.Env)
uid, gid, err := getUserGroup(c.cmd.Chown, replacementEnvs) 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 { if err != nil {
return errors.Wrap(err, "getting user group from chown") return errors.Wrap(err, "getting user group from chown")
} }

View File

@ -20,7 +20,6 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"os/user"
"strings" "strings"
"syscall" "syscall"
@ -41,8 +40,7 @@ type RunCommand struct {
// for testing // for testing
var ( var (
userLookup = user.Lookup userLookup = util.LookupUser
userLookupID = user.LookupId
) )
func (r *RunCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { func (r *RunCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
@ -151,11 +149,7 @@ func addDefaultHOME(u string, envs []string) ([]string, error) {
// Otherwise the user is set to uid and HOME is / // Otherwise the user is set to uid and HOME is /
userObj, err := userLookup(u) userObj, err := userLookup(u)
if err != nil { if err != nil {
if uo, e := userLookupID(u); e == nil { return nil, fmt.Errorf("lookup user %v: %w", u, err)
userObj = uo
} else {
return nil, err
}
} }
return append(envs, fmt.Sprintf("%s=%s", constants.HOME, userObj.HomeDir)), nil return append(envs, fmt.Sprintf("%s=%s", constants.HOME, userObj.HomeDir)), nil
@ -256,6 +250,7 @@ func (cr *CachingRunCommand) MetadataOnly() bool {
return false return false
} }
// todo: this should create the workdir if it doesn't exist, atleast this is what docker does
func setWorkDirIfExists(workdir string) string { func setWorkDirIfExists(workdir string) string {
if _, err := os.Lstat(workdir); err == nil { if _, err := os.Lstat(workdir); err == nil {
return workdir return workdir

View File

@ -18,7 +18,6 @@ package commands
import ( import (
"archive/tar" "archive/tar"
"bytes" "bytes"
"errors"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
@ -38,7 +37,6 @@ func Test_addDefaultHOME(t *testing.T) {
user string user string
mockUser *user.User mockUser *user.User
lookupError error lookupError error
mockUserID *user.User
initial []string initial []string
expected []string expected []string
}{ }{
@ -81,19 +79,18 @@ func Test_addDefaultHOME(t *testing.T) {
}, },
}, },
{ {
name: "USER is set using the UID", name: "USER is set using the UID",
user: "newuser", user: "1000",
lookupError: errors.New("User not found"), mockUser: &user.User{
mockUserID: &user.User{ Username: "1000",
Username: "user", HomeDir: "/",
HomeDir: "/home/user",
}, },
initial: []string{ initial: []string{
"PATH=/something/else", "PATH=/something/else",
}, },
expected: []string{ expected: []string{
"PATH=/something/else", "PATH=/something/else",
"HOME=/home/user", "HOME=/",
}, },
}, },
{ {
@ -113,11 +110,10 @@ func Test_addDefaultHOME(t *testing.T) {
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
original := userLookup
userLookup = func(username string) (*user.User, error) { return test.mockUser, test.lookupError } 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() { defer func() {
userLookup = user.Lookup userLookup = original
userLookupID = user.LookupId
}() }()
actual, err := addDefaultHOME(test.user, test.initial) actual, err := addDefaultHOME(test.user, test.initial)
testutil.CheckErrorAndDeepEqual(t, false, err, test.expected, actual) testutil.CheckErrorAndDeepEqual(t, false, err, test.expected, actual)

View File

@ -28,11 +28,6 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// for testing
var (
Lookup = util.Lookup
)
type UserCommand struct { type UserCommand struct {
BaseCommand BaseCommand
cmd *instructions.UserCommand cmd *instructions.UserCommand

View File

@ -16,12 +16,10 @@ limitations under the License.
package commands package commands
import ( import (
"fmt"
"os/user" "os/user"
"testing" "testing"
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile" "github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
"github.com/GoogleContainerTools/kaniko/pkg/util"
"github.com/GoogleContainerTools/kaniko/testutil" "github.com/GoogleContainerTools/kaniko/testutil"
v1 "github.com/google/go-containerregistry/pkg/v1" v1 "github.com/google/go-containerregistry/pkg/v1"
@ -109,13 +107,6 @@ func TestUpdateUser(t *testing.T) {
User: test.user, 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{}) buildArgs := dockerfile.NewBuildArgs([]string{})
err := cmd.ExecuteCommand(cfg, buildArgs) err := cmd.ExecuteCommand(cfg, buildArgs)
testutil.CheckErrorAndDeepEqual(t, false, err, test.expectedUID, cfg.User) testutil.CheckErrorAndDeepEqual(t, false, err, test.expectedUID, cfg.User)

View File

@ -23,7 +23,6 @@ import (
"os" "os"
"os/user" "os/user"
"path/filepath" "path/filepath"
reflect "reflect"
"strconv" "strconv"
"strings" "strings"
@ -39,7 +38,7 @@ import (
// for testing // for testing
var ( var (
getUIDAndGID = GetUIDAndGIDFromString getUIDAndGIDFunc = getUIDAndGID
) )
const ( const (
@ -353,7 +352,7 @@ func GetUserGroup(chownStr string, env []string) (int64, int64, error) {
return -1, -1, err return -1, -1, err
} }
uid32, gid32, err := getUIDAndGID(chown, true) uid32, gid32, err := getUIDAndGIDFromString(chown, true)
if err != nil { if err != nil {
return -1, -1, err return -1, -1, err
} }
@ -364,86 +363,112 @@ func GetUserGroup(chownStr string, env []string) (int64, int64, error) {
// Extract user and group id from a string formatted 'user:group'. // 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 // If fallbackToUID is set, the gid is equal to uid if the group is not specified
// otherwise gid is set to zero. // 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, ":") userAndGroup := strings.Split(userGroupString, ":")
userStr := userAndGroup[0] userStr := userAndGroup[0]
var groupStr string var groupStr string
if len(userAndGroup) > 1 { if len(userAndGroup) > 1 {
groupStr = userAndGroup[1] groupStr = userAndGroup[1]
} }
return getUIDAndGIDFunc(userStr, groupStr, fallbackToUID)
if reflect.TypeOf(userStr).String() == "int" {
return 0, 0, nil
}
uidStr, gidStr, err := GetUserFromUsername(userStr, groupStr, fallbackToUID)
if err != nil {
return 0, 0, err
}
// uid and gid need to be fit into uint32
uid64, err := strconv.ParseUint(uidStr, 10, 32)
if err != nil {
return 0, 0, err
}
gid64, err := strconv.ParseUint(gidStr, 10, 32)
if err != nil {
return 0, 0, err
}
return uint32(uid64), uint32(gid64), nil
} }
func GetUserFromUsername(userStr string, groupStr string, fallbackToUID bool) (string, string, error) { func getUIDAndGID(userStr string, groupStr string, fallbackToUID bool) (uint32, uint32, error) {
// Lookup by username user, err := LookupUser(userStr)
userObj, err := Lookup(userStr)
if err != nil { if err != nil {
return "", "", err return 0, 0, err
}
uid32, err := getUID(user.Uid)
if err != nil {
return 0, 0, err
} }
// Same dance with groups gid, err := getGIDFromName(groupStr, fallbackToUID)
var group *user.Group if err != nil {
if groupStr != "" { if errors.Is(err, fallbackToUIDError) {
group, err = user.LookupGroup(groupStr) return uid32, uid32, nil
}
return 0, 0, err
}
return uid32, gid, nil
}
// 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 0, fallbackToUIDOrError(err, fallbackToUID)
}
return uint32(gid), nil
}
// 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 err != nil {
if _, ok := err.(user.UnknownGroupError); !ok { return getGID(groupStr, fallbackToUID)
return "", "", err
}
group, err = user.LookupGroupId(groupStr)
if err != nil {
return "", "", err
}
} }
} }
return getGID(group.Gid, fallbackToUID)
uid := userObj.Uid
gid := "0"
if fallbackToUID {
gid = userObj.Gid
}
if group != nil {
gid = group.Gid
}
return uid, gid, nil
} }
func Lookup(userStr string) (*user.User, error) { var fallbackToUIDError = new(fallbackToUIDErrorType)
type fallbackToUIDErrorType struct{}
func (e fallbackToUIDErrorType) Error() string {
return "fallback to uid"
}
func fallbackToUIDOrError(err error, fallbackToUID bool) error {
if fallbackToUID {
return fallbackToUIDError
}
return err
}
// 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) {
userObj, err := user.Lookup(userStr) userObj, err := user.Lookup(userStr)
if err != nil { 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 return nil, err
} }
// Lookup by id // Lookup by id
u, e := user.LookupId(userStr) userObj, err = user.LookupId(userStr)
if e != nil { if err != nil {
return nil, err 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 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
}

View File

@ -555,28 +555,32 @@ func Test_RemoteUrls(t *testing.T) {
func TestGetUserGroup(t *testing.T) { func TestGetUserGroup(t *testing.T) {
tests := []struct { tests := []struct {
description string description string
chown string chown string
env []string env []string
mock func(string, bool) (uint32, uint32, error) mockIDGetter func(userStr string, groupStr string, fallbackToUID bool) (uint32, uint32, error)
expectedU int64 // needed, in case uid is a valid number, but group is a name
expectedG int64 mockGroupIDGetter func(groupStr string) (*user.Group, error)
shdErr bool expectedU int64
expectedG int64
shdErr bool
}{ }{
{ {
description: "non empty chown", description: "non empty chown",
chown: "some:some", chown: "some:some",
env: []string{}, env: []string{},
mock: func(string, bool) (uint32, uint32, error) { return 100, 1000, nil }, mockIDGetter: func(string, string, bool) (uint32, uint32, error) {
expectedU: 100, return 100, 1000, nil
expectedG: 1000, },
expectedU: 100,
expectedG: 1000,
}, },
{ {
description: "non empty chown with env replacement", description: "non empty chown with env replacement",
chown: "some:$foo", chown: "some:$foo",
env: []string{"foo=key"}, env: []string{"foo=key"},
mock: func(c string, t bool) (uint32, uint32, error) { mockIDGetter: func(userStr string, groupStr string, fallbackToUID bool) (uint32, uint32, error) {
if c == "some:key" { if userStr == "some" && groupStr == "key" {
return 10, 100, nil return 10, 100, nil
} }
return 0, 0, fmt.Errorf("did not resolve environment variable") return 0, 0, fmt.Errorf("did not resolve environment variable")
@ -586,7 +590,7 @@ func TestGetUserGroup(t *testing.T) {
}, },
{ {
description: "empty chown string", description: "empty chown string",
mock: func(c string, t bool) (uint32, uint32, error) { mockIDGetter: func(string, string, bool) (uint32, uint32, error) {
return 0, 0, fmt.Errorf("should not be called") return 0, 0, fmt.Errorf("should not be called")
}, },
expectedU: -1, expectedU: -1,
@ -595,9 +599,11 @@ func TestGetUserGroup(t *testing.T) {
} }
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.description, func(t *testing.T) { t.Run(tc.description, func(t *testing.T) {
original := getUIDAndGID originalIDGetter := getUIDAndGIDFunc
defer func() { getUIDAndGID = original }() defer func() {
getUIDAndGID = tc.mock getUIDAndGIDFunc = originalIDGetter
}()
getUIDAndGIDFunc = tc.mockIDGetter
uid, gid, err := GetUserGroup(tc.chown, tc.env) uid, gid, err := GetUserGroup(tc.chown, tc.env)
testutil.CheckErrorAndDeepEqual(t, tc.shdErr, err, uid, tc.expectedU) testutil.CheckErrorAndDeepEqual(t, tc.shdErr, err, uid, tc.expectedU)
testutil.CheckErrorAndDeepEqual(t, tc.shdErr, err, gid, tc.expectedG) testutil.CheckErrorAndDeepEqual(t, tc.shdErr, err, gid, tc.expectedG)
@ -661,33 +667,191 @@ func TestResolveEnvironmentReplacementList(t *testing.T) {
} }
func Test_GetUIDAndGIDFromString(t *testing.T) { func Test_GetUIDAndGIDFromString(t *testing.T) {
currentUser, err := user.Current() currentUser := testutil.GetCurrentUser(t)
if err != nil {
t.Fatalf("Cannot get current user: %s", err)
}
groups, err := currentUser.GroupIds()
if err != nil || len(groups) == 0 {
t.Fatalf("Cannot get groups for current user: %s", err)
}
primaryGroupObj, err := user.LookupGroupId(groups[0])
if err != nil {
t.Fatalf("Could not lookup name of group %s: %s", groups[0], err)
}
primaryGroup := primaryGroupObj.Name
testCases := []string{ type args struct {
fmt.Sprintf("%s:%s", currentUser.Uid, currentUser.Gid), userGroupStr string
fmt.Sprintf("%s:%s", currentUser.Username, currentUser.Gid), fallbackToUID bool
fmt.Sprintf("%s:%s", currentUser.Uid, primaryGroup), }
fmt.Sprintf("%s:%s", currentUser.Username, primaryGroup),
type expected struct {
userID uint32
groupID uint32
}
currentUserUID, _ := strconv.ParseUint(currentUser.Uid, 10, 32)
currentUserGID, _ := strconv.ParseUint(currentUser.Gid, 10, 32)
expectedCurrentUser := expected{
userID: uint32(currentUserUID),
groupID: uint32(currentUserGID),
}
testCases := []struct {
testname string
args args
expected expected
wantErr bool
}{
{
testname: "current user uid and gid",
args: args{
userGroupStr: fmt.Sprintf("%d:%d", currentUserUID, currentUserGID),
},
expected: expectedCurrentUser,
},
{
testname: "current user username and gid",
args: args{
userGroupStr: fmt.Sprintf("%s:%d", currentUser.Username, currentUserGID),
},
expected: expectedCurrentUser,
},
{
testname: "current user username and primary group",
args: args{
userGroupStr: fmt.Sprintf("%s:%s", currentUser.Username, currentUser.PrimaryGroup),
},
expected: expectedCurrentUser,
},
{
testname: "current user uid and primary group",
args: args{
userGroupStr: fmt.Sprintf("%d:%s", currentUserUID, currentUser.PrimaryGroup),
},
expected: expectedCurrentUser,
},
{
testname: "non-existing valid uid and gid",
args: args{
userGroupStr: fmt.Sprintf("%d:%d", 1001, 50000),
},
expected: expected{
userID: 1001,
groupID: 50000,
},
},
{
testname: "uid and existing group",
args: args{
userGroupStr: fmt.Sprintf("%d:%s", 1001, currentUser.PrimaryGroup),
},
expected: expected{
userID: 1001,
groupID: uint32(currentUserGID),
},
},
{
testname: "uid and non existing group-name with fallbackToUID",
args: args{
userGroupStr: fmt.Sprintf("%d:%s", 1001, "hello-world-group"),
fallbackToUID: true,
},
expected: expected{
userID: 1001,
groupID: 1001,
},
},
{
testname: "uid and non existing group-name",
args: args{
userGroupStr: fmt.Sprintf("%d:%s", 1001, "hello-world-group"),
},
wantErr: true,
},
{
testname: "name and non existing gid",
args: args{
userGroupStr: fmt.Sprintf("%s:%d", currentUser.Username, 50000),
},
expected: expected{
userID: expectedCurrentUser.userID,
groupID: 50000,
},
},
{
testname: "only uid and fallback is false",
args: args{
userGroupStr: fmt.Sprintf("%d", currentUserUID),
fallbackToUID: false,
},
wantErr: true,
},
{
testname: "only uid and fallback is true",
args: args{
userGroupStr: fmt.Sprintf("%d", currentUserUID),
fallbackToUID: true,
},
expected: expected{
userID: expectedCurrentUser.userID,
groupID: expectedCurrentUser.userID,
},
},
{
testname: "non-existing user without group",
args: args{
userGroupStr: "helloworlduser",
},
wantErr: true,
},
} }
expectedU, _ := strconv.ParseUint(currentUser.Uid, 10, 32)
expectedG, _ := strconv.ParseUint(currentUser.Gid, 10, 32)
for _, tt := range testCases { for _, tt := range testCases {
uid, gid, err := GetUIDAndGIDFromString(tt, false) uid, gid, err := getUIDAndGIDFromString(tt.args.userGroupStr, tt.args.fallbackToUID)
if uid != uint32(expectedU) || gid != uint32(expectedG) || err != nil { testutil.CheckError(t, tt.wantErr, err)
t.Errorf("Could not correctly decode %s to uid/gid %d:%d. Result: %d:%d", tt, expectedU, expectedG, if uid != tt.expected.userID || gid != tt.expected.groupID {
t.Errorf("%v failed. Could not correctly decode %s to uid/gid %d:%d. Result: %d:%d",
tt.testname,
tt.args.userGroupStr,
tt.expected.userID, tt.expected.groupID,
uid, gid) uid, gid)
} }
} }
} }
func TestLookupUser(t *testing.T) {
currentUser := testutil.GetCurrentUser(t)
type args struct {
userStr string
}
tests := []struct {
testname string
args args
expected *user.User
wantErr bool
}{
{
testname: "non-existing user",
args: args{
userStr: "foobazbar",
},
wantErr: true,
},
{
testname: "uid",
args: args{
userStr: "30000",
},
expected: &user.User{
Uid: "30000",
HomeDir: "/",
},
wantErr: false,
},
{
testname: "current user",
args: args{
userStr: currentUser.Username,
},
expected: currentUser.User,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.testname, func(t *testing.T) {
got, err := LookupUser(tt.args.userStr)
testutil.CheckErrorAndDeepEqual(t, tt.wantErr, err, tt.expected, got)
})
}
}

View File

@ -43,8 +43,10 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
const DoNotChangeUID = -1 const (
const DoNotChangeGID = -1 DoNotChangeUID = -1
DoNotChangeGID = -1
)
const ( const (
snapshotTimeout = "SNAPSHOT_TIMEOUT_DURATION" snapshotTimeout = "SNAPSHOT_TIMEOUT_DURATION"
@ -539,6 +541,26 @@ func FilepathExists(path string) bool {
return !os.IsNotExist(err) return !os.IsNotExist(err)
} }
// resetFileOwnershipIfNotMatching function changes ownership of the file at path to newUID and newGID.
// If the ownership already matches, chown is not executed.
func resetFileOwnershipIfNotMatching(path string, newUID, newGID uint32) error {
fsInfo, err := os.Lstat(path)
if err != nil {
return errors.Wrap(err, "getting stat of present file")
}
stat, ok := fsInfo.Sys().(*syscall.Stat_t)
if !ok {
return fmt.Errorf("can't convert fs.FileInfo of %v to linux syscall.Stat_t", path)
}
if stat.Uid != newUID && stat.Gid != newGID {
err = os.Chown(path, int(newUID), int(newGID))
if err != nil {
return errors.Wrap(err, "reseting file ownership to root")
}
}
return nil
}
// CreateFile creates a file at path and copies over contents from the reader // CreateFile creates a file at path and copies over contents from the reader
func CreateFile(path string, reader io.Reader, perm os.FileMode, uid uint32, gid uint32) error { func CreateFile(path string, reader io.Reader, perm os.FileMode, uid uint32, gid uint32) error {
// Create directory path if it doesn't exist // Create directory path if it doesn't exist
@ -546,6 +568,15 @@ func CreateFile(path string, reader io.Reader, perm os.FileMode, uid uint32, gid
return errors.Wrap(err, "creating parent dir") return errors.Wrap(err, "creating parent dir")
} }
// if the file is already created with ownership other than root, reset the ownership
if FilepathExists(path) {
logrus.Debugf("file at %v already exists, resetting file ownership to root", path)
err := resetFileOwnershipIfNotMatching(path, 0, 0)
if err != nil {
return errors.Wrap(err, "reseting file ownership")
}
}
dest, err := os.Create(path) dest, err := os.Create(path)
if err != nil { if err != nil {
return errors.Wrap(err, "creating file") return errors.Wrap(err, "creating file")
@ -793,7 +824,12 @@ func mkdirAllWithPermissions(path string, mode os.FileMode, uid, gid int64) erro
} }
if uid > math.MaxUint32 || gid > math.MaxUint32 { if uid > math.MaxUint32 || gid > math.MaxUint32 {
// due to https://github.com/golang/go/issues/8537 // due to https://github.com/golang/go/issues/8537
return errors.New(fmt.Sprintf("Numeric User-ID or Group-ID greater than %v are not properly supported.", uint64(math.MaxUint32))) return errors.New(
fmt.Sprintf(
"Numeric User-ID or Group-ID greater than %v are not properly supported.",
uint64(math.MaxUint32),
),
)
} }
if err := os.Chown(path, int(uid), int(gid)); err != nil { if err := os.Chown(path, int(uid), int(gid)); err != nil {
return err return err
@ -851,7 +887,6 @@ func CreateTargetTarfile(tarpath string) (*os.File, error) {
} }
} }
return os.Create(tarpath) return os.Create(tarpath)
} }
// Returns true if a file is a symlink // Returns true if a file is a symlink
@ -1009,8 +1044,8 @@ type walkFSResult struct {
func WalkFS( func WalkFS(
dir string, dir string,
existingPaths map[string]struct{}, existingPaths map[string]struct{},
changeFunc func(string) (bool, error)) ([]string, map[string]struct{}) { changeFunc func(string) (bool, error),
) ([]string, map[string]struct{}) {
timeOutStr := os.Getenv(snapshotTimeout) timeOutStr := os.Getenv(snapshotTimeout)
if timeOutStr == "" { if timeOutStr == "" {
logrus.Tracef("Environment '%s' not set. Using default snapshot timeout '%s'", snapshotTimeout, defaultTimeout) logrus.Tracef("Environment '%s' not set. Using default snapshot timeout '%s'", snapshotTimeout, defaultTimeout)
@ -1102,7 +1137,6 @@ func GetFSInfoMap(dir string, existing map[string]os.FileInfo) (map[string]os.Fi
fileMap[path] = fi fileMap[path] = fi
foundPaths = append(foundPaths, path) foundPaths = append(foundPaths, path)
} }
} }
return nil return nil
}, },

View File

@ -26,5 +26,9 @@ import (
// groupIDs returns all of the group ID's a user is a member of // groupIDs returns all of the group ID's a user is a member of
func groupIDs(u *user.User) ([]string, error) { func groupIDs(u *user.User) ([]string, error) {
// user can have no gid if it's a non existing user
if u.Gid == "" {
return []string{}, nil
}
return u.GroupIds() return u.GroupIds()
} }

View File

@ -45,6 +45,11 @@ type group struct {
func groupIDs(u *user.User) ([]string, error) { func groupIDs(u *user.User) ([]string, error) {
logrus.Infof("Performing slow lookup of group ids for %s", u.Username) logrus.Infof("Performing slow lookup of group ids for %s", u.Username)
// user can have no gid if it's a non existing user
if u.Gid == "" {
return []string{}, nil
}
f, err := os.Open(groupFile) f, err := os.Open(groupFile)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "open") return nil, errors.Wrap(err, "open")

View File

@ -17,6 +17,7 @@ limitations under the License.
package util package util
import ( import (
"fmt"
"strconv" "strconv"
"syscall" "syscall"
@ -25,18 +26,19 @@ import (
) )
func SyscallCredentials(userStr string) (*syscall.Credential, error) { func SyscallCredentials(userStr string) (*syscall.Credential, error) {
uid, gid, err := GetUIDAndGIDFromString(userStr, true) uid, gid, err := getUIDAndGIDFromString(userStr, true)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "get uid/gid") return nil, errors.Wrap(err, "get uid/gid")
} }
u, err := Lookup(userStr) u, err := LookupUser(fmt.Sprint(uid))
if err != nil { if err != nil {
return nil, errors.Wrap(err, "lookup") return nil, errors.Wrap(err, "lookup")
} }
logrus.Infof("Util.Lookup returned: %+v", u) logrus.Infof("Util.Lookup returned: %+v", u)
var groups []uint32 // initiliaze empty
groups := []uint32{}
gidStr, err := groupIDs(u) gidStr, err := groupIDs(u)
if err != nil { if err != nil {

View File

@ -0,0 +1,99 @@
/*
Copyright 2020 Google LLC
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.
*/
package util
import (
"fmt"
"strconv"
"syscall"
"testing"
"github.com/GoogleContainerTools/kaniko/testutil"
)
func TestSyscallCredentials(t *testing.T) {
currentUser := testutil.GetCurrentUser(t)
uid, _ := strconv.ParseUint(currentUser.Uid, 10, 32)
currentUserUID32 := uint32(uid)
gid, _ := strconv.ParseUint(currentUser.Gid, 10, 32)
currentUserGID32 := uint32(gid)
currentUserGroupIDsU32 := []uint32{}
currentUserGroupIDs, _ := currentUser.GroupIds()
for _, id := range currentUserGroupIDs {
id32, _ := strconv.ParseUint(id, 10, 32)
currentUserGroupIDsU32 = append(currentUserGroupIDsU32, uint32(id32))
}
type args struct {
userStr string
}
tests := []struct {
name string
args args
want *syscall.Credential
wantErr bool
}{
{
name: "non-existing user without group",
args: args{
userStr: "helloworld-user",
},
wantErr: true,
},
{
name: "non-existing uid without group",
args: args{
userStr: "50000",
},
want: &syscall.Credential{
Uid: 50000,
// because fallback is enabled
Gid: 50000,
Groups: []uint32{},
},
},
{
name: "non-existing uid with existing gid",
args: args{
userStr: fmt.Sprintf("50000:%d", currentUserGID32),
},
want: &syscall.Credential{
Uid: 50000,
Gid: currentUserGID32,
Groups: []uint32{},
},
},
{
name: "existing username with non-existing gid",
args: args{
userStr: fmt.Sprintf("%s:50000", currentUser.Username),
},
want: &syscall.Credential{
Uid: currentUserUID32,
Gid: 50000,
Groups: currentUserGroupIDsU32,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := SyscallCredentials(tt.args.userStr)
testutil.CheckErrorAndDeepEqual(t, tt.wantErr, err, tt.want, got)
})
}
}

View File

@ -14,14 +14,16 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
set -e DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
#set -e
RED='\033[0;31m' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
RESET='\033[0m' RESET='\033[0m'
echo "Running go tests..." echo "Running go tests..."
go test -cover -v -timeout 60s `go list ./... | grep -v vendor | grep -v integration` | sed ''/PASS/s//$(printf "${GREEN}PASS${RESET}")/'' | sed ''/FAIL/s//$(printf "${RED}FAIL${RESET}")/'' go test -cover -coverprofile=out/coverage.out -v -timeout 60s `go list ./... | grep -v vendor | grep -v integration` | sed ''/PASS/s//$(printf "${GREEN}PASS${RESET}")/'' | sed ''/FAIL/s//$(printf "${RED}FAIL${RESET}")/''
GO_TEST_EXIT_CODE=${PIPESTATUS[0]} GO_TEST_EXIT_CODE=${PIPESTATUS[0]}
if [[ $GO_TEST_EXIT_CODE -ne 0 ]]; then if [[ $GO_TEST_EXIT_CODE -ne 0 ]]; then
exit $GO_TEST_EXIT_CODE exit $GO_TEST_EXIT_CODE
@ -29,15 +31,15 @@ fi
echo "Running validation scripts..." echo "Running validation scripts..."
scripts=( scripts=(
"hack/boilerplate.sh" "$DIR/../hack/boilerplate.sh"
"hack/gofmt.sh" "$DIR/../hack/gofmt.sh"
"hack/linter.sh" "$DIR/../hack/linter.sh"
) )
fail=0 fail=0
for s in "${scripts[@]}" for s in "${scripts[@]}"
do do
echo "RUN ${s}" echo "RUN ${s}"
if "./${s}"; then if "${s}"; then
echo -e "${GREEN}PASSED${RESET} ${s}" echo -e "${GREEN}PASSED${RESET} ${s}"
else else
echo -e "${RED}FAILED${RESET} ${s}" echo -e "${RED}FAILED${RESET} ${s}"

View File

@ -20,6 +20,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"os/user"
"path/filepath" "path/filepath"
"reflect" "reflect"
"testing" "testing"
@ -41,6 +42,33 @@ func SetupFiles(path string, files map[string]string) error {
return nil return nil
} }
type CurrentUser struct {
*user.User
PrimaryGroup string
}
func GetCurrentUser(t *testing.T) CurrentUser {
currentUser, err := user.Current()
if err != nil {
t.Fatalf("Cannot get current user: %s", err)
}
groups, err := currentUser.GroupIds()
if err != nil || len(groups) == 0 {
t.Fatalf("Cannot get groups for current user: %s", err)
}
primaryGroupObj, err := user.LookupGroupId(groups[0])
if err != nil {
t.Fatalf("Could not lookup name of group %s: %s", groups[0], err)
}
primaryGroup := primaryGroupObj.Name
return CurrentUser{
User: currentUser,
PrimaryGroup: primaryGroup,
}
}
func CheckDeepEqual(t *testing.T, expected, actual interface{}) { func CheckDeepEqual(t *testing.T, expected, actual interface{}) {
t.Helper() t.Helper()
if diff := cmp.Diff(actual, expected); diff != "" { if diff := cmp.Diff(actual, expected); diff != "" {