ref: moving the groups implementation to the release

This CL moves the groups implementation from the experimental repo to
the v23 and ref repos. It also updates the import paths accordingly
and adds missing .api files.

MultiPart: 2/2
Change-Id: I3d5963083a802a6f75ed910319a3fbf6a60ebc7a
diff --git a/services/groups/README b/services/groups/README
new file mode 100644
index 0000000..fa22167
--- /dev/null
+++ b/services/groups/README
@@ -0,0 +1,6 @@
+WORK IN PROGRESS. DO NOT DEPEND ON ANYTHING IN THIS DIRECTORY.
+
+This directory provides an implementation of groups for managing access control.
+
+Group support is under development. Code and interfaces in this directory may
+change at any time.
diff --git a/services/groups/groupsd/main.go b/services/groups/groupsd/main.go
new file mode 100644
index 0000000..2d51c6d
--- /dev/null
+++ b/services/groups/groupsd/main.go
@@ -0,0 +1,77 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Daemon groupsd implements the v.io/v23/services/groups interfaces for
+// managing access control groups.
+package main
+
+// Example invocation:
+// groupsd --v23.tcp.address="127.0.0.1:0" --name=groupsd
+
+import (
+	"flag"
+
+	"v.io/v23"
+	"v.io/v23/security"
+	"v.io/v23/security/access"
+	"v.io/x/lib/vlog"
+	"v.io/x/ref/lib/security/securityflag"
+	"v.io/x/ref/lib/signals"
+	_ "v.io/x/ref/runtime/factories/generic"
+	"v.io/x/ref/services/groups/internal/server"
+	"v.io/x/ref/services/groups/internal/store/memstore"
+)
+
+var (
+	name = flag.String("name", "", "Name to mount at.")
+)
+
+// defaultPerms returns a permissions object that grants all permissions to the
+// provided blessing patterns.
+func defaultPerms(blessingPatterns []security.BlessingPattern) access.Permissions {
+	perms := access.Permissions{}
+	for _, tag := range access.AllTypicalTags() {
+		for _, bp := range blessingPatterns {
+			perms.Add(bp, string(tag))
+		}
+	}
+	return perms
+}
+
+// TODO(ashankar, jsimsa): Implement groupsd using the cmdline package.
+func main() {
+	ctx, shutdown := v23.Init()
+	defer shutdown()
+
+	s, err := v23.NewServer(ctx)
+	if err != nil {
+		vlog.Fatal("v23.NewServer() failed: ", err)
+	}
+	if _, err := s.Listen(v23.GetListenSpec(ctx)); err != nil {
+		vlog.Fatal("s.Listen() failed: ", err)
+	}
+
+	perms, err := securityflag.PermissionsFromFlag()
+	if err != nil {
+		vlog.Fatal("securityflag.PermissionsFromFlag() failed: ", err)
+	}
+
+	if perms != nil {
+		vlog.Info("Using permissions from command line flag.")
+	} else {
+		vlog.Info("No permissions flag provided. Giving local principal all permissions.")
+		perms = defaultPerms(security.DefaultBlessingPatterns(v23.GetPrincipal(ctx)))
+	}
+
+	m := server.NewManager(memstore.New(), perms)
+
+	// Publish the service in the mount table.
+	if err := s.ServeDispatcher(*name, m); err != nil {
+		vlog.Fatal("s.ServeDispatcher() failed: ", err)
+	}
+	vlog.Info("Mounted at: ", *name)
+
+	// Wait forever.
+	<-signals.ShutdownOnSignals(ctx)
+}
diff --git a/services/groups/internal/server/doc.go b/services/groups/internal/server/doc.go
new file mode 100644
index 0000000..25f2d73
--- /dev/null
+++ b/services/groups/internal/server/doc.go
@@ -0,0 +1,6 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package server provides an implementation of the groups.Group RPC interface.
+package server
diff --git a/services/groups/internal/server/group.go b/services/groups/internal/server/group.go
new file mode 100644
index 0000000..532c4de
--- /dev/null
+++ b/services/groups/internal/server/group.go
@@ -0,0 +1,219 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server
+
+// TODO(sadovsky): Check Resolve access on parent where applicable. Relatedly,
+// convert ErrNoExist and ErrNoAccess to ErrNoExistOrNoAccess where needed to
+// preserve privacy.
+
+import (
+	"v.io/v23/context"
+	"v.io/v23/rpc"
+	"v.io/v23/security"
+	"v.io/v23/security/access"
+	"v.io/v23/services/groups"
+	"v.io/v23/verror"
+	"v.io/x/ref/services/groups/internal/store"
+)
+
+type group struct {
+	name string
+	m    *manager
+}
+
+var _ groups.GroupServerMethods = (*group)(nil)
+
+// TODO(sadovsky): Reserve buckets for users.
+func (g *group) Create(ctx *context.T, call rpc.ServerCall, perms access.Permissions, entries []groups.BlessingPatternChunk) error {
+	// Perform Permissions check.
+	// TODO(sadovsky): Enable this Permissions check and acquire a lock on the
+	// group server Permissions.
+	if false {
+		if err := g.authorize(ctx, call.Security(), g.m.perms); err != nil {
+			return err
+		}
+	}
+	if perms == nil {
+		perms = access.Permissions{}
+		blessings, _ := security.RemoteBlessingNames(ctx, call.Security())
+		if len(blessings) == 0 {
+			// The blessings presented by the caller do not give it a name for this
+			// operation. We could create a world-accessible group, but it seems safer
+			// to return an error.
+			return groups.NewErrNoBlessings(ctx)
+		}
+		for _, tag := range access.AllTypicalTags() {
+			for _, b := range blessings {
+				perms.Add(security.BlessingPattern(b), string(tag))
+			}
+		}
+	}
+	entrySet := map[groups.BlessingPatternChunk]struct{}{}
+	for _, v := range entries {
+		entrySet[v] = struct{}{}
+	}
+	gd := groupData{Perms: perms, Entries: entrySet}
+	if err := g.m.st.Insert(g.name, gd); err != nil {
+		// TODO(sadovsky): We are leaking the fact that this group exists. If the
+		// client doesn't have access to this group, we should probably return an
+		// opaque error. (Reserving buckets for users will help.)
+		if verror.ErrorID(err) == store.ErrKeyExists.ID {
+			return verror.New(verror.ErrExist, ctx, g.name)
+		}
+		return verror.New(verror.ErrInternal, ctx, err)
+	}
+	return nil
+}
+
+func (g *group) Delete(ctx *context.T, call rpc.ServerCall, version string) error {
+	if err := g.readModifyWrite(ctx, call.Security(), version, func(gd *groupData, versionSt string) error {
+		return g.m.st.Delete(g.name, versionSt)
+	}); err != nil && verror.ErrorID(err) != verror.ErrNoExist.ID {
+		return err
+	}
+	return nil
+}
+
+func (g *group) Add(ctx *context.T, call rpc.ServerCall, entry groups.BlessingPatternChunk, version string) error {
+	return g.update(ctx, call.Security(), version, func(gd *groupData) {
+		if gd.Entries == nil {
+			gd.Entries = map[groups.BlessingPatternChunk]struct{}{}
+		}
+		gd.Entries[entry] = struct{}{}
+	})
+}
+
+func (g *group) Remove(ctx *context.T, call rpc.ServerCall, entry groups.BlessingPatternChunk, version string) error {
+	return g.update(ctx, call.Security(), version, func(gd *groupData) {
+		delete(gd.Entries, entry)
+	})
+}
+
+func (g *group) Get(ctx *context.T, call rpc.ServerCall, req groups.GetRequest, reqVersion string) (groups.GetResponse, string, error) {
+	gd, resVersion, err := g.getInternal(ctx, call.Security())
+	if err != nil {
+		return groups.GetResponse{}, "", err
+	}
+
+	// If version is set and matches the Group's current version,
+	// send an empty response (the equivalent of "HTTP 304 Not Modified").
+	if reqVersion == resVersion {
+		return groups.GetResponse{}, resVersion, nil
+	}
+
+	return groups.GetResponse{Entries: gd.Entries}, resVersion, nil
+}
+
+func (g *group) Relate(ctx *context.T, call rpc.ServerCall, blessings map[string]struct{}, hint groups.ApproximationType, reqVersion string, visitedGroups map[string]struct{}) (map[string]struct{}, []groups.Approximation, string, error) {
+	gd, resVersion, err := g.getInternal(ctx, call.Security())
+	if err != nil {
+		return nil, nil, "", err
+	}
+
+	// If version is set and matches the Group's current version,
+	// send an empty response (the equivalent of "HTTP 304 Not Modified").
+	if reqVersion == resVersion {
+		return nil, nil, resVersion, nil
+	}
+
+	remainder := make(map[string]struct{})
+	var approximations []groups.Approximation
+	for p := range gd.Entries {
+		rem, apprxs := groups.Match(ctx, security.BlessingPattern(p), hint, visitedGroups, blessings)
+		remainder = union(remainder, rem)
+		approximations = append(approximations, apprxs...)
+	}
+
+	return remainder, approximations, resVersion, nil
+}
+
+func (g *group) SetPermissions(ctx *context.T, call rpc.ServerCall, perms access.Permissions, version string) error {
+	return g.update(ctx, call.Security(), version, func(gd *groupData) {
+		gd.Perms = perms
+	})
+}
+
+func (g *group) GetPermissions(ctx *context.T, call rpc.ServerCall) (perms access.Permissions, version string, err error) {
+	gd, version, err := g.getInternal(ctx, call.Security())
+	if err != nil {
+		return nil, "", err
+	}
+	return gd.Perms, version, nil
+}
+
+////////////////////////////////////////
+// Internal helpers
+
+// Returns a VDL-compatible error.
+func (g *group) authorize(ctx *context.T, call security.Call, perms access.Permissions) error {
+	//auth, _ := access.TypicalTagTypePermissionsAuthorizer(perms)
+	auth := access.TypicalTagTypePermissionsAuthorizer(perms)
+	if err := auth.Authorize(ctx, call); err != nil {
+		return verror.New(verror.ErrNoAccess, ctx, err)
+	}
+	return nil
+}
+
+// Returns a VDL-compatible error. Performs access check.
+func (g *group) getInternal(ctx *context.T, call security.Call) (gd groupData, version string, err error) {
+	version, err = g.m.st.Get(g.name, &gd)
+	if err != nil {
+		if verror.ErrorID(err) == store.ErrUnknownKey.ID {
+			return groupData{}, "", verror.New(verror.ErrNoExist, ctx, g.name)
+		}
+		return groupData{}, "", verror.New(verror.ErrInternal, ctx, err)
+	}
+	if err := g.authorize(ctx, call, gd.Perms); err != nil {
+		return groupData{}, "", err
+	}
+	return gd, version, nil
+}
+
+// Returns a VDL-compatible error. Performs access check.
+func (g *group) update(ctx *context.T, call security.Call, version string, fn func(gd *groupData)) error {
+	return g.readModifyWrite(ctx, call, version, func(gd *groupData, versionSt string) error {
+		fn(gd)
+		return g.m.st.Update(g.name, *gd, versionSt)
+	})
+}
+
+// Returns a VDL-compatible error. Performs access check.
+// fn should perform the "modify, write" part of "read, modify, write", and
+// should return a Store error.
+func (g *group) readModifyWrite(ctx *context.T, call security.Call, version string, fn func(gd *groupData, versionSt string) error) error {
+	// Transaction retry loop.
+	for i := 0; i < 3; i++ {
+		gd, versionSt, err := g.getInternal(ctx, call)
+		if err != nil {
+			return err
+		}
+		// Fail early if possible.
+		if version != "" && version != versionSt {
+			return verror.NewErrBadVersion(ctx)
+		}
+		if err := fn(&gd, versionSt); err != nil {
+			if verror.ErrorID(err) == verror.ErrBadVersion.ID {
+				// Retry on version error if the original version was empty.
+				if version != "" {
+					return err
+				}
+			} else {
+				// Abort on non-version error.
+				return verror.New(verror.ErrInternal, ctx, err)
+			}
+		} else {
+			return nil
+		}
+	}
+	return groups.NewErrExcessiveContention(ctx)
+}
+
+// union merges set s2 into s1 and returns s1.
+func union(s1, s2 map[string]struct{}) map[string]struct{} {
+	for k := range s2 {
+		s1[k] = struct{}{}
+	}
+	return s1
+}
diff --git a/services/groups/internal/server/manager.go b/services/groups/internal/server/manager.go
new file mode 100644
index 0000000..280aee4
--- /dev/null
+++ b/services/groups/internal/server/manager.go
@@ -0,0 +1,35 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server
+
+import (
+	"strings"
+
+	"v.io/v23/rpc"
+	"v.io/v23/security"
+	"v.io/v23/security/access"
+	"v.io/v23/services/groups"
+	"v.io/x/ref/services/groups/internal/store"
+)
+
+type manager struct {
+	st    store.Store
+	perms access.Permissions
+}
+
+var _ rpc.Dispatcher = (*manager)(nil)
+
+func NewManager(st store.Store, perms access.Permissions) *manager {
+	return &manager{st: st, perms: perms}
+}
+
+func (m *manager) Lookup(suffix string) (interface{}, security.Authorizer, error) {
+	suffix = strings.TrimPrefix(suffix, "/")
+	// TODO(sadovsky): Check that suffix is a valid group name.
+	// TODO(sadovsky): Use a real authorizer. Note, this authorizer will be
+	// relatively permissive. Stricter access control happens in the individual
+	// RPC methods. See syncgroupserver/main.go for example.
+	return groups.GroupServer(&group{name: suffix, m: m}), nil, nil
+}
diff --git a/services/groups/internal/server/server_test.go b/services/groups/internal/server/server_test.go
new file mode 100644
index 0000000..20fa159
--- /dev/null
+++ b/services/groups/internal/server/server_test.go
@@ -0,0 +1,570 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server_test
+
+import (
+	"io/ioutil"
+	"os"
+	"reflect"
+	"runtime/debug"
+	"testing"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/naming"
+	"v.io/v23/security"
+	"v.io/v23/security/access"
+	"v.io/v23/services/groups"
+	"v.io/v23/verror"
+	"v.io/x/lib/vlog"
+	_ "v.io/x/ref/runtime/factories/generic"
+	"v.io/x/ref/services/groups/internal/server"
+	"v.io/x/ref/services/groups/internal/store"
+	"v.io/x/ref/services/groups/internal/store/gkv"
+	"v.io/x/ref/services/groups/internal/store/memstore"
+	"v.io/x/ref/test/testutil"
+)
+
+func Fatal(t *testing.T, args ...interface{}) {
+	debug.PrintStack()
+	t.Fatal(args...)
+}
+
+func Fatalf(t *testing.T, format string, args ...interface{}) {
+	debug.PrintStack()
+	t.Fatalf(format, args...)
+}
+
+func getEntriesOrDie(t *testing.T, ctx *context.T, g groups.GroupClientStub) map[groups.BlessingPatternChunk]struct{} {
+	res, _, err := g.Get(ctx, groups.GetRequest{}, "")
+	if err != nil {
+		Fatalf(t, "Get failed: %v", err)
+	}
+	return res.Entries
+}
+
+func getPermsOrDie(t *testing.T, ctx *context.T, g groups.GroupClientStub) access.Permissions {
+	res, _, err := g.GetPermissions(ctx)
+	if err != nil {
+		Fatalf(t, "GetPermissions failed: %v", err)
+	}
+	return res
+}
+
+func getVersionOrDie(t *testing.T, ctx *context.T, g groups.GroupClientStub) string {
+	_, version, err := g.Get(ctx, groups.GetRequest{}, "")
+	if err != nil {
+		Fatalf(t, "Get failed: %v", err)
+	}
+	return version
+}
+
+func bpc(chunk string) groups.BlessingPatternChunk {
+	return groups.BlessingPatternChunk(chunk)
+}
+
+func bpcSet(chunks ...string) map[groups.BlessingPatternChunk]struct{} {
+	res := map[groups.BlessingPatternChunk]struct{}{}
+	for _, chunk := range chunks {
+		res[bpc(chunk)] = struct{}{}
+	}
+	return res
+}
+
+func bpcSlice(chunks ...string) []groups.BlessingPatternChunk {
+	res := []groups.BlessingPatternChunk{}
+	for _, chunk := range chunks {
+		res = append(res, bpc(chunk))
+	}
+	return res
+}
+
+func entriesEqual(a, b map[groups.BlessingPatternChunk]struct{}) bool {
+	// Unlike DeepEqual, we treat nil and empty maps as equivalent.
+	if len(a) == 0 && len(b) == 0 {
+		return true
+	}
+	return reflect.DeepEqual(a, b)
+}
+
+// TODO(sadovsky): Write storage engine tests, then maybe drop this constant.
+const useMemstore = false
+
+func newServer(ctx *context.T) (string, func()) {
+	s, err := v23.NewServer(ctx)
+	if err != nil {
+		vlog.Fatal("v23.NewServer() failed: ", err)
+	}
+	eps, err := s.Listen(v23.GetListenSpec(ctx))
+	if err != nil {
+		vlog.Fatal("s.Listen() failed: ", err)
+	}
+
+	// TODO(sadovsky): Pass in perms and test perms-checking in Group.Create().
+	perms := access.Permissions{}
+	var st store.Store
+	var file *os.File
+
+	if useMemstore {
+		st = memstore.New()
+	} else {
+		file, err = ioutil.TempFile("", "")
+		if err != nil {
+			vlog.Fatal("ioutil.TempFile() failed: ", err)
+		}
+		st, err = gkv.New(file.Name())
+		if err != nil {
+			vlog.Fatal("gkv.New() failed: ", err)
+		}
+	}
+
+	m := server.NewManager(st, perms)
+
+	if err := s.ServeDispatcher("", m); err != nil {
+		vlog.Fatal("s.ServeDispatcher() failed: ", err)
+	}
+
+	name := naming.JoinAddressName(eps[0].String(), "")
+	return name, func() {
+		s.Stop()
+		if file != nil {
+			os.Remove(file.Name())
+		}
+	}
+}
+
+func setupOrDie() (clientCtx *context.T, serverName string, cleanup func()) {
+	ctx, shutdown := v23.Init()
+	cp, sp := testutil.NewPrincipal("client"), testutil.NewPrincipal("server")
+
+	// Have the server principal bless the client principal as "client".
+	blessings, err := sp.Bless(cp.PublicKey(), sp.BlessingStore().Default(), "client", security.UnconstrainedUse())
+	if err != nil {
+		vlog.Fatal("sp.Bless() failed: ", err)
+	}
+	// Have the client present its "client" blessing when talking to the server.
+	if _, err := cp.BlessingStore().Set(blessings, "server"); err != nil {
+		vlog.Fatal("cp.BlessingStore().Set() failed: ", err)
+	}
+	// Have the client treat the server's public key as an authority on all
+	// blessings that match the pattern "server".
+	if err := cp.AddToRoots(blessings); err != nil {
+		vlog.Fatal("cp.AddToRoots() failed: ", err)
+	}
+
+	clientCtx, err = v23.WithPrincipal(ctx, cp)
+	if err != nil {
+		vlog.Fatal("v23.WithPrincipal() failed: ", err)
+	}
+	serverCtx, err := v23.WithPrincipal(ctx, sp)
+	if err != nil {
+		vlog.Fatal("v23.WithPrincipal() failed: ", err)
+	}
+
+	serverName, stopServer := newServer(serverCtx)
+	cleanup = func() {
+		stopServer()
+		shutdown()
+	}
+	return
+}
+
+////////////////////////////////////////
+// Test cases
+
+func TestCreate(t *testing.T) {
+	ctx, serverName, cleanup := setupOrDie()
+	defer cleanup()
+
+	// Create a group with a default perms and no entries.
+	g := groups.GroupClient(naming.JoinAddressName(serverName, "grpA"))
+	if err := g.Create(ctx, nil, nil); err != nil {
+		t.Fatalf("Create failed: %v", err)
+	}
+	// Verify perms of created group.
+	perms := access.Permissions{}
+	for _, tag := range access.AllTypicalTags() {
+		perms.Add(security.BlessingPattern("server/client"), string(tag))
+	}
+	gotPermissions, wantPermissions := getPermsOrDie(t, ctx, g), perms
+	if !reflect.DeepEqual(gotPermissions, wantPermissions) {
+		t.Errorf("Permissions do not match: got %v, want %v", gotPermissions, wantPermissions)
+	}
+	// Verify entries of created group.
+	got, want := getEntriesOrDie(t, ctx, g), bpcSet()
+	if !entriesEqual(got, want) {
+		t.Errorf("Entries do not match: got %v, want %v", got, want)
+	}
+
+	// Creating same group again should fail, since the group already exists.
+	g = groups.GroupClient(naming.JoinAddressName(serverName, "grpA"))
+	if err := g.Create(ctx, nil, nil); verror.ErrorID(err) != verror.ErrExist.ID {
+		t.Fatalf("Create should have failed: %v", err)
+	}
+
+	// Create a group with perms and a few entries, including some redundant ones.
+	g = groups.GroupClient(naming.JoinAddressName(serverName, "grpB"))
+	perms = access.Permissions{}
+	// Allow Admin and Read so that we can call GetPermissions and Get.
+	for _, tag := range []access.Tag{access.Admin, access.Read} {
+		perms.Add(security.BlessingPattern("server/client"), string(tag))
+	}
+	if err := g.Create(ctx, perms, bpcSlice("foo", "bar", "foo")); err != nil {
+		t.Fatalf("Create failed: %v", err)
+	}
+	// Verify perms of created group.
+	gotPermissions, wantPermissions = getPermsOrDie(t, ctx, g), perms
+	if !reflect.DeepEqual(gotPermissions, wantPermissions) {
+		t.Errorf("Permissions do not match: got %v, want %v", gotPermissions, wantPermissions)
+	}
+	// Verify entries of created group.
+	got, want = getEntriesOrDie(t, ctx, g), bpcSet("foo", "bar")
+	if !entriesEqual(got, want) {
+		t.Errorf("Entries do not match: got %v, want %v", got, want)
+	}
+}
+
+func TestDelete(t *testing.T) {
+	ctx, serverName, cleanup := setupOrDie()
+	defer cleanup()
+
+	// Create a group with a default perms and no entries, check that we can
+	// delete it.
+	g := groups.GroupClient(naming.JoinAddressName(serverName, "grpA"))
+	if err := g.Create(ctx, nil, nil); err != nil {
+		t.Fatalf("Create failed: %v", err)
+	}
+	// Delete with bad version should fail.
+	if err := g.Delete(ctx, "20"); verror.ErrorID(err) != verror.ErrBadVersion.ID {
+		t.Fatalf("Delete should have failed with version error: %v", err)
+	}
+	// Delete with correct version should succeed.
+	version := getVersionOrDie(t, ctx, g)
+	if err := g.Delete(ctx, version); err != nil {
+		t.Fatalf("Delete failed: %v", err)
+	}
+	// Check that the group was actually deleted.
+	if _, _, err := g.Get(ctx, groups.GetRequest{}, ""); verror.ErrorID(err) != verror.ErrNoExist.ID {
+		t.Fatal("Group was not deleted")
+	}
+
+	// Create a group with several entries, check that we can delete it.
+	g = groups.GroupClient(naming.JoinAddressName(serverName, "grpB"))
+	if err := g.Create(ctx, nil, bpcSlice("foo", "bar", "foo")); err != nil {
+		t.Fatalf("Create failed: %v", err)
+	}
+	// Delete with empty version should succeed.
+	if err := g.Delete(ctx, ""); err != nil {
+		t.Fatalf("Delete failed: %v", err)
+	}
+	// Check that the group was actually deleted.
+	if _, _, err := g.Get(ctx, groups.GetRequest{}, ""); verror.ErrorID(err) != verror.ErrNoExist.ID {
+		t.Fatal("Group was not deleted")
+	}
+	// Check that Delete is idempotent.
+	if err := g.Delete(ctx, ""); err != nil {
+		t.Fatalf("Delete failed: %v", err)
+	}
+	// Check that we can recreate a group that was deleted.
+	if err := g.Create(ctx, nil, nil); err != nil {
+		t.Fatalf("Create failed: %v", err)
+	}
+
+	// Create a group with perms that disallow Delete(), check that Delete()
+	// fails.
+	g = groups.GroupClient(naming.JoinAddressName(serverName, "grpC"))
+	perms := access.Permissions{}
+	perms.Add(security.BlessingPattern("server/client"), string(access.Admin))
+	if err := g.Create(ctx, perms, nil); err != nil {
+		t.Fatalf("Create failed: %v", err)
+	}
+	// Delete should fail (no access).
+	if err := g.Delete(ctx, ""); verror.ErrorID(err) != verror.ErrNoAccess.ID {
+		t.Fatalf("Delete should have failed with access error: %v", err)
+	}
+}
+
+func TestPerms(t *testing.T) {
+	ctx, serverName, cleanup := setupOrDie()
+	defer cleanup()
+
+	// Create a group with a default perms.
+	g := groups.GroupClient(naming.JoinAddressName(serverName, "grpA"))
+	if err := g.Create(ctx, nil, nil); err != nil {
+		t.Fatalf("Create failed: %v", err)
+	}
+
+	// Use "ac" so the code below can exactly match code in syncbase.
+	// TODO(sadovsky): All Vanadium {Set,Get}Permissions tests ought to share this
+	// test implementation.
+	ac := g
+
+	// Mirrors syncbase/v23/syncbase/testutil/layer.go.
+	myperms := access.Permissions{}
+	myperms.Add(security.BlessingPattern("server/client"), string(access.Admin))
+	// Demonstrate that myperms differs from the current perms.
+	if reflect.DeepEqual(myperms, getPermsOrDie(t, ctx, ac)) {
+		t.Fatalf("Permissions should not match: %v", myperms)
+	}
+
+	var permsBefore, permsAfter access.Permissions
+	var versionBefore, versionAfter string
+
+	getPermsAndVersionOrDie := func() (access.Permissions, string) {
+		perms, version, err := ac.GetPermissions(ctx)
+		if err != nil {
+			// Use Fatalf rather than t.Fatalf so we get a stack trace.
+			Fatalf(t, "GetPermissions failed: %v", err)
+		}
+		return perms, version
+	}
+
+	// SetPermissions with bad version should fail.
+	permsBefore, versionBefore = getPermsAndVersionOrDie()
+	if err := ac.SetPermissions(ctx, myperms, "20"); verror.ErrorID(err) != verror.ErrBadVersion.ID {
+		t.Fatalf("SetPermissions should have failed with version error: %v", err)
+	}
+	// Since SetPermissions failed, perms and version should not have changed.
+	permsAfter, versionAfter = getPermsAndVersionOrDie()
+	if !reflect.DeepEqual(permsAfter, permsBefore) {
+		t.Errorf("Perms do not match: got %v, want %v", permsAfter, permsBefore)
+	}
+	if versionAfter != versionBefore {
+		t.Errorf("Versions do not match: got %v, want %v", versionAfter, versionBefore)
+	}
+
+	// SetPermissions with correct version should succeed.
+	permsBefore, versionBefore = permsAfter, versionAfter
+	if err := ac.SetPermissions(ctx, myperms, versionBefore); err != nil {
+		t.Fatalf("SetPermissions failed: %v", err)
+	}
+	// Check that perms and version actually changed.
+	permsAfter, versionAfter = getPermsAndVersionOrDie()
+	if !reflect.DeepEqual(permsAfter, myperms) {
+		t.Errorf("Perms do not match: got %v, want %v", permsAfter, myperms)
+	}
+	if versionBefore == versionAfter {
+		t.Errorf("Versions should not match: %v", versionBefore)
+	}
+
+	// SetPermissions with empty version should succeed.
+	permsBefore, versionBefore = permsAfter, versionAfter
+	myperms.Add(security.BlessingPattern("server/client"), string(access.Read))
+	if err := ac.SetPermissions(ctx, myperms, ""); err != nil {
+		t.Fatalf("SetPermissions failed: %v", err)
+	}
+	// Check that perms and version actually changed.
+	permsAfter, versionAfter = getPermsAndVersionOrDie()
+	if !reflect.DeepEqual(permsAfter, myperms) {
+		t.Errorf("Perms do not match: got %v, want %v", permsAfter, myperms)
+	}
+	if versionBefore == versionAfter {
+		t.Errorf("Versions should not match: %v", versionBefore)
+	}
+
+	// SetPermissions with unchanged perms should succeed, and version should
+	// still change.
+	permsBefore, versionBefore = permsAfter, versionAfter
+	if err := ac.SetPermissions(ctx, myperms, ""); err != nil {
+		t.Fatalf("SetPermissions failed: %v", err)
+	}
+	// Check that perms did not change and version did change.
+	permsAfter, versionAfter = getPermsAndVersionOrDie()
+	if !reflect.DeepEqual(permsAfter, permsBefore) {
+		t.Errorf("Perms do not match: got %v, want %v", permsAfter, permsBefore)
+	}
+	if versionBefore == versionAfter {
+		t.Errorf("Versions should not match: %v", versionBefore)
+	}
+
+	// Take away our access. SetPermissions and GetPermissions should fail.
+	if err := ac.SetPermissions(ctx, access.Permissions{}, ""); err != nil {
+		t.Fatalf("SetPermissions failed: %v", err)
+	}
+	if _, _, err := ac.GetPermissions(ctx); verror.ErrorID(err) != verror.ErrNoAccess.ID {
+		t.Fatalf("GetPermissions should have failed with access error: %v", err)
+	}
+	if err := ac.SetPermissions(ctx, myperms, ""); verror.ErrorID(err) != verror.ErrNoAccess.ID {
+		t.Fatalf("SetPermissions should have failed with access error: %v", err)
+	}
+}
+
+// Mirrors TestRemove.
+func TestAdd(t *testing.T) {
+	ctx, serverName, cleanup := setupOrDie()
+	defer cleanup()
+
+	// Create a group with a default perms and no entries.
+	g := groups.GroupClient(naming.JoinAddressName(serverName, "grpA"))
+	if err := g.Create(ctx, nil, nil); err != nil {
+		t.Fatalf("Create failed: %v", err)
+	}
+	// Verify entries of created group.
+	got, want := getEntriesOrDie(t, ctx, g), bpcSet()
+	if !entriesEqual(got, want) {
+		t.Errorf("Entries do not match: got %v, want %v", got, want)
+	}
+
+	var versionBefore, versionAfter string
+	versionBefore = getVersionOrDie(t, ctx, g)
+	// Add with bad version should fail.
+	if err := g.Add(ctx, bpc("foo"), "20"); verror.ErrorID(err) != verror.ErrBadVersion.ID {
+		t.Fatalf("Add should have failed with version error: %v", err)
+	}
+	// Version should not have changed.
+	versionAfter = getVersionOrDie(t, ctx, g)
+	if versionAfter != versionBefore {
+		t.Errorf("Versions do not match: got %v, want %v", versionAfter, versionBefore)
+	}
+
+	// Add an entry, verify it was added and the version changed.
+	versionBefore = versionAfter
+	if err := g.Add(ctx, bpc("foo"), versionBefore); err != nil {
+		t.Fatalf("Add failed: %v", err)
+	}
+	got, want = getEntriesOrDie(t, ctx, g), bpcSet("foo")
+	if !entriesEqual(got, want) {
+		t.Errorf("Entries do not match: got %v, want %v", got, want)
+	}
+	versionAfter = getVersionOrDie(t, ctx, g)
+	if versionBefore == versionAfter {
+		t.Errorf("Versions should not match: %v", versionBefore)
+	}
+
+	// Add another entry, verify it was added and the version changed.
+	versionBefore = versionAfter
+	// Add with empty version should succeed.
+	if err := g.Add(ctx, bpc("bar"), ""); err != nil {
+		t.Fatalf("Add failed: %v", err)
+	}
+	got, want = getEntriesOrDie(t, ctx, g), bpcSet("foo", "bar")
+	if !entriesEqual(got, want) {
+		t.Errorf("Entries do not match: got %v, want %v", got, want)
+	}
+	versionAfter = getVersionOrDie(t, ctx, g)
+	if versionBefore == versionAfter {
+		t.Errorf("Versions should not match: %v", versionBefore)
+	}
+
+	// Add "bar" again, verify entries are still ["foo", "bar"] and the version
+	// changed.
+	versionBefore = versionAfter
+	if err := g.Add(ctx, bpc("bar"), versionBefore); err != nil {
+		t.Fatalf("Add failed: %v", err)
+	}
+	got, want = getEntriesOrDie(t, ctx, g), bpcSet("foo", "bar")
+	if !entriesEqual(got, want) {
+		t.Errorf("Entries do not match: got %v, want %v", got, want)
+	}
+	versionAfter = getVersionOrDie(t, ctx, g)
+	if versionBefore == versionAfter {
+		t.Errorf("Versions should not match: %v", versionBefore)
+	}
+
+	// Create a group with perms that disallow Add(), check that Add() fails.
+	g = groups.GroupClient(naming.JoinAddressName(serverName, "grpB"))
+	perms := access.Permissions{}
+	perms.Add(security.BlessingPattern("server/client"), string(access.Admin))
+	if err := g.Create(ctx, perms, nil); err != nil {
+		t.Fatalf("Create failed: %v", err)
+	}
+	// Add should fail (no access).
+	if err := g.Add(ctx, bpc("foo"), ""); verror.ErrorID(err) != verror.ErrNoAccess.ID {
+		t.Fatalf("Add should have failed with access error: %v", err)
+	}
+}
+
+// Mirrors TestAdd.
+func TestRemove(t *testing.T) {
+	ctx, serverName, cleanup := setupOrDie()
+	defer cleanup()
+
+	// Create a group with a default perms and two entries.
+	g := groups.GroupClient(naming.JoinAddressName(serverName, "grpA"))
+	if err := g.Create(ctx, nil, bpcSlice("foo", "bar")); err != nil {
+		t.Fatalf("Create failed: %v", err)
+	}
+	// Verify entries of created group.
+	got, want := getEntriesOrDie(t, ctx, g), bpcSet("foo", "bar")
+	if !entriesEqual(got, want) {
+		t.Errorf("Entries do not match: got %v, want %v", got, want)
+	}
+
+	var versionBefore, versionAfter string
+	versionBefore = getVersionOrDie(t, ctx, g)
+	// Remove with bad version should fail.
+	if err := g.Remove(ctx, bpc("foo"), "20"); verror.ErrorID(err) != verror.ErrBadVersion.ID {
+		t.Fatalf("Remove should have failed with version error: %v", err)
+	}
+	// Version should not have changed.
+	versionAfter = getVersionOrDie(t, ctx, g)
+	if versionAfter != versionBefore {
+		t.Errorf("Versions do not match: got %v, want %v", versionAfter, versionBefore)
+	}
+
+	// Remove an entry, verify it was removed and the version changed.
+	versionBefore = versionAfter
+	if err := g.Remove(ctx, bpc("foo"), versionBefore); err != nil {
+		t.Fatalf("Remove failed: %v", err)
+	}
+	got, want = getEntriesOrDie(t, ctx, g), bpcSet("bar")
+	if !entriesEqual(got, want) {
+		t.Errorf("Entries do not match: got %v, want %v", got, want)
+	}
+	versionAfter = getVersionOrDie(t, ctx, g)
+	if versionBefore == versionAfter {
+		t.Errorf("Versions should not match: %v", versionBefore)
+	}
+
+	// Remove another entry, verify it was removed and the version changed.
+	versionBefore = versionAfter
+	// Remove with empty version should succeed.
+	if err := g.Remove(ctx, bpc("bar"), ""); err != nil {
+		t.Fatalf("Remove failed: %v", err)
+	}
+	got, want = getEntriesOrDie(t, ctx, g), bpcSet()
+	if !entriesEqual(got, want) {
+		t.Errorf("Entries do not match: got %v, want %v", got, want)
+	}
+	versionAfter = getVersionOrDie(t, ctx, g)
+	if versionBefore == versionAfter {
+		t.Errorf("Versions should not match: %v", versionBefore)
+	}
+
+	// Remove "bar" again, verify entries are still [] and the version changed.
+	versionBefore = versionAfter
+	if err := g.Remove(ctx, bpc("bar"), versionBefore); err != nil {
+		t.Fatalf("Remove failed: %v", err)
+	}
+	got, want = getEntriesOrDie(t, ctx, g), bpcSet()
+	if !entriesEqual(got, want) {
+		t.Errorf("Entries do not match: got %v, want %v", got, want)
+	}
+	versionAfter = getVersionOrDie(t, ctx, g)
+	if versionBefore == versionAfter {
+		t.Errorf("Versions should not match: %v", versionBefore)
+	}
+
+	// Create a group with perms that disallow Remove(), check that Remove()
+	// fails.
+	g = groups.GroupClient(naming.JoinAddressName(serverName, "grpB"))
+	perms := access.Permissions{}
+	perms.Add(security.BlessingPattern("server/client"), string(access.Admin))
+	if err := g.Create(ctx, perms, bpcSlice("foo", "bar")); err != nil {
+		t.Fatalf("Create failed: %v", err)
+	}
+	// Remove should fail (no access).
+	if err := g.Remove(ctx, bpc("foo"), ""); verror.ErrorID(err) != verror.ErrNoAccess.ID {
+		t.Fatalf("Remove should have failed with access error: %v", err)
+	}
+}
+
+func TestGet(t *testing.T) {
+	// TODO(sadovsky): Implement.
+}
+
+func TestRest(t *testing.T) {
+	// TODO(sadovsky): Implement.
+}
diff --git a/services/groups/internal/server/types.vdl b/services/groups/internal/server/types.vdl
new file mode 100644
index 0000000..49e18b3
--- /dev/null
+++ b/services/groups/internal/server/types.vdl
@@ -0,0 +1,17 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server
+
+import (
+	"v.io/v23/services/groups"
+	"v.io/v23/security/access"
+)
+
+// groupData represents the persistent state of a group. (The group name is
+// persisted as the store entry key.)
+type groupData struct {
+	Perms   access.Permissions
+	Entries set[groups.BlessingPatternChunk]
+}
diff --git a/services/groups/internal/server/types.vdl.go b/services/groups/internal/server/types.vdl.go
new file mode 100644
index 0000000..0cbc820
--- /dev/null
+++ b/services/groups/internal/server/types.vdl.go
@@ -0,0 +1,33 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file was auto-generated by the vanadium vdl tool.
+// Source: types.vdl
+
+package server
+
+import (
+	// VDL system imports
+	"v.io/v23/vdl"
+
+	// VDL user imports
+	"v.io/v23/security/access"
+	"v.io/v23/services/groups"
+)
+
+// groupData represents the persistent state of a group. (The group name is
+// persisted as the store entry key.)
+type groupData struct {
+	Perms   access.Permissions
+	Entries map[groups.BlessingPatternChunk]struct{}
+}
+
+func (groupData) __VDLReflect(struct {
+	Name string `vdl:"v.io/x/ref/services/groups/internal/server.groupData"`
+}) {
+}
+
+func init() {
+	vdl.Register((*groupData)(nil))
+}
diff --git a/services/groups/internal/store/gkv/store.go b/services/groups/internal/store/gkv/store.go
new file mode 100644
index 0000000..107aa33
--- /dev/null
+++ b/services/groups/internal/store/gkv/store.go
@@ -0,0 +1,196 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package gkv provides a simple implementation of server.Store that uses
+// gkvlite for persistence. It's meant as a stopgap solution until the Syncbase
+// storage engine is ready for consumption by other Vanadium modules. Since it's
+// a stopgap, it doesn't bother with entry-level locking.
+package gkv
+
+import (
+	"os"
+	"strconv"
+	"sync"
+
+	"github.com/steveyen/gkvlite"
+
+	"v.io/v23/vdl"
+	"v.io/v23/verror"
+	"v.io/v23/vom"
+	"v.io/x/ref/services/groups/internal/store"
+)
+
+const collectionName string = "c"
+
+type entry struct {
+	Value   interface{}
+	Version uint64
+}
+
+// TODO(sadovsky): Compaction.
+type gkv struct {
+	mu   sync.Mutex
+	err  error
+	file *os.File
+	kvst *gkvlite.Store
+	coll *gkvlite.Collection
+}
+
+var _ store.Store = (*gkv)(nil)
+
+func New(filename string) (store.Store, error) {
+	file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0600)
+	if err != nil {
+		return nil, convertError(err)
+	}
+	kvst, err := gkvlite.NewStore(file)
+	if err != nil {
+		file.Close()
+		return nil, convertError(err)
+	}
+	res := &gkv{file: file, kvst: kvst}
+	coll := kvst.GetCollection(collectionName)
+	// Create collection if needed.
+	if coll == nil {
+		coll = kvst.SetCollection(collectionName, nil)
+		if err := res.flush(); err != nil {
+			res.Close()
+			return nil, convertError(err)
+		}
+	}
+	res.coll = coll
+	return res, nil
+}
+
+func (st *gkv) Get(k string, v interface{}) (version string, err error) {
+	st.mu.Lock()
+	defer st.mu.Unlock()
+	if st.err != nil {
+		return "", convertError(st.err)
+	}
+	e, err := st.get(k)
+	if err != nil {
+		return "", err
+	}
+	if err := vdl.Convert(v, e.Value); err != nil {
+		return "", convertError(err)
+	}
+	return strconv.FormatUint(e.Version, 10), nil
+}
+
+func (st *gkv) Insert(k string, v interface{}) error {
+	st.mu.Lock()
+	defer st.mu.Unlock()
+	if st.err != nil {
+		return convertError(st.err)
+	}
+	if _, err := st.get(k); verror.ErrorID(err) != store.ErrUnknownKey.ID {
+		if err != nil {
+			return err
+		}
+		return verror.New(store.ErrKeyExists, nil, k)
+	}
+	return st.put(k, &entry{Value: v})
+}
+
+func (st *gkv) Update(k string, v interface{}, version string) error {
+	st.mu.Lock()
+	defer st.mu.Unlock()
+	if st.err != nil {
+		return convertError(st.err)
+	}
+	e, err := st.get(k)
+	if err != nil {
+		return err
+	}
+	if err := e.checkVersion(version); err != nil {
+		return err
+	}
+	return st.put(k, &entry{Value: v, Version: e.Version + 1})
+}
+
+func (st *gkv) Delete(k string, version string) error {
+	st.mu.Lock()
+	defer st.mu.Unlock()
+	if st.err != nil {
+		return convertError(st.err)
+	}
+	e, err := st.get(k)
+	if err != nil {
+		return err
+	}
+	if err := e.checkVersion(version); err != nil {
+		return err
+	}
+	return st.delete(k)
+}
+
+func (st *gkv) Close() error {
+	st.mu.Lock()
+	defer st.mu.Unlock()
+	if st.err != nil {
+		return convertError(st.err)
+	}
+	st.err = verror.New(verror.ErrCanceled, nil, "closed store")
+	st.kvst.Close()
+	return convertError(st.file.Close())
+}
+
+////////////////////////////////////////
+// Internal helpers
+
+// get, put, delete, and flush all assume st.mu is held.
+func (st *gkv) get(k string) (*entry, error) {
+	bytes, err := st.coll.Get([]byte(k))
+	if err != nil {
+		return nil, convertError(err)
+	}
+	if bytes == nil {
+		return nil, verror.New(store.ErrUnknownKey, nil, k)
+	}
+	e := &entry{}
+	if err := vom.Decode(bytes, e); err != nil {
+		return nil, convertError(err)
+	}
+	return e, nil
+}
+
+func (st *gkv) put(k string, e *entry) error {
+	bytes, err := vom.Encode(e)
+	if err != nil {
+		return convertError(err)
+	}
+	if err := st.coll.Set([]byte(k), bytes); err != nil {
+		return convertError(err)
+	}
+	return convertError(st.flush())
+}
+
+func (st *gkv) delete(k string) error {
+	if _, err := st.coll.Delete([]byte(k)); err != nil {
+		return convertError(err)
+	}
+	return convertError(st.flush())
+}
+
+func (st *gkv) flush() error {
+	if err := st.kvst.Flush(); err != nil {
+		return convertError(err)
+	}
+	// TODO(sadovsky): Better handling for the case where kvst.Flush() succeeds
+	// but file.Sync() fails. See discussion in v.io/c/11829.
+	return convertError(st.file.Sync())
+}
+
+func (e *entry) checkVersion(version string) error {
+	newVersion := strconv.FormatUint(e.Version, 10)
+	if version != newVersion {
+		return verror.NewErrBadVersion(nil)
+	}
+	return nil
+}
+
+func convertError(err error) error {
+	return verror.Convert(verror.IDAction{}, nil, err)
+}
diff --git a/services/groups/internal/store/memstore/store.go b/services/groups/internal/store/memstore/store.go
new file mode 100644
index 0000000..c086c44
--- /dev/null
+++ b/services/groups/internal/store/memstore/store.go
@@ -0,0 +1,122 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package memstore provides a simple, in-memory implementation of server.Store.
+// Since it's a prototype implementation, it doesn't bother with entry-level
+// locking.
+package memstore
+
+import (
+	"strconv"
+	"sync"
+
+	"v.io/v23/vdl"
+	"v.io/v23/verror"
+	"v.io/x/ref/services/groups/internal/store"
+)
+
+type entry struct {
+	Value   interface{}
+	Version uint64
+}
+
+type memstore struct {
+	mu   sync.Mutex
+	err  error
+	data map[string]*entry
+}
+
+var _ store.Store = (*memstore)(nil)
+
+func New() store.Store {
+	return &memstore{data: map[string]*entry{}}
+}
+
+func (st *memstore) Get(k string, v interface{}) (version string, err error) {
+	st.mu.Lock()
+	defer st.mu.Unlock()
+	if st.err != nil {
+		return "", convertError(st.err)
+	}
+	e, ok := st.data[k]
+	if !ok {
+		return "", verror.New(store.ErrUnknownKey, nil, k)
+	}
+	if err := vdl.Convert(v, e.Value); err != nil {
+		return "", convertError(err)
+	}
+	return strconv.FormatUint(e.Version, 10), nil
+}
+
+func (st *memstore) Insert(k string, v interface{}) error {
+	st.mu.Lock()
+	defer st.mu.Unlock()
+	if st.err != nil {
+		return convertError(st.err)
+	}
+	if _, ok := st.data[k]; ok {
+		return verror.New(store.ErrKeyExists, nil, k)
+	}
+	st.data[k] = &entry{Value: v}
+	return nil
+}
+
+func (st *memstore) Update(k string, v interface{}, version string) error {
+	st.mu.Lock()
+	defer st.mu.Unlock()
+	if st.err != nil {
+		return convertError(st.err)
+	}
+	e, ok := st.data[k]
+	if !ok {
+		return verror.New(store.ErrUnknownKey, nil, k)
+	}
+	if err := e.checkVersion(version); err != nil {
+		return err
+	}
+	st.data[k] = &entry{Value: v, Version: e.Version + 1}
+	return nil
+}
+
+func (st *memstore) Delete(k string, version string) error {
+	st.mu.Lock()
+	defer st.mu.Unlock()
+	if st.err != nil {
+		return convertError(st.err)
+	}
+	e, ok := st.data[k]
+	if !ok {
+		return verror.New(store.ErrUnknownKey, nil, k)
+	}
+	if err := e.checkVersion(version); err != nil {
+		return err
+	}
+	delete(st.data, k)
+	return nil
+}
+
+func (st *memstore) Close() error {
+	st.mu.Lock()
+	defer st.mu.Unlock()
+	if st.err != nil {
+		return convertError(st.err)
+	}
+	st.err = verror.New(verror.ErrCanceled, nil, "closed store")
+	return nil
+}
+
+////////////////////////////////////////
+// Internal helpers
+
+func (e *entry) checkVersion(version string) error {
+	newVersion := strconv.FormatUint(e.Version, 10)
+	if version != newVersion {
+		return verror.NewErrBadVersion(nil)
+	}
+	return nil
+}
+
+func convertError(err error) error {
+	return verror.Convert(verror.IDAction{}, nil, err)
+}
diff --git a/services/groups/internal/store/model.go b/services/groups/internal/store/model.go
new file mode 100644
index 0000000..6f51152
--- /dev/null
+++ b/services/groups/internal/store/model.go
@@ -0,0 +1,35 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package store
+
+// Store is a key-value store that uses versions for optimistic concurrency
+// control. The versions passed to Update and Delete must come from Get. If in
+// the meantime some client has called Update or Delete on the same key, the
+// version will be stale and the method call will fail.
+//
+// Note, this API disallows empty versions to simplify implementation. The group
+// server is the only client of this API and always specifies versions.
+type Store interface {
+	// Get returns the value for the given key (decoding into v).
+	// Fails if the given key is unknown (ErrUnknownKey).
+	Get(k string, v interface{}) (version string, err error)
+
+	// Insert writes the given value for the given key.
+	// Fails if an entry already exists for the given key (ErrKeyExists).
+	Insert(k string, v interface{}) error
+
+	// Update writes the given value for the given key.
+	// Fails if the given key is unknown (ErrUnknownKey).
+	// Fails if version doesn't match (ErrBadVersion).
+	Update(k string, v interface{}, version string) error
+
+	// Delete deletes the entry for the given key.
+	// Fails if the given key is unknown (ErrUnknownKey).
+	// Fails if version doesn't match (ErrBadVersion).
+	Delete(k string, version string) error
+
+	// Close closes the store. All subsequent method calls will fail.
+	Close() error
+}
diff --git a/services/groups/internal/store/model.vdl b/services/groups/internal/store/model.vdl
new file mode 100644
index 0000000..a6acab0
--- /dev/null
+++ b/services/groups/internal/store/model.vdl
@@ -0,0 +1,13 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package store
+
+error (
+	// KeyExists means the given key already exists in the store.
+	KeyExists() {"en":"Key exists{:_}"}
+
+	// UnknownKey means the given key does not exist in the store.
+	UnknownKey() {"en":"Unknown key{:_}"}
+)
diff --git a/services/groups/internal/store/model.vdl.go b/services/groups/internal/store/model.vdl.go
new file mode 100644
index 0000000..aa66235
--- /dev/null
+++ b/services/groups/internal/store/model.vdl.go
@@ -0,0 +1,37 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file was auto-generated by the vanadium vdl tool.
+// Source: model.vdl
+
+package store
+
+import (
+	// VDL system imports
+	"v.io/v23/context"
+	"v.io/v23/i18n"
+	"v.io/v23/verror"
+)
+
+var (
+	// KeyExists means the given key already exists in the store.
+	ErrKeyExists = verror.Register("v.io/x/ref/services/groups/internal/store.KeyExists", verror.NoRetry, "{1:}{2:} Key exists{:_}")
+	// UnknownKey means the given key does not exist in the store.
+	ErrUnknownKey = verror.Register("v.io/x/ref/services/groups/internal/store.UnknownKey", verror.NoRetry, "{1:}{2:} Unknown key{:_}")
+)
+
+func init() {
+	i18n.Cat().SetWithBase(i18n.LangID("en"), i18n.MsgID(ErrKeyExists.ID), "{1:}{2:} Key exists{:_}")
+	i18n.Cat().SetWithBase(i18n.LangID("en"), i18n.MsgID(ErrUnknownKey.ID), "{1:}{2:} Unknown key{:_}")
+}
+
+// NewErrKeyExists returns an error with the ErrKeyExists ID.
+func NewErrKeyExists(ctx *context.T) error {
+	return verror.New(ErrKeyExists, ctx)
+}
+
+// NewErrUnknownKey returns an error with the ErrUnknownKey ID.
+func NewErrUnknownKey(ctx *context.T) error {
+	return verror.New(ErrUnknownKey, ctx)
+}