veyron/services/mgmt/application: add support for per envelope acls

This change extends the applicationd daemon with per envelope ACLs.

Change-Id: Ib0ab7efa39f82723418e3f959877d05e6e22eb08
diff --git a/services/mgmt/application/applicationd/main.go b/services/mgmt/application/applicationd/main.go
index 1a4b6c6..a09993e 100644
--- a/services/mgmt/application/applicationd/main.go
+++ b/services/mgmt/application/applicationd/main.go
@@ -9,7 +9,6 @@
 
 	"v.io/core/veyron/lib/signals"
 	"v.io/core/veyron/profiles/roaming"
-	vflag "v.io/core/veyron/security/flag"
 	"v.io/core/veyron/services/mgmt/application/impl"
 )
 
@@ -37,7 +36,7 @@
 	}
 	defer server.Stop()
 
-	dispatcher, err := impl.NewDispatcher(*store, vflag.NewAuthorizerOrDie())
+	dispatcher, err := impl.NewDispatcher(*store)
 	if err != nil {
 		vlog.Fatalf("NewDispatcher() failed: %v", err)
 	}
diff --git a/services/mgmt/application/impl/acl_test.go b/services/mgmt/application/impl/acl_test.go
new file mode 100644
index 0000000..9c0d6ab
--- /dev/null
+++ b/services/mgmt/application/impl/acl_test.go
@@ -0,0 +1,413 @@
+package impl_test
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"os"
+	"reflect"
+	"syscall"
+	"testing"
+
+	"v.io/core/veyron2"
+	"v.io/core/veyron2/context"
+	"v.io/core/veyron2/naming"
+	"v.io/core/veyron2/rt"
+	"v.io/core/veyron2/security"
+	"v.io/core/veyron2/services/mgmt/application"
+	"v.io/core/veyron2/services/security/access"
+	"v.io/core/veyron2/vdl/vdlutil"
+	"v.io/core/veyron2/verror"
+	"v.io/core/veyron2/vlog"
+
+	"v.io/core/veyron/lib/modules"
+	"v.io/core/veyron/lib/signals"
+	"v.io/core/veyron/lib/testutil"
+	tsecurity "v.io/core/veyron/lib/testutil/security"
+	"v.io/core/veyron/services/mgmt/application/impl"
+	mgmttest "v.io/core/veyron/services/mgmt/lib/testutil"
+	"v.io/core/veyron/services/mgmt/repository"
+)
+
+const (
+	repoCmd = "repository"
+)
+
+var globalRT veyron2.Runtime
+var globalCtx *context.T
+
+// This is also a modules world.
+// Insert necessary code here to be a modules test.
+func init() {
+	// TODO(rjkroege): Remove when vom2 is ready.
+	vdlutil.Register(&naming.VDLMountedServer{})
+
+	modules.RegisterChild(repoCmd, "", appRepository)
+	testutil.Init()
+
+	var err error
+	if globalRT, err = rt.New(); err != nil {
+		panic(err)
+	}
+	globalCtx = globalRT.NewContext()
+	globalRT.Namespace().CacheCtl(naming.DisableCache(true))
+}
+
+// TestHelperProcess is the entrypoint for the modules commands in a
+// a test subprocess.
+func TestHelperProcess(t *testing.T) {
+	modules.DispatchInTest()
+}
+
+func appRepository(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+	args = args[1:]
+	if len(args) < 2 {
+		vlog.Fatalf("repository expected at least name and store arguments and optionally ACL flags per TaggedACLMapFromFlag")
+	}
+	publishName := args[0]
+	storedir := args[1]
+
+	defer fmt.Fprintf(stdout, "%v terminating\n", publishName)
+	defer vlog.VI(1).Infof("%v terminating", publishName)
+	defer globalRT.Cleanup()
+	server, endpoint := mgmttest.NewServer(globalRT)
+	defer server.Stop()
+
+	name := naming.JoinAddressName(endpoint, "")
+	vlog.VI(1).Infof("applicationd name: %v", name)
+
+	dispatcher, err := impl.NewDispatcher(storedir)
+	if err != nil {
+		vlog.Fatalf("Failed to create repository dispatcher: %v", err)
+	}
+	if err := server.ServeDispatcher(publishName, dispatcher); err != nil {
+		vlog.Fatalf("Serve(%v) failed: %v", publishName, err)
+	}
+
+	fmt.Fprintf(stdout, "ready:%d\n", os.Getpid())
+	<-signals.ShutdownOnSignals(globalCtx)
+
+	return nil
+}
+
+func TestApplicationUpdateACL(t *testing.T) {
+	sh, deferFn := mgmttest.CreateShellAndMountTable(t, globalRT)
+	defer deferFn()
+
+	// setup mock up directory to put state in
+	storedir, cleanup := mgmttest.SetupRootDir(t, "application")
+	defer cleanup()
+
+	selfRT := globalRT
+	otherRT := mgmttest.NewRuntime(t, globalRT)
+	defer otherRT.Cleanup()
+	idp := tsecurity.NewIDProvider("root")
+
+	// By default, selfRT and otherRT will have blessings generated based on the
+	// username/machine name running this process. Since these blessings will appear
+	// in ACLs, give them recognizable names.
+	if err := idp.Bless(selfRT.Principal(), "self"); err != nil {
+		t.Fatal(err)
+	}
+	if err := idp.Bless(otherRT.Principal(), "other"); err != nil {
+		t.Fatal(err)
+	}
+
+	crDir, crEnv := mgmttest.CredentialsForChild(globalRT, "repo")
+	defer os.RemoveAll(crDir)
+
+	// Make server credentials derived from the test harness.
+	_, nms := mgmttest.RunShellCommand(t, sh, crEnv, repoCmd, "repo", storedir)
+	pid := mgmttest.ReadPID(t, nms)
+	defer syscall.Kill(pid, syscall.SIGINT)
+
+	otherStub := repository.ApplicationClient("repo/search/v1", otherRT.Client())
+
+	// Create example envelopes.
+	envelopeV1 := application.Envelope{
+		Args:   []string{"--help"},
+		Env:    []string{"DEBUG=1"},
+		Binary: "/veyron/name/of/binary",
+	}
+
+	// Envelope putting as other should fail.
+	// TODO(rjkroege): Validate that it is failed with permission denied.
+	if err := otherStub.Put(otherRT.NewContext(), []string{"base"}, envelopeV1); err == nil {
+		t.Fatalf("Put() wrongly didn't fail")
+	}
+
+	// Envelope putting as self should succeed.
+	selfStub := repository.ApplicationClient("repo/search/v1", selfRT.Client())
+	if err := selfStub.Put(selfRT.NewContext(), []string{"base"}, envelopeV1); err != nil {
+		t.Fatalf("Put() failed: %v", err)
+	}
+
+	selfStub = repository.ApplicationClient("repo", selfRT.Client())
+	acl, etag, err := selfStub.GetACL(selfRT.NewContext())
+	if !verror.Is(err, impl.ErrNotFound.ID) {
+		t.Fatalf("GetACL should have failed with ErrNotFound but was: %v", err)
+	}
+	if etag != "default" {
+		t.Fatalf("getACL expected:default, got:%v(%v)", etag, acl)
+	}
+	if acl != nil {
+		t.Fatalf("GetACL got %v, expected %v", acl, nil)
+	}
+
+	vlog.VI(2).Infof("self attempting to give other permission to update application")
+	newACL := make(access.TaggedACLMap)
+	for _, tag := range access.AllTypicalTags() {
+		newACL.Add("root/self", string(tag))
+		newACL.Add("root/other", string(tag))
+	}
+	if err := selfStub.SetACL(selfRT.NewContext(), newACL, ""); err != nil {
+		t.Fatalf("SetACL failed: %v", err)
+	}
+
+	acl, etag, err = selfStub.GetACL(selfRT.NewContext())
+	if err != nil {
+		t.Fatalf("GetACL should not have failed: %v", err)
+	}
+	expected := newACL
+	if got := acl; !reflect.DeepEqual(expected.Normalize(), got.Normalize()) {
+		t.Errorf("got %#v, exected %#v ", got, expected)
+	}
+
+	// Envelope putting as other should now succeed.
+	if err := otherStub.Put(otherRT.NewContext(), []string{"base"}, envelopeV1); err != nil {
+		t.Fatalf("Put() wrongly failed: %v", err)
+	}
+
+	// Other takes control.
+	otherStub = repository.ApplicationClient("repo/", otherRT.Client())
+	acl, etag, err = otherStub.GetACL(otherRT.NewContext())
+	if err != nil {
+		t.Fatalf("GetACL 2 should not have failed: %v", err)
+	}
+	acl["Admin"] = access.ACL{
+		In:    []security.BlessingPattern{"root/other"},
+		NotIn: []string{}}
+	if err = otherStub.SetACL(otherRT.NewContext(), acl, etag); err != nil {
+		t.Fatalf("SetACL failed: %v", err)
+	}
+
+	// Self is now locked out but other isn't.
+	if _, _, err = selfStub.GetACL(selfRT.NewContext()); err == nil {
+		t.Fatalf("GetACL should not have succeeded")
+	}
+	acl, _, err = otherStub.GetACL(otherRT.NewContext())
+	if err != nil {
+		t.Fatalf("GetACL should not have failed: %v", err)
+	}
+	expected = access.TaggedACLMap{
+		"Admin": access.ACL{
+			In:    []security.BlessingPattern{"root/other"},
+			NotIn: []string{}},
+		"Read": access.ACL{In: []security.BlessingPattern{"root/other",
+			"root/self"},
+			NotIn: []string{}},
+		"Write": access.ACL{In: []security.BlessingPattern{"root/other",
+			"root/self"},
+			NotIn: []string{}},
+		"Debug": access.ACL{In: []security.BlessingPattern{"root/other",
+			"root/self"},
+			NotIn: []string{}},
+		"Resolve": access.ACL{In: []security.BlessingPattern{"root/other",
+			"root/self"},
+			NotIn: []string{}}}
+
+	if got := acl; !reflect.DeepEqual(expected.Normalize(), got.Normalize()) {
+		t.Errorf("got %#v, exected %#v ", got, expected)
+	}
+}
+
+func TestPerAppACL(t *testing.T) {
+	sh, deferFn := mgmttest.CreateShellAndMountTable(t, globalRT)
+	defer deferFn()
+
+	// setup mock up directory to put state in
+	storedir, cleanup := mgmttest.SetupRootDir(t, "application")
+	defer cleanup()
+
+	selfRT := globalRT
+	otherRT := mgmttest.NewRuntime(t, globalRT)
+	defer otherRT.Cleanup()
+	idp := tsecurity.NewIDProvider("root")
+
+	// By default, selfRT and otherRT will have blessings generated based on the
+	// username/machine name running this process. Since these blessings will appear
+	// in ACLs, give them recognizable names.
+	if err := idp.Bless(selfRT.Principal(), "self"); err != nil {
+		t.Fatal(err)
+	}
+	if err := idp.Bless(otherRT.Principal(), "other"); err != nil {
+		t.Fatal(err)
+	}
+
+	crDir, crEnv := mgmttest.CredentialsForChild(globalRT, "repo")
+	defer os.RemoveAll(crDir)
+
+	// Make a server with the same credential as test harness.
+	_, nms := mgmttest.RunShellCommand(t, sh, crEnv, repoCmd, "repo", storedir)
+	pid := mgmttest.ReadPID(t, nms)
+	defer syscall.Kill(pid, syscall.SIGINT)
+
+	// Create example envelope.
+	envelopeV1 := application.Envelope{
+		Args:   []string{"--help"},
+		Env:    []string{"DEBUG=1"},
+		Binary: "/veyron/name/of/binary",
+	}
+
+	// Upload the envelope at two different names.
+	selfStub := repository.ApplicationClient("repo/search/v1", selfRT.Client())
+	if err := selfStub.Put(selfRT.NewContext(), []string{"base"}, envelopeV1); err != nil {
+		t.Fatalf("Put() failed: %v", err)
+	}
+	selfStub = repository.ApplicationClient("repo/search/v2", selfRT.Client())
+	if err := selfStub.Put(selfRT.NewContext(), []string{"base"}, envelopeV1); err != nil {
+		t.Fatalf("Put() failed: %v", err)
+	}
+
+	// Self can access ACLs but other can't.
+	for _, path := range []string{"repo/search", "repo/search/v1", "repo/search/v2"} {
+		selfStub = repository.ApplicationClient(path, selfRT.Client())
+		acl, etag, err := selfStub.GetACL(selfRT.NewContext())
+		if !verror.Is(err, impl.ErrNotFound.ID) {
+			t.Fatalf("GetACL should have failed with ErrNotFound but was: %v", err)
+		}
+		if etag != "default" {
+			t.Fatalf("GetACL expected:default, got:%v(%v)", etag, acl)
+		}
+		if acl != nil {
+			t.Fatalf("GetACL got %v, expected %v", acl, nil)
+		}
+		otherStub := repository.ApplicationClient(path, otherRT.Client())
+		if _, _, err := otherStub.GetACL(otherRT.NewContext()); err == nil {
+			t.Fatalf("GetACL didn't fail for other when it should have.")
+		}
+	}
+
+	// Self gives other full access only to repo/search/v1.
+	selfStub = repository.ApplicationClient("repo/search/v1", selfRT.Client())
+	newACL := make(access.TaggedACLMap)
+	for _, tag := range access.AllTypicalTags() {
+		newACL.Add("root/self", string(tag))
+		newACL.Add("root/other", string(tag))
+	}
+	if err := selfStub.SetACL(selfRT.NewContext(), newACL, ""); err != nil {
+		t.Fatalf("SetACL failed: %v", err)
+	}
+
+	// Other can now access this location.
+	otherStub := repository.ApplicationClient("repo/search/v1", otherRT.Client())
+	acl, _, err := otherStub.GetACL(otherRT.NewContext())
+	if err != nil {
+		t.Fatalf("GetACL should not have failed: %v", err)
+	}
+	expected := access.TaggedACLMap{
+		"Admin": access.ACL{
+			In: []security.BlessingPattern{"root/other",
+				"root/self"},
+			NotIn: []string{}},
+		"Read": access.ACL{In: []security.BlessingPattern{"root/other",
+			"root/self"},
+			NotIn: []string{}},
+		"Write": access.ACL{In: []security.BlessingPattern{"root/other",
+			"root/self"},
+			NotIn: []string{}},
+		"Debug": access.ACL{In: []security.BlessingPattern{"root/other",
+			"root/self"},
+			NotIn: []string{}},
+		"Resolve": access.ACL{In: []security.BlessingPattern{"root/other",
+			"root/self"},
+			NotIn: []string{}}}
+	if got := acl; !reflect.DeepEqual(expected.Normalize(), got.Normalize()) {
+		t.Errorf("got %#v, exected %#v ", got, expected)
+	}
+
+	// But other locations should be unaffected and other cannot access.
+	for _, path := range []string{"repo/search", "repo/search/v2"} {
+		otherStub := repository.ApplicationClient(path, otherRT.Client())
+		if _, _, err := otherStub.GetACL(otherRT.NewContext()); err == nil {
+			t.Fatalf("GetACL didn't fail for other when it should have.")
+		}
+	}
+
+	// Self gives other write perms on base.
+	selfStub = repository.ApplicationClient("repo/", selfRT.Client())
+	newACL = make(access.TaggedACLMap)
+	for _, tag := range access.AllTypicalTags() {
+		newACL.Add("root/self", string(tag))
+	}
+	newACL["Write"] = access.ACL{In: []security.BlessingPattern{"root/other", "root/self"}}
+	if err := selfStub.SetACL(selfRT.NewContext(), newACL, ""); err != nil {
+		t.Fatalf("SetACL failed: %v", err)
+	}
+
+	// Other can now upload an envelope at both locations.
+	for _, path := range []string{"repo/search/v1", "repo/search/v2"} {
+		otherStub = repository.ApplicationClient(path, otherRT.Client())
+		if err := otherStub.Put(otherRT.NewContext(), []string{"base"}, envelopeV1); err != nil {
+			t.Fatalf("Put() failed: %v", err)
+		}
+	}
+
+	// But self didn't give other ACL modification permissions.
+	for _, path := range []string{"repo/search", "repo/search/v2"} {
+		otherStub := repository.ApplicationClient(path, otherRT.Client())
+		if _, _, err := otherStub.GetACL(otherRT.NewContext()); err == nil {
+			t.Fatalf("GetACL didn't fail for other when it should have.")
+		}
+	}
+}
+
+func TestInitialACLSet(t *testing.T) {
+	sh, deferFn := mgmttest.CreateShellAndMountTable(t, globalRT)
+	defer deferFn()
+
+	// Setup mock up directory to put state in.
+	storedir, cleanup := mgmttest.SetupRootDir(t, "application")
+	defer cleanup()
+
+	selfRT := globalRT
+	otherRT := mgmttest.NewRuntime(t, globalRT)
+	defer otherRT.Cleanup()
+	idp := tsecurity.NewIDProvider("root")
+
+	// Make a recognizable principal name.
+	if err := idp.Bless(selfRT.Principal(), "self"); err != nil {
+		t.Fatal(err)
+	}
+	crDir, crEnv := mgmttest.CredentialsForChild(globalRT, "repo")
+	defer os.RemoveAll(crDir)
+
+	// Make an TAM for use on the command line.
+	expected := access.TaggedACLMap{
+		"Admin": access.ACL{
+			In: []security.BlessingPattern{"root/rubberchicken",
+				"root/self"},
+			NotIn: []string{},
+		},
+	}
+
+	b := new(bytes.Buffer)
+	if err := expected.WriteTo(b); err != nil {
+		t.Fatal(err)
+	}
+
+	// Start a server with the same credential as test harness.
+	_, nms := mgmttest.RunShellCommand(t, sh, crEnv, repoCmd, "--veyron.acl.literal", b.String(), "repo", storedir)
+	pid := mgmttest.ReadPID(t, nms)
+	defer syscall.Kill(pid, syscall.SIGINT)
+
+	// It should have the correct starting ACLs from the command line.
+	selfStub := repository.ApplicationClient("repo", selfRT.Client())
+	acl, _, err := selfStub.GetACL(selfRT.NewContext())
+	if err != nil {
+		t.Fatalf("GetACL should not have failed: %v", err)
+	}
+	if got := acl; !reflect.DeepEqual(expected.Normalize(), got.Normalize()) {
+		t.Errorf("got %#v, exected %#v ", got, expected)
+	}
+}
diff --git a/services/mgmt/application/impl/dispatcher.go b/services/mgmt/application/impl/dispatcher.go
index eaaf8d2..a825137 100644
--- a/services/mgmt/application/impl/dispatcher.go
+++ b/services/mgmt/application/impl/dispatcher.go
@@ -3,34 +3,87 @@
 import (
 	"path/filepath"
 
-	"v.io/core/veyron/services/mgmt/repository"
-
-	"v.io/core/veyron/services/mgmt/lib/fs"
-	"v.io/core/veyron2/ipc"
+	"v.io/core/veyron2/naming"
 	"v.io/core/veyron2/security"
+	"v.io/core/veyron2/services/security/access"
+	"v.io/core/veyron2/verror"
+	"v.io/core/veyron2/vlog"
+
+	"v.io/core/veyron/security/flag"
+	"v.io/core/veyron/services/mgmt/lib/fs"
+	"v.io/core/veyron/services/mgmt/repository"
 )
 
 // dispatcher holds the state of the application repository dispatcher.
 type dispatcher struct {
 	store     *fs.Memstore
-	auth      security.Authorizer
 	storeRoot string
 }
 
