-
Notifications
You must be signed in to change notification settings - Fork 5
fix(devcontainer): preserve remote user ownership #1135
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "name": "snapshot non-root", | ||
| "build": { | ||
| "dockerfile": "Dockerfile" | ||
| }, | ||
| "remoteUser": "devsyuser", | ||
| "runArgs": ["--add-host=host.docker.internal:host-gateway"], | ||
| "containerEnv": { | ||
| "DEVSY_INSECURE_DOCKER_INTERNAL": "true" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| FROM ghcr.io/devsy-org/test-images/base:ubuntu | ||
|
|
||
| RUN useradd --create-home --shell /bin/bash devsyuser |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |||||||||||||||||||||||||||
| package copy | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||
| "errors" | ||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||
| "os/user" | ||||||||||||||||||||||||||||
| "path/filepath" | ||||||||||||||||||||||||||||
|
|
@@ -231,3 +232,42 @@ func mustReadFile(t *testing.T, path string) []byte { | |||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| return b | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Chowning a file to a different owner requires privileges, so pointing | ||||||||||||||||||||||||||||
| // ChownR at root as an unprivileged user exercises the denied-failure path | ||||||||||||||||||||||||||||
| // deterministically. | ||||||||||||||||||||||||||||
| func TestChownRDeniedFailuresAreTyped(t *testing.T) { | ||||||||||||||||||||||||||||
| root := t.TempDir() | ||||||||||||||||||||||||||||
| file := filepath.Join(root, "f.txt") | ||||||||||||||||||||||||||||
| //nolint:gosec // G306 — test temp file | ||||||||||||||||||||||||||||
| if err := os.WriteFile(file, []byte("x"), 0o600); err != nil { | ||||||||||||||||||||||||||||
| t.Fatalf("write: %v", err) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| err := ChownR(root, "root") | ||||||||||||||||||||||||||||
| var failures ChownFailures | ||||||||||||||||||||||||||||
| if !errors.As(err, &failures) { | ||||||||||||||||||||||||||||
| t.Fatalf("ChownR err = %v, want ChownFailures", err) | ||||||||||||||||||||||||||||
|
Comment on lines
+247
to
+250
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Skip the denied-path test when the process is root. When the test runs as root, Proposed fix func TestChownRDeniedFailuresAreTyped(t *testing.T) {
+ if os.Geteuid() == 0 {
+ t.Skip("requires an unprivileged user")
+ }
root := t.TempDir()📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if !failures.AllDenied() { | ||||||||||||||||||||||||||||
| t.Errorf("AllDenied() = false for %v", failures) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| for _, f := range failures { | ||||||||||||||||||||||||||||
| if !deniedByFilesystem(f.Err) { | ||||||||||||||||||||||||||||
| t.Errorf("%s: unexpected cause %v", f.Path, f.Err) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| func TestChownRSameOwnerSucceeds(t *testing.T) { | ||||||||||||||||||||||||||||
| root := t.TempDir() | ||||||||||||||||||||||||||||
| file := filepath.Join(root, "f.txt") | ||||||||||||||||||||||||||||
| //nolint:gosec // G306 — test temp file | ||||||||||||||||||||||||||||
| if err := os.WriteFile(file, []byte("x"), 0o600); err != nil { | ||||||||||||||||||||||||||||
| t.Fatalf("write: %v", err) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if err := ChownR(root, currentUserName(t)); err != nil { | ||||||||||||||||||||||||||||
| t.Fatalf("ChownR same owner: %v", err) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Persist the resolved remote user in snapshots. Snapshot creation currently stores only
MergedConfig.RemoteUser, while the effective user may come fromcontainerUser,devsy.user, Docker-inspectedUser, or the root fallback. For those configurations, restore receives an empty remote user and does not preserve the effective runtime user, so restored containers can run as root and recreate workspace ownership failures. Storedevcontainerconfig.GetRemoteUser(result)when building the manifest, and add coverage for acontainerUser-only workspace.📍 Affects 2 files
cmd/snapshot/create.go#L346-L346(this comment)pkg/snapshot/manifest.go#L148-L150🤖 Prompt for AI Agents