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)
}