-var _ ipc.Dispatcher = (*dispatcher)(nil)
-
 // NewDispatcher is the dispatcher factory. storeDir is a path to a directory in which to
 // serialize the applicationd state.
-func NewDispatcher(storeDir string, authorizer security.Authorizer) (*dispatcher, error) {
+func NewDispatcher(storeDir string) (*dispatcher, error) {
 	store, err := fs.NewMemstore(filepath.Join(storeDir, "applicationdstate.db"))
 	if err != nil {
 		return nil, err
 	}
-	return &dispatcher{store: store, storeRoot: storeDir, auth: authorizer}, nil
+
+	acls, err := flag.TaggedACLMapFromFlag()
+	if err != nil {
+		return nil, err
+	}
+	if acls != nil {
+		store.Lock()
+		defer store.Unlock()
+
+		// (Re)set the root ACLs.
+		path := naming.Join("/acls", "data")
+		_, tag, err := getACL(store, path)
+		if !verror.Is(err, ErrNotFound.ID) {
+			return nil, err
+		}
+		if err := setACL(store, path, acls, tag); err != nil {
+			return nil, err
+		}
+	}
+
+	return &dispatcher{store: store, storeRoot: storeDir}, nil
 }
 
 // DISPATCHER INTERFACE IMPLEMENTATION
 
+// getAuthorizer searches the provided list of paths in the Memstore hierarchy
+// for an TaggedACLMap and uses it to produce an authorizer or returns nil
+// to get a nil Authorizer.
+func getAuthorizer(store *fs.Memstore, paths []string) (security.Authorizer, error) {
+	for _, p := range paths {
+		if tam, _, err := getACL(store, p); err == nil {
+			auth, err := access.TaggedACLAuthorizer(tam, access.TypicalTagType())
+			if err != nil {
+				vlog.Errorf("Successfully obtained an ACL from Memstore but TaggedACLAuthorizer couldn't use it: %v", err)
+				return nil, err
+			}
+			return auth, nil
+		} else if !verror.Is(err, ErrNotFound.ID) {
+			vlog.Errorf("Internal error obtaining ACL from Memstore: %v", err)
+		}
+	}
+	return nil, nil
+}
+
 func (d *dispatcher) Lookup(suffix string) (interface{}, security.Authorizer, error) {
-	return repository.ApplicationServer(NewApplicationService(d.store, d.storeRoot, suffix)), d.auth, nil
+	app, version, _ := parse(nil, suffix)
+	// TODO(rjkroege@google.com): Implement ACL inheritance.
+	// Construct the search hierarchy for ACLs.
+	sp := []string{
+		naming.Join("/acls", app, version, "data"),
+		naming.Join("/acls", app, "data"),
+		naming.Join("/acls", "data"),
+	}
+
+	auth, err := getAuthorizer(d.store, sp)
+	if err != nil {
+		return nil, nil, err
+	}
+	return repository.ApplicationServer(NewApplicationService(d.store, d.storeRoot, suffix)), auth, nil
 }
diff --git a/services/mgmt/application/impl/impl_test.go b/services/mgmt/application/impl/impl_test.go
index 90f2c32..52a0dd1 100644
--- a/services/mgmt/application/impl/impl_test.go
+++ b/services/mgmt/application/impl/impl_test.go
@@ -1,4 +1,4 @@
-package impl
+package impl_test
 
 import (
 	"io/ioutil"
@@ -6,28 +6,21 @@
 	"reflect"
 	"testing"
 
-	"v.io/core/veyron2"
 	"v.io/core/veyron2/naming"
-	"v.io/core/veyron2/rt"
 	"v.io/core/veyron2/services/mgmt/application"
 	"v.io/core/veyron2/verror2"
 
 	"v.io/core/veyron/lib/testutil"
-	"v.io/core/veyron/profiles"
+	_ "v.io/core/veyron/profiles/static"
+	"v.io/core/veyron/services/mgmt/application/impl"
+	mgmttest "v.io/core/veyron/services/mgmt/lib/testutil"
 	"v.io/core/veyron/services/mgmt/repository"
 )
 
 // TestInterface tests that the implementation correctly implements
 // the Application interface.
 func TestInterface(t *testing.T) {
-	ctx := runtime.NewContext()
-
-	// Setup and start the application repository server.
-	server, err := runtime.NewServer()
-	if err != nil {
-		t.Fatalf("NewServer() failed: %v", err)
-	}
-	defer server.Stop()
+	ctx := globalRT.NewContext()
 
 	dir, prefix := "", ""
 	store, err := ioutil.TempDir(dir, prefix)
@@ -35,24 +28,22 @@
 		t.Fatalf("TempDir(%q, %q) failed: %v", dir, prefix, err)
 	}
 	defer os.RemoveAll(store)
-	dispatcher, err := NewDispatcher(store, nil)
+	dispatcher, err := impl.NewDispatcher(store)
 	if err != nil {
-		t.Fatalf("NewDispatcher() failed: %v", err)
+		t.Fatalf("impl.NewDispatcher() failed: %v", err)
 	}
 
-	endpoints, err := server.Listen(profiles.LocalListenSpec)
-	if err != nil {
-		t.Fatalf("Listen(%s) failed: %v", profiles.LocalListenSpec, err)
-	}
+	server, endpoint := mgmttest.NewServer(globalRT)
+	defer server.Stop()
+
 	if err := server.ServeDispatcher("", dispatcher); err != nil {
 		t.Fatalf("Serve(%v) failed: %v", dispatcher, err)
 	}
-	endpoint := endpoints[0]
 
 	// Create client stubs for talking to the server.
-	stub := repository.ApplicationClient(naming.JoinAddressName(endpoint.String(), "search"))
-	stubV1 := repository.ApplicationClient(naming.JoinAddressName(endpoint.String(), "search/v1"))
-	stubV2 := repository.ApplicationClient(naming.JoinAddressName(endpoint.String(), "search/v2"))
+	stub := repository.ApplicationClient(naming.JoinAddressName(endpoint, "search"))
+	stubV1 := repository.ApplicationClient(naming.JoinAddressName(endpoint, "search/v1"))
+	stubV2 := repository.ApplicationClient(naming.JoinAddressName(endpoint, "search/v2"))
 
 	// Create example envelopes.
 	envelopeV1 := application.Envelope{
@@ -73,8 +64,8 @@
 	if err := stubV2.Put(ctx, []string{"base"}, envelopeV2); err != nil {
 		t.Fatalf("Put() failed: %v", err)
 	}
-	if err := stub.Put(ctx, []string{"base", "media"}, envelopeV1); err == nil || !verror2.Is(err, errInvalidSuffix.ID) {
-		t.Fatalf("Unexpected error: expected %v, got %v", errInvalidSuffix, err)
+	if err := stub.Put(ctx, []string{"base", "media"}, envelopeV1); err == nil || !verror2.Is(err, impl.ErrInvalidSuffix.ID) {
+		t.Fatalf("Unexpected error: expected %v, got %v", impl.ErrInvalidSuffix, err)
 	}
 
 	// Test Match(), trying to retrieve both existing and non-existing
@@ -92,18 +83,18 @@
 	if !reflect.DeepEqual(envelopeV1, output) {
 		t.Fatalf("Unexpected output: expected %v, got %v", envelopeV1, output)
 	}
-	if _, err := stubV2.Match(ctx, []string{"media"}); err == nil || !verror2.Is(err, errNotFound.ID) {
-		t.Fatalf("Unexpected error: expected %v, got %v", errNotFound, err)
+	if _, err := stubV2.Match(ctx, []string{"media"}); err == nil || !verror2.Is(err, impl.ErrNotFound.ID) {
+		t.Fatalf("Unexpected error: expected %v, got %v", impl.ErrNotFound, err)
 	}
-	if _, err := stubV2.Match(ctx, []string{}); err == nil || !verror2.Is(err, errNotFound.ID) {
-		t.Fatalf("Unexpected error: expected %v, got %v", errNotFound, err)
+	if _, err := stubV2.Match(ctx, []string{}); err == nil || !verror2.Is(err, impl.ErrNotFound.ID) {
+		t.Fatalf("Unexpected error: expected %v, got %v", impl.ErrNotFound, err)
 	}
-	if _, err := stub.Match(ctx, []string{"media"}); err == nil || !verror2.Is(err, errInvalidSuffix.ID) {
-		t.Fatalf("Unexpected error: expected %v, got %v", errInvalidSuffix, err)
+	if _, err := stub.Match(ctx, []string{"media"}); err == nil || !verror2.Is(err, impl.ErrInvalidSuffix.ID) {
+		t.Fatalf("Unexpected error: expected %v, got %v", impl.ErrInvalidSuffix, err)
 	}
 
 	// Test Glob
-	matches, err := testutil.GlobName(ctx, naming.JoinAddressName(endpoint.String(), ""), "...")
+	matches, err := testutil.GlobName(ctx, naming.JoinAddressName(endpoint, ""), "...")
 	if err != nil {
 		t.Errorf("Unexpected Glob error: %v", err)
 	}
@@ -125,14 +116,14 @@
 	if output, err = stubV1.Match(ctx, []string{"media"}); err != nil {
 		t.Fatalf("Match() failed: %v", err)
 	}
-	if err := stubV1.Remove(ctx, "base"); err == nil || !verror2.Is(err, errNotFound.ID) {
-		t.Fatalf("Unexpected error: expected %v, got %v", errNotFound, err)
+	if err := stubV1.Remove(ctx, "base"); err == nil || !verror2.Is(err, impl.ErrNotFound.ID) {
+		t.Fatalf("Unexpected error: expected %v, got %v", impl.ErrNotFound, err)
 	}
 	if err := stub.Remove(ctx, "base"); err != nil {
 		t.Fatalf("Remove() failed: %v", err)
 	}
-	if err := stubV2.Remove(ctx, "media"); err == nil || !verror2.Is(err, errNotFound.ID) {
-		t.Fatalf("Unexpected error: expected %v, got %v", errNotFound, err)
+	if err := stubV2.Remove(ctx, "media"); err == nil || !verror2.Is(err, impl.ErrNotFound.ID) {
+		t.Fatalf("Unexpected error: expected %v, got %v", impl.ErrNotFound, err)
 	}
 	if err := stubV1.Remove(ctx, "media"); err != nil {
 		t.Fatalf("Remove() failed: %v", err)
@@ -140,14 +131,14 @@
 
 	// Finally, use Match() to test that Remove really removed the
 	// application envelopes.
-	if _, err := stubV1.Match(ctx, []string{"base"}); err == nil || !verror2.Is(err, errNotFound.ID) {
-		t.Fatalf("Unexpected error: expected %v, got %v", errNotFound, err)
+	if _, err := stubV1.Match(ctx, []string{"base"}); err == nil || !verror2.Is(err, impl.ErrNotFound.ID) {
+		t.Fatalf("Unexpected error: expected %v, got %v", impl.ErrNotFound, err)
 	}
-	if _, err := stubV1.Match(ctx, []string{"media"}); err == nil || !verror2.Is(err, errNotFound.ID) {
-		t.Fatalf("Unexpected error: expected %v, got %v", errNotFound, err)
+	if _, err := stubV1.Match(ctx, []string{"media"}); err == nil || !verror2.Is(err, impl.ErrNotFound.ID) {
+		t.Fatalf("Unexpected error: expected %v, got %v", impl.ErrNotFound, err)
 	}
-	if _, err := stubV2.Match(ctx, []string{"base"}); err == nil || !verror2.Is(err, errNotFound.ID) {
-		t.Fatalf("Unexpected error: expected %v, got %v", errNotFound, err)
+	if _, err := stubV2.Match(ctx, []string{"base"}); err == nil || !verror2.Is(err, impl.ErrNotFound.ID) {
+		t.Fatalf("Unexpected error: expected %v, got %v", impl.ErrNotFound, err)
 	}
 
 	// Shutdown the application repository server.
@@ -156,22 +147,7 @@
 	}
 }
 
-var runtime veyron2.Runtime
-
-func init() {
-	var err error
-	runtime, err = rt.New()
-	if err != nil {
-		panic(err)
-	}
-}
-
 func TestPreserveAcrossRestarts(t *testing.T) {
-	server, err := runtime.NewServer()
-	if err != nil {
-		t.Fatalf("NewServer() failed: %v", err)
-	}
-	defer server.Stop()
 	dir, prefix := "", ""
 	storedir, err := ioutil.TempDir(dir, prefix)
 	if err != nil {
@@ -179,22 +155,20 @@
 	}
 	defer os.RemoveAll(storedir)
 
-	dispatcher, err := NewDispatcher(storedir, nil)
+	dispatcher, err := impl.NewDispatcher(storedir)
 	if err != nil {
-		t.Fatalf("NewDispatcher() failed: %v", err)
+		t.Fatalf("impl.NewDispatcher() failed: %v", err)
 	}
 
-	endpoints, err := server.Listen(profiles.LocalListenSpec)
-	if err != nil {
-		t.Fatalf("Listen(%s) failed: %v", profiles.LocalListenSpec, err)
-	}
-	endpoint := endpoints[0]
+	server, endpoint := mgmttest.NewServer(globalRT)
+	defer server.Stop()
+
 	if err := server.ServeDispatcher("", dispatcher); err != nil {
 		t.Fatalf("Serve(%v) failed: %v", dispatcher, err)
 	}
 
 	// Create client stubs for talking to the server.
-	stubV1 := repository.ApplicationClient(naming.JoinAddressName(endpoint.String(), "search/v1"))
+	stubV1 := repository.ApplicationClient(naming.JoinAddressName(endpoint, "search/v1"))
 
 	// Create example envelopes.
 	envelopeV1 := application.Envelope{
@@ -203,12 +177,12 @@
 		Binary: "/veyron/name/of/binary",
 	}
 
-	if err := stubV1.Put(runtime.NewContext(), []string{"media"}, envelopeV1); err != nil {
+	if err := stubV1.Put(globalRT.NewContext(), []string{"media"}, envelopeV1); err != nil {
 		t.Fatalf("Put() failed: %v", err)
 	}
 
 	// There is content here now.
-	output, err := stubV1.Match(runtime.NewContext(), []string{"media"})
+	output, err := stubV1.Match(globalRT.NewContext(), []string{"media"})
 	if err != nil {
 		t.Fatalf("Match(%v) failed: %v", "media", err)
 	}
@@ -218,29 +192,22 @@
 
 	server.Stop()
 
-	// Setup and start a second application server in its place.
-	server, err = runtime.NewServer()
+	// Setup and start a second application server.
+	dispatcher, err = impl.NewDispatcher(storedir)
 	if err != nil {
-		t.Fatalf("NewServer() failed: %v", err)
+		t.Fatalf("impl.NewDispatcher() failed: %v", err)
 	}
+
+	server, endpoint = mgmttest.NewServer(globalRT)
 	defer server.Stop()
 
-	dispatcher, err = NewDispatcher(storedir, nil)
-	if err != nil {
-		t.Fatalf("NewDispatcher() failed: %v", err)
-	}
-	endpoints, err = server.Listen(profiles.LocalListenSpec)
-	if err != nil {
-		t.Fatalf("Listen(%s) failed: %v", profiles.LocalListenSpec, err)
-	}
-	endpoint = endpoints[0]
 	if err := server.ServeDispatcher("", dispatcher); err != nil {
 		t.Fatalf("Serve(%v) failed: %v", dispatcher, err)
 	}
 
-	stubV1 = repository.ApplicationClient(naming.JoinAddressName(endpoint.String(), "search/v1"))
+	stubV1 = repository.ApplicationClient(naming.JoinAddressName(endpoint, "search/v1"))
 
-	output, err = stubV1.Match(runtime.NewContext(), []string{"media"})
+	output, err = stubV1.Match(globalRT.NewContext(), []string{"media"})
 	if err != nil {
 		t.Fatalf("Match(%v) failed: %v", "media", err)
 	}
diff --git a/services/mgmt/application/impl/service.go b/services/mgmt/application/impl/service.go
index 816a505..b0c6cc4 100644
--- a/services/mgmt/application/impl/service.go
+++ b/services/mgmt/application/impl/service.go
@@ -3,11 +3,14 @@
 import (
 	"strings"
 
+	"v.io/core/veyron/services/mgmt/lib/acls"
 	"v.io/core/veyron/services/mgmt/lib/fs"
+
 	"v.io/core/veyron2/ipc"
 	"v.io/core/veyron2/naming"
 	"v.io/core/veyron2/services/mgmt/application"
-	"v.io/core/veyron2/verror2"
+	"v.io/core/veyron2/services/security/access"
+	verror "v.io/core/veyron2/verror2"
 	"v.io/core/veyron2/vlog"
 )
 
@@ -27,9 +30,10 @@
 const pkgPath = "v.io/core/veyron/services/mgmt/application/impl/"
 
 var (
-	errInvalidSuffix   = verror2.Register(pkgPath+".invalidSuffix", verror2.NoRetry, "")
-	errOperationFailed = verror2.Register(pkgPath+".operationFailed", verror2.NoRetry, "")
-	errNotFound        = verror2.Register(pkgPath+".notFound", verror2.NoRetry, "")
+	ErrInvalidSuffix   = verror.Register(pkgPath+".InvalidSuffix", verror.NoRetry, "{1:}{2:} invalid suffix{:_}")
+	ErrOperationFailed = verror.Register(pkgPath+".OperationFailed", verror.NoRetry, "{1:}{2:} operation failed{:_}")
+	ErrNotFound        = verror.Register(pkgPath+".NotFound", verror.NoRetry, "{1:}{2:} not found{:_}")
+	ErrInvalidBlessing = verror.Register(pkgPath+".InvalidBlessing", verror.NoRetry, "{1:}{2:} invalid blessing{:_}")
 )
 
 // NewApplicationService returns a new Application service implementation.
@@ -45,7 +49,7 @@
 	case 1:
 		return tokens[0], "", nil
 	default:
-		return "", "", verror2.Make(errInvalidSuffix, context.Context())
+		return "", "", verror.Make(ErrInvalidSuffix, context.Context())
 	}
 }
 
@@ -57,7 +61,7 @@
 		return empty, err
 	}
 	if version == "" {
-		return empty, verror2.Make(errInvalidSuffix, context.Context())
+		return empty, verror.Make(ErrInvalidSuffix, context.Context())
 	}
 
 	i.store.Lock()
@@ -75,7 +79,7 @@
 		}
 		return envelope, nil
 	}
-	return empty, verror2.Make(errNotFound, context.Context())
+	return empty, verror.Make(ErrNotFound, context.Context())
 }
 
 func (i *appRepoService) Put(context ipc.ServerContext, profiles []string, envelope application.Envelope) error {
@@ -85,7 +89,7 @@
 		return err
 	}
 	if version == "" {
-		return verror2.Make(errInvalidSuffix, context.Context())
+		return verror.Make(ErrInvalidSuffix, context.Context())
 	}
 	i.store.Lock()
 	defer i.store.Unlock()
@@ -101,11 +105,11 @@
 		object := i.store.BindObject(path)
 		_, err := object.Put(context, envelope)
 		if err != nil {
-			return verror2.Make(errOperationFailed, context.Context())
+			return verror.Make(ErrOperationFailed, context.Context())
 		}
 	}
 	if err := i.store.BindTransaction(tname).Commit(context); err != nil {
-		return verror2.Make(errOperationFailed, context.Context())
+		return verror.Make(ErrOperationFailed, context.Context())
 	}
 	return nil
 }
@@ -130,16 +134,16 @@
 	object := i.store.BindObject(path)
 	found, err := object.Exists(context)
 	if err != nil {
-		return verror2.Make(errOperationFailed, context.Context())
+		return verror.Make(ErrOperationFailed, context.Context())
 	}
 	if !found {
-		return verror2.Make(errNotFound, context.Context())
+		return verror.Make(ErrNotFound, context.Context())
 	}
 	if err := object.Remove(context); err != nil {
-		return verror2.Make(errOperationFailed, context.Context())
+		return verror.Make(ErrOperationFailed, context.Context())
 	}
 	if err := i.store.BindTransaction(tname).Commit(context); err != nil {
-		return verror2.Make(errOperationFailed, context.Context())
+		return verror.Make(ErrOperationFailed, context.Context())
 	}
 	return nil
 }
@@ -209,9 +213,9 @@
 				return nil, nil
 			}
 		}
-		return nil, verror2.Make(errNotFound, nil)
+		return nil, verror.Make(ErrNotFound, nil)
 	default:
-		return nil, verror2.Make(errNotFound, nil)
+		return nil, verror.Make(ErrNotFound, nil)
 	}
 
 	ch := make(chan string, len(results))
