| // 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) |
| } |