veyron/services/mgmt/node: Initial implementation of device claim in the NodeManager

Change-Id: I89b6689cccebc3e3256999027d804a4ccc058a58
diff --git a/security/util.go b/security/util.go
index 541aa90..b709c31 100644
--- a/security/util.go
+++ b/security/util.go
@@ -11,6 +11,13 @@
 
 var nullACL security.ACL
 
+// OpenACL creates an ACL that grants access to all principals.
+func OpenACL() security.ACL {
+	acl := security.ACL{}
+	acl.In = map[security.BlessingPattern]security.LabelSet{security.AllPrincipals: security.AllLabels}
+	return acl
+}
+
 // LoadIdentity reads a PrivateID from r, assuming that it was written using
 // SaveIdentity.
 func LoadIdentity(r io.Reader) (security.PrivateID, error) {
diff --git a/services/mgmt/node/impl/dispatcher.go b/services/mgmt/node/impl/dispatcher.go
index 9d00df3..bf01b56 100644
--- a/services/mgmt/node/impl/dispatcher.go
+++ b/services/mgmt/node/impl/dispatcher.go
@@ -2,15 +2,22 @@
 
 import (
 	"fmt"
+	"os"
+	"path/filepath"
 	"strings"
 
+	vsecurity "veyron/security"
+	vflag "veyron/security/flag"
+	"veyron/security/serialization"
 	inode "veyron/services/mgmt/node"
 	"veyron/services/mgmt/node/config"
 
 	"veyron2/ipc"
+	"veyron2/rt"
 	"veyron2/security"
 	"veyron2/services/mgmt/node"
 	"veyron2/verror"
+	"veyron2/vlog"
 )
 
 // internalState wraps state shared between different node manager
@@ -43,21 +50,92 @@
 	errUpdateNoOp         = verror.NotFoundf("no different version available")
 	errNotExist           = verror.NotFoundf("object does not exist")
 	errInvalidOperation   = verror.BadArgf("invalid operation")
+	errInvalidBlessing    = verror.BadArgf("invalid claim blessing")
 )
 
 // NewDispatcher is the node manager dispatcher factory.