@@ -221,3 +225,72 @@
 	close(ch)
 	return ch, nil
 }
+
+func (i *appRepoService) GetACL(ctx ipc.ServerContext) (acl access.TaggedACLMap, etag string, err error) {
+	i.store.Lock()
+	defer i.store.Unlock()
+	path := naming.Join("/acls", i.suffix, "data")
+	return getACL(i.store, path)
+}
+
+func (i *appRepoService) SetACL(ctx ipc.ServerContext, acl access.TaggedACLMap, etag string) error {
+	i.store.Lock()
+	defer i.store.Unlock()
+	path := naming.Join("/acls", i.suffix, "data")
+	return setACL(i.store, path, acl, etag)
+}
+
+// getACL fetches a TaggedACLMap out of the Memstore at the provided path.
+// path is expected to already have been cleaned by naming.Join or its ilk.
+func getACL(store *fs.Memstore, path string) (access.TaggedACLMap, string, error) {
+	entry, err := store.BindObject(path).Get(nil)
+
+	if verror.Is(err, fs.ErrNotInMemStore.ID) {
+		// No ACL exists
+		return nil, "default", verror.Make(ErrNotFound, nil)
+	} else if err != nil {
+		vlog.Errorf("getACL: internal failure in fs.Memstore")
+		return nil, "", err
+	}
+
+	acl, ok := entry.Value.(access.TaggedACLMap)
+	if !ok {
+		return nil, "", err
+	}
+
+	etag, err := acls.ComputeEtag(acl)
+	if err != nil {
+		return nil, "", err
+	}
+	return acl, etag, nil
+}
+
+// setACL wites a TaggedACLMap into the Memstore at the provided path.
+// where path is expected to have already been cleaned by naming.Join.
+func setACL(store *fs.Memstore, path string, acl access.TaggedACLMap, etag string) error {
+	_, oetag, err := getACL(store, path)
+	if verror.Is(err, ErrNotFound.ID) {
+		oetag = etag
+	} else if err != nil {
+		return err
+	}
+
+	if oetag != etag {
+		return verror.Make(access.BadEtag, nil, etag, oetag)
+	}
+
+	tname, err := store.BindTransactionRoot("").CreateTransaction(nil)
+	if err != nil {
+		return err
+	}
+
+	object := store.BindObject(path)
+
+	if _, err := object.Put(nil, acl); err != nil {
+		return err
+	}
+	if err := store.BindTransaction(tname).Commit(nil); err != nil {
+		return verror.Make(ErrOperationFailed, nil)
+	}
+	return nil
+}
diff --git a/services/mgmt/device/impl/impl_test.go b/services/mgmt/device/impl/impl_test.go
index d521e5f..d5c7242 100644
--- a/services/mgmt/device/impl/impl_test.go
+++ b/services/mgmt/device/impl/impl_test.go
@@ -308,7 +308,7 @@
 	envelope, cleanup := startMockRepos(t)
 	defer cleanup()
 
