Skip to content

Commit de6fc15

Browse files
authored
fix: update ownership of user homedir (#238)
Fixes #229 If a user mounts a Docker volume into /home/$USER, Docker will automatically assign permissions root:root to it as the envbuilder container runs as root by default. The resulting container will then have /home/$USER owned by root:root. The user will be unable to write any files there until they manually fix the permissions, which would require root privileges. This PR adds a step to fix ownership of /home/$USER to the uid:gid we get from UserInfo.
1 parent 13e31d1 commit de6fc15

File tree

2 files changed

+80
-6
lines changed

2 files changed

+80
-6
lines changed

envbuilder.go

+24-3
Original file line numberDiff line numberDiff line change
@@ -759,13 +759,34 @@ func Run(ctx context.Context, options Options) error {
759759
//
760760
// We need to change the ownership of the files to the user that will
761761
// be running the init script.
762-
filepath.Walk(options.WorkspaceFolder, func(path string, info os.FileInfo, err error) error {
762+
if chownErr := filepath.Walk(options.WorkspaceFolder, func(path string, _ os.FileInfo, err error) error {
763763
if err != nil {
764764
return err
765765
}
766766
return os.Chown(path, userInfo.uid, userInfo.gid)
767-
})
768-
endStage("👤 Updated the ownership of the workspace!")
767+
}); chownErr != nil {
768+
options.Logger(notcodersdk.LogLevelError, "chown %q: %s", userInfo.user.HomeDir, chownErr.Error())
769+
endStage("⚠️ Failed to the ownership of the workspace, you may need to fix this manually!")
770+
} else {
771+
endStage("👤 Updated the ownership of the workspace!")
772+
}
773+
}
774+
775+
// We may also need to update the ownership of the user homedir.
776+
// Skip this step if the user is root.
777+
if userInfo.uid != 0 {
778+
endStage := startStage("🔄 Updating ownership of %s...", userInfo.user.HomeDir)
779+
if chownErr := filepath.Walk(userInfo.user.HomeDir, func(path string, _ fs.FileInfo, err error) error {
780+
if err != nil {
781+
return err
782+
}
783+
return os.Chown(path, userInfo.uid, userInfo.gid)
784+
}); chownErr != nil {
785+
options.Logger(notcodersdk.LogLevelError, "chown %q: %s", userInfo.user.HomeDir, chownErr.Error())
786+
endStage("⚠️ Failed to update ownership of %s, you may need to fix this manually!", userInfo.user.HomeDir)
787+
} else {
788+
endStage("🏡 Updated ownership of %s!", userInfo.user.HomeDir)
789+
}
769790
}
770791

771792
err = os.MkdirAll(options.WorkspaceFolder, 0o755)

integration/integration_test.go

+56-3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import (
3030
"github.com/docker/docker/api/types"
3131
"github.com/docker/docker/api/types/container"
3232
"github.com/docker/docker/api/types/filters"
33+
"github.com/docker/docker/api/types/mount"
34+
"github.com/docker/docker/api/types/volume"
3335
"github.com/docker/docker/client"
3436
"github.com/docker/docker/pkg/stdcopy"
3537
"github.com/go-git/go-billy/v5/memfs"
@@ -1354,6 +1356,40 @@ COPY --from=a /root/date.txt /date.txt`, testImageAlpine, testImageAlpine),
13541356
})
13551357
}
13561358

1359+
func TestChownHomedir(t *testing.T) {
1360+
t.Parallel()
1361+
1362+
// Ensures that a Git repository with a devcontainer.json is cloned and built.
1363+
srv := createGitServer(t, gitServerOptions{
1364+
files: map[string]string{
1365+
".devcontainer/devcontainer.json": `{
1366+
"name": "Test",
1367+
"build": {
1368+
"dockerfile": "Dockerfile"
1369+
},
1370+
}`,
1371+
".devcontainer/Dockerfile": fmt.Sprintf(`FROM %s
1372+
RUN useradd test \
1373+
--create-home \
1374+
--shell=/bin/bash \
1375+
--uid=1001 \
1376+
--user-group
1377+
USER test
1378+
`, testImageUbuntu), // Note: this isn't reproducible with Alpine for some reason.
1379+
},
1380+
})
1381+
1382+
// Run envbuilder with a Docker volume mounted to homedir
1383+
volName := fmt.Sprintf("%s%d-home", t.Name(), time.Now().Unix())
1384+
ctr, err := runEnvbuilder(t, options{env: []string{
1385+
envbuilderEnv("GIT_URL", srv.URL),
1386+
}, volumes: map[string]string{volName: "/home/test"}})
1387+
require.NoError(t, err)
1388+
1389+
output := execContainer(t, ctr, "stat -c %u:%g /home/test/")
1390+
require.Equal(t, "1001:1001", strings.TrimSpace(output))
1391+
}
1392+
13571393
type setupInMemoryRegistryOpts struct {
13581394
Username string
13591395
Password string
@@ -1465,8 +1501,9 @@ func cleanOldEnvbuilders() {
14651501
}
14661502

14671503
type options struct {
1468-
binds []string
1469-
env []string
1504+
binds []string
1505+
env []string
1506+
volumes map[string]string
14701507
}
14711508

14721509
// runEnvbuilder starts the envbuilder container with the given environment
@@ -1479,6 +1516,21 @@ func runEnvbuilder(t *testing.T, options options) (string, error) {
14791516
t.Cleanup(func() {
14801517
cli.Close()
14811518
})
1519+
mounts := make([]mount.Mount, 0)
1520+
for volName, volPath := range options.volumes {
1521+
mounts = append(mounts, mount.Mount{
1522+
Type: mount.TypeVolume,
1523+
Source: volName,
1524+
Target: volPath,
1525+
})
1526+
_, err = cli.VolumeCreate(ctx, volume.CreateOptions{
1527+
Name: volName,
1528+
})
1529+
require.NoError(t, err)
1530+
t.Cleanup(func() {
1531+
_ = cli.VolumeRemove(ctx, volName, true)
1532+
})
1533+
}
14821534
ctr, err := cli.ContainerCreate(ctx, &container.Config{
14831535
Image: "envbuilder:latest",
14841536
Env: options.env,
@@ -1488,10 +1540,11 @@ func runEnvbuilder(t *testing.T, options options) (string, error) {
14881540
}, &container.HostConfig{
14891541
NetworkMode: container.NetworkMode("host"),
14901542
Binds: options.binds,
1543+
Mounts: mounts,
14911544
}, nil, nil, "")
14921545
require.NoError(t, err)
14931546
t.Cleanup(func() {
1494-
cli.ContainerRemove(ctx, ctr.ID, container.RemoveOptions{
1547+
_ = cli.ContainerRemove(ctx, ctr.ID, container.RemoveOptions{
14951548
RemoveVolumes: true,
14961549
Force: true,
14971550
})

0 commit comments

Comments
 (0)