blob: a45651d4586aa8a171793daffb735285739b0126 [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.
// This is WIP to implement and test algorithms that match blessings
// against blessing patterns that involve groups. We expect to
// eventually migrate some of this code to v23/security/pattern.go.
package groups
import (
"errors"
"regexp"
"strings"
"v.io/v23/context"
"v.io/v23/security"
"v.io/v23/verror"
)
const (
GroupStart = "<grp:" // GroupStart indicates the start of a group name in a blessing pattern.
GroupEnd = ">" // GroupEnd indicates the end of a group name in a blessing pattern.
)
var (
exactMatch = convertToSet("")
grpRegexp = regexp.MustCompile(GroupStart + "[^" + GroupEnd + "]*" + GroupEnd)
grpStartToken = strings.TrimSuffix(GroupStart, security.ChainSeparator)
)
// Match matches blessing names against a pattern. It returns an empty array if
// the presented blessing names do not match the pattern, or a set of suffixes
// when the pattern is a prefix of the blessing names. Match approximates the
// result if any errors are encountered during the matching process.
//
// Match assumes presented blessing names are valid. ctx is used to make any
// outgoing RPCs.
// TODO(hpucha): Enhance to add versioning, scrub Approximation content for privacy.
func Match(ctx *context.T, p security.BlessingPattern, hint ApproximationType, visitedGroups map[string]struct{}, blessings map[string]struct{}) (map[string]struct{}, []Approximation) {
if visitedGroups == nil {
visitedGroups = make(map[string]struct{})
}
g := &grpClient{
hint: hint,
visited: visitedGroups,
rpcHandler: groupClientRPCImpl{},
}
return g.match(ctx, p, blessings), g.apprxs
}
type grpClient struct {
hint ApproximationType
version string
visited map[string]struct{}
apprxs []Approximation
rpcHandler groupClientRPC
}
func (g *grpClient) match(ctx *context.T, p security.BlessingPattern, blessings map[string]struct{}) map[string]struct{} {
if len(p) == 0 {
return nil
}
// Note: The correct answer here is that the returned set must
// consist of exactMatch and every proper suffix of the
// blessing string. Since we do not allow any components after
// security.AllPrincipals, returning exactMatch is sufficient.
if p == security.AllPrincipals {
return exactMatch
}
patTokens, err := splitPattern(p)
if err != nil {
// Approximate the result.
errTmp := verror.New(verror.ErrBadArg, ctx, "malformed pattern", p)
g.apprxs = append(g.apprxs, Approximation{Reason: string(verror.ErrorID(errTmp)), Details: errTmp.Error()})
return approxRemainder(g.hint, blessings)
}
glob := true
if patTokens[len(patTokens)-1] == string(security.NoExtension) {
glob = false
patTokens = patTokens[:len(patTokens)-1]
}
remainder := make(map[string]struct{})
for b := range blessings {
rem := g.matchedByBlessing(ctx, patTokens, b)
if glob {
remainder = union(remainder, rem)
continue
}
for s := range rem {
if s == "" {
remainder = union(remainder, exactMatch)
break
}
}
}
return remainder
}
func (g *grpClient) matchedByBlessing(ctx *context.T, patTokens []string, b string) map[string]struct{} {
remainder := convertToSet(b)
for _, patTok := range patTokens {
removeEmptyStrings(remainder)
if len(remainder) == 0 {
return nil
}
if group := groupName(patTok); group != "" {
remainder = g.remainder(ctx, group, remainder)
continue
}
// Pattern token is not a group reference.
newRemainder := make(map[string]struct{})
for b := range remainder {
btok, suffix := leftMostToken(b)
if patTok == btok {
newRemainder[suffix] = struct{}{}
}
}
remainder = newRemainder
}
return remainder
}
func (g *grpClient) remainder(ctx *context.T, groupName string, blessingChunks map[string]struct{}) map[string]struct{} {
var err error
var apprxs []Approximation
var remainder map[string]struct{}
if _, ok := g.visited[groupName]; ok {
err = verror.New(ErrCycleFound, ctx, groupName, g.visited)
} else {
visited := copyMap(g.visited)
visited[groupName] = struct{}{}
remainder, apprxs, _, err = g.rpcHandler.relate(ctx, groupName, blessingChunks, g.hint, g.version, visited)
}
g.apprxs = append(g.apprxs, apprxs...)
if err != nil {
// For any error, suitably approximate the result.
g.apprxs = append(g.apprxs, Approximation{Reason: string(verror.ErrorID(err)), Details: err.Error()})
return approxRemainder(g.hint, blessingChunks)
}
return remainder
}
///////////////////////////////
// Helper functions
// splitPattern splits a pattern into valid tokens separated by ":"
// (security.ChainSeparator). If any token in a pattern is invalid, it returns
// an error.
//
// A group token (<grp:[grpname]>) is treated as indivisible. A pattern token
// is valid if it is a valid blessing extension or a single valid group token.
// A token that concatenates two group tokens, or a group token concatenated
// with a valid blessing extension, is invalid.
//
// TODO(hpucha): Is there a better way to do this split using regex
// matching?
func splitPattern(p security.BlessingPattern) ([]string, error) {
// Check that the blessing pattern is valid without the
// presence of group tokens (see v23/security/pattern.go).
if pTmp := security.BlessingPattern(grpRegexp.ReplaceAllLiteralString(string(p), "X")); !pTmp.IsValid() {
return nil, errors.New("invalid pattern: non-group tokens are invalid")
}
pStr := string(p)
var patTokens []string
for pStr != "" {
tokens := strings.SplitN(pStr, security.ChainSeparator, 2)
if !strings.Contains(tokens[0], grpStartToken) {
// Token is a blessing extension.
patTokens = append(patTokens, tokens[0])
if len(tokens) != 2 {
break
}
pStr = tokens[1]
continue
}
// Check for a valid group token.
if !strings.HasPrefix(tokens[0], grpStartToken) {
// Invalid group token. <grp: is not at the beginning of a token.
return nil, errors.New("invalid pattern: group token has a prefix")
}
endpos := strings.Index(pStr, GroupEnd)
if endpos < 0 {
// Invalid group token. Matching ">" not found.
return nil, errors.New("invalid pattern: group end delimiter not found")
}
if endpos < len(pStr)-1 && pStr[endpos+1] != security.ChainSeparator[0] {
// The pattern must either end here, or be followed by a /.
return nil, errors.New("invalid pattern: group token has a suffix")
}
grpName := pStr[len(GroupStart):endpos]
if grpName == "" {
return nil, errors.New("invalid pattern: group token is empty")
}
patTokens = append(patTokens, pStr[0:endpos+1])
// Skip the / and check if any string is remaining.
if endpos+2 >= len(pStr) {
return patTokens, nil
}
pStr = pStr[endpos+2:]
}
return patTokens, nil
}
// groupName extracts the group name from a pattern token.
func groupName(pattok string) string {
if strings.HasPrefix(pattok, GroupStart) && strings.HasSuffix(pattok, GroupEnd) {
return pattok[len(GroupStart) : len(pattok)-len(GroupEnd)]
}
return ""
}
// leftMostToken returns the left most token and the remaining suffix
// of a blessing name.
func leftMostToken(b string) (string, string) {
tokens := strings.SplitN(b, security.ChainSeparator, 2)
suffix := ""
if len(tokens) == 2 {
suffix = tokens[1]
}
return tokens[0], suffix
}
// removeEmptyStrings removes any empty blessing chunks from the
// remainder set.
func removeEmptyStrings(remainder map[string]struct{}) {
for b := range remainder {
if b == "" {
delete(remainder, b)
}
}
}
func copyMap(in map[string]struct{}) map[string]struct{} {
out := make(map[string]struct{}, len(in))
for k := range in {
out[k] = struct{}{}
}
return out
}
func approxRemainder(hint ApproximationType, blessingChunks map[string]struct{}) map[string]struct{} {
if hint == ApproximationTypeUnder {
return nil
}
remainder := make(map[string]struct{})
for bc := range blessingChunks {
properSuffixes(remainder, bc)
}
return remainder
}
func properSuffixes(suffixes map[string]struct{}, bc string) {
parts := strings.SplitN(bc, security.ChainSeparator, 2)
for len(parts) == 2 {
suffixes[parts[1]] = struct{}{}
parts = strings.SplitN(parts[1], security.ChainSeparator, 2)
}
suffixes[""] = struct{}{}
}
// 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
}
// groupClientRPC is the interface for the group client making the Relate RPC.
//
// Used for faking RPC handling in tests.
// TODO(hpucha/ashankar): Investigate if injecting a mock rpc.Client for tests is possible.
type groupClientRPC interface {
relate(ctx *context.T, groupName string, blessingChunks map[string]struct{}, hint ApproximationType, version string, vGrps map[string]struct{}) (map[string]struct{}, []Approximation, string, error)
}
type groupClientRPCImpl struct{}
func (g groupClientRPCImpl) relate(ctx *context.T, groupName string, blessingChunks map[string]struct{}, hint ApproximationType, version string, vGrps map[string]struct{}) (map[string]struct{}, []Approximation, string, error) {
client := GroupClient(groupName)
return client.Relate(ctx, blessingChunks, hint, version, vGrps)
}