-func NewDispatcher(auth security.Authorizer, config *config.State) (*dispatcher, error) {
+func NewDispatcher(config *config.State) (*dispatcher, error) {
 	if err := config.Validate(); err != nil {
 		return nil, fmt.Errorf("Invalid config %v: %v", config, err)
 	}
-	return &dispatcher{
-		auth: auth,
+	d := &dispatcher{
 		internal: &internalState{
 			callback: newCallbackState(config.Name),
 			updating: newUpdatingState(),
 		},
 		config: config,
-	}, nil
+	}
+	// Prefer ACLs in the nodemanager data directory if they exist.
+	if data, sig, err := d.getACLFiles(os.O_RDONLY); err != nil {
+		if d.auth = vflag.NewAuthorizerOrDie(); d.auth == nil {
+			// If there are no specified ACLs we grant nodemanager access to all
+			// principal until it is claimed.
+			d.auth = vsecurity.NewACLAuthorizer(vsecurity.OpenACL())
+		}
+	} else {
+		defer data.Close()
+		defer sig.Close()
+		reader, err := serialization.NewVerifyingReader(data, sig, rt.R().Identity().PublicKey())
+		if err != nil {
+			return nil, fmt.Errorf("Failed to read nodemanager ACL file:%v", err)
+		}
+		acl, err := vsecurity.LoadACL(reader)
+		if err != nil {
+			return nil, fmt.Errorf("Failed to load nodemanager ACL:%v", err)
+		}
+		d.auth = vsecurity.NewACLAuthorizer(acl)
+	}
+	return d, nil
+}
+
+func (d *dispatcher) getACLFiles(flag int) (aclData *os.File, aclSig *os.File, err error) {
+	nodedata := filepath.Join(d.config.Root, "node-manager", "node-data")
+	perm := os.FileMode(0700)
+	if err = os.MkdirAll(nodedata, perm); err != nil {
+		return
+	}
+	if aclData, err = os.OpenFile(filepath.Join(nodedata, "acl.nodemanager"), flag, perm); err != nil {
+		return
+	}
+	if aclSig, err = os.OpenFile(filepath.Join(nodedata, "acl.signature"), flag, perm); err != nil {
+		return
+	}
+	return
+}
+
+func (d *dispatcher) claimNodeManager(id security.PublicID) error {
+	// TODO(gauthamt): Should we start trusting these identity providers?
+	if id.Names() == nil {
+		vlog.Errorf("Identity provider for device claimer is not trusted")
+		return errOperationFailed
+	}
+	rt.R().PublicIDStore().Add(id, security.AllPrincipals)
+	// Create ACLs to transfer nodemanager permissions to the provided identity.
+	acl := security.ACL{In: make(map[security.BlessingPattern]security.LabelSet)}
+	for _, name := range id.Names() {
+		acl.In[security.BlessingPattern(name)] = security.AllLabels
+	}
+	d.auth = vsecurity.NewACLAuthorizer(acl)
+	// Write out the ACLs so that it will persist across restarts.
+	data, sig, err := d.getACLFiles(os.O_CREATE | os.O_RDWR)
+	if err != nil {
+		vlog.Errorf("Failed to create ACL files:%v", err)
+		return errOperationFailed
+	}
+	writer, err := serialization.NewSigningWriteCloser(data, sig, rt.R().Identity(), nil)
+	if err != nil {
+		vlog.Errorf("Failed to create NewSigningWriteCloser:%v", err)
+		return errOperationFailed
+	}
+	if err = vsecurity.SaveACL(writer, acl); err != nil {
+		vlog.Errorf("Failed to SaveACL:%v", err)
+		return errOperationFailed
+	}
+	if err = writer.Close(); err != nil {
+		vlog.Errorf("Failed to Close() SigningWriteCloser:%v", err)
+		return errOperationFailed
+	}
+	return nil
 }
 
 // DISPATCHER INTERFACE IMPLEMENTATION
@@ -83,6 +161,7 @@
 			callback: d.internal.callback,
 			updating: d.internal.updating,
 			config:   d.config,
+			disp:     d,
 		})
 	case appsSuffix:
 		receiver = node.NewServerApplication(&appInvoker{
diff --git a/services/mgmt/node/impl/impl_test.go b/services/mgmt/node/impl/impl_test.go
index 6d217b2..2dd443f 100644
--- a/services/mgmt/node/impl/impl_test.go
+++ b/services/mgmt/node/impl/impl_test.go
@@ -12,16 +12,20 @@
 	"strings"
 	"syscall"
 	"testing"
+	"time"
 
 	"veyron/lib/signals"
 	"veyron/lib/testutil/blackbox"
+	tsecurity "veyron/lib/testutil/security"
 	"veyron/services/mgmt/lib/exec"
 	"veyron/services/mgmt/node/config"
 	"veyron/services/mgmt/node/impl"
 
+	"veyron2"
 	"veyron2/ipc"
 	"veyron2/naming"
 	"veyron2/rt"
+	"veyron2/security"
 	"veyron2/services/mgmt/application"
 	"veyron2/services/mgmt/node"
 	"veyron2/verror"
@@ -105,7 +109,7 @@
 		configState.Root, configState.Origin, configState.CurrentLink = args[0], args[1], args[2]
 	}
 
-	dispatcher, err := impl.NewDispatcher(nil, configState)
+	dispatcher, err := impl.NewDispatcher(configState)
 	if err != nil {
 		vlog.Fatalf("Failed to create node manager dispatcher: %v", err)
 	}
@@ -597,3 +601,105 @@
 	nm.Expect("nm terminating")
 	nm.ExpectEOFAndWait()
 }
