blob: 4dd20e2c1edfc71a238a9f7e319f9934256ae215 [file] [log] [blame]
package server
import (
"v.io/core/veyron2/ipc"
"v.io/core/veyron2/security"
"v.io/core/veyron2/services/security/access"
"v.io/core/veyron2/services/security/groups"
"v.io/core/veyron2/verror"
)
type group struct {
name string
m *manager
}
var _ groups.GroupServerMethods = (*group)(nil)
// TODO(sadovsky): verror.NewNoExistOrNoAccess and verror.NewInternal do not
// accept extra arguments, so in various places below we call verror.New
// directly.
// TODO(sadovsky): What prevents "mike" from creating group "adam:AllMyDevices"?
// It seems we need either (a) identity providers to manage group servers and
// reserve buckets for users they've blessed, or (b) some way to determine the
// user name from a blessing and enforce that group names start with user names.
func (g *group) Create(ctx ipc.ServerContext, acl access.TaggedACLMap, entries []groups.BlessingPatternChunk) error {
// Perform ACL check.
// TODO(sadovsky): Enable this ACL check and acquire a lock on the group
// server ACL.
if false {
if err := g.authorize(ctx, g.m.acl); err != nil {
return err
}
}
if acl == nil {
acl = access.TaggedACLMap{}
blessings, _ := ctx.RemoteBlessings().ForContext(ctx)
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.Context())
}
for _, tag := range access.AllTypicalTags() {
for _, b := range blessings {
acl.Add(security.BlessingPattern(b), string(tag))
}
}
}
entrySet := map[groups.BlessingPatternChunk]struct{}{}
for _, v := range entries {
entrySet[v] = struct{}{}
}
gd := groupData{ACL: acl, 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. (That's not a great solution either. Having per-user
// buckets seems like a better solution.)
if _, ok := err.(*ErrKeyAlreadyExists); ok {
return groups.NewErrGroupAlreadyExists(ctx.Context(), g.name)
}
return verror.New(verror.ErrInternal, ctx.Context(), err)
}
return nil
}
func (g *group) Delete(ctx ipc.ServerContext, etag string) error {
return g.readModifyWrite(ctx, etag, func(gd *groupData, etagSt string) error {
return g.m.st.Delete(g.name, etagSt)
})
}
func (g *group) Add(ctx ipc.ServerContext, entry groups.BlessingPatternChunk, etag string) error {
return g.update(ctx, etag, func(gd *groupData) {
gd.Entries[entry] = struct{}{}
})
}
func (g *group) Remove(ctx ipc.ServerContext, entry groups.BlessingPatternChunk, etag string) error {
return g.update(ctx, etag, func(gd *groupData) {
delete(gd.Entries, entry)
})
}
// TODO(sadovsky): Replace fake implementation with real implementation.
func (g *group) Get(ctx ipc.ServerContext, req groups.GetRequest, reqEtag string) (res groups.GetResponse, etag string, err error) {
gd, etag, err := g.getInternal(ctx)
if err != nil {
return groups.GetResponse{}, "", err
}
return groups.GetResponse{Entries: gd.Entries}, etag, nil
}
// TODO(sadovsky): Replace fake implementation with real implementation.
func (g *group) Rest(ctx ipc.ServerContext, req groups.RestRequest, reqEtag string) (res groups.RestResponse, etag string, err error) {
_, etag, err = g.getInternal(ctx)
if err != nil {
return groups.RestResponse{}, "", err
}
return groups.RestResponse{}, etag, nil
}
func (g *group) SetACL(ctx ipc.ServerContext, acl access.TaggedACLMap, etag string) error {
return g.update(ctx, etag, func(gd *groupData) {
gd.ACL = acl
})
}
func (g *group) GetACL(ctx ipc.ServerContext) (acl access.TaggedACLMap, etag string, err error) {
gd, etag, err := g.getInternal(ctx)
if err != nil {
return nil, "", err
}
return gd.ACL, etag, nil
}
////////////////////////////////////////
// Internal helpers
// Returns a VDL-compatible error.
func (g *group) authorize(ctx ipc.ServerContext, acl access.TaggedACLMap) error {
// TODO(sadovsky): We ignore the returned error since TypicalTagType is
// guaranteed to return a valid tagType. It would be nice to have an
// alternative function that assumes TypicalTagType, since presumably that's
// the overwhelmingly common case.
auth, _ := access.TaggedACLAuthorizer(acl, access.TypicalTagType())
if err := auth.Authorize(ctx); err != nil {
// TODO(sadovsky): Return NoAccess if appropriate.
return verror.New(verror.ErrNoExistOrNoAccess, ctx.Context(), err)
}
return nil
}
// Returns a VDL-compatible error. Performs access check.
func (g *group) getInternal(ctx ipc.ServerContext) (gd groupData, etag string, err error) {
v, etag, err := g.m.st.Get(g.name)
if err != nil {
if _, ok := err.(*ErrUnknownKey); ok {
// TODO(sadovsky): Return NoExist if appropriate.
return groupData{}, "", verror.New(verror.ErrNoExistOrNoAccess, ctx.Context(), g.name)
} else {
return groupData{}, "", verror.New(verror.ErrInternal, ctx.Context(), err)
}
}
gd, ok := v.(groupData)
if !ok {
return groupData{}, "", verror.New(verror.ErrInternal, ctx.Context(), "bad value for key: "+g.name)
}
if err := g.authorize(ctx, gd.ACL); err != nil {
return groupData{}, "", err
}
return gd, etag, nil
}
// Returns a VDL-compatible error. Performs access check.
func (g *group) update(ctx ipc.ServerContext, etag string, fn func(gd *groupData)) error {
return g.readModifyWrite(ctx, etag, func(gd *groupData, etagSt string) error {
fn(gd)
return g.m.st.Update(g.name, *gd, etagSt)
})
}
// 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 ipc.ServerContext, etag string, fn func(gd *groupData, etagSt string) error) error {
// Transaction retry loop.
for i := 0; i < 3; i++ {
gd, etagSt, err := g.getInternal(ctx)
if err != nil {
return err
}
// Fail early if possible.
if etag != "" && etag != etagSt {
return groups.NewErrBadEtag(ctx.Context(), etag, etagSt)
}
if err := fn(&gd, etagSt); err != nil {
if err, ok := err.(*ErrBadEtag); ok {
// Retry on etag error if the original etag was empty.
if etag != "" {
return groups.NewErrBadEtag(ctx.Context(), err.OldEtag, err.NewEtag)
}
} else {
// Abort on non-etag error.
return verror.New(verror.ErrInternal, ctx.Context(), err)
}
} else {
return nil
}
}
return groups.NewErrExcessiveContention(ctx.Context())
}