-	root, cleanup := setupRootDir(t)
+	root, cleanup := mgmttest.SetupRootDir(t, "devicemanager")
 	defer cleanup()
 
 	// Current link does not have to live in the root dir, but it's
@@ -546,7 +546,7 @@
 	envelope, cleanup := startMockRepos(t)
 	defer cleanup()
 
-	root, cleanup := setupRootDir(t)
+	root, cleanup := mgmttest.SetupRootDir(t, "devicemanager")
 	defer cleanup()
 
 	// Create a script wrapping the test target that implements suidhelper.
@@ -767,7 +767,7 @@
 	envelope, cleanup := startMockRepos(t)
 	defer cleanup()
 
-	root, cleanup := setupRootDir(t)
+	root, cleanup := mgmttest.SetupRootDir(t, "devicemanager")
 	defer cleanup()
 
 	crDir, crEnv := mgmttest.CredentialsForChild(globalRT, "devicemanager")
@@ -839,7 +839,7 @@
 	envelope, cleanup := startMockRepos(t)
 	defer cleanup()
 
-	root, cleanup := setupRootDir(t)
+	root, cleanup := mgmttest.SetupRootDir(t, "devicemanager")
 	defer cleanup()
 
 	var (
@@ -944,7 +944,7 @@
 func TestDeviceManagerInstallation(t *testing.T) {
 	sh, deferFn := mgmttest.CreateShellAndMountTable(t, globalRT)
 	defer deferFn()
-	testDir, cleanup := setupRootDir(t)
+	testDir, cleanup := mgmttest.SetupRootDir(t, "devicemanager")
 	defer cleanup()
 
 	// Create a script wrapping the test target that implements suidhelper.
@@ -1002,7 +1002,7 @@
 	envelope, cleanup := startMockRepos(t)
 	defer cleanup()
 
-	root, cleanup := setupRootDir(t)
+	root, cleanup := mgmttest.SetupRootDir(t, "devicemanager")
 	defer cleanup()
 
 	crDir, crEnv := mgmttest.CredentialsForChild(globalRT, "devicemanager")
@@ -1168,7 +1168,7 @@
 
 	defer startRealBinaryRepository(t)()
 
-	root, cleanup := setupRootDir(t)
+	root, cleanup := mgmttest.SetupRootDir(t, "devicemanager")
 	defer cleanup()
 
 	crDir, crEnv := mgmttest.CredentialsForChild(globalRT, "devicemanager")
@@ -1232,7 +1232,7 @@
 	sh, deferFn := mgmttest.CreateShellAndMountTable(t, globalRT)
 	defer deferFn()
 
-	root, cleanup := setupRootDir(t)
+	root, cleanup := mgmttest.SetupRootDir(t, "devicemanager")
 	defer cleanup()
 
 	var (
@@ -1335,7 +1335,7 @@
 	envelope, cleanup := startMockRepos(t)
 	defer cleanup()
 
-	root, cleanup := setupRootDir(t)
+	root, cleanup := mgmttest.SetupRootDir(t, "devicemanager")
 	defer cleanup()
 
 	var (
diff --git a/services/mgmt/device/impl/mock_repo_test.go b/services/mgmt/device/impl/mock_repo_test.go
index 56ae7be..82644a3 100644
--- a/services/mgmt/device/impl/mock_repo_test.go
+++ b/services/mgmt/device/impl/mock_repo_test.go
@@ -13,6 +13,7 @@
 	"v.io/core/veyron2/services/mgmt/application"
 	"v.io/core/veyron2/services/mgmt/binary"
 	"v.io/core/veyron2/services/mgmt/repository"
+	"v.io/core/veyron2/services/security/access"
 	"v.io/core/veyron2/verror2"
 	"v.io/core/veyron2/vlog"
 
@@ -66,6 +67,14 @@
 	return i.envelope, nil
 }
 
+func (i *arInvoker) GetACL(ipc.ServerContext) (acl access.TaggedACLMap, etag string, err error) {
+	return nil, "", nil
+}
+
+func (i *arInvoker) SetACL(_ ipc.ServerContext, acl access.TaggedACLMap, etag string) error {
+	return nil
+}
+
 // brInvoker holds the state of a binary repository invocation mock.  It always
 // serves the current running binary.
 type brInvoker struct{}
diff --git a/services/mgmt/device/impl/util_test.go b/services/mgmt/device/impl/util_test.go
index 6cf2b0e..593b6e6 100644
--- a/services/mgmt/device/impl/util_test.go
+++ b/services/mgmt/device/impl/util_test.go
@@ -26,12 +26,6 @@
 )
 
 const (
-	// Setting this environment variable to any non-empty value avoids
-	// removing the device manager's workspace for successful test runs (for
-	// failed test runs, this is already the case).  This is useful when
-	// developing test cases.
-	preserveDMWorkspaceEnv = "VEYRON_TEST_PRESERVE_DM_WORKSPACE"
-
 	// TODO(caprita): Set the timeout in a more principled manner.
 	stopTimeout = 20 // In seconds.
 )
@@ -48,29 +42,6 @@
 	}
 }
 
