veyron/services/node/impl/node*: helper has uname associations

This change adds support to the node manager to manage associations between
Veyron identities and UNIX usernames and to use this association when
invoking apps via the suidhelper.

Change-Id: Ie0435a3d8aec1593a7910c542cf02f17e39c5bc3
diff --git a/services/mgmt/node/impl/app_invoker.go b/services/mgmt/node/impl/app_invoker.go
index 7dc6543..6c2ab54 100644
--- a/services/mgmt/node/impl/app_invoker.go
+++ b/services/mgmt/node/impl/app_invoker.go
@@ -105,6 +105,7 @@
 	"veyron.io/veyron/veyron2/services/mgmt/application"
 	"veyron.io/veyron/veyron2/services/mounttable"
 	"veyron.io/veyron/veyron2/services/mounttable/types"
+	"veyron.io/veyron/veyron2/verror"
 	"veyron.io/veyron/veyron2/vlog"
 
 	vexec "veyron.io/veyron/veyron/lib/exec"
@@ -153,6 +154,7 @@
 	// suffix.  It is used to identify an application, installation, or
 	// instance.
 	suffix []string
+	uat    systemNameIdentityAssociation
 }
 
 func saveEnvelope(dir string, envelope *application.Envelope) error {
@@ -387,9 +389,52 @@
 	return instanceDir, instanceID, nil
 }
 
+// isSetuid is defined like this so we can override its
+// implementation for tests.
+var isSetuid = func(fileStat os.FileInfo) bool {
+	vlog.VI(2).Infof("running the original isSetuid")
+	return fileStat.Mode()&os.ModeSetuid == os.ModeSetuid
+}
+
+// systemAccountForHelper returns the uname that the helper uses to invoke the
+// application. If the helper exists and is setuid, the node manager
+// requires that there is a uname associated with the Veyron
+// identity that requested starting an application.
+// TODO(rjkroege): This function assumes a desktop installation target
+// and is probably not a good fit in other contexts. Revisit the design
+// as appropriate. This function also internalizes a decision as to when
+// it is possible to start an application that needs to be made explicit.
+func systemAccountForHelper(helperStat os.FileInfo, identityNames []string, uat systemNameIdentityAssociation) (systemName string, err error) {
+	haveHelper := isSetuid(helperStat)
+	systemName, present := uat.associatedSystemAccount(identityNames)
+
+	switch {
+	case haveHelper && present:
+		return systemName, nil
+	case haveHelper && !present:
+		// The helper is owned by the node manager and installed as setuid root.
+		// Therefore, the node manager must never run an app as itself to
+		// prevent an app trivially granting itself root permissions.
+		// There must be an associated uname for the account in this case.
+		return "", verror.NoAccessf("use of setuid helper requires an associated uname.")
+	case !haveHelper:
+		// When the helper is not setuid, the helper can't change the
+		// app's uid so just run the app as the node manager's uname
+		// whether or not there is an association.
+		vlog.VI(1).Infof("helper not setuid. Node manager will invoke app with its own userid")
+		user, err := user.Current()
+		if err != nil {
+			vlog.Errorf("user.Current() failed: %v", err)
+			return "", errOperationFailed
+		}
+		return user.Username, nil
+	}
+	return "", errOperationFailed
+}
+
 // TODO(rjkroege): Turning on the setuid feature of the suidhelper
 // requires an installer with root permissions to install it in <config.Root>/helper
