Files
teleport/lib/expression/parser.go
T
Kevin 0ab79253a5 Revise predicate language docs and add contains(set, item) (#64864)
* Revise predicate language docs with clearer examples; add contains(set, item)

* Revise example
2026-04-01 19:11:15 +00:00

388 lines
12 KiB
Go

/*
* Teleport
* Copyright (C) 2024 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/>.
*/
package expression
import (
"strings"
"time"
"github.com/coreos/go-semver/semver"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/parse"
"github.com/gravitational/teleport/lib/utils/set"
"github.com/gravitational/teleport/lib/utils/typical"
)
type evaluationEnvVar map[string]typical.Variable
// DefaultParserSpec is the default parser specification.
// Contains useful functions for manipulating and comparing strings.
func DefaultParserSpec[evaluationEnv any]() typical.ParserSpec[evaluationEnv] {
return typical.ParserSpec[evaluationEnv]{
Functions: map[string]typical.Function{
"set": typical.UnaryVariadicFunction[evaluationEnv](
func(args ...string) (Set, error) {
return NewSet(args...), nil
}),
"dict": typical.UnaryVariadicFunction[evaluationEnv](
func(pairs ...pair) (Dict, error) {
return NewDict(pairs...)
}),
"pair": typical.BinaryFunction[evaluationEnv](
func(a, b any) (pair, error) {
return pair{a, b}, nil
}),
"union": typical.UnaryVariadicFunction[evaluationEnv](
func(sets ...Set) (Set, error) {
return union(sets...), nil
}),
"ifelse": typical.TernaryFunction[evaluationEnv](
func(cond bool, a, b any) (any, error) {
if cond {
return a, nil
}
return b, nil
}),
"strings.upper": typical.UnaryFunction[evaluationEnv](
func(input any) (any, error) {
return StringTransform("strings.upper", input, strings.ToUpper)
}),
"strings.lower": typical.UnaryFunction[evaluationEnv](
func(input any) (any, error) {
return StringTransform("strings.lower", input, strings.ToLower)
}),
"strings.replaceall": typical.TernaryFunction[evaluationEnv](
func(input any, match string, replacement string) (any, error) {
f := func(s string) string {
return strings.ReplaceAll(s, match, replacement)
}
return StringTransform("strings.replaceall", input, f)
}),
"choose": typical.UnaryVariadicFunction[evaluationEnv](
func(opts ...option) (any, error) {
return choose(opts...)
}),
"option": typical.BinaryFunction[evaluationEnv](
func(cond bool, v any) (option, error) {
return option{cond, v}, nil
}),
"email.local": typical.UnaryFunction[evaluationEnv](
func(emails Set) (Set, error) {
locals, err := parse.EmailLocal(emails.items())
if err != nil {
return Set{}, trace.Wrap(err)
}
return NewSet(locals...), nil
}),
"regexp.match": typical.BinaryFunction[evaluationEnv](
func(inputs Set, expression string) (bool, error) {
match, err := utils.RegexMatchesAny(inputs.items(), expression)
if err != nil {
return false, trace.Wrap(err, "invalid regular expression %q", expression)
}
return match, nil
}),
"regexp.replace": typical.TernaryFunction[evaluationEnv](
func(inputs Set, match string, replacement string) (Set, error) {
replaced, err := parse.RegexpReplace(inputs.items(), match, replacement)
if err != nil {
return Set{}, trace.Wrap(err)
}
return NewSet(replaced...), nil
}),
"strings.split": typical.BinaryFunction[evaluationEnv](
func(inputs Set, sep string) (Set, error) {
var outputs []string
for input := range inputs.s {
outputs = append(outputs, strings.Split(input, sep)...)
}
return NewSet(outputs...), nil
}),
"time.RFC3339": typical.UnaryFunction[evaluationEnv](
func(input string) (time.Time, error) {
return time.Parse(time.RFC3339, input)
}),
"before": typical.BinaryFunction[evaluationEnv](
func(t time.Time, other time.Time) (bool, error) {
return t.Before(other), nil
}),
"after": typical.BinaryFunction[evaluationEnv](
func(t time.Time, other time.Time) (bool, error) {
return t.After(other), nil
}),
"between": typical.TernaryFunction[evaluationEnv](
func(value any, arg1 any, arg2 any) (bool, error) {
// If the value provided is a time, do a time comparison
if t, ok := value.(time.Time); ok {
firstTime, ok1 := arg1.(time.Time)
secondTime, ok2 := arg2.(time.Time)
if !ok1 || !ok2 {
return false, trace.BadParameter("the time parameters provided are invalid time values")
}
if firstTime.After(secondTime) {
firstTime, secondTime = secondTime, firstTime
}
return t.After(firstTime) && t.Before(secondTime), nil
}
// If it's not a time, try semver comparison
return SemverBetween(value, arg1, arg2)
}),
"contains": typical.BinaryFunction[evaluationEnv](
func(s Set, str string) (bool, error) {
return s.contains(str), nil
}),
"contains_any": typical.BinaryFunction[evaluationEnv](
func(s1, s2 Set) (bool, error) {
for v := range s2.s {
if s1.contains(v) {
return true, nil
}
}
return false, nil
}),
"contains_all": typical.BinaryFunction[evaluationEnv](
func(s1, s2 Set) (bool, error) {
for v := range s2.s {
if !s1.contains(v) {
return false, nil
}
}
return len(s2.s) > 0, nil
}),
"is_empty": typical.UnaryFunction[evaluationEnv](
func(s Set) (bool, error) {
return len(s.s) == 0, nil
}),
//TODO(rudream): add newer_than, older_than, and between functions to predicate docs
"newer_than": typical.BinaryFunction[evaluationEnv](SemverGt),
"older_than": typical.BinaryFunction[evaluationEnv](SemverLt),
},
Methods: map[string]typical.Function{
"add": typical.BinaryVariadicFunction[evaluationEnv](
func(s Set, values ...string) (Set, error) {
return s.add(values...), nil
}),
"contains": typical.BinaryFunction[evaluationEnv](
func(s Set, str string) (bool, error) {
return s.contains(str), nil
}),
"put": typical.TernaryFunction[evaluationEnv](
func(d Dict, key string, value Set) (Dict, error) {
return d.put(key, value), nil
}),
"add_values": typical.TernaryVariadicFunction[evaluationEnv](
func(d Dict, key string, values ...string) (Dict, error) {
return d.addValues(key, values...), nil
}),
"remove": typical.BinaryVariadicFunction[evaluationEnv](
func(r remover, items ...string) (any, error) {
return r.remove(items...), nil
}),
"before": typical.BinaryFunction[evaluationEnv](
func(t time.Time, other time.Time) (bool, error) {
return t.Before(other), nil
}),
"after": typical.BinaryFunction[evaluationEnv](
func(t time.Time, other time.Time) (bool, error) {
return t.After(other), nil
}),
"between": typical.BinaryVariadicFunction[evaluationEnv](
func(t time.Time, interval ...time.Time) (bool, error) {
if len(interval) != 2 {
return false, trace.BadParameter("between expected 2 parameters: got %v", len(interval))
}
first, second := interval[0], interval[1]
if first.After(second) {
first, second = second, first
}
return t.After(first) && t.Before(second), nil
}),
"contains_any": typical.BinaryFunction[evaluationEnv](
func(s1, s2 Set) (bool, error) {
for v := range s2.s {
if s1.contains(v) {
return true, nil
}
}
return false, nil
}),
"contains_all": typical.BinaryFunction[evaluationEnv](
func(s1, s2 Set) (bool, error) {
for v := range s2.s {
if !s1.contains(v) {
return false, nil
}
}
return len(s2.s) > 0, nil
}),
"isempty": typical.UnaryFunction[evaluationEnv](
func(s Set) (bool, error) {
return len(s.s) == 0, nil
}),
},
}
}
// NewTraitsExpressionParser returns new expression parser using evaluation environment and default parser spec.
func NewTraitsExpressionParser[TEnv any](vars evaluationEnvVar) (*typical.Parser[TEnv, any], error) {
defParserSpec := DefaultParserSpec[TEnv]()
defParserSpec.Variables = vars
parser, err := typical.NewParser[TEnv, any](defParserSpec)
if err != nil {
return nil, trace.Wrap(err)
}
return parser, nil
}
// traitsMapResultToSet returns Set for result type string or Set and errors if the result
// cannot be evaluated to either Set or string.
func traitsMapResultToSet(result any, expr string) (Set, error) {
switch v := result.(type) {
case string:
return NewSet(v), nil
case Set:
return v, nil
default:
return Set{}, trace.BadParameter("traits_map expression must evaluate to type string or set, the following expression evaluates to %T: %q", result, expr)
}
}
// StringSliceMapFromDict returns string slice map from a Dict.
func StringSliceMapFromDict(d Dict) map[string][]string {
m := make(map[string][]string, len(d))
for key, s := range d {
m[key] = s.items()
}
return m
}
// DictFromStringSliceMap returns Dict from a string slices map type.
func DictFromStringSliceMap(m map[string][]string) Dict {
d := make(Dict, len(m))
for key, values := range m {
d[key] = NewSet(values...)
}
return d
}
// StringTransform transforms string formt.
func StringTransform(name string, input any, f func(string) string) (any, error) {
switch typedInput := input.(type) {
case string:
return f(typedInput), nil
case Set:
return Set{set.Transform(typedInput.s, f)}, nil
default:
return nil, trace.BadParameter("failed to evaluate argument to %s: expected string or set, got value of type %T", name, input)
}
}
// remover is an interface used so that the parser can call the "remove" method
// on both set and dict.
type remover interface {
remove(items ...string) any
}
func choose(options ...option) (any, error) {
for _, opt := range options {
if opt.condition {
return opt.value, nil
}
}
return nil, trace.BadParameter(`evaluating choose expression: no option could be selected, consider adding a default option by hardcoding the condition to "true"`)
}
type option struct {
condition bool
value any
}
// SemverGt compares two semantic versions and returns true if a > b.
func SemverGt(a, b any) (bool, error) {
va, err := ToSemver(a)
if va == nil || err != nil {
return false, err
}
vb, err := ToSemver(b)
if vb == nil || err != nil {
return false, err
}
return va.Compare(*vb) > 0, nil
}
// SemverLt compares two semantic versions and returns true if a < b.
func SemverLt(a, b any) (bool, error) {
va, err := ToSemver(a)
if va == nil || err != nil {
return false, err
}
vb, err := ToSemver(b)
if vb == nil || err != nil {
return false, err
}
return va.Compare(*vb) < 0, nil
}
// SemverEq compares two semantic versions and returns true if a == b.
func SemverEq(a, b any) (bool, error) {
va, err := ToSemver(a)
if va == nil || err != nil {
return false, err
}
vb, err := ToSemver(b)
if vb == nil || err != nil {
return false, err
}
return va.Compare(*vb) == 0, nil
}
// SemverBetween checks if c is between versions a and b (inclusive of a, exclusive of b).
func SemverBetween(c, a, b any) (bool, error) {
gt, err := SemverGt(c, a)
if err != nil {
return false, err
}
eq, err := SemverEq(c, a)
if err != nil {
return false, err
}
lt, err := SemverLt(c, b)
if err != nil {
return false, err
}
return (gt || eq) && lt, nil
}
// ToSemver converts a value to a semantic version.
func ToSemver(anyV any) (*semver.Version, error) {
switch v := anyV.(type) {
case *semver.Version:
return v, nil
case string:
return semver.NewVersion(v)
default:
return nil, trace.BadParameter("type %T cannot be parsed as semver.Version", v)
}
}