diff --git a/docs/4.4/enterprise/ssh-rbac.md b/docs/4.4/enterprise/ssh-rbac.md index 97cef86d436d2..acd8a159a1810 100644 --- a/docs/4.4/enterprise/ssh-rbac.md +++ b/docs/4.4/enterprise/ssh-rbac.md @@ -123,11 +123,7 @@ spec: 'environment': ['test', 'staging'] # regular expressions are also supported, for example the equivalent # of the list example above can be expressed as: - 'environment': '{{regexp.match("^test|staging$")}}' - # or using the simpler legacy syntax: 'environment': '^test|staging$' - # negative regular expressions can be used to avoid strict deny rules: - 'environment': '{{regexp.not_match("prod")}}' # defines roles that this user can can request. # needed for teleport's request workflow @@ -261,11 +257,7 @@ spec: 'environment': ['test', 'staging'] # regular expressions are also supported, for example the equivalent # of the list example above can be expressed as: - 'environment': '{{regexp.match("^test|staging$")}}' - # or using the simpler legacy syntax: 'environment': '^test|staging$' - # negative regular expressions can be used to avoid strict deny rules: - 'environment': '{{regexp.not_match("prod")}}' ``` diff --git a/lib/services/role.go b/lib/services/role.go index bad9982b4b6b8..2427ada1b91a8 100644 --- a/lib/services/role.go +++ b/lib/services/role.go @@ -710,18 +710,6 @@ func (r *RoleV3) CheckAndSetDefaults() error { if key == Wildcard && !(len(val) == 1 && val[0] == Wildcard) { return trace.BadParameter("selector *: is not supported") } - for _, l := range val { - if _, err := parse.NewMatcher(l); err != nil { - return trace.Wrap(err) - } - } - } - for _, val := range r.Spec.Deny.NodeLabels { - for _, l := range val { - if _, err := parse.NewMatcher(l); err != nil { - return trace.Wrap(err) - } - } } for i := range r.Spec.Allow.Rules { err := r.Spec.Allow.Rules[i].CheckAndSetDefaults() @@ -1597,19 +1585,11 @@ func MatchLabels(selector Labels, target map[string]string) (bool, string, error } if !utils.SliceContainsStr(selectorValues, Wildcard) { - matched := false - for _, expr := range selectorValues { - m, err := parse.NewMatcher(expr) - if err != nil { - return false, "", trace.Wrap(err) - } - if m.Match(targetVal) { - matched = true - break - } - } - if !matched { - return false, fmt.Sprintf("no value match: got %q want: %q", targetVal, selectorValues), nil + result, err := utils.SliceMatchesRegex(targetVal, selectorValues) + if err != nil { + return false, "", trace.Wrap(err) + } else if !result { + return false, fmt.Sprintf("no value match: got '%v' want: '%v'", targetVal, selectorValues), nil } } } diff --git a/lib/services/role_test.go b/lib/services/role_test.go index 022b23a528d85..faf90e07dc743 100644 --- a/lib/services/role_test.go +++ b/lib/services/role_test.go @@ -749,64 +749,6 @@ func (s *RoleSuite) TestCheckAccess(c *C) { {server: serverC2, login: "admin", hasAccess: true}, }, }, - { - name: "role matches a regexp label", - roles: []RoleV3{ - RoleV3{ - Metadata: Metadata{ - Name: "name1", - Namespace: defaults.Namespace, - }, - Spec: RoleSpecV3{ - Options: RoleOptions{ - MaxSessionTTL: Duration(20 * time.Hour), - }, - Allow: RoleConditions{ - Logins: []string{"admin"}, - NodeLabels: Labels{"role": []string{`{{regexp.match("worker.*")}}`}}, - Namespaces: []string{defaults.Namespace, namespaceC}, - }, - }, - }, - }, - checks: []check{ - {server: serverA, login: "root", hasAccess: false}, - {server: serverA, login: "admin", hasAccess: false}, - {server: serverB, login: "root", hasAccess: false}, - {server: serverB, login: "admin", hasAccess: true}, - {server: serverC, login: "root", hasAccess: false}, - {server: serverC, login: "admin", hasAccess: false}, - }, - }, - { - name: "role matches a negative regexp label", - roles: []RoleV3{ - RoleV3{ - Metadata: Metadata{ - Name: "name1", - Namespace: defaults.Namespace, - }, - Spec: RoleSpecV3{ - Options: RoleOptions{ - MaxSessionTTL: Duration(20 * time.Hour), - }, - Allow: RoleConditions{ - Logins: []string{"admin"}, - NodeLabels: Labels{"role": []string{`{{regexp.not_match("db.*")}}`}}, - Namespaces: []string{defaults.Namespace, namespaceC}, - }, - }, - }, - }, - checks: []check{ - {server: serverA, login: "root", hasAccess: false}, - {server: serverA, login: "admin", hasAccess: false}, - {server: serverB, login: "root", hasAccess: false}, - {server: serverB, login: "admin", hasAccess: true}, - {server: serverC, login: "root", hasAccess: false}, - {server: serverC, login: "admin", hasAccess: false}, - }, - }, } for i, tc := range testCases { diff --git a/lib/utils/parse/parse.go b/lib/utils/parse/parse.go index 6a48655397e1f..55d9b2514de40 100644 --- a/lib/utils/parse/parse.go +++ b/lib/utils/parse/parse.go @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +// TODO(awly): combine Expression and Matcher. It should be possible to write: +// `{{regexp.match(email.local(external.trait_name))}}` package parse import ( diff --git a/lib/utils/replace.go b/lib/utils/replace.go index 4d68ae7ed0f9d..a32c83e3b8c84 100644 --- a/lib/utils/replace.go +++ b/lib/utils/replace.go @@ -46,5 +46,32 @@ func ReplaceRegexp(expression string, replaceWith string, input string) (string, return expr.ReplaceAllString(input, replaceWith), nil } +// SliceMatchesRegex checks if input matches any of the expressions. The +// match is always evaluated as a regex either an exact match or regexp. +func SliceMatchesRegex(input string, expressions []string) (bool, error) { + for _, expression := range expressions { + if !strings.HasPrefix(expression, "^") || !strings.HasSuffix(expression, "$") { + // replace glob-style wildcards with regexp wildcards + // for plain strings, and quote all characters that could + // be interpreted in regular expression + expression = "^" + GlobToRegexp(expression) + "$" + } + + expr, err := regexp.Compile(expression) + if err != nil { + return false, trace.BadParameter(err.Error()) + } + + // Since the expression is always surrounded by ^ and $ this is an exact + // match for either a a plain string (for example ^hello$) or for a regexp + // (for example ^hel*o$). + if expr.MatchString(input) { + return true, nil + } + } + + return false, nil +} + var replaceWildcard = regexp.MustCompile(`(\\\*)`) var reExpansion = regexp.MustCompile(`\$[^\$]+`)