blob: 241345a9ed6a146e9f8adec011355ca6285c8372 [file] [log] [blame]
// 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)
}