mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-30 18:29:21 +08:00
Prefer the stdlib version of the experimental package.
Includes a depguard update to prevent future regressions,
as well as an e-ref update to appease depguard.
Generated with:
$ gofmt -w -r '"golang.org/x/exp/slices" -> "slices"' . && \
make fix-imports
438 lines
11 KiB
Go
438 lines
11 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/>.
|
|
*/
|
|
|
|
package typical_test
|
|
|
|
import (
|
|
"errors"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gravitational/trace"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/exp/maps"
|
|
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
"github.com/gravitational/teleport/lib/utils/typical"
|
|
)
|
|
|
|
func TestParser(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type env struct {
|
|
labels map[string]string
|
|
traits map[string][]string
|
|
}
|
|
|
|
parser, err := typical.NewParser[env, bool](typical.ParserSpec{
|
|
Variables: map[string]typical.Variable{
|
|
"labels": typical.DynamicVariable(func(e env) (map[string]string, error) {
|
|
return e.labels, nil
|
|
}),
|
|
"dynamic.labels": typical.DynamicMapFunction(func(e env, key string) (string, error) {
|
|
return e.labels[key], nil
|
|
}),
|
|
"traits": typical.DynamicVariable(func(e env) (map[string][]string, error) {
|
|
return e.traits, nil
|
|
}),
|
|
"true": true,
|
|
"false": false,
|
|
"namespaces": []string{"internal", "external"},
|
|
},
|
|
Functions: map[string]typical.Function{
|
|
"not": typical.UnaryFunction[env](func(b bool) (bool, error) {
|
|
return !b, nil
|
|
}),
|
|
"contains": typical.BinaryFunction[env](func(list []string, item string) (bool, error) {
|
|
return slices.Contains(list, item), nil
|
|
}),
|
|
"ifelse": typical.TernaryFunction[env](func(cond bool, a any, b any) (any, error) {
|
|
if cond {
|
|
return a, nil
|
|
}
|
|
return b, nil
|
|
}),
|
|
"concat": typical.UnaryVariadicFunction[env](func(strs ...string) (string, error) {
|
|
return strings.Join(strs, ""), nil
|
|
}),
|
|
"contains_all": typical.BinaryVariadicFunction[env](func(list []string, strs ...string) (bool, error) {
|
|
for _, str := range strs {
|
|
if !slices.Contains(list, str) {
|
|
return false, nil
|
|
}
|
|
}
|
|
return true, nil
|
|
}),
|
|
"error": typical.UnaryFunction[env](func(msg string) (any, error) {
|
|
return nil, errors.New(msg)
|
|
}),
|
|
"head": typical.UnaryFunction[env](func(list []string) (string, error) {
|
|
if len(list) == 0 {
|
|
return "", trace.BadParameter("list has length 0")
|
|
}
|
|
return list[0], nil
|
|
}),
|
|
"labels_matching": typical.UnaryFunctionWithEnv(func(e env, keyExpr string) ([]string, error) {
|
|
var matchingLabels []string
|
|
for key, value := range e.labels {
|
|
match, err := utils.MatchString(key, keyExpr)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
if match {
|
|
matchingLabels = append(matchingLabels, value)
|
|
}
|
|
}
|
|
return matchingLabels, nil
|
|
}),
|
|
},
|
|
Methods: map[string]typical.Function{
|
|
"add_trait_values": typical.TernaryVariadicFunction[env](func(m map[string][]string, key string, values ...string) (map[string][]string, error) {
|
|
c := maps.Clone(m)
|
|
c[key] = append(c[key], values...)
|
|
return c, nil
|
|
}),
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
e := env{
|
|
labels: map[string]string{
|
|
"env": "staging",
|
|
"team": "dev",
|
|
},
|
|
traits: map[string][]string{
|
|
"allow-env": {"dev", "staging"},
|
|
"logins": {"root", "ubuntu"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
desc string
|
|
expr string
|
|
expectParseError []string
|
|
expectEvaluationError []string
|
|
expectMatch bool
|
|
}{
|
|
{
|
|
desc: "empty expression",
|
|
expectParseError: []string{
|
|
"empty expression",
|
|
},
|
|
},
|
|
{
|
|
desc: "unknown variable",
|
|
expr: "nothing",
|
|
expectParseError: []string{
|
|
`unknown identifier: "nothing"`,
|
|
},
|
|
},
|
|
{
|
|
desc: "wrong result type",
|
|
expr: `"not a bool"`,
|
|
expectParseError: []string{
|
|
"expression evaluated to unexpected type",
|
|
"expected type bool, got value (not a bool) with type (string)",
|
|
},
|
|
},
|
|
{
|
|
desc: "literal",
|
|
expr: "true",
|
|
expectMatch: true,
|
|
},
|
|
{
|
|
desc: "unary function expression",
|
|
expr: "not(true)",
|
|
expectMatch: false,
|
|
},
|
|
{
|
|
desc: "unary function wrong type",
|
|
expr: `not("true")`,
|
|
expectParseError: []string{
|
|
"parsing argument to (not)",
|
|
"expected type bool, got value (true) with type (string)",
|
|
},
|
|
},
|
|
{
|
|
desc: "negation of literal",
|
|
expr: "!true",
|
|
expectMatch: false,
|
|
},
|
|
{
|
|
desc: "negation of expression",
|
|
expr: "!not(true)",
|
|
expectMatch: true,
|
|
},
|
|
{
|
|
desc: "negation of wrong type",
|
|
expr: `!"test"`,
|
|
expectParseError: []string{
|
|
"parsing target of (!) operator",
|
|
"expected type bool, got value (test) with type (string)",
|
|
},
|
|
},
|
|
{
|
|
desc: "and literals",
|
|
expr: "true && false",
|
|
expectMatch: false,
|
|
},
|
|
{
|
|
desc: "and expressions",
|
|
expr: "not(true) && not(false) && true",
|
|
expectMatch: false,
|
|
},
|
|
{
|
|
desc: "and with wrong type",
|
|
expr: `true && "test"`,
|
|
expectParseError: []string{
|
|
"parsing rhs of (&&) operator",
|
|
"expected type bool, got value (test) with type (string)",
|
|
},
|
|
},
|
|
{
|
|
desc: "or literals",
|
|
expr: "true || false",
|
|
expectMatch: true,
|
|
},
|
|
{
|
|
desc: "or expressions",
|
|
expr: "not(true) || not(false) || true",
|
|
expectMatch: true,
|
|
},
|
|
{
|
|
desc: "unary func no args",
|
|
expr: "not()",
|
|
expectParseError: []string{
|
|
"function (not) accepts 1 argument, given 0",
|
|
},
|
|
},
|
|
{
|
|
desc: "expression as argument",
|
|
expr: "not(not(true))",
|
|
expectMatch: true,
|
|
},
|
|
{
|
|
desc: "literal string equality",
|
|
expr: `"test1" == "test2"`,
|
|
expectMatch: false,
|
|
},
|
|
{
|
|
desc: "binary function with map lookups",
|
|
expr: `contains(traits["allow-env"], labels["env"])`,
|
|
expectMatch: true,
|
|
},
|
|
{
|
|
desc: "dynamic map lookup",
|
|
expr: `contains(traits["allow-env"], dynamic.labels["env"])`,
|
|
expectMatch: true,
|
|
},
|
|
{
|
|
desc: "map.key syntax",
|
|
expr: `contains(traits.logins, "root")`,
|
|
expectMatch: true,
|
|
},
|
|
{
|
|
desc: "key with wrong type",
|
|
expr: `traits[false]`,
|
|
expectParseError: []string{
|
|
"parsing key of index expression",
|
|
"expected type string",
|
|
},
|
|
},
|
|
{
|
|
desc: "indexing non-map",
|
|
expr: `concat("a", "b")["key"]`,
|
|
expectParseError: []string{
|
|
"cannot take index of unexpected type",
|
|
"expected type map",
|
|
"got expression returning type (string)",
|
|
},
|
|
},
|
|
{
|
|
desc: "argument is expression returning wrong type",
|
|
expr: `contains(traits, "root")`,
|
|
expectParseError: []string{
|
|
"parsing first argument to (contains)",
|
|
"expected type []string, got expression returning type (map[string][]string)",
|
|
},
|
|
},
|
|
{
|
|
desc: "binary function with too many arguments",
|
|
expr: `contains(traits["logins"], "root", "user")`,
|
|
expectParseError: []string{
|
|
"function (contains) accepts 2 arguments, given 3",
|
|
},
|
|
},
|
|
{
|
|
desc: "string works as []string",
|
|
expr: `contains("test", "test")`,
|
|
expectMatch: true,
|
|
},
|
|
{
|
|
desc: "string expression works as []string",
|
|
expr: `contains(concat("te", "st"), "test")`,
|
|
expectMatch: true,
|
|
},
|
|
{
|
|
desc: "correct runtime type",
|
|
expr: `ifelse(true, true, "test") || true`,
|
|
expectMatch: true,
|
|
},
|
|
{
|
|
desc: "incorrect runtime type",
|
|
expr: `ifelse(false, true, "test") || true`,
|
|
expectEvaluationError: []string{
|
|
"evaluating lhs of (||) operator",
|
|
"expected type bool, got value (test) with type (string)",
|
|
},
|
|
},
|
|
{
|
|
desc: "expression as interface argument",
|
|
expr: `ifelse(false, labels["env"], labels["team"]) == "dev"`,
|
|
expectMatch: true,
|
|
},
|
|
{
|
|
desc: "unary variadic function",
|
|
expr: `concat("Hello", ", ", "World!") == "Hello, World!"`,
|
|
expectMatch: true,
|
|
},
|
|
{
|
|
desc: "unary variadic function wrong type",
|
|
expr: `concat("Hello", ", ", "World!", false) == "Hello, World!"`,
|
|
expectParseError: []string{
|
|
"expected type string, got value (false) with type (bool)",
|
|
},
|
|
},
|
|
{
|
|
desc: "binary variadic function",
|
|
expr: `contains_all(traits["logins"], "root", "ubuntu")`,
|
|
expectMatch: true,
|
|
},
|
|
{
|
|
desc: "ternary variadic method",
|
|
expr: `contains_all(
|
|
traits.add_trait_values("logins",
|
|
"usera", "userb", "userc",
|
|
)["logins"],
|
|
"root",
|
|
"userc",
|
|
"userb",
|
|
"usera",
|
|
)`,
|
|
expectMatch: true,
|
|
},
|
|
{
|
|
desc: "unary func with env",
|
|
expr: `contains_all(labels_matching("*"), "staging", "dev")`,
|
|
expectMatch: true,
|
|
},
|
|
{
|
|
desc: "unary func with env no arg",
|
|
expr: `contains_all(labels_matching(), "staging", "dev")`,
|
|
expectParseError: []string{
|
|
"function (labels_matching) accepts 1 argument, given 0",
|
|
},
|
|
},
|
|
{
|
|
desc: "unary func with env wrong type",
|
|
expr: `contains_all(labels_matching(traits["username"]), "staging", "dev")`,
|
|
expectParseError: []string{
|
|
"parsing argument to (labels_matching)",
|
|
"expected type string, got expression returning type ([]string)",
|
|
},
|
|
},
|
|
{
|
|
desc: "unsupported function",
|
|
expr: `compare(traits.logins, "user")`,
|
|
expectParseError: []string{
|
|
"unsupported function: compare",
|
|
},
|
|
},
|
|
{
|
|
desc: "unmatched parens",
|
|
expr: "not(true,",
|
|
expectParseError: []string{
|
|
"expected ')', found 'EOF'",
|
|
},
|
|
},
|
|
{
|
|
desc: "error evaluating key",
|
|
expr: `labels[error("haha")] == "test"`,
|
|
expectEvaluationError: []string{
|
|
"evaluating key of index expression",
|
|
"haha",
|
|
},
|
|
},
|
|
{
|
|
desc: "error evaluating dynamic key",
|
|
expr: `dynamic.labels[error("haha")] == "test"`,
|
|
expectEvaluationError: []string{
|
|
"evaluating key of index expression",
|
|
"haha",
|
|
},
|
|
},
|
|
{
|
|
desc: "error evaluating argument",
|
|
expr: `not(error("haha"))`,
|
|
expectEvaluationError: []string{
|
|
"evaluating argument to function (not)",
|
|
"haha",
|
|
},
|
|
},
|
|
} {
|
|
tc := tc
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
t.Parallel()
|
|
expr, err := parser.Parse(tc.expr)
|
|
for _, msg := range tc.expectParseError {
|
|
require.ErrorContains(t, err, msg)
|
|
}
|
|
if len(tc.expectParseError) > 0 {
|
|
return
|
|
}
|
|
require.NoError(t, err, trace.DebugReport(err))
|
|
|
|
match, err := expr.Evaluate(e)
|
|
for _, msg := range tc.expectEvaluationError {
|
|
require.ErrorContains(t, err, msg)
|
|
}
|
|
if len(tc.expectEvaluationError) > 0 {
|
|
return
|
|
}
|
|
require.NoError(t, err, trace.DebugReport(err))
|
|
|
|
require.Equal(t, tc.expectMatch, match)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUnknownIdentifier(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
parser, err := typical.NewParser[struct{}, bool](typical.ParserSpec{})
|
|
require.NoError(t, err)
|
|
|
|
_, err = parser.Parse("unknown")
|
|
|
|
var u typical.UnknownIdentifierError
|
|
require.ErrorAs(t, err, &u)
|
|
require.ErrorAs(t, trace.Wrap(err), &u)
|
|
require.Equal(t, "unknown", u.Identifier())
|
|
}
|