Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Regexp support for node lables #2262

Merged
merged 3 commits into from
Oct 15, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions lib/services/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -1395,30 +1395,37 @@ func MatchLogin(selectors []string, login string) (bool, string) {

// MatchLabels matches selector against target. Empty selector matches
// nothing, wildcard matches everything.
func MatchLabels(selector Labels, target map[string]string) (bool, string) {
func MatchLabels(selector Labels, target map[string]string) (bool, string, error) {
// Empty selector matches nothing.
if len(selector) == 0 {
return false, "no match, empty selector"
return false, "no match, empty selector", nil
}

// *: * matches everything even empty target set.
selectorValues := selector[Wildcard]
if len(selectorValues) == 1 && selectorValues[0] == Wildcard {
return true, "matched"
return true, "matched", nil
}

// Perform full match.
for key, selectorValues := range selector {
targetVal, hasKey := target[key]

if !hasKey {
return false, fmt.Sprintf("no key match: '%v'", key)
return false, fmt.Sprintf("no key match: '%v'", key), nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use %q instead of '%v'

}
if !utils.SliceContainsStr(selectorValues, Wildcard) && !utils.SliceContainsStr(selectorValues, targetVal) {
return false, fmt.Sprintf("no value match: got '%v' want: '%v'", targetVal, selectorValues)

if !utils.SliceContainsStr(selectorValues, Wildcard) {
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just use %q that will properly quote the string

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used to do this, but then I noticed when customer paste us logs from systemd they end up being quoted and escaped which makes logs difficult to read.

}
}
}

return true, "matched"
return true, "matched", nil
}

// RoleNames returns a slice with role names
Expand Down Expand Up @@ -1558,7 +1565,10 @@ func (set RoleSet) CheckAccessToServer(login string, s Server) error {
// the deny role set prohibits access.
for _, role := range set {
matchNamespace, namespaceMessage := MatchNamespace(role.GetNamespaces(Deny), s.GetNamespace())
matchLabels, labelsMessage := MatchLabels(role.GetNodeLabels(Deny), s.GetAllLabels())
matchLabels, labelsMessage, err := MatchLabels(role.GetNodeLabels(Deny), s.GetAllLabels())
if err != nil {
return trace.Wrap(err)
}
matchLogin, loginMessage := MatchLogin(role.GetLogins(Deny), login)
if matchNamespace && (matchLabels || matchLogin) {
if log.GetLevel() == log.DebugLevel {
Expand All @@ -1575,7 +1585,10 @@ func (set RoleSet) CheckAccessToServer(login string, s Server) error {
// one role in the role set to be granted access.
for _, role := range set {
matchNamespace, namespaceMessage := MatchNamespace(role.GetNamespaces(Allow), s.GetNamespace())
matchLabels, labelsMessage := MatchLabels(role.GetNodeLabels(Allow), s.GetAllLabels())
matchLabels, labelsMessage, err := MatchLabels(role.GetNodeLabels(Allow), s.GetAllLabels())
if err != nil {
return trace.Wrap(err)
}
matchLogin, loginMessage := MatchLogin(role.GetLogins(Allow), login)
if matchNamespace && matchLabels && matchLogin {
return nil
Expand Down
38 changes: 38 additions & 0 deletions lib/services/role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,13 @@ func (s *RoleSuite) TestCheckAccess(c *C) {
Labels: map[string]string{"role": "db", "status": "follower"},
},
}
serverC2 := &ServerV2{
Metadata: Metadata{
Name: "c2",
Namespace: namespaceC,
Labels: map[string]string{"role": "db01", "status": "follower01"},
},
}
testCases := []struct {
name string
roles []RoleV3
Expand Down Expand Up @@ -648,6 +655,37 @@ func (s *RoleSuite) TestCheckAccess(c *C) {
{server: serverC, login: "admin", hasAccess: true},
},
},
{
name: "one role needs to access servers sharing the partially same label value",
roles: []RoleV3{
RoleV3{
Metadata: Metadata{
Name: "name1",
Namespace: namespaceC,
},
Spec: RoleSpecV3{
Options: RoleOptions{
MaxSessionTTL: Duration{20 * time.Hour},
},
Allow: RoleConditions{
Logins: []string{"admin"},
NodeLabels: Labels{"role": []string{"^db(.*)$"}, "status": []string{"follow*"}},
Namespaces: []string{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: false},
{server: serverC, login: "root", hasAccess: false},
{server: serverC, login: "admin", hasAccess: true},
{server: serverC2, login: "root", hasAccess: false},
{server: serverC2, login: "admin", hasAccess: true},
},
},
}
for i, tc := range testCases {

Expand Down
27 changes: 27 additions & 0 deletions lib/utils/replace.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(`\$[^\$]+`)