-func genCmd(instanceDir string, helperPath string) (*exec.Cmd, error) {
+func genCmd(instanceDir string, helperPath string, uat systemNameIdentityAssociation, identityNames []string) (*exec.Cmd, error) {
 	versionLink := filepath.Join(instanceDir, "version")
 	versionDir, err := filepath.EvalSymlinks(versionLink)
 	if err != nil {
@@ -414,18 +459,11 @@
 	cmd := exec.Command(helperPath)
 
 	cmd.Args = append(cmd.Args, "--username")
-	if helperStat.Mode()&os.ModeSetuid == 0 {
-		vlog.Errorf("helper not setuid. Node manager will invoke app with its own userid")
-		user, err := user.Current()
-		if err != nil {
-			vlog.Errorf("user.Current() failed: %v", err)
-			return nil, errOperationFailed
-		}
-		cmd.Args = append(cmd.Args, user.Username)
-	} else {
-		// TODO(rjkroege): Use the username associated with the veyron identity.
-		return nil, errOperationFailed
+	uname, err := systemAccountForHelper(helperStat, identityNames, uat)
+	if err != nil {
+		return nil, err
 	}
+	cmd.Args = append(cmd.Args, uname)
 
 	// TODO(caprita): Also pass in configuration info like NAMESPACE_ROOT to
 	// the app (to point to the device mounttable).
@@ -510,11 +548,11 @@
 	return nil
 }
 
-func (i *appInvoker) run(instanceDir string) error {
+func (i *appInvoker) run(instanceDir string, blessings []string) error {
 	if err := transitionInstance(instanceDir, suspended, starting); err != nil {
 		return err
 	}
-	cmd, err := genCmd(instanceDir, filepath.Join(i.config.Root, "helper"))
+	cmd, err := genCmd(instanceDir, filepath.Join(i.config.Root, "helper"), i.uat, blessings)
 	if err == nil {
 		err = i.startCmd(instanceDir, cmd)
 	}
@@ -525,10 +563,10 @@
 	return transitionInstance(instanceDir, starting, started)
 }
 
-func (i *appInvoker) Start(ipc.ServerContext) ([]string, error) {
+func (i *appInvoker) Start(call ipc.ServerContext) ([]string, error) {
 	instanceDir, instanceID, err := i.newInstance()
 	if err == nil {
-		err = i.run(instanceDir)
+		err = i.run(instanceDir, call.RemoteBlessings().ForContext(call))
 	}
 	if err != nil {
 		cleanupDir(instanceDir)
@@ -551,12 +589,13 @@
 	return instanceDir, nil
 }
 
-func (i *appInvoker) Resume(ipc.ServerContext) error {
+// TODO(rjkroege): Only the original invoking identity may resume an application.
+func (i *appInvoker) Resume(call ipc.ServerContext) error {
 	instanceDir, err := i.instanceDir()
 	if err != nil {
 		return err
 	}
-	return i.run(instanceDir)
+	return i.run(instanceDir, call.RemoteBlessings().ForContext(call))
 }
 
 func stopAppRemotely(appVON string) error {
diff --git a/services/mgmt/node/impl/args_darwin_test.go b/services/mgmt/node/impl/args_darwin_test.go
new file mode 100644
index 0000000..bf4b58d
--- /dev/null
+++ b/services/mgmt/node/impl/args_darwin_test.go
@@ -0,0 +1,5 @@
+package impl_test
+
+const (
+	testUserName = "_uucp"
+)
diff --git a/services/mgmt/node/impl/args_linux_test.go b/services/mgmt/node/impl/args_linux_test.go
new file mode 100644
index 0000000..07c557f
--- /dev/null
+++ b/services/mgmt/node/impl/args_linux_test.go
@@ -0,0 +1,5 @@
+package impl_test
+
+const (
+	testUserName = "uucp"
+)
diff --git a/services/mgmt/node/impl/association_state.go b/services/mgmt/node/impl/association_state.go
new file mode 100644
index 0000000..04f04f3
--- /dev/null
+++ b/services/mgmt/node/impl/association_state.go
@@ -0,0 +1,50 @@
+package impl
+
+import (
+	"veyron.io/veyron/veyron2/services/mgmt/node"
+)
+
+// TODO(rjk): Replace this with disk-backed storage in the node
+// manager's directory hierarchy.
+type systemNameIdentityAssociation map[string]string
+
+// associatedUname returns a system name from the identity to system
+// name association store if one exists for any of the listed
+// identities.
+func (u systemNameIdentityAssociation) associatedSystemAccount(identityNames []string) (string, bool) {
+	systemName := ""
+	present := false
+
+	for _, n := range identityNames {
+		if systemName, present = u[n]; present {
+			break
+		}
+	}
+	return systemName, present
+}
+
+func (u systemNameIdentityAssociation) getAssociations() ([]node.Association, error) {
+	assocs := make([]node.Association, 0)
+	for k, v := range u {
+		assocs = append(assocs, node.Association{k, v})
+	}
+	return assocs, nil
+}
+
+func (u systemNameIdentityAssociation) addAssociations(identityNames []string, systemName string) error {
+	for _, n := range identityNames {
+		u[n] = systemName
+	}
+	return nil
+}
+
+func (u systemNameIdentityAssociation) deleteAssociations(identityNames []string) error {
+	for _, n := range identityNames {
+		delete(u, n)
+	}
+	return nil
+}
+
+func newSystemNameIdentityAssociation() systemNameIdentityAssociation {
+	return make(map[string]string)
+}
diff --git a/services/mgmt/node/impl/dispatcher.go b/services/mgmt/node/impl/dispatcher.go
index ed930d9..3631710 100644
--- a/services/mgmt/node/impl/dispatcher.go
+++ b/services/mgmt/node/impl/dispatcher.go
@@ -49,7 +49,8 @@
 	config *config.State
 	// dispatcherMutex is a lock for coordinating concurrent access to some
 	// dispatcher methods.
-	mu sync.RWMutex
+	mu  sync.RWMutex
+	uat systemNameIdentityAssociation
 }
 
 var _ ipc.Dispatcher = (*dispatcher)(nil)
@@ -83,6 +84,7 @@
 			updating: newUpdatingState(),
 		},
 		config: config,
+		uat:    newSystemNameIdentityAssociation(),
 	}
 	// If there exists a signed ACL from a previous instance we prefer that.
 	aclFile, sigFile, _ := d.getACLFilePaths()
@@ -235,6 +237,7 @@
 			updating: d.internal.updating,
 			config:   d.config,
 			disp:     d,
+			uat:      d.uat,
 		})
 		return ipc.ReflectInvoker(receiver), d.auth, nil
 	case appsSuffix:
@@ -242,6 +245,7 @@
 			callback: d.internal.callback,
 			config:   d.config,
 			suffix:   components[1:],
+			uat:      d.uat,
 		})
 		// TODO(caprita,rjkroege): Once we implement per-object ACLs
 		// (i.e. each installation and instance), replace d.auth with
diff --git a/services/mgmt/node/impl/impl_test.go b/services/mgmt/node/impl/impl_test.go
index b3012f5..83b9587 100644
--- a/services/mgmt/node/impl/impl_test.go
+++ b/services/mgmt/node/impl/impl_test.go
@@ -47,7 +47,7 @@
 // TestHelperProcess is blackbox boilerplate.
 func TestHelperProcess(t *testing.T) {
 	// All TestHelperProcess invocations need a Runtime. Create it here.
-	rt.Init()
+	rt.Init(veyron2.ForceNewSecurityModel{})
 
 	// Disable the cache because we will be manipulating/using the namespace
 	// across multiple processes and want predictable behaviour without
@@ -232,7 +232,7 @@
 	output := "#!/bin/bash\n"
 	output += "VEYRON_SUIDHELPER_TEST=1"
 	output += " "
-	output += "exec" + " " + os.Args[0] + " -test.run=TestSuidHelper $*"
+	output += "exec" + " " + os.Args[0] + " " + "-minuid=1" + " " + "-test.run=TestSuidHelper $*"
 	output += "\n"
 
 	vlog.VI(1).Infof("script\n%s", output)
@@ -594,11 +594,7 @@
 	// Start an instance of the app.
 	instance1ID := startApp(t, appID)
 
-	u, err := user.Current()
-	if err != nil {
-		t.Fatalf("user.Current() failed: %v", err)
-	}
-	verifyHelperArgs(t, <-pingCh, u.Username) // Wait until the app pings us that it's ready.
+	verifyHelperArgs(t, <-pingCh, userName(t)) // Wait until the app pings us that it's ready.
 
 	v1EP1 := resolve(t, "appV1", 1)[0]
 
@@ -607,7 +603,7 @@
 	resolveExpectNotFound(t, "appV1")
 
 	resumeApp(t, appID, instance1ID)
-	verifyHelperArgs(t, <-pingCh, u.Username) // Wait until the app pings us that it's ready.
+	verifyHelperArgs(t, <-pingCh, userName(t)) // Wait until the app pings us that it's ready.
 	oldV1EP1 := v1EP1
 	if v1EP1 = resolve(t, "appV1", 1)[0]; v1EP1 == oldV1EP1 {
 		t.Fatalf("Expected a new endpoint for the app after suspend/resume")
@@ -615,7 +611,7 @@
 
 	// Start a second instance.
 	instance2ID := startApp(t, appID)
-	verifyHelperArgs(t, <-pingCh, u.Username) // Wait until the app pings us that it's ready.
+	verifyHelperArgs(t, <-pingCh, userName(t)) // Wait until the app pings us that it's ready.
 
 	// There should be two endpoints mounted as "appV1", one for each
 	// instance of the app.
@@ -661,7 +657,7 @@
 
 	// Resume first instance.
 	resumeApp(t, appID, instance1ID)
-	verifyHelperArgs(t, <-pingCh, u.Username) // Wait until the app pings us that it's ready.
+	verifyHelperArgs(t, <-pingCh, userName(t)) // Wait until the app pings us that it's ready.
 	// Both instances should still be running the first version of the app.
 	// Check that the mounttable contains two endpoints, one of which is
 	// v1EP2.
@@ -685,7 +681,7 @@
 
 	// Start a third instance.
 	instance3ID := startApp(t, appID)
-	verifyHelperArgs(t, <-pingCh, u.Username) // Wait until the app pings us that it's ready.
+	verifyHelperArgs(t, <-pingCh, userName(t)) // Wait until the app pings us that it's ready.
 	resolve(t, "appV2", 1)
 
 	// Stop second instance.
@@ -701,7 +697,7 @@
 
 	// Start a fourth instance.  It should be started from version 1.
 	instance4ID := startApp(t, appID)
-	verifyHelperArgs(t, <-pingCh, u.Username) // Wait until the app pings us that it's ready.
+	verifyHelperArgs(t, <-pingCh, userName(t)) // Wait until the app pings us that it's ready.
 	resolve(t, "appV1", 1)
 	stopApp(t, appID, instance4ID)
 	resolveExpectNotFound(t, "appV1")
@@ -1103,3 +1099,233 @@
 	tsecurity.SetDefaultBlessings(p, b)
 	return nil
 }
+
+// Code to make Association lists sortable.
+type byIdentity []node.Association
+
+func (a byIdentity) Len() int           { return len(a) }
+func (a byIdentity) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+func (a byIdentity) Less(i, j int) bool { return a[i].IdentityName < a[j].IdentityName }
+
+func listAndVerifyAssociations(t *testing.T, stub node.Node, run veyron2.Runtime, expected []node.Association) {
+	assocs, err := stub.ListAssociations(run.NewContext())
+	if err != nil {
+		t.Fatalf("ListAssociations failed %v", err)
+	}
+	sort.Sort(byIdentity(assocs))
+	sort.Sort(byIdentity(expected))
+	if !reflect.DeepEqual(assocs, expected) {
+		t.Fatalf("ListAssociations() got %v, expected  %v", assocs, expected)
+	}
+}
+
+// TODO(rjkroege): Verify that associations persist across restarts
+// once permanent storage is added.
+func TestAccountAssociation(t *testing.T) {
+	defer setupLocalNamespace(t)()
+
+	root, cleanup := setupRootDir(t)
+	defer cleanup()
+
+	var (
+		proot = newRootPrincipal("root")
+		// The two "processes"/runtimes which will act as IPC clients to
+		// the nodemanager process.
+		selfRT  = rt.R()
+		otherRT = newRuntime(t)
+	)
+	defer otherRT.Cleanup()
+	// By default, selfRT and otherRT will have blessings generated based
+	// on the username/machine name running this process. Since these
+	// blessings will appear in test expecations, give them readable
+	// names.
+	if err := setDefaultBlessings(selfRT.Principal(), proot, "self"); err != nil {
+		t.Fatal(err)
+	}
+	if err := setDefaultBlessings(otherRT.Principal(), proot, "other"); err != nil {
+		t.Fatal(err)
+	}
+
+	// Set up the node manager.
+	nm := blackbox.HelperCommand(t, "nodeManager", "nm", root, "unused app repo name", "unused curr link")
+	defer setupChildCommand(nm)()
+	if err := nm.Cmd.Start(); err != nil {
+		t.Fatalf("Start() failed: %v", err)
+	}
+	defer nm.Cleanup()
+	readPID(t, nm)
+
+	nodeStub, err := node.BindNode("nm//nm")
+	if err != nil {
+		t.Fatalf("BindNode failed %v", err)
+	}
+
+	// Attempt to list associations on the node manager without having
+	// claimed it.
+	if list, err := nodeStub.ListAssociations(otherRT.NewContext()); err != nil || list != nil {
+		t.Fatalf("ListAssociations should fail on unclaimed node manager but did not: %v", err)
+	}
+	// self claims the node manager.
+	if err = nodeStub.Claim(selfRT.NewContext(), &granter{p: selfRT.Principal(), extension: "alice"}); err != nil {
+		t.Fatalf("Claim failed: %v", err)
+	}
+
+	vlog.VI(2).Info("Verify that associations start out empty.")
+	listAndVerifyAssociations(t, nodeStub, selfRT, []node.Association(nil))
+
+	if err = nodeStub.AssociateAccount(selfRT.NewContext(), []string{"root/self", "root/other"}, "alice_system_account"); err != nil {
+		t.Fatalf("ListAssociations failed %v", err)
+	}
+	vlog.VI(2).Info("Added association should appear.")
+	listAndVerifyAssociations(t, nodeStub, selfRT, []node.Association{
+		{
+			"root/self",
+			"alice_system_account",
+		},
+		{
+			"root/other",
+			"alice_system_account",
+		},
+	})
+
+	if err = nodeStub.AssociateAccount(selfRT.NewContext(), []string{"root/self", "root/other"}, "alice_other_account"); err != nil {
+		t.Fatalf("AssociateAccount failed %v", err)
+	}
+	vlog.VI(2).Info("Change the associations and the change should appear.")
+	listAndVerifyAssociations(t, nodeStub, selfRT, []node.Association{
+		{
+			"root/self",
+			"alice_other_account",
+		},
+		{
+			"root/other",
+			"alice_other_account",
+		},
+	})
+
+	err = nodeStub.AssociateAccount(selfRT.NewContext(), []string{"root/other"}, "")
+	if err != nil {
+		t.Fatalf("AssociateAccount failed %v", err)
+	}
+	vlog.VI(2).Info("Verify that we can remove an association.")
+	listAndVerifyAssociations(t, nodeStub, selfRT, []node.Association{
+		{
+			"root/self",
+			"alice_other_account",
+		},
+	})
+}
+
+// userName is a helper function to determine the system name that the
+// test is running under.
+func userName(t *testing.T) string {
+	u, err := user.Current()
+	if err != nil {
+		t.Fatalf("user.Current() failed: %v", err)
+	}
+	return u.Username
+}
+
+func TestAppWithSuidHelper(t *testing.T) {
+	// Set up mount table, application, and binary repositories.
+	defer setupLocalNamespace(t)()
+	envelope, cleanup := startApplicationRepository()
+	defer cleanup()
+	defer startBinaryRepository()()
+
+	root, cleanup := setupRootDir(t)
+	defer cleanup()
+
+	var (
+		proot = newRootPrincipal("root")
+		// The two "processes"/runtimes which will act as IPC clients to
+		// the nodemanager process.
+		selfRT  = rt.R()
+		otherRT = newRuntime(t)
+	)
+	defer otherRT.Cleanup()
+
+	// By default, selfRT and otherRT will have blessings generated
+	// based on the username/machine name running this process. Since
+	// these blessings can appear in debugging output, give them
+	// recognizable names.
+	if err := setDefaultBlessings(selfRT.Principal(), proot, "self"); err != nil {
+		t.Fatal(err)
+	}
+	if err := setDefaultBlessings(otherRT.Principal(), proot, "other"); err != nil {
+		t.Fatal(err)
+	}
+
+	// Create a script wrapping the test target that implements
+	// suidhelper.
+	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.
+	nm := blackbox.HelperCommand(t, "nodeManager", "-mocksetuid", "nm", root, "unused app repo name", "unused curr link")
+	defer setupChildCommand(nm)()
+	if err := nm.Cmd.Start(); err != nil {
+		t.Fatalf("Start() failed: %v", err)
+	}
+	defer nm.Cleanup()
+	readPID(t, nm)
+
+	nodeStub, err := node.BindNode("nm//nm")
+	if err != nil {
+		t.Fatalf("BindNode failed %v", err)
+	}
+
+	// Create the local server that the app uses to tell us which system name
+	// the node manager wished to run it as.
+	server, _ := newServer()
+	defer server.Stop()
+	pingCh := make(chan string, 1)
+	if err := server.Serve("pingserver", ipc.LeafDispatcher(pingServerDisp(pingCh), nil)); err != nil {
+		t.Fatalf("Serve(%q, <dispatcher>) failed: %v", "pingserver", err)
+	}
+
+	// Create an envelope for the app.
+	app := blackbox.HelperCommand(t, "app", "appV1")
+	defer setupChildCommandWithBlessing(app, "alice/child")()
+	appTitle := "google naps"
+	*envelope = *envelopeFromCmd(appTitle, app.Cmd)
+
+	// Install and start the app as root/self.
+	appID := installApp(t, selfRT)
+
+	// Claim the nodemanager with selfRT as root/self/alice
+	if err = nodeStub.Claim(selfRT.NewContext(), &granter{p: selfRT.Principal(), extension: "alice"}); err != nil {
+		t.Fatal(err)
+	}
+
+	// Start an instance of the app but this time it should fail: we do
+	// not have an associated uname for the invoking identity.
+	startAppExpectError(t, appID, verror.NoAccess, selfRT)
+
+	// Create an association for selfRT
+	if err = nodeStub.AssociateAccount(selfRT.NewContext(), []string{"root/self"}, testUserName); err != nil {
+		t.Fatalf("AssociateAccount failed %v", err)
+	}
+
+	startApp(t, appID, selfRT)
+	verifyHelperArgs(t, <-pingCh, testUserName) // Wait until the app pings us that it's ready.
+
+	vlog.VI(2).Infof("other attempting to run an app without access. Should fail.")
+	startAppExpectError(t, appID, verror.NoAccess, otherRT)
+
+	// Self will now let other also run apps.
+	if err = nodeStub.AssociateAccount(selfRT.NewContext(), []string{"root/other"}, testUserName); err != nil {
+		t.Fatalf("AssociateAccount failed %v", err)
+	}
+	// Add Start to the ACL list for root/other.
+	newACL := security.ACL{In: map[security.BlessingPattern]security.LabelSet{"root/other/...": security.AllLabels}}
+	if err = nodeStub.SetACL(selfRT.NewContext(), newACL, ""); err != nil {
+		t.Fatalf("SetACL failed %v", err)
+	}
+
+	vlog.VI(2).Infof("other attempting to run an app with access. Should succeed.")
+	startApp(t, appID, otherRT)
+	verifyHelperArgs(t, <-pingCh, testUserName) // Wait until the app pings us that it's ready.
+
+	// TODO(rjkroege): Make sure that all apps have been terminated.
+}
diff --git a/services/mgmt/node/impl/node_invoker.go b/services/mgmt/node/impl/node_invoker.go
index 92bbb80..22a0431 100644
--- a/services/mgmt/node/impl/node_invoker.go
+++ b/services/mgmt/node/impl/node_invoker.go
@@ -49,6 +49,7 @@
 
 	vexec "veyron.io/veyron/veyron/lib/exec"
 	"veyron.io/veyron/veyron/lib/glob"
+	"veyron.io/veyron/veyron/lib/netstate"
 	"veyron.io/veyron/veyron/services/mgmt/node/config"
 	"veyron.io/veyron/veyron/services/mgmt/profile"
 )
@@ -88,6 +89,7 @@
 	callback *callbackState
 	config   *config.State
 	disp     *dispatcher
+	uat      systemNameIdentityAssociation
 }
 
 func (i *nodeInvoker) Claim(call ipc.ServerContext) error {
@@ -400,3 +402,39 @@
 	}
 	return nil
 }
