blob: 2bc7f1c547aa03f2b799408bcc99ae2c47551834 [file] [log] [blame]
// Copyright 2016 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 common
import (
"sort"
"strings"
"v.io/v23/context"
"v.io/v23/rpc"
"v.io/v23/security"
"v.io/v23/security/access"
wire "v.io/v23/services/syncbase"
"v.io/v23/verror"
"v.io/x/ref/services/syncbase/store"
)
// ValidatePerms does basic sanity checking on the provided perms:
// - Perms can contain only tags in the provided whitelist.
// - At least one admin must be included to avoid permanently losing access.
func ValidatePerms(ctx *context.T, perms access.Permissions, allowTags []access.Tag) error {
// Perms cannot be empty or nil.
if len(perms) == 0 {
return NewErrPermsEmpty(ctx)
}
// Perms cannot contain any tags not in the allowTags whitelist.
allowTagsSet := make(map[string]struct{}, len(allowTags))
for _, tag := range allowTags {
allowTagsSet[string(tag)] = struct{}{}
}
var disallowedTags []string
for tag, _ := range perms {
if _, ok := allowTagsSet[tag]; !ok {
disallowedTags = append(disallowedTags, tag)
}
}
if len(disallowedTags) > 0 {
sort.Strings(disallowedTags)
return NewErrPermsDisallowedTags(ctx, disallowedTags, access.TagStrings(allowTags...))
}
// Perms must include at least one Admin.
// TODO(ivanpi): More sophisticated admin verification, e.g. check that NotIn
// doesn't blacklist all possible In matches.
if adminAcl, ok := perms[string(access.Admin)]; !ok || len(adminAcl.In) == 0 {
return NewErrPermsNoAdmin(ctx)
}
// TODO(ivanpi): Check that perms are enforceable? It would make validation
// context-dependent.
return nil
}
// AnyOfTagsAuthorizer provides an authorizer that allows blessings matching any
// pattern in perms corresponding to any of the provided tags.
func AnyOfTagsAuthorizer(tags []access.Tag, perms access.Permissions) *anyOfTagsAuthorizer {
return &anyOfTagsAuthorizer{
tags: tags,
perms: perms,
}
}
type anyOfTagsAuthorizer struct {
tags []access.Tag
perms access.Permissions
}
func (a *anyOfTagsAuthorizer) Authorize(ctx *context.T, call security.Call) error {
blessings, invalid := security.RemoteBlessingNames(ctx, call)
for _, tag := range a.tags {
if acl, exists := a.perms[string(tag)]; exists && acl.Includes(blessings...) {
// At least one tag in the set had a matching pattern.
return nil
}
}
// None of the tags had a matching pattern. Deny access.
return verror.New(verror.ErrNoAccess, ctx,
access.NewErrNoPermissions(ctx, blessings, invalid, strings.Join(access.TagStrings(a.tags...), " ∨ ")))
}
// CheckImplicitPerms performs an authorization check against the implicit
// permissions derived from the blessing pattern in the Id. It returns the
// generated implicit perms or an authorization error.
// TODO(ivanpi): Change to check against the specific blessing used for signing
// instead of any blessing in call.Security().
func CheckImplicitPerms(ctx *context.T, call rpc.ServerCall, id wire.Id, allowedTags []access.Tag) (access.Permissions, error) {
implicitPerms := access.Permissions{}.Add(security.BlessingPattern(id.Blessing), access.TagStrings(allowedTags...)...)
// Note, allowedTags is expected to contain access.Admin.
if err := AnyOfTagsAuthorizer([]access.Tag{access.Admin}, implicitPerms).Authorize(ctx, call.Security()); err != nil {
return nil, verror.New(wire.ErrUnauthorizedCreateId, ctx, id.Blessing, id.Name, err)
}
return implicitPerms, nil
}
// PermserData is persistent metadata about an object, including perms.
type PermserData interface {
// GetPerms returns the perms for the object.
GetPerms() access.Permissions
}
// Permser is an object in the hierarchy that supports retrieving perms and
// authorizing access to existence checks. Access checks on Permser objects
// using Get{Data,Perms}With*Auth functions below should be done in the same
// transaction as any store modification to ensure that concurrent ACL changes
// invalidate the modification.
type Permser interface {
// GetDataWithExistAuth must return a nil error only if the object exists and
// the caller is authorized to know it (Resolve access up to the parent and
// any access tag on self, or Resolve access up to grandparent and Read or
// Write on parent). Otherwise, the returned error must not leak existence
// data (ErrNoExistOrNoAccess must be returned instead of more specific
// errors such as ErrNoExist if the caller is not authorized to know about
// an object's existence).
// If the error is nil, PermserData must be populated with object metadata
// loaded from the store and the method must return perms of the object's
// parent and the object itself.
// A typical implementation calls GetPermsWithExistAndParentResolveAuth on
// the object's parent, followed by GetDataWithExistAuthStep.
GetDataWithExistAuth(ctx *context.T, call rpc.ServerCall, st store.StoreReader, v PermserData) (parentPerms, perms access.Permissions, existErr error)
// PermserData returns a zero-value PermserData for this object.
PermserData() PermserData
}
// getDataWithExistAndParentResolveAuth is equivalent to
// GetPermsWithExistAndParentResolveAuth, in addition populating the loaded
// PermserData into v.
func getDataWithExistAndParentResolveAuth(ctx *context.T, call rpc.ServerCall, at Permser, st store.StoreReader, v PermserData) (access.Permissions, error) {
parentPerms, perms, existErr := at.GetDataWithExistAuth(ctx, call, st, v)
if existErr != nil {
return nil, existErr
}
return perms, AnyOfTagsAuthorizer([]access.Tag{access.Resolve}, parentPerms).Authorize(ctx, call.Security())
}
// GetPermsWithExistAndParentResolveAuth returns a nil error only if the object
// exists, the client is authorized to know it and has resolve access on all
// objects up to and including this object's parent.
func GetPermsWithExistAndParentResolveAuth(ctx *context.T, call rpc.ServerCall, at Permser, st store.StoreReader) (access.Permissions, error) {
return getDataWithExistAndParentResolveAuth(ctx, call, at, st, at.PermserData())
}
// GetDataWithAuth is equivalent to GetPermsWithAuth, in addition populating
// the loaded PermserData into v.
func GetDataWithAuth(ctx *context.T, call rpc.ServerCall, at Permser, tags []access.Tag, st store.StoreReader, v PermserData) (access.Permissions, error) {
perms, existErr := getDataWithExistAndParentResolveAuth(ctx, call, at, st, v)
if existErr != nil {
return nil, existErr
}
return perms, AnyOfTagsAuthorizer(tags, perms).Authorize(ctx, call.Security())
}
// GetPermsWithAuth returns a nil error only if the client has exist and parent
// resolve access (see GetPermsWithExistAndParentResolveAuth) as well as at
// least one of the specified tags on the object itself.
func GetPermsWithAuth(ctx *context.T, call rpc.ServerCall, at Permser, tags []access.Tag, st store.StoreReader) (access.Permissions, error) {
return GetDataWithAuth(ctx, call, at, tags, st, at.PermserData())
}
// GetDataWithExistAuthStep is a helper intended for use in GetDataWithExistAuth
// implementations. It assumes Resolve access up to and including the object's
// grandparent. It loads the object's metadata from the store into v, returning
// ErrNoExistOrNoAccess, ErrNoExist or other errors when appropriate; if the
// caller is not authorized for exist access, ErrNoExistOrNoAccess is always
// returned. If a nil StoreReader is passed in, the object is assumed to not
// exist.
func GetDataWithExistAuthStep(ctx *context.T, call rpc.ServerCall, name string, parentPerms access.Permissions, st store.StoreReader, k string, v PermserData) error {
if st == nil {
return fuzzifyErrorForExists(ctx, call, name, parentPerms, nil, verror.New(verror.ErrNoExist, ctx, name))
}
if getErr := store.Get(ctx, st, k, v); getErr != nil {
if verror.ErrorID(getErr) == verror.ErrNoExist.ID {
getErr = verror.New(verror.ErrNoExist, ctx, name)
}
return fuzzifyErrorForExists(ctx, call, name, parentPerms, nil, getErr)
}
return fuzzifyErrorForExists(ctx, call, name, parentPerms, v.GetPerms(), nil)
}
// fuzzifyErrorForExists passes through the original error only if the caller is
// authorized for exist access. Otherwise, ErrNoExistOrNoAccess is returned
// instead. It assumes Resolve access up to and including the object's
// grandparent.
func fuzzifyErrorForExists(ctx *context.T, call rpc.ServerCall, name string, parentPerms, perms access.Permissions, originalErr error) error {
if parentRWErr := AnyOfTagsAuthorizer([]access.Tag{access.Read, access.Write}, parentPerms).Authorize(ctx, call.Security()); parentRWErr == nil {
// Read or Write on parent, return original error.
return originalErr
}
fuzzyErr := verror.New(verror.ErrNoExistOrNoAccess, ctx, name)
if perms == nil {
// Exit early if object does not exist - caller cannot have any perms on it.
return fuzzyErr
}
// No Read or Write on parent, caller must have both Resolve on parent and at
// least one tag on the object itself to get the original error.
if parentXErr := AnyOfTagsAuthorizer([]access.Tag{access.Resolve}, parentPerms).Authorize(ctx, call.Security()); parentXErr != nil {
return fuzzyErr
}
if selfAnyErr := AnyOfTagsAuthorizer(access.AllTypicalTags(), perms).Authorize(ctx, call.Security()); selfAnyErr != nil {
return fuzzyErr
}
return originalErr
}
// ErrorToExists converts the error returned from GetDataWithExistAuth into
// the Exists RPC result, suppressing ErrNoExist.
func ErrorToExists(err error) (bool, error) {
if err == nil {
return true, nil
}
if verror.ErrorID(err) == verror.ErrNoExist.ID {
return false, nil
}
return false, err
}