+
+type granter struct {
+	ipc.CallOpt
+	self security.PrivateID
+}
+
+func (g granter) Grant(id security.PublicID) (security.PublicID, error) {
+	return g.self.Bless(id, "claimernode", 10*time.Minute, nil)
+}
+
+func newRuntimeClient(t *testing.T, id security.PrivateID) (veyron2.Runtime, ipc.Client) {
+	runtime, err := rt.New(veyron2.RuntimeID(id))
+	if err != nil {
+		t.Fatalf("rt.New() failed: %v", err)
+	}
+	runtime.Namespace().SetRoots(rt.R().Namespace().Roots()[0])
+	nodeClient, err := runtime.NewClient()
+	if err != nil {
+		t.Fatalf("rt.NewClient() failed %v", err)
+	}
+	return runtime, nodeClient
+}
+
+func tryInstall(rt veyron2.Runtime, c ipc.Client) error {
+	appsName := "nm//apps"
+	stub, err := node.BindApplication(appsName, c)
+	if err != nil {
+		return fmt.Errorf("BindApplication(%v) failed: %v", appsName, err)
+	}
+	if _, err = stub.Install(rt.NewContext(), "ar"); err != nil {
+		return fmt.Errorf("Install failed: %v", err)
+	}
+	return nil
+}
+
+// TestNodeManagerClaim claims a nodemanager and tests ACL permissions on its methods.
+func TestNodeManagerClaim(t *testing.T) {
+	// Set up mount table, application, and binary repositories.
+	defer setupLocalNamespace(t)()
+	envelope, cleanup := startApplicationRepository()
+	defer cleanup()
+	defer startBinaryRepository()()
+
+	root, cleanup := setupRootDir()
+	defer cleanup()
+
+	// 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", "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)
+
+	// Create an envelope for an app.
+	app := blackbox.HelperCommand(t, "app", "app1")
+	defer setupChildCommand(app)()
+	appTitle := "google naps"
+	*envelope = *envelopeFromCmd(appTitle, app.Cmd)
+
+	nodeStub, err := node.BindNode("nm//nm")
+	if err != nil {
+		t.Fatalf("BindNode failed: %v", err)
+	}
+
+	// Create a new identity and runtime.
+	newIdentity := tsecurity.NewBlessedIdentity(rt.R().Identity(), "claimer")
+	newRT, nodeClient := newRuntimeClient(t, newIdentity)
+	defer newRT.Cleanup()
+
+	// Nodemanager should have open ACLs before we claim it and so an Install
+	// should succeed.
+	if err = tryInstall(newRT, nodeClient); err != nil {
+		t.Fatalf("%v", err)
+	}
+	// Claim the nodemanager with this identity.
+	if err = nodeStub.Claim(rt.R().NewContext(), granter{self: newIdentity}); err != nil {
+		t.Fatalf("Claim failed: %v", err)
+	}
+	if err = tryInstall(newRT, nodeClient); err != nil {
+		t.Fatalf("%v", err)
+	}
+	// Try to install with a new identity. This should fail.
+	newIdentity = tsecurity.NewBlessedIdentity(rt.R().Identity(), "random")
+	newRT, nodeClient = newRuntimeClient(t, newIdentity)
+	defer newRT.Cleanup()
+	if err = tryInstall(newRT, nodeClient); err == nil {
+		t.Fatalf("Install should have failed with random identity")
+	}
+	// Try to install with the original identity. This should still work as the original identity
+	// name is a prefix of the identity used by newRT.
+	nodeClient, err = rt.R().NewClient()
+	if err != nil {
+		t.Fatalf("rt.NewClient() failed %v", err)
+	}
+	if err = tryInstall(rt.R(), nodeClient); err != nil {
+		t.Fatalf("%v", err)
+	}
+	// TODO(gauthamt): Test that ACLs persist across nodemanager restarts
+}
diff --git a/services/mgmt/node/impl/node_invoker.go b/services/mgmt/node/impl/node_invoker.go
index 02d87a6..0557d7e 100644
--- a/services/mgmt/node/impl/node_invoker.go
+++ b/services/mgmt/node/impl/node_invoker.go
@@ -11,6 +11,9 @@
 //       noded.sh              - a shell script to start the binary
 //     <version 2 timestamp>
 //     ...
+//     node-data/
+//       acl.nodemanager
+//	 acl.signature
 //
 // 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
@@ -81,6 +84,16 @@
 	updating *updatingState
 	callback *callbackState
 	config   *iconfig.State
+	disp     *dispatcher
+}
+
+func (i *nodeInvoker) Claim(call ipc.ServerContext) error {
+	// Get the blessing to be used by the claimant
+	blessing := call.Blessing()
+	if blessing == nil {
+		return errInvalidBlessing
+	}
+	return i.disp.claimNodeManager(blessing)
 }
 
 func (*nodeInvoker) Describe(ipc.ServerContext) (node.Description, error) {
diff --git a/services/mgmt/node/noded/main.go b/services/mgmt/node/noded/main.go
index 12e6267..4e2175e 100644
--- a/services/mgmt/node/noded/main.go
+++ b/services/mgmt/node/noded/main.go
@@ -4,7 +4,6 @@
 	"flag"
 
 	"veyron/lib/signals"
-	vflag "veyron/security/flag"
 	"veyron/services/mgmt/node/config"
 	"veyron/services/mgmt/node/impl"
 
@@ -46,7 +45,7 @@
 	// TODO(caprita): We need a way to set config fields outside of the
 	// update mechanism (since that should ideally be an opaque
 	// implementation detail).
-	dispatcher, err := impl.NewDispatcher(vflag.NewAuthorizerOrDie(), configState)
+	dispatcher, err := impl.NewDispatcher(configState)
 	if err != nil {
 		vlog.Fatalf("Failed to create dispatcher: %v", err)
 	}