diff options
author | Dan Duvall <dduvall@wikimedia.org> | 2018-02-21 16:18:47 -0800 |
---|---|---|
committer | Dan Duvall <dduvall@wikimedia.org> | 2018-03-05 13:22:10 -0800 |
commit | 47526283fea7df1734ef5b9a5da5c810bf76a29a (patch) | |
tree | 7d6ed2530acdaded093f410939aa9df250130c0a /build | |
parent | f606d212fd94769294b1ebdaa6ec224458281d22 (diff) | |
download | blubber-47526283fea7df1734ef5b9a5da5c810bf76a29a.tar.gz |
Fix application files/runtime permissions scheme
Summary:
Introduces new `lives` configuration that provides the name/UID/GID of
the user that will own application files and installed dependencies.
This new configuration is distinct from `runs` in that the former
determines application file location ownership and the latter now only
determines runtime process ownership. Default configuration has also
been introduced for both config sections.
In addition to the new configuration, a new `build.CopyAs` instruction
has been introduced that ensures correct UID/GID ownership of files
copied into the container image, and all unqualified `build.Copy`
instructions are wrapped by the new `build.CopyAs` instruction using the
UID/GID appropriate for the current build phase. A new `build.User`
instruction is also introduced and injected into the build at the start
of certain phases to enforce ownership of `build.Run` processes.
This effective process/file ownership model is:
PhasePrivileged - "root"
PhasePrivilegedDropped - lives.as
PhasePreInstall - lives.as
PhaseInstall - lives.as
PhasePostInstall - runs.as
Fixes T187372
Test Plan: Run `go test ./...`.
Reviewers: thcipriani, hashar, demon, #release-engineering-team
Reviewed By: thcipriani, #release-engineering-team
Subscribers: mmodell
Tags: #release-engineering-team
Maniphest Tasks: T187372
Differential Revision: https://phabricator.wikimedia.org/D984
Diffstat (limited to 'build')
-rw-r--r-- | build/instructions.go | 29 | ||||
-rw-r--r-- | build/instructions_test.go | 12 | ||||
-rw-r--r-- | build/macros.go | 60 | ||||
-rw-r--r-- | build/macros_test.go | 61 |
4 files changed, 162 insertions, 0 deletions
diff --git a/build/instructions.go b/build/instructions.go index 3035b9f..91caa6b 100644 --- a/build/instructions.go +++ b/build/instructions.go @@ -78,6 +78,22 @@ func (copy Copy) Compile() []string { return append(quoteAll(copy.Sources), quote(copy.Destination)) } +// CopyAs is a concrete build instruction for copying source +// files/directories and setting their ownership to the given UID/GID. +// +type CopyAs struct { + UID uint // owner UID + GID uint // owner GID + Copy +} + +// Compile returns the variant name unquoted and all quoted CopyAs instruction +// fields. +// +func (ca CopyAs) Compile() []string { + return append([]string{fmt.Sprintf("%d:%d", ca.UID, ca.GID)}, ca.Copy.Compile()...) +} + // CopyFrom is a concrete build instruction for copying source // files/directories from one variant image to another. // @@ -121,6 +137,19 @@ func (label Label) Compile() []string { return compileSortedKeyValues(label.Definitions) } +// User is a build instruction for setting which user will run future +// commands. +// +type User struct { + Name string // user name +} + +// Compile returns the quoted user name. +// +func (user User) Compile() []string { + return []string{quote(user.Name)} +} + // Volume is a concrete build instruction for defining a volume mount point // within the container. // diff --git a/build/instructions_test.go b/build/instructions_test.go index 4098711..f6e3628 100644 --- a/build/instructions_test.go +++ b/build/instructions_test.go @@ -36,6 +36,12 @@ func TestCopy(t *testing.T) { assert.Equal(t, []string{`"source1"`, `"source2"`, `"dest"`}, i.Compile()) } +func TestCopyAs(t *testing.T) { + i := build.CopyAs{123, 124, build.Copy{[]string{"source1", "source2"}, "dest"}} + + assert.Equal(t, []string{"123:124", `"source1"`, `"source2"`, `"dest"`}, i.Compile()) +} + func TestCopyFrom(t *testing.T) { i := build.CopyFrom{"foo", build.Copy{[]string{"source1", "source2"}, "dest"}} @@ -70,6 +76,12 @@ func TestLabel(t *testing.T) { }, i.Compile()) } +func TestUser(t *testing.T) { + i := build.User{"foo"} + + assert.Equal(t, []string{`"foo"`}, i.Compile()) +} + func TestVolume(t *testing.T) { i := build.Volume{"/foo/dir"} diff --git a/build/macros.go b/build/macros.go new file mode 100644 index 0000000..5d3422e --- /dev/null +++ b/build/macros.go @@ -0,0 +1,60 @@ +package build + +import ( + "fmt" +) + +// ApplyUser wraps any build.Copy instructions as build.CopyAs using the given +// UID/GID. +// +func ApplyUser(uid uint, gid uint, instructions []Instruction) []Instruction { + applied := make([]Instruction, len(instructions)) + + for i, instruction := range instructions { + if copy, iscopy := instruction.(Copy); iscopy { + applied[i] = CopyAs{uid, gid, copy} + } else { + applied[i] = instruction + } + } + + return applied +} + +// Chown returns a build.Run instruction for setting ownership on the given +// path. +// +func Chown(uid uint, gid uint, path string) Run { + return Run{"chown %s:%s", []string{fmt.Sprint(uid), fmt.Sprint(gid), path}} +} + +// CreateDirectory returns a build.Run instruction for creating the given +// directory. +// +func CreateDirectory(path string) Run { + return Run{"mkdir -p", []string{path}} +} + +// CreateUser returns build.Run instructions for creating the given user +// account and group. +// +func CreateUser(name string, uid uint, gid uint) []Run { + return []Run{ + {"groupadd -o -g %s -r", []string{fmt.Sprint(gid), name}}, + {"useradd -o -m -d %s -r -g %s -u %s", []string{homeDir(name), name, fmt.Sprint(uid), name}}, + } +} + +// Home returns a build.Env instruction for setting the user's home directory. +// +func Home(name string) Env { + return Env{map[string]string{"HOME": homeDir(name)}} +} + +func homeDir(name string) string { + if name == "root" { + return "/root" + } + + return "/home/" + name +} diff --git a/build/macros_test.go b/build/macros_test.go new file mode 100644 index 0000000..e47cf8d --- /dev/null +++ b/build/macros_test.go @@ -0,0 +1,61 @@ +package build_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "phabricator.wikimedia.org/source/blubber/build" +) + +func TestApplyUser(t *testing.T) { + instructions := []build.Instruction{ + build.Copy{[]string{"foo"}, "bar"}, + build.Copy{[]string{"baz"}, "qux"}, + } + + assert.Equal(t, + []build.Instruction{ + build.CopyAs{123, 223, build.Copy{[]string{"foo"}, "bar"}}, + build.CopyAs{123, 223, build.Copy{[]string{"baz"}, "qux"}}, + }, + build.ApplyUser(123, 223, instructions), + ) +} + +func TestChown(t *testing.T) { + i := build.Chown(123, 124, "/foo") + + assert.Equal(t, []string{`chown "123":"124" "/foo"`}, i.Compile()) +} + +func TestCreateDirectory(t *testing.T) { + i := build.CreateDirectory("/foo") + + assert.Equal(t, []string{`mkdir -p "/foo"`}, i.Compile()) +} + +func TestCreateUser(t *testing.T) { + i := build.CreateUser("foo", 123, 124) + + if assert.Len(t, i, 2) { + assert.Equal(t, []string{`groupadd -o -g "124" -r "foo"`}, i[0].Compile()) + assert.Equal(t, []string{`useradd -o -m -d "/home/foo" -r -g "foo" -u "123" "foo"`}, i[1].Compile()) + } +} + +func TestHome(t *testing.T) { + t.Run("root", func(t *testing.T) { + assert.Equal(t, + build.Env{map[string]string{"HOME": "/root"}}, + build.Home("root"), + ) + }) + + t.Run("non-root", func(t *testing.T) { + assert.Equal(t, + build.Env{map[string]string{"HOME": "/home/foo"}}, + build.Home("foo"), + ) + }) +} |