veyron/services/mgmt/node: make suid helper external to the node manager.

The path to the helper should be provided as a config option to the node manager,
and the node manager's internal organization of its root dir should remain
an implementation detail, hidden from code not in mgmt/node/impl.

Change-Id: I04d1ff66e79a2d15f608c13e746f42a83d5d97bc
diff --git a/services/mgmt/node/config/config.go b/services/mgmt/node/config/config.go
index 52eaae9..e7afa99 100644
--- a/services/mgmt/node/config/config.go
+++ b/services/mgmt/node/config/config.go
@@ -50,6 +50,9 @@
 	// manager is started.  Node manager is expected to mutate this during
 	// a self-update.  Must be non-empty.
 	CurrentLink string
+	// Helper is the path to the setuid helper for running applications as
+	// specific users.
+	Helper string
 }
 
 // Validate checks the config state.
@@ -63,6 +66,9 @@
 	if c.CurrentLink == "" {
 		return fmt.Errorf("CurrentLink cannot be empty")
 	}
+	if c.Helper == "" {
+		return fmt.Errorf("Helper must be specified")
+	}
 	return nil
 }
 
@@ -83,6 +89,7 @@
 		Root:        os.Getenv(RootEnv),
 		Origin:      os.Getenv(OriginEnv),
 		CurrentLink: os.Getenv(CurrentLinkEnv),
+		Helper:      os.Getenv(HelperEnv),
 	}, nil
 }
 
@@ -104,6 +111,7 @@
 		RootEnv:        c.Root,
 		OriginEnv:      c.Origin,
 		CurrentLinkEnv: c.CurrentLink,
+		HelperEnv:      c.Helper,
 	}
 	// We need to manually pass the namespace roots to the child, since we
 	// currently don't have a way for the child to obtain this information
diff --git a/services/mgmt/node/config/config_test.go b/services/mgmt/node/config/config_test.go
index ccaa875..5735b87 100644
--- a/services/mgmt/node/config/config_test.go
+++ b/services/mgmt/node/config/config_test.go
@@ -44,6 +44,7 @@
 		Root:        "fidos/doghouse",
 		Origin:      "pet/store",
 		CurrentLink: currLink,
+		Helper:      "santas/little/helper",
 	}
 	if err := state.Validate(); err != nil {
 		t.Errorf("Config state %v failed to validate: %v", state, err)
@@ -83,6 +84,7 @@
 		Root:        "b",
 		Origin:      "c",
 		CurrentLink: "d",
+		Helper:      "e",
 	}
 	if err := state.Validate(); err != nil {
 		t.Errorf("Config state %v failed to validate: %v", state, err)
@@ -99,6 +101,10 @@
 	if err := state.Validate(); err == nil {
 		t.Errorf("Config state %v should have failed to validate.", state)
 	}
+	state.Name, state.Helper = "anything", ""
+	if err := state.Validate(); err == nil {
+		t.Errorf("Config state %v should have failed to validate.", state)
+	}
 }
 
 // TestQuoteEnv checks the QuoteEnv method.
diff --git a/services/mgmt/node/config/const.go b/services/mgmt/node/config/const.go
index c57cd98..1c5aee5 100644
--- a/services/mgmt/node/config/const.go
+++ b/services/mgmt/node/config/const.go
@@ -18,4 +18,7 @@
 	// CurrentLinkEnv is the name of the environment variable that holds
 	// the path to the soft link that points to the current node manager.
 	CurrentLinkEnv = "VEYRON_NM_CURRENT"
+	// HelperEnv is the name of the environment variable that holds the path
+	// to the suid helper used to start apps as specific system users.
+	HelperEnv = "VEYRON_NM_HELPER"
 )
diff --git a/services/mgmt/node/impl/app_invoker.go b/services/mgmt/node/impl/app_invoker.go
index c359a2a..e14dc1e 100644
--- a/services/mgmt/node/impl/app_invoker.go
+++ b/services/mgmt/node/impl/app_invoker.go
@@ -7,7 +7,6 @@
 // TODO(caprita): Not all is yet implemented.
 //
 // <config.Root>/
-//   helper                         - the setuidhelper binary to invoke an application as a specified user.
 //   app-<hash 1>/                  - the application dir is named using a hash of the application title
 //     installation-<id 1>/         - installations are labelled with ids
 //       <status>                   - one of the values for installationState enum
@@ -34,6 +33,9 @@
 //   app-<hash 2>
 //   ...
 //
+// The node manager uses the suid helper binary to invoke an application as a
+// specified user.  The path to the helper is specified as config.Helper.
+
 // When node manager starts up, it goes through all instances and resumes the
 // ones that are not suspended.  If the application was still running, it
 // suspends it first.  If an application fails to resume, it stays suspended.
@@ -538,12 +540,12 @@
 	return nil
 }
 
