veyron/services/mgmt/node/impl: persist account associations

Serialize blessing to system account names mappings.

Change-Id: I71669a8e95512558696104a8d5450ed65f5e1324
diff --git a/services/mgmt/node/impl/app_invoker.go b/services/mgmt/node/impl/app_invoker.go
index 6c2ab54..1171507 100644
--- a/services/mgmt/node/impl/app_invoker.go
+++ b/services/mgmt/node/impl/app_invoker.go
@@ -154,7 +154,7 @@
 	// suffix.  It is used to identify an application, installation, or
 	// instance.
 	suffix []string
-	uat    systemNameIdentityAssociation
+	uat    BlessingSystemAssociationStore
 }
 
 func saveEnvelope(dir string, envelope *application.Envelope) error {
@@ -404,9 +404,9 @@
 // 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) {
+func systemAccountForHelper(helperStat os.FileInfo, identityNames []string, uat BlessingSystemAssociationStore) (systemName string, err error) {
 	haveHelper := isSetuid(helperStat)
-	systemName, present := uat.associatedSystemAccount(identityNames)
+	systemName, present := uat.SystemAccountForBlessings(identityNames)
 
 	switch {
 	case haveHelper && present:
@@ -434,7 +434,7 @@
 
 // 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, uat systemNameIdentityAssociation, identityNames []string) (*exec.Cmd, error) {
+func genCmd(instanceDir string, helperPath string, uat BlessingSystemAssociationStore, identityNames []string) (*exec.Cmd, error) {
 	versionLink := filepath.Join(instanceDir, "version")
 	versionDir, err := filepath.EvalSymlinks(versionLink)
 	if err != nil {
diff --git a/services/mgmt/node/impl/association_state.go b/services/mgmt/node/impl/association_state.go
index 04f04f3..8fe738f 100644
--- a/services/mgmt/node/impl/association_state.go
+++ b/services/mgmt/node/impl/association_state.go
@@ -1,50 +1,124 @@
 package impl
 
 import (
+	"os"
+	"path/filepath"
 	"veyron.io/veyron/veyron2/services/mgmt/node"
+	"veyron.io/veyron/veyron2/verror"
+
+	"encoding/json"
+	"sync"
 )
 
-// TODO(rjk): Replace this with disk-backed storage in the node
-// manager's directory hierarchy.
-type systemNameIdentityAssociation map[string]string
+// BlessingSystemAssociationStore manages a persisted association between
+// Veyron blessings and system account names.
+type BlessingSystemAssociationStore interface {
+	// SystemAccountForBlessings returns a system name from the blessing to
+	// system name association store if one exists for any of the listed
+	// blessings.
+	SystemAccountForBlessings(blessings []string) (string, bool)
 
-// 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) {
+	// AllBlessingSystemAssociations returns all of the current Blessing to system
+	// account associations.
+	AllBlessingSystemAssociations() ([]node.Association, error)
+
+	// AssociateSystemAccountForBlessings associates the provided systenName with each
+	// provided blessing.
+	AssociateSystemAccountForBlessings(blessings []string, systemName string) error
+
+	// DisassociateSystemAccountForBlessings removes associations for the provided blessings.
+	DisassociateSystemAccountForBlessings(blessings []string) error
+}
+
+type association struct {
+	data     map[string]string
+	filename string
+	sync.Mutex
+}
+
+func (u *association) SystemAccountForBlessings(blessings []string) (string, bool) {
+	u.Lock()
+	defer u.Unlock()
+
 	systemName := ""
 	present := false
 
-	for _, n := range identityNames {
-		if systemName, present = u[n]; present {
+	for _, n := range blessings {
+		if systemName, present = u.data[n]; present {
 			break
 		}
 	}
 	return systemName, present
 }
 
-func (u systemNameIdentityAssociation) getAssociations() ([]node.Association, error) {
+func (u *association) AllBlessingSystemAssociations() ([]node.Association, error) {
+	u.Lock()
+	defer u.Unlock()
 	assocs := make([]node.Association, 0)
-	for k, v := range u {
+
+	for k, v := range u.data {
 		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
+func (u *association) serialize() (err error) {
+	f, err := os.OpenFile(u.filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+	if err != nil {
+		return verror.NoExistf("Could not open association file for writing %s: %v", u.filename, err)
 	}
-	return nil
+	defer func() {
+		if closerr := f.Close(); closerr != nil {
+			err = closerr
+		}
+	}()
+
+	enc := json.NewEncoder(f)
+	return enc.Encode(u.data)
 }
 
-func (u systemNameIdentityAssociation) deleteAssociations(identityNames []string) error {
-	for _, n := range identityNames {
-		delete(u, n)
+func (u *association) AssociateSystemAccountForBlessings(blessings []string, systemName string) error {
+	u.Lock()
+	defer u.Unlock()
+
+	for _, n := range blessings {
+		u.data[n] = systemName
 	}
-	return nil
+	return u.serialize()
 }
 
-func newSystemNameIdentityAssociation() systemNameIdentityAssociation {
-	return make(map[string]string)
+func (u *association) DisassociateSystemAccountForBlessings(blessings []string) error {
+	u.Lock()
+	defer u.Unlock()
+
+	for _, n := range blessings {
+		delete(u.data, n)
+	}
+	return u.serialize()
+}
+
+func NewBlessingSystemAssociationStore(root string) (BlessingSystemAssociationStore, error) {
+	nddir := filepath.Join(root, "node-manager", "node-data")
+	if err := os.MkdirAll(nddir, os.FileMode(0700)); err != nil {
+		return nil, verror.NoExistf("Could not create node-data directory %s: %v\n", nddir, err)
+	}
+	msf := filepath.Join(nddir, "associated.accounts")
+
+	f, err := os.Open(msf)
+	if err != nil && os.IsExist(err) {
+		return nil, verror.NoExistf("Could not open association file %s: %v\n", msf, err)
+
+	}
+	defer f.Close()
+
+	a := &association{filename: msf, data: make(map[string]string)}
+
+	if err == nil {
+		dec := json.NewDecoder(f)
+		err := dec.Decode(&a.data)
+		if err != nil {
+			return nil, verror.NoExistf("Could not read association file %s: %v\n", msf, err)
+		}
+	}
+	return BlessingSystemAssociationStore(a), nil
 }
diff --git a/services/mgmt/node/impl/association_state_test.go b/services/mgmt/node/impl/association_state_test.go
new file mode 100644
index 0000000..4d313dc
--- /dev/null
+++ b/services/mgmt/node/impl/association_state_test.go
@@ -0,0 +1,159 @@
+package impl_test
+
+import (
+	"io"
+	"io/ioutil"
+	"os"
+	"path"
+	"testing"
+	"veyron.io/veyron/veyron/services/mgmt/node/impl"
+	"veyron.io/veyron/veyron2/services/mgmt/node"
+)
+
+// TestAssociationPersistance verifies correct operation of association
+// persistance code.
+func TestAssociationPersistance(t *testing.T) {
+	td, err := ioutil.TempDir("", "nmtest")
+	if err != nil {
+		t.Fatalf("TempDir failed: %v", err)
+	}
+	defer os.RemoveAll(td)
+	nbsa1, err := impl.NewBlessingSystemAssociationStore(td)
+	if err != nil {
+		t.Fatalf("NewBlessingSystemAssociationStore failed: %v", err)
+	}
+
+	// Insert an association.
+	err = nbsa1.AssociateSystemAccountForBlessings([]string{"alice", "bob"}, "alice_account")
+	if err != nil {
+		t.Fatalf("AssociateSystemAccount failed: %v", err)
+	}
+
+	got1, err := nbsa1.AllBlessingSystemAssociations()
+	if err != nil {
+		t.Fatalf("AllBlessingSystemAssociations failed: %v", err)
+	}
+
+	compareAssociations(t, got1, []node.Association{
+		{
+			"alice",
+			"alice_account",
+		},
+		{
+			"bob",
+			"alice_account",
+		},
+	})
+
+	nbsa2, err := impl.NewBlessingSystemAssociationStore(td)
+	if err != nil {
+		t.Fatalf("NewBlessingSystemAssociationStore failed: %v", err)
+	}
+
+	got2, err := nbsa2.AllBlessingSystemAssociations()
+	if err != nil {
+		t.Fatalf("AllBlessingSystemAssociations failed: %v", err)
+	}
+	compareAssociations(t, got1, got2)
+
+	sysacc, have := nbsa2.SystemAccountForBlessings([]string{"bob"})
+	if expected := true; have != expected {
+		t.Fatalf("SystemAccountForBlessings failed. got %v, expected %v", have, expected)
+	}
+	if expected := "alice_account"; sysacc != expected {
+		t.Fatalf("SystemAccountForBlessings failed. got %v, expected %v", sysacc, expected)
+	}
+
+	sysacc, have = nbsa2.SystemAccountForBlessings([]string{"doug"})
+	if expected := false; have != expected {
+		t.Fatalf("SystemAccountForBlessings failed. got %v, expected %v", have, expected)
+	}
+	if expected := ""; sysacc != expected {
+		t.Fatalf("SystemAccountForBlessings failed. got %v, expected %v", sysacc, expected)
+	}
+
+	// Remove "bob".
+	err = nbsa1.DisassociateSystemAccountForBlessings([]string{"bob"})
+	if err != nil {
+		t.Fatalf("DisassociateSystemAccountForBlessings failed: %v", err)
+	}
+
+	// Verify that "bob" has been removed.
+	got1, err = nbsa1.AllBlessingSystemAssociations()
+	if err != nil {
+		t.Fatalf("AllBlessingSystemAssociations failed: %v", err)
+	}
+	compareAssociations(t, got1, []node.Association{
+		{
+			"alice",
+			"alice_account",
+		},
+	})
+
+	err = nbsa1.AssociateSystemAccountForBlessings([]string{"alice", "bob"}, "alice_other_account")
+	if err != nil {
+		t.Fatalf("AssociateSystemAccount failed: %v", err)
+	}
+	// Verify that "bob" and "alice" have new values.
+	got1, err = nbsa1.AllBlessingSystemAssociations()
+	if err != nil {
+		t.Fatalf("AllBlessingSystemAssociations failed: %v", err)
+	}
+	compareAssociations(t, got1, []node.Association{
+		{
+			"alice",
+			"alice_other_account",
+		},
+		{
+			"bob",
+			"alice_other_account",
+		},
+	})
+
+	// Make future serialization attempts fail.
+	if err := os.RemoveAll(td); err != nil {
+		t.Fatalf("os.RemoveAll: couldn't delete %s: %v", td, err)
+	}
+	err = nbsa1.AssociateSystemAccountForBlessings([]string{"doug"}, "alice_account")
+	if err == nil {
+		t.Fatalf("AssociateSystemAccount should have failed but didn't")
+	}
+}
+
+func TestAssociationPersistanceDetectsBadStartingConditions(t *testing.T) {
+	dir := "/i-am-hoping-that-there-is-no-such-directory"
+	nbsa1, err := impl.NewBlessingSystemAssociationStore(dir)
+	if nbsa1 != nil || err == nil {
+		t.Fatalf("bad root directory %s ought to have caused an error", dir)
+	}
+
+	// Create a NewBlessingSystemAssociationStore directory as a side-effect.
+	nbsa1, err = impl.NewBlessingSystemAssociationStore(os.TempDir())
+	defer os.RemoveAll(path.Join(os.TempDir(), "node-manager"))
+	if err != nil {
+		t.Fatalf("NewBlessingSystemAssociationStore failed: %v", err)
+	}
+
+	tpath := path.Join(os.TempDir(), "node-manager", "node-data", "associated.accounts")
+	f, err := os.Create(tpath)
+	if err != nil {
+		t.Fatalf("could not open backing file for setup: %v", err)
+	}
+
+	if _, err := io.WriteString(f, "bad-json\""); err != nil {
+		t.Fatalf("could not write to test file  %s: %v", tpath, err)
+	}
+	f.Close()
+
+	nbsa1, err = impl.NewBlessingSystemAssociationStore(dir)
+	if nbsa1 != nil || err == nil {
+		t.Fatalf("invalid JSON ought to have caused an error")
+	}
+
+	// This test will fail if executed as root or if your system is configured oddly.
+	unreadableFile := "/dev/autofs"
+	nbsa1, err = impl.NewBlessingSystemAssociationStore(unreadableFile)
+	if nbsa1 != nil || err == nil {
+		t.Fatalf("unreadable file %s ought to have caused an error", unreadableFile)
+	}
+}
diff --git a/services/mgmt/node/impl/dispatcher.go b/services/mgmt/node/impl/dispatcher.go
index 3631710..ffc2cf6 100644
--- a/services/mgmt/node/impl/dispatcher.go
+++ b/services/mgmt/node/impl/dispatcher.go
@@ -50,7 +50,7 @@
 	// dispatcherMutex is a lock for coordinating concurrent access to some
 	// dispatcher methods.
 	mu  sync.RWMutex
-	uat systemNameIdentityAssociation
+	uat BlessingSystemAssociationStore
 }
 
 var _ ipc.Dispatcher = (*dispatcher)(nil)
@@ -77,6 +77,10 @@
 	if err := config.Validate(); err != nil {
 		return nil, fmt.Errorf("invalid config %v: %v", config, err)
 	}
+	uat, err := NewBlessingSystemAssociationStore(config.Root)
+	if err != nil {
+		return nil, fmt.Errorf("cannot create persistent store for identity to system account associations: %v", err)
+	}
 	d := &dispatcher{
 		etag: "default",
 		internal: &internalState{
@@ -84,7 +88,7 @@
 			updating: newUpdatingState(),
 		},
 		config: config,
-		uat:    newSystemNameIdentityAssociation(),
+		uat:    uat,
 	}
 	// If there exists a signed ACL from a previous instance we prefer that.
 	aclFile, sigFile, _ := d.getACLFilePaths()
diff --git a/services/mgmt/node/impl/impl_test.go b/services/mgmt/node/impl/impl_test.go
index 48fd309..9fcee59 100644
--- a/services/mgmt/node/impl/impl_test.go
+++ b/services/mgmt/node/impl/impl_test.go
@@ -1077,23 +1077,12 @@
 	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)
-	}
+	compareAssociations(t, assocs, expected)
 }
 
 // TODO(rjkroege): Verify that associations persist across restarts
diff --git a/services/mgmt/node/impl/node_invoker.go b/services/mgmt/node/impl/node_invoker.go
index 22a0431..a751d11 100644
--- a/services/mgmt/node/impl/node_invoker.go
+++ b/services/mgmt/node/impl/node_invoker.go
@@ -14,6 +14,7 @@
 //     node-data/
 //       acl.nodemanager
 //	 acl.signature
+//	 associated.accounts
 //
 // The node manager is always expected to be started through the symbolic link
 // passed in as config.CurrentLink, which is monitored by an init daemon. This
@@ -89,7 +90,7 @@
 	callback *callbackState
 	config   *config.State
 	disp     *dispatcher
-	uat      systemNameIdentityAssociation
+	uat      BlessingSystemAssociationStore
 }
 
 func (i *nodeInvoker) Claim(call ipc.ServerContext) error {
@@ -422,10 +423,10 @@
 	}
 
 	if accountName == "" {
-		return i.uat.deleteAssociations(identityNames)
+		return i.uat.DisassociateSystemAccountForBlessings(identityNames)
 	} else {
 		// TODO(rjkroege): Optionally verify here that the required uname is a valid.
-		return i.uat.addAssociations(identityNames, accountName)
+		return i.uat.AssociateSystemAccountForBlessings(identityNames, accountName)
 	}
 }
 
@@ -436,5 +437,5 @@
 	if err := sameMachineCheck(call); err != nil {
 		return nil, err
 	}
-	return i.uat.getAssociations()
+	return i.uat.AllBlessingSystemAssociations()
 }
diff --git a/services/mgmt/node/impl/util_test.go b/services/mgmt/node/impl/util_test.go
index 92be69a..fd543dd 100644
--- a/services/mgmt/node/impl/util_test.go
+++ b/services/mgmt/node/impl/util_test.go
@@ -5,6 +5,8 @@
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"reflect"
+	"sort"
 	"testing"
 
 	"veyron.io/veyron/veyron2"
@@ -279,3 +281,18 @@
 		t.Fatalf("Uninstall(%v) failed: %v", appID, err)
 	}
 }
+
+// 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 compareAssociations(t *testing.T, got, expected []node.Association) {
+	sort.Sort(byIdentity(got))
+	sort.Sort(byIdentity(expected))
+	if !reflect.DeepEqual(got, expected) {
+		t.Fatalf("ListAssociations() got %v, expected %v", got, expected)
+	}
+}