+
+func sameMachineCheck(call ipc.ServerContext) error {
+	switch local, err := netstate.SameMachine(call.RemoteEndpoint().Addr()); {
+	case err != nil:
+		return err
+	case local == false:
+		vlog.Errorf("SameMachine() indicates that endpoint is not on the same node")
+		return errOperationFailed
+	}
+	return nil
+}
+
+// TODO(rjkroege): Make it possible for users on the same system to also
+// associate their accounts with their identities.
+func (i *nodeInvoker) AssociateAccount(call ipc.ServerContext, identityNames []string, accountName string) error {
+	if err := sameMachineCheck(call); err != nil {
+		return err
+	}
+
+	if accountName == "" {
+		return i.uat.deleteAssociations(identityNames)
+	} else {
+		// TODO(rjkroege): Optionally verify here that the required uname is a valid.
+		return i.uat.addAssociations(identityNames, accountName)
+	}
+}
+
+func (i *nodeInvoker) ListAssociations(call ipc.ServerContext) (associations []node.Association, err error) {
+	// Temporary code. Dump this.
+	vlog.VI(2).Infof("ListAssociations given blessings: %v\n", call.RemoteBlessings().ForContext(call))
+
+	if err := sameMachineCheck(call); err != nil {
+		return nil, err
+	}
+	return i.uat.getAssociations()
+}
diff --git a/services/mgmt/node/impl/only_for_test.go b/services/mgmt/node/impl/only_for_test.go
index a0a1d71..3261c42 100644
--- a/services/mgmt/node/impl/only_for_test.go
+++ b/services/mgmt/node/impl/only_for_test.go
@@ -1,6 +1,7 @@
 package impl
 
 import (
+	"flag"
 	"os"
 	"path/filepath"
 
@@ -10,6 +11,8 @@
 // This file contains code in the impl package that we only want built for tests
 // (it exposes public API methods that we don't want to normally expose).
 
+var mockIsSetuid = flag.Bool("mocksetuid", false, "set flag to pretend to have a helper with setuid permissions")
+
 func (c *callbackState) leaking() bool {
 	c.Lock()
 	defer c.Unlock()
@@ -28,4 +31,10 @@
 			vlog.Errorf("Rename(%v, %v) failed: %v", dir, renamed, err)
 		}
 	}
+	isSetuid = possiblyMockIsSetuid
+}
+
+func possiblyMockIsSetuid(fileStat os.FileInfo) bool {
+	vlog.VI(2).Infof("Mock isSetuid is reporting: %v", *mockIsSetuid)
+	return *mockIsSetuid
 }
diff --git a/services/mgmt/node/impl/util_test.go b/services/mgmt/node/impl/util_test.go
index 37d3cfb..106e514 100644
--- a/services/mgmt/node/impl/util_test.go
+++ b/services/mgmt/node/impl/util_test.go
@@ -150,6 +150,14 @@
 // The following set of functions are convenience wrappers around various app
 // management methods.
 
+func ort(opt []veyron2.Runtime) veyron2.Runtime {
+	if len(opt) > 0 {
+		return opt[0]
+	} else {
+		return rt.R()
+	}
+}
+
 func appStub(t *testing.T, nameComponents ...string) node.Application {
 	appsName := "nm//apps"
 	appName := naming.Join(append([]string{appsName}, nameComponents...)...)
@@ -160,16 +168,16 @@
 	return stub
 }
 
-func installApp(t *testing.T) string {
-	appID, err := appStub(t).Install(rt.R().NewContext(), "ar")
+func installApp(t *testing.T, opt ...veyron2.Runtime) string {
+	appID, err := appStub(t).Install(ort(opt).NewContext(), "ar")
 	if err != nil {
 		t.Fatalf("Install failed: %v", err)
 	}
 	return appID
 }
 
-func startAppImpl(t *testing.T, appID string) (string, error) {
-	if instanceIDs, err := appStub(t, appID).Start(rt.R().NewContext()); err != nil {
+func startAppImpl(t *testing.T, appID string, opt []veyron2.Runtime) (string, error) {
+	if instanceIDs, err := appStub(t, appID).Start(ort(opt).NewContext()); err != nil {
 		return "", err
 	} else {
 		if want, got := 1, len(instanceIDs); want != got {
@@ -179,40 +187,40 @@
 	}
 }
 
-func startApp(t *testing.T, appID string) string {
-	instanceID, err := startAppImpl(t, appID)
+func startApp(t *testing.T, appID string, opt ...veyron2.Runtime) string {
+	instanceID, err := startAppImpl(t, appID, opt)
 	if err != nil {
 		t.Fatalf("Start(%v) failed: %v", appID, err)
 	}
 	return instanceID
 }
 
-func startAppExpectError(t *testing.T, appID string, expectedError verror.ID) {
-	if _, err := startAppImpl(t, appID); err == nil || !verror.Is(err, expectedError) {
+func startAppExpectError(t *testing.T, appID string, expectedError verror.ID, opt ...veyron2.Runtime) {
+	if _, err := startAppImpl(t, appID, opt); err == nil || !verror.Is(err, expectedError) {
 		t.Fatalf("Start(%v) expected to fail with %v, got %v instead", appID, expectedError, err)
 	}
 }
 
-func stopApp(t *testing.T, appID, instanceID string) {
-	if err := appStub(t, appID, instanceID).Stop(rt.R().NewContext(), 5); err != nil {
+func stopApp(t *testing.T, appID, instanceID string, opt ...veyron2.Runtime) {
+	if err := appStub(t, appID, instanceID).Stop(ort(opt).NewContext(), 5); err != nil {
 		t.Fatalf("Stop(%v/%v) failed: %v", appID, instanceID, err)
 	}
 }
 
-func suspendApp(t *testing.T, appID, instanceID string) {
-	if err := appStub(t, appID, instanceID).Suspend(rt.R().NewContext()); err != nil {
+func suspendApp(t *testing.T, appID, instanceID string, opt ...veyron2.Runtime) {
+	if err := appStub(t, appID, instanceID).Suspend(ort(opt).NewContext()); err != nil {
 		t.Fatalf("Suspend(%v/%v) failed: %v", appID, instanceID, err)
 	}
 }
 
-func resumeApp(t *testing.T, appID, instanceID string) {
-	if err := appStub(t, appID, instanceID).Resume(rt.R().NewContext()); err != nil {
+func resumeApp(t *testing.T, appID, instanceID string, opt ...veyron2.Runtime) {
+	if err := appStub(t, appID, instanceID).Resume(ort(opt).NewContext()); err != nil {
 		t.Fatalf("Resume(%v/%v) failed: %v", appID, instanceID, err)
 	}
 }
 
-func updateApp(t *testing.T, appID string) {
-	if err := appStub(t, appID).Update(rt.R().NewContext()); err != nil {
+func updateApp(t *testing.T, appID string, opt ...veyron2.Runtime) {
+	if err := appStub(t, appID).Update(ort(opt).NewContext()); err != nil {
 		t.Fatalf("Update(%v) failed: %v", appID, err)
 	}
 }