| // 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/lib/set" |
| "v.io/x/ref/services/groups/internal/store" |
| ) |
| |
| type group struct { |
| name string |
| m *manager |
| } |
| |
| var _ groups.GroupServerMethods = (*group)(nil) |
| |
| // TODO(sadovsky): Limit the number of groups that a particular user |
| // (v23/conventsions.GetClientUserId) can create? |
| func (g *group) Create(ctx *context.T, call rpc.ServerCall, perms access.Permissions, entries []groups.BlessingPatternChunk) error { |
| if err := g.m.createAuthorizer.Authorize(ctx, call.Security()); 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) |
| set.String.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) |
| } |