-func (i *appInvoker) run(instanceDir, systemName, helper string) error {
+func (i *appInvoker) run(instanceDir, systemName string) error {
 	if err := transitionInstance(instanceDir, suspended, starting); err != nil {
 		return err
 	}
 
-	cmd, err := genCmd(instanceDir, helper, systemName)
+	cmd, err := genCmd(instanceDir, i.config.Helper, systemName)
 	if err == nil {
 		err = i.startCmd(instanceDir, cmd)
 	}
@@ -561,8 +563,7 @@
 		return nil, err
 	}
 
-	helper := filepath.Join(i.config.Root, "helper")
-	systemName, err := systemAccountForHelper(helper, call.RemoteBlessings().ForContext(call), i.uat)
+	systemName, err := systemAccountForHelper(i.config.Helper, call.RemoteBlessings().ForContext(call), i.uat)
 	if err != nil {
 		cleanupDir(instanceDir)
 		return nil, err
@@ -573,7 +574,7 @@
 		return nil, err
 	}
 
-	if err = i.run(instanceDir, systemName, helper); err != nil {
+	if err = i.run(instanceDir, systemName); err != nil {
 		cleanupDir(instanceDir)
 		return nil, err
 	}
@@ -605,8 +606,7 @@
 		return err
 	}
 
-	helper := filepath.Join(i.config.Root, "helper")
-	systemName, err := systemAccountForHelper(helper, call.RemoteBlessings().ForContext(call), i.uat)
+	systemName, err := systemAccountForHelper(i.config.Helper, call.RemoteBlessings().ForContext(call), i.uat)
 	if err != nil {
 		return err
 	}
@@ -619,7 +619,7 @@
 	if startSystemName != systemName {
 		return verror.NoAccessf("Not allowed to resume an application under a different system name.")
 	}
-	return i.run(instanceDir, systemName, helper)
+	return i.run(instanceDir, systemName)
 }
 
 func stopAppRemotely(appVON string) error {
diff --git a/services/mgmt/node/impl/impl_test.go b/services/mgmt/node/impl/impl_test.go
index efc1c5c..e520c98 100644
--- a/services/mgmt/node/impl/impl_test.go
+++ b/services/mgmt/node/impl/impl_test.go
@@ -154,10 +154,10 @@
 	// for example, the node manager is invoked 'by hand' instead of via a
 	// script prepared by a previous version of the node manager.
 	if len(args) > 0 {
-		if want, got := 3, len(args); want != got {
+		if want, got := 4, len(args); want != got {
 			vlog.Fatalf("expected %d additional arguments, got %d instead", want, got)
 		}
-		configState.Root, configState.Origin, configState.CurrentLink = args[0], args[1], args[2]
+		configState.Root, configState.Helper, configState.Origin, configState.CurrentLink = args[0], args[1], args[2], args[3]
 	}
 	dispatcher, err := impl.NewDispatcher(configState)
 	if err != nil {
@@ -274,7 +274,7 @@
 
 // generateSuidHelperScript builds a script to execute the test target as
 // a suidhelper instance and installs it in <root>/helper.
-func generateSuidHelperScript(t *testing.T, root string) {
+func generateSuidHelperScript(t *testing.T, root string) string {
 	output := "#!/bin/bash\n"
 	output += "VEYRON_SUIDHELPER_TEST=1"
 	output += " "
@@ -286,10 +286,13 @@
 	if err := os.MkdirAll(root, 0755); err != nil {
 		t.Fatalf("MkdirAll failed: %v", err)
 	}
-	path := filepath.Join(root, "helper")
+	// Helper does not need to live under the node manager's root dir, but
+	// we put it there for convenience.
+	path := filepath.Join(root, "helper.sh")
 	if err := ioutil.WriteFile(path, []byte(output), 0755); err != nil {
 		t.Fatalf("WriteFile(%v) failed: %v", path, err)
 	}
+	return path
 }
 
 // readPID waits for the "ready:<PID>" line from the child and parses out the
@@ -330,7 +333,7 @@
 
 	crDir, crEnv := credentialsForChild("anyvalue")
 	defer os.RemoveAll(crDir)
-	nmArgs := []string{"factoryNM", root, mockApplicationRepoName, currLink}
+	nmArgs := []string{"factoryNM", root, "unused_helper", mockApplicationRepoName, currLink}
 	args, env := sh.CommandEnvelope(nodeManagerCmd, crEnv, nmArgs...)
 
 	scriptPathFactory := generateNodeManagerScript(t, root, args, env)
@@ -560,14 +563,14 @@
 	defer cleanup()
 
 	// Create a script wrapping the test target that implements suidhelper.
-	generateSuidHelperScript(t, root)
+	helperPath := generateSuidHelperScript(t, root)
 
 	crDir, crEnv := credentialsForChild("anyvalue")
 	defer os.RemoveAll(crDir)
 
 	// Set up the node manager.  Since we won't do node manager updates,
 	// don't worry about its application envelope and current link.
-	nmh, nms := runShellCommand(t, sh, crEnv, nodeManagerCmd, "nm", root, "unused app repo name", "unused curr link")
+	nmh, nms := runShellCommand(t, sh, crEnv, nodeManagerCmd, "nm", root, helperPath, "unused_app_rep_ name", "unused_curr_link")
 	readPID(t, nms)
 
 	// Create the local server that the app uses to let us know it's ready.
@@ -766,9 +769,12 @@
 	crDir, crEnv := credentialsForChild("anyvalue")
 	defer os.RemoveAll(crDir)
 
+	// Create a script wrapping the test target that implements suidhelper.
+	helperPath := generateSuidHelperScript(t, root)
+
 	// Set up the node manager.  Since we won't do node manager updates,
 	// don't worry about its application envelope and current link.
-	_, nms := runShellCommand(t, sh, crEnv, nodeManagerCmd, "nm", root, "unused app repo name", "unused curr link")
+	_, nms := runShellCommand(t, sh, crEnv, nodeManagerCmd, "nm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
 	pid := readPID(t, nms)
 	defer syscall.Kill(pid, syscall.SIGINT)
 
@@ -803,9 +809,6 @@
 		t.Fatalf("Install should have failed from otherRT")
 	}
 
-	// Create a script wrapping the test target that implements suidhelper.
-	generateSuidHelperScript(t, root)
-
 	// Create the local server that the app uses to let us know it's ready.
 	pingCh, cleanup := setupPingServer(t)
 	defer cleanup()
@@ -859,7 +862,7 @@
 
 	// Set up the node manager.  Since we won't do node manager updates,
 	// don't worry about its application envelope and current link.
-	_, nms := runShellCommand(t, sh, crEnv, nodeManagerCmd, "nm", root, "unused app repo name", "unused curr link")
+	_, nms := runShellCommand(t, sh, crEnv, nodeManagerCmd, "nm", root, "unused_helper", "unused_app_repo_name", "unused_curr_link")
 	pid := readPID(t, nms)
 	defer syscall.Kill(pid, syscall.SIGINT)
 
@@ -947,7 +950,7 @@
 	argsForNodeManager = append(argsForNodeManager, "nm")
 
 	// Add vars to instruct the installer how to configure the node manager.
-	installerEnv := []string{config.RootEnv + "=" + root, config.CurrentLinkEnv + "=" + currLink}
+	installerEnv := []string{config.RootEnv + "=" + root, config.CurrentLinkEnv + "=" + currLink, config.HelperEnv + "=" + "unused"}
 	installerh, installers := runShellCommand(t, sh, installerEnv, installerCmd, argsForNodeManager...)
 	installers.ExpectEOF()
 	installerh.Shutdown(os.Stderr, os.Stderr)
@@ -988,15 +991,15 @@
 	crDir, crEnv := credentialsForChild("anyvalue")
 	defer os.RemoveAll(crDir)
 
+	// Create a script wrapping the test target that implements suidhelper.
+	helperPath := generateSuidHelperScript(t, root)
+
 	// Set up the node manager.  Since we won't do node manager updates,
 	// don't worry about its application envelope and current link.
-	_, nms := runShellCommand(t, sh, crEnv, nodeManagerCmd, "nm", root, "unused app repo name", "unused curr link")
+	_, nms := runShellCommand(t, sh, crEnv, nodeManagerCmd, "nm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
 	pid := readPID(t, nms)
 	defer syscall.Kill(pid, syscall.SIGINT)
 
-	// Create a script wrapping the test target that implements suidhelper.
-	generateSuidHelperScript(t, root)
-
 	// Create the local server that the app uses to let us know it's ready.
 	pingCh, cleanup := setupPingServer(t)
 	defer cleanup()
@@ -1141,7 +1144,7 @@
 	crFile, crEnv := credentialsForChild("anyvalue")
 	defer os.RemoveAll(crFile)
 
-	_, nms := runShellCommand(t, sh, crEnv, nodeManagerCmd, "nm", root, "unused app repo name", "unused curr link")
+	_, nms := runShellCommand(t, sh, crEnv, nodeManagerCmd, "nm", root, "unused_helper", "unused_app_repo_name", "unused_curr_link")
 	pid := readPID(t, nms)
 	defer syscall.Kill(pid, syscall.SIGINT)
 
@@ -1253,9 +1256,9 @@
 
 	// Create a script wrapping the test target that implements
 	// suidhelper.
-	generateSuidHelperScript(t, root)
+	helperPath := generateSuidHelperScript(t, root)
 
-	_, nms := runShellCommand(t, sh, crEnv, nodeManagerCmd, "-mocksetuid", "nm", root, "unused app repo name", "unused curr link")
+	_, nms := runShellCommand(t, sh, crEnv, nodeManagerCmd, "-mocksetuid", "nm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
 	pid := readPID(t, nms)
 	defer syscall.Kill(pid, syscall.SIGINT)