summaryrefslogtreecommitdiff
path: root/build
diff options
context:
space:
mode:
authorDan Duvall <dduvall@wikimedia.org>2018-02-21 16:18:47 -0800
committerDan Duvall <dduvall@wikimedia.org>2018-03-05 13:22:10 -0800
commit47526283fea7df1734ef5b9a5da5c810bf76a29a (patch)
tree7d6ed2530acdaded093f410939aa9df250130c0a /build
parentf606d212fd94769294b1ebdaa6ec224458281d22 (diff)
downloadblubber-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.go29
-rw-r--r--build/instructions_test.go12
-rw-r--r--build/macros.go60
-rw-r--r--build/macros_test.go61
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"),
+ )
+ })
+}