-// setupRootDir sets up and returns the local filesystem location that the
-// device manager is told to use, as well as a cleanup function.
-func setupRootDir(t *testing.T) (string, func()) {
-	rootDir, err := ioutil.TempDir("", "devicemanager")
-	if err != nil {
-		t.Fatalf("Failed to set up temporary dir for test: %v", err)
-	}
-	// On some operating systems (e.g. darwin) os.TempDir() can return a
-	// symlink. To avoid having to account for this eventuality later,
-	// evaluate the symlink.
-	rootDir, err = filepath.EvalSymlinks(rootDir)
-	if err != nil {
-		vlog.Fatalf("EvalSymlinks(%v) failed: %v", rootDir, err)
-	}
-	return rootDir, func() {
-		if t.Failed() || os.Getenv(preserveDMWorkspaceEnv) != "" {
-			t.Logf("You can examine the device manager workspace at %v", rootDir)
-		} else {
-			os.RemoveAll(rootDir)
-		}
-	}
-}
-
 // resolveExpectNotFound verifies that the given name is not in the mounttable.
 func resolveExpectNotFound(t *testing.T, name string) {
 	if results, err := globalRT.Namespace().Resolve(globalRT.NewContext(), name); err == nil {
diff --git a/services/mgmt/lib/fs/simplestore.go b/services/mgmt/lib/fs/simplestore.go
index 9f8cd04..2f7abb2 100644
--- a/services/mgmt/lib/fs/simplestore.go
+++ b/services/mgmt/lib/fs/simplestore.go
@@ -11,7 +11,9 @@
 	"sync"
 
 	"v.io/core/veyron/services/mgmt/profile"
+
 	"v.io/core/veyron2/services/mgmt/application"
+	"v.io/core/veyron2/services/security/access"
 	verror "v.io/core/veyron2/verror2"
 )
 
@@ -57,6 +59,7 @@
 func init() {
 	gob.Register(profile.Specification{})
 	gob.Register(application.Envelope{})
+	gob.Register(access.TaggedACLMap{})
 }
 
 // NewMemstore persists the Memstore to os.TempDir() if no file is
@@ -335,7 +338,7 @@
 		return nil, verror.Make(ErrWithoutTransaction, nil, "Put()")
 	}
 	switch v := envelope.(type) {
-	case application.Envelope, profile.Specification:
+	case application.Envelope, profile.Specification, access.TaggedACLMap:
 		o.ms.puts[o.path] = v
 		delete(o.ms.removes, o.path)
 		o.Value = o.path
diff --git a/services/mgmt/lib/testutil/modules.go b/services/mgmt/lib/testutil/modules.go
index ed6c087..d9ec550 100644
--- a/services/mgmt/lib/testutil/modules.go
+++ b/services/mgmt/lib/testutil/modules.go
@@ -1,7 +1,9 @@
 package testutil
 
 import (
+	"io/ioutil"
 	"os"
+	"path/filepath"
 	"strconv"
 	"testing"
 
@@ -18,6 +20,14 @@
 	"v.io/core/veyron/lib/testutil/security"
 )
 
+const (
+	// Setting this environment variable to any non-empty value avoids
+	// removing the generated workspace for successful test runs (for
+	// failed test runs, this is already the case).  This is useful when
+	// developing test cases.
+	preserveWorkspaceEnv = "VEYRON_TEST_PRESERVE_WORKSPACE"
+)
+
 // StartRootMT sets up a root mount table for tests.
 func StartRootMT(t *testing.T, sh *modules.Shell) (string, modules.Handle) {
 	h, err := sh.Start(core.RootMTCommand, nil, "--", "--veyron.tcp.address=127.0.0.1:0")
@@ -137,3 +147,27 @@
 	t.Fatalf(testutil.FormatLogLine(2, "failed to extract pid: %v", m))
 	return 0
 }
+
+// SetupRootDir sets up and returns a directory for the root and returns
+// a cleanup function.
+func SetupRootDir(t *testing.T, prefix string) (string, func()) {
+	rootDir, err := ioutil.TempDir("", prefix)
+	if err != nil {
+		t.Fatalf("Failed to set up temporary dir for test: %v", err)
+	}
+	// On some operating systems (e.g. darwin) os.TempDir() can return a
+	// symlink. To avoid having to account for this eventuality later,
+	// evaluate the symlink.
+	rootDir, err = filepath.EvalSymlinks(rootDir)
+	if err != nil {
+		vlog.Fatalf("EvalSymlinks(%v) failed: %v", rootDir, err)
+	}
+
+	return rootDir, func() {
+		if t.Failed() || os.Getenv(preserveWorkspaceEnv) != "" {
+			t.Logf("You can examine the %s workspace at %v", prefix, rootDir)
+		} else {
+			os.RemoveAll(rootDir)
+		}
+	}
+}
diff --git a/services/mgmt/repository/repository.vdl.go b/services/mgmt/repository/repository.vdl.go
index b96abee..75842f2 100644
--- a/services/mgmt/repository/repository.vdl.go
+++ b/services/mgmt/repository/repository.vdl.go
@@ -207,7 +207,7 @@
 }
 
 func (s implApplicationServerStub) Describe__() []__ipc.InterfaceDesc {
-	return []__ipc.InterfaceDesc{ApplicationDesc, repository.ApplicationDesc}
+	return []__ipc.InterfaceDesc{ApplicationDesc, repository.ApplicationDesc, access.ObjectDesc}
 }
 
 // ApplicationDesc describes the Application interface.