mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-02 23:31:08 +08:00
* Copy expression package out of /e * Add doc string for default parser spec. * Add more info to default parser spec doc string * Fix typo in doc string * Add missing comments including license info
295 lines
9.7 KiB
Go
295 lines
9.7 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 (
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/gravitational/trace"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/gravitational/teleport/lib/utils/typical"
|
|
)
|
|
|
|
func TestEvaluateTraitsMap(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
baseInputTraits := map[string][]string{
|
|
"groups": []string{"devs", "security"},
|
|
"username": []string{"alice"},
|
|
}
|
|
|
|
tests := []struct {
|
|
desc string
|
|
expressions map[string][]string
|
|
inputTraits map[string][]string
|
|
expectedTraits map[string][]string
|
|
errorContains []string
|
|
}{
|
|
{
|
|
desc: "no rules",
|
|
inputTraits: baseInputTraits,
|
|
expectedTraits: map[string][]string{},
|
|
},
|
|
{
|
|
desc: "simple traits map",
|
|
expressions: map[string][]string{"groups": []string{
|
|
"user.spec.traits.groups",
|
|
}},
|
|
inputTraits: baseInputTraits,
|
|
expectedTraits: map[string][]string{
|
|
"groups": []string{"devs", "security"},
|
|
},
|
|
},
|
|
{
|
|
desc: "wrong map return type",
|
|
expressions: map[string][]string{"groups": []string{"user.spec.traits"}},
|
|
errorContains: []string{
|
|
"traits_map expression must evaluate to type string or set, the following expression evaluates to expression.Dict:",
|
|
},
|
|
},
|
|
{
|
|
desc: "ifelse",
|
|
expressions: map[string][]string{
|
|
"a": []string{
|
|
`ifelse(true, "correct", "wrong")`,
|
|
`ifelse(false, "wrong", "correct")`,
|
|
`ifelse(ifelse(true, true, false), "correct", "wrong")`,
|
|
`set(ifelse(true, "correct", "wrong"), "correct")`,
|
|
},
|
|
"groups": []string{
|
|
`ifelse(true, user.spec.traits.groups, "wrong")`,
|
|
},
|
|
},
|
|
inputTraits: baseInputTraits,
|
|
expectedTraits: map[string][]string{
|
|
"a": []string{"correct"},
|
|
"groups": baseInputTraits["groups"],
|
|
},
|
|
},
|
|
{
|
|
desc: "set methods",
|
|
expressions: map[string][]string{
|
|
"extragroups": []string{`user.spec.traits.groups.add("extra", "surplus")`},
|
|
"fewergroups": []string{`user.spec.traits.groups.remove("security")`},
|
|
"nogroups": []string{`user.spec.traits.groups.remove("devs", "security").add("test").remove("test").remove("not-a-group")`},
|
|
"groups-by-another-name": []string{`user.spec.traits.groups.remove("not-a-group")`},
|
|
"logins": []string{
|
|
// user.spec.traits.groups does not contain "admins", so we
|
|
// expect to just get the username.
|
|
`ifelse(user.spec.traits.groups.contains("admins"), user.spec.traits.username.add("root"), user.spec.traits.username)`,
|
|
// user.spec.traits.groups does contain "security", so expect
|
|
// the "security-team" login.
|
|
`ifelse(user.spec.traits.groups.contains("security"), "security-team", set())`,
|
|
},
|
|
},
|
|
inputTraits: baseInputTraits,
|
|
expectedTraits: map[string][]string{
|
|
"extragroups": append([]string{"extra", "surplus"}, baseInputTraits["groups"]...),
|
|
"fewergroups": []string{"devs"},
|
|
"nogroups": []string{},
|
|
"groups-by-another-name": baseInputTraits["groups"],
|
|
"logins": []string{"alice", "security-team"},
|
|
},
|
|
},
|
|
{
|
|
desc: "set union",
|
|
expressions: map[string][]string{
|
|
"groups": []string{`union(user.spec.traits.groups, set("test1", "test2"))`},
|
|
"fruits": []string{`union(set("apple", "banana"), set("cherry"), set("dragonfruit", "eggplant"))`},
|
|
},
|
|
inputTraits: baseInputTraits,
|
|
expectedTraits: map[string][]string{
|
|
"groups": append([]string{"test1", "test2"}, baseInputTraits["groups"]...),
|
|
"fruits": []string{"apple", "banana", "cherry", "dragonfruit", "eggplant"},
|
|
},
|
|
},
|
|
{
|
|
desc: "string helpers",
|
|
expressions: map[string][]string{
|
|
"lower": []string{
|
|
`strings.lower("APPLE")`,
|
|
`strings.lower("BaNaNa")`,
|
|
`strings.lower(set("cherry", "dragonFRUIT"))`,
|
|
`strings.lower(user.spec.traits.username)`,
|
|
},
|
|
"upper": []string{
|
|
`strings.upper("APPLE")`,
|
|
`strings.upper("BaNaNa")`,
|
|
`strings.upper(set("cherry", "dragonFRUIT"))`,
|
|
`strings.upper(user.spec.traits.username)`,
|
|
},
|
|
"replaced": []string{
|
|
`strings.replaceall("snake_case_example", "_", "-")`,
|
|
`strings.replaceall(strings.replaceall("user@example.com", "@", "_"), ".", "-")`,
|
|
`strings.replaceall(set("dev-team", "platform-team"), "-team", "")`,
|
|
},
|
|
},
|
|
inputTraits: baseInputTraits,
|
|
expectedTraits: map[string][]string{
|
|
"lower": []string{"apple", "banana", "cherry", "dragonfruit", "alice"},
|
|
"upper": []string{"APPLE", "BANANA", "CHERRY", "DRAGONFRUIT", "ALICE"},
|
|
"replaced": []string{"snake-case-example", "user_example-com", "dev", "platform"},
|
|
},
|
|
},
|
|
{
|
|
desc: "choose",
|
|
expressions: map[string][]string{
|
|
"choose_first": []string{
|
|
`choose(option(true, "first"), option(false, "second"))`,
|
|
},
|
|
"choose_second": []string{
|
|
`choose(option(false, "first"), option(true, "second"))`,
|
|
},
|
|
"groups": []string{
|
|
`choose(
|
|
option(user.spec.traits.username.contains("alice"), set("devs", "security", "requester")),
|
|
option(user.spec.traits.username.contains("bob"), set("security", "reviewer")),
|
|
option(user.spec.traits.username.contains("charlie"), set("devs")),
|
|
option(true, set()),
|
|
)`,
|
|
},
|
|
},
|
|
inputTraits: baseInputTraits,
|
|
expectedTraits: map[string][]string{
|
|
"choose_first": []string{"first"},
|
|
"choose_second": []string{"second"},
|
|
"groups": []string{"devs", "security", "requester"},
|
|
},
|
|
},
|
|
{
|
|
// Test that user.spec.traits traits dict can by indexed like
|
|
// user.spec.traits["trait"] as well as user.spec.traits.trait (the latter syntax
|
|
// does not support traits containing hyphens or some other special
|
|
// characters).
|
|
desc: "dict index",
|
|
expressions: map[string][]string{
|
|
"test": {`user.spec.traits.test`},
|
|
"with-hyphen": {`user.spec.traits["with-hyphen"]`},
|
|
},
|
|
inputTraits: map[string][]string{
|
|
"test": {"test"},
|
|
"with-hyphen": {"-"},
|
|
},
|
|
expectedTraits: map[string][]string{
|
|
"test": {"test"},
|
|
"with-hyphen": {"-"},
|
|
},
|
|
},
|
|
{
|
|
// Test that return value of helper (contains) can be handled by `||`,
|
|
// and return value of `||` can be handled by helper (ifelse).
|
|
desc: "boolean expressions",
|
|
expressions: map[string][]string{
|
|
"groups": {
|
|
`ifelse(user.spec.traits.groups.contains("security") || user.spec.traits.groups.contains("it"),
|
|
user.spec.traits.groups.add("admins"),
|
|
user.spec.traits.groups)`,
|
|
},
|
|
},
|
|
inputTraits: baseInputTraits,
|
|
expectedTraits: map[string][]string{
|
|
"groups": {"devs", "security", "admins"},
|
|
},
|
|
},
|
|
{
|
|
desc: "traits_map quoted or unquoted strings",
|
|
expressions: map[string][]string{
|
|
"test": {`a`, `"b"`},
|
|
},
|
|
expectedTraits: map[string][]string{
|
|
"test": {"a", "b"},
|
|
},
|
|
},
|
|
{
|
|
desc: "bad regexp",
|
|
expressions: map[string][]string{
|
|
"logins": {`regexp.replace(user.spec.traits.email, "(.*@example.com", "$1")`},
|
|
},
|
|
inputTraits: map[string][]string{
|
|
"email": {"alice@example.com"},
|
|
},
|
|
errorContains: []string{
|
|
"evaluating function (regexp.replace)",
|
|
`invalid regexp "(.*@example.com"`,
|
|
},
|
|
},
|
|
{
|
|
desc: "strings.split",
|
|
expressions: map[string][]string{
|
|
"logins": {`strings.split(user.spec.traits.commaLogins, ",")`},
|
|
"localEmails": {
|
|
`email.local(strings.split(user.spec.traits.oneSpaceEmails, " "))`,
|
|
`email.local(strings.split(user.spec.traits.twoSpaceEmails, " "))`,
|
|
`email.local(strings.split(user.spec.traits.singleEmail, ","))`,
|
|
},
|
|
},
|
|
inputTraits: map[string][]string{
|
|
"commaLogins": {"alice,bob,charlie"},
|
|
"oneSpaceEmails": {"alice@example.com bob@example.com charlie@example.com"},
|
|
"twoSpaceEmails": {"darrell@example.com esther@example.com"},
|
|
"singleEmail": {"frank@example.com"},
|
|
},
|
|
expectedTraits: map[string][]string{
|
|
"logins": {"alice", "bob", "charlie"},
|
|
"localEmails": {"alice", "bob", "charlie", "darrell", "esther", "frank"},
|
|
},
|
|
},
|
|
}
|
|
|
|
type evaluationEnv struct {
|
|
Traits Dict
|
|
}
|
|
|
|
typicalEnvVar := map[string]typical.Variable{
|
|
"true": true,
|
|
"false": false,
|
|
"user.spec.traits": typical.DynamicMap[evaluationEnv, Set](func(env evaluationEnv) (Dict, error) {
|
|
return env.Traits, nil
|
|
}),
|
|
}
|
|
|
|
attributeParser, err := NewTraitsExpressionParser[evaluationEnv](typicalEnvVar)
|
|
require.NoError(t, err)
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
result, err := EvaluateTraitsMap[evaluationEnv](
|
|
evaluationEnv{
|
|
Traits: DictFromStringSliceMap(tc.inputTraits),
|
|
},
|
|
tc.expressions,
|
|
func(input string) (typical.Expression[evaluationEnv, any], error) {
|
|
expr, err := attributeParser.Parse(input)
|
|
return expr, trace.Wrap(err)
|
|
})
|
|
if len(tc.errorContains) > 0 {
|
|
for _, contains := range tc.errorContains {
|
|
require.ErrorContains(t, err, contains, "error string does not contain expected snippet")
|
|
}
|
|
return
|
|
}
|
|
require.NoError(t, err, trace.DebugReport(err))
|
|
require.Empty(t, cmp.Diff(tc.expectedTraits, StringSliceMapFromDict(result), cmpopts.SortSlices(func(a, b string) bool { return a < b })))
|
|
})
|
|
}
|
|
}
|