mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-31 20:39:37 +08:00
Signed-off-by: Zac Bergquist <zac.bergquist@goteleport.com> Co-authored-by: horus <horus.li@gmail.com>
422 lines
12 KiB
Go
422 lines
12 KiB
Go
/*
|
|
* Teleport
|
|
* Copyright (C) 2023 Gravitational, Inc.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
// TODO(nklaassen): evaluate the risks and utility of allowing traits to be used
|
|
// as regular expressions. The only thing blocking this today is that all trait
|
|
// values are lists and the regex must be a single value. It could be possible
|
|
// to write:
|
|
// `{{regexp.match(email.local(head(external.trait_name)))}}`
|
|
package parse
|
|
|
|
import (
|
|
"net/mail"
|
|
"regexp"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/gravitational/trace"
|
|
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
"github.com/gravitational/teleport/lib/utils/typical"
|
|
)
|
|
|
|
const (
|
|
// EmailLocalFnName is a name for email.local function
|
|
EmailLocalFnName = "email.local"
|
|
// RegexpMatchFnName is a name for regexp.match function.
|
|
RegexpMatchFnName = "regexp.match"
|
|
// RegexpNotMatchFnName is a name for regexp.not_match function.
|
|
RegexpNotMatchFnName = "regexp.not_match"
|
|
// RegexpReplaceFnName is a name for regexp.replace function.
|
|
RegexpReplaceFnName = "regexp.replace"
|
|
)
|
|
|
|
var (
|
|
traitsTemplateParser = mustNewTraitsTemplateParser()
|
|
|
|
matcherParser = mustNewMatcherParser()
|
|
|
|
reVariable = regexp.MustCompile(
|
|
// prefix is anything that is not { or }
|
|
`^(?P<prefix>[^}{]*)` +
|
|
// variable is anything in brackets {{}} that is not { or }
|
|
`{{(?P<expression>\s*[^}{]*\s*)}}` +
|
|
// suffix is anything that is not { or }
|
|
`(?P<suffix>[^}{]*)$`,
|
|
)
|
|
)
|
|
|
|
// TraitsTemplateExpression can interpolate user trait values into a string
|
|
// template to produce some values.
|
|
type TraitsTemplateExpression struct {
|
|
// prefix is a prefix of the expression
|
|
prefix string
|
|
// suffix is a suffix of the expression
|
|
suffix string
|
|
// expr is the expression AST
|
|
expr traitsTemplateExpression
|
|
}
|
|
|
|
// NewTraitsTemplateExpression parses expressions like {{external.foo}} or {{internal.bar}},
|
|
// or a literal value like "prod". Call Interpolate on the returned Expression
|
|
// to get the final value based on user traits.
|
|
func NewTraitsTemplateExpression(value string) (*TraitsTemplateExpression, error) {
|
|
match := reVariable.FindStringSubmatch(value)
|
|
if len(match) == 0 {
|
|
if strings.Contains(value, "{{") || strings.Contains(value, "}}") {
|
|
return nil, trace.BadParameter(
|
|
"%q is using template brackets '{{' or '}}', however expression does not parse, make sure the format is {{expression}}",
|
|
value,
|
|
)
|
|
}
|
|
expr := typical.LiteralExpr[traitsTemplateEnv, []string]{
|
|
Value: []string{value},
|
|
}
|
|
return &TraitsTemplateExpression{expr: expr}, nil
|
|
}
|
|
|
|
prefix, value, suffix := match[1], match[2], match[3]
|
|
|
|
expr, err := parseTraitsTemplateExpression(value)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return &TraitsTemplateExpression{
|
|
prefix: strings.TrimLeftFunc(prefix, unicode.IsSpace),
|
|
suffix: strings.TrimRightFunc(suffix, unicode.IsSpace),
|
|
expr: expr,
|
|
}, nil
|
|
}
|
|
|
|
// Interpolate interpolates the variable adding prefix and suffix if present.
|
|
// The returned error is trace.NotFound in case the expression contains a variable
|
|
// and this variable is not found on any trait, nil in case of success,
|
|
// and BadParameter otherwise.
|
|
func (e *TraitsTemplateExpression) Interpolate(varValidation func(namespace, name string) error, traits map[string][]string) ([]string, error) {
|
|
result, err := e.expr.Evaluate(traitsTemplateEnv{
|
|
traits: traits,
|
|
traitValidator: varValidation,
|
|
})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
var out []string
|
|
for _, val := range result {
|
|
// Filter out values that mapped to the empty string.
|
|
if len(val) > 0 {
|
|
out = append(out, e.prefix+val+e.suffix)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
type traitsTemplateEnv struct {
|
|
traits map[string][]string
|
|
traitValidator func(namespace, name string) error
|
|
}
|
|
|
|
type traitsTemplateExpression typical.Expression[traitsTemplateEnv, []string]
|
|
|
|
func parseTraitsTemplateExpression(exprString string) (traitsTemplateExpression, error) {
|
|
expr, err := traitsTemplateParser.Parse(exprString)
|
|
return expr, trace.Wrap(err)
|
|
}
|
|
|
|
func mustNewTraitsTemplateParser() *typical.CachedParser[traitsTemplateEnv, []string] {
|
|
parser, err := newTraitsTemplateParser()
|
|
if err != nil {
|
|
panic(trace.Wrap(err, "failed to create template parser (this is a bug)"))
|
|
}
|
|
return parser
|
|
}
|
|
|
|
func newTraitsTemplateParser() (*typical.CachedParser[traitsTemplateEnv, []string], error) {
|
|
traitsVariable := func(name string) typical.Variable {
|
|
return typical.DynamicMapFunction(func(e traitsTemplateEnv, key string) ([]string, error) {
|
|
if e.traitValidator != nil {
|
|
if err := e.traitValidator(name, key); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
}
|
|
values, ok := e.traits[key]
|
|
if !ok {
|
|
return nil, trace.NotFound("trait not found: %s.%s", name, key)
|
|
}
|
|
return values, nil
|
|
})
|
|
}
|
|
|
|
parser, err := typical.NewCachedParser[traitsTemplateEnv, []string](typical.ParserSpec{
|
|
Variables: map[string]typical.Variable{
|
|
"external": traitsVariable("external"),
|
|
"internal": traitsVariable("internal"),
|
|
},
|
|
Functions: map[string]typical.Function{
|
|
EmailLocalFnName: typical.UnaryFunction[traitsTemplateEnv](EmailLocal),
|
|
RegexpReplaceFnName: typical.TernaryFunction[traitsTemplateEnv](RegexpReplace),
|
|
},
|
|
}, typical.WithInvalidNamespaceHack())
|
|
return parser, trace.Wrap(err)
|
|
}
|
|
|
|
// EmailLocal returns a new list which is a result of getting the local part of
|
|
// each email from the input list.
|
|
func EmailLocal(inputs []string) ([]string, error) {
|
|
return stringListMap(inputs, func(email string) (string, error) {
|
|
if email == "" {
|
|
return "", trace.BadParameter(
|
|
"found empty %q argument",
|
|
EmailLocalFnName,
|
|
)
|
|
}
|
|
addr, err := mail.ParseAddress(email)
|
|
if err != nil {
|
|
return "", trace.BadParameter(
|
|
"failed to parse %q argument %q: %s",
|
|
EmailLocalFnName,
|
|
email,
|
|
err,
|
|
)
|
|
}
|
|
parts := strings.SplitN(addr.Address, "@", 2)
|
|
if len(parts) != 2 {
|
|
return "", trace.BadParameter(
|
|
"could not find local part in %q argument %q, %q",
|
|
EmailLocalFnName,
|
|
email,
|
|
addr.Address,
|
|
)
|
|
}
|
|
return parts[0], nil
|
|
})
|
|
}
|
|
|
|
// RegexpReplace returns a new list which is the result of replacing each instance
|
|
// of [match] with [replacement] for each item in the input list.
|
|
func RegexpReplace(inputs []string, match string, replacement string) ([]string, error) {
|
|
re, err := newRegexp(match, false)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err, "invalid regexp %q", match)
|
|
}
|
|
return stringListMap(inputs, func(in string) (string, error) {
|
|
// Filter out inputs which do not match the regexp at all.
|
|
if !re.MatchString(in) {
|
|
return "", nil
|
|
}
|
|
return re.ReplaceAllString(in, replacement), nil
|
|
})
|
|
}
|
|
|
|
// MatchExpression is a match expression.
|
|
type MatchExpression struct {
|
|
// prefix is a prefix of the expression
|
|
prefix string
|
|
// suffix is a suffix of the expression
|
|
suffix string
|
|
// matcher is the matcher in the expression
|
|
matcher Matcher
|
|
}
|
|
|
|
// Matcher matches strings against some internal criteria (e.g. a regexp)
|
|
type Matcher interface {
|
|
Match(in string) bool
|
|
}
|
|
|
|
// MatcherFn converts function to a matcher interface
|
|
type MatcherFn func(in string) bool
|
|
|
|
// Match matches string against a regexp
|
|
func (fn MatcherFn) Match(in string) bool {
|
|
return fn(in)
|
|
}
|
|
|
|
// NewAnyMatcher returns a matcher function based
|
|
// on incoming values
|
|
func NewAnyMatcher(in []string) (Matcher, error) {
|
|
matchers := make([]Matcher, len(in))
|
|
for i, v := range in {
|
|
m, err := NewMatcher(v)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
matchers[i] = m
|
|
}
|
|
return MatcherFn(func(in string) bool {
|
|
for _, m := range matchers {
|
|
if m.Match(in) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}), nil
|
|
}
|
|
|
|
// NewMatcher parses a matcher expression. Currently supported expressions:
|
|
// - string literal: `foo`
|
|
// - wildcard expression: `*` or `foo*bar`
|
|
// - regexp expression: `^foo$`
|
|
// - regexp function calls:
|
|
// - positive match: `{{regexp.match("foo.*")}}`
|
|
// - negative match: `{{regexp.not_match("foo.*")}}`
|
|
//
|
|
// These expressions do not support variable interpolation (e.g.
|
|
// `{{internal.logins}}`), like Expression does.
|
|
func NewMatcher(value string) (*MatchExpression, error) {
|
|
match := reVariable.FindStringSubmatch(value)
|
|
if len(match) == 0 {
|
|
if strings.Contains(value, "{{") || strings.Contains(value, "}}") {
|
|
return nil, trace.BadParameter(
|
|
"%q is using template brackets '{{' or '}}', however expression does not parse, make sure the format is {{expression}}",
|
|
value,
|
|
)
|
|
}
|
|
|
|
re, err := newRegexp(value, true)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err, "parsing match expression")
|
|
}
|
|
return &MatchExpression{
|
|
matcher: matcher{re},
|
|
}, nil
|
|
}
|
|
|
|
prefix, value, suffix := match[1], match[2], match[3]
|
|
matcher, err := parseMatcherExpression(value)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return &MatchExpression{
|
|
prefix: prefix,
|
|
suffix: suffix,
|
|
matcher: matcher,
|
|
}, nil
|
|
}
|
|
|
|
func (e *MatchExpression) Match(in string) bool {
|
|
if !strings.HasPrefix(in, e.prefix) || !strings.HasSuffix(in, e.suffix) {
|
|
return false
|
|
}
|
|
in = strings.TrimPrefix(in, e.prefix)
|
|
in = strings.TrimSuffix(in, e.suffix)
|
|
|
|
return e.matcher.Match(in)
|
|
}
|
|
|
|
// match expressions currently have no environment (you can't access any traits
|
|
// or other variables).
|
|
type matcherEnv struct{}
|
|
|
|
func parseMatcherExpression(raw string) (Matcher, error) {
|
|
matchExpr, err := matcherParser.Parse(raw)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err, "parsing match expression")
|
|
}
|
|
matcher, err := matchExpr.Evaluate(matcherEnv{})
|
|
return matcher, trace.Wrap(err, "evaluating match expression")
|
|
}
|
|
|
|
func mustNewMatcherParser() *typical.CachedParser[matcherEnv, Matcher] {
|
|
parser, err := newMatcherParser()
|
|
if err != nil {
|
|
panic(trace.Wrap(err, "failed to create match parser (this is a bug)"))
|
|
}
|
|
return parser
|
|
}
|
|
|
|
func newMatcherParser() (*typical.CachedParser[matcherEnv, Matcher], error) {
|
|
parser, err := typical.NewCachedParser[matcherEnv, Matcher](typical.ParserSpec{
|
|
Functions: map[string]typical.Function{
|
|
RegexpMatchFnName: typical.UnaryFunction[matcherEnv](regexpMatch),
|
|
RegexpNotMatchFnName: typical.UnaryFunction[matcherEnv](regexpNotMatch),
|
|
},
|
|
})
|
|
return parser, trace.Wrap(err)
|
|
}
|
|
|
|
func regexpMatch(match string) (Matcher, error) {
|
|
re, err := newRegexp(match, false)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err, "parsing argument to regexp.match")
|
|
}
|
|
return matcher{re}, nil
|
|
}
|
|
|
|
func regexpNotMatch(match string) (Matcher, error) {
|
|
re, err := newRegexp(match, false)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err, "parsing argument to regexp.not_match")
|
|
}
|
|
return notMatcher{re}, nil
|
|
}
|
|
|
|
type matcher struct {
|
|
re *regexp.Regexp
|
|
}
|
|
|
|
func (m matcher) Match(in string) bool {
|
|
return m.re.MatchString(in)
|
|
}
|
|
|
|
type notMatcher struct {
|
|
re *regexp.Regexp
|
|
}
|
|
|
|
func (n notMatcher) Match(in string) bool {
|
|
return !n.re.MatchString(in)
|
|
}
|
|
|
|
func stringListMap(inputs []string, f func(string) (string, error)) ([]string, error) {
|
|
out := make([]string, 0, len(inputs))
|
|
for _, input := range inputs {
|
|
mapped, err := f(input)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
// Filter out values that mapped to the empty string.
|
|
if len(mapped) == 0 {
|
|
continue
|
|
}
|
|
out = append(out, mapped)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func newRegexp(raw string, escape bool) (*regexp.Regexp, error) {
|
|
if escape {
|
|
if !strings.HasPrefix(raw, "^") || !strings.HasSuffix(raw, "$") {
|
|
// replace glob-style wildcards with regexp wildcards
|
|
// for plain strings, and quote all characters that could
|
|
// be interpreted in regular expression
|
|
raw = "^" + utils.GlobToRegexp(raw) + "$"
|
|
}
|
|
}
|
|
|
|
re, err := regexp.Compile(raw)
|
|
if err != nil {
|
|
return nil, trace.BadParameter(
|
|
"failed to parse regexp %q: %v",
|
|
raw,
|
|
err,
|
|
)
|
|
}
|
|
return re, nil
|
|
}
|