export MXID patterns parsing to external lib

This commit is contained in:
Aine
2022-09-14 13:46:56 +03:00
parent 14c0ebf1f1
commit 97aacbf143
6 changed files with 9 additions and 333 deletions

View File

@@ -1,104 +0,0 @@
package utils
import (
"fmt"
"regexp"
"strings"
)
// WildcardMXIDsToRegexes converts a list of wildcard patterns to a list of regular expressions
func WildcardMXIDsToRegexes(wildCardPatterns []string) ([]*regexp.Regexp, error) {
regexPatterns := make([]*regexp.Regexp, len(wildCardPatterns))
for idx, wildCardPattern := range wildCardPatterns {
regex, err := parseMXIDWildcard(wildCardPattern)
if err != nil {
return nil, fmt.Errorf("failed to parse allowed user rule `%s`: %s", wildCardPattern, err)
}
regexPatterns[idx] = regex
}
return regexPatterns, nil
}
// Match tells if the given user id is allowed to use the bot, according to the given whitelist
func Match(userID string, allowed []*regexp.Regexp) bool {
for _, regex := range allowed {
if regex.MatchString(userID) {
return true
}
}
return false
}
// parseMXIDWildcard parses a user whitelisting wildcard rule and returns a regular expression which corresponds to it
//
// Example conversion: `@bot.*.something:*.example.com` -> `^bot\.([^:@]*)\.something:([^:@]*)\.example.com$`
// Example of recognized wildcard patterns: `@someone:example.com`, `@*:example.com`, `@bot.*:example.com`, `@someone:*`, `@someone:*.example.com`
//
// The `*` wildcard character is normally interpretted as "a number of literal characters or an empty string".
// Our implementation below matches this (yielding `([^:@])*`), which could provide a slightly suboptimal regex in these cases:
// - `@*:example.com` -> `^@([^:@])*:example\.com$`, although `^@([^:@])+:example\.com$` would be preferable
// - `@someone:*` -> `@someone:([^:@])*$`, although `@someone:([^:@])+$` would be preferable
// When it's a bare wildcard (`*`, instead of `*.example.com`) we likely prefer to yield a regex that matches **at least one character**.
// This probably doesn't matter because mxids that we'll match against are all valid and fully complete.
func parseMXIDWildcard(wildCardRule string) (*regexp.Regexp, error) {
if !strings.HasPrefix(wildCardRule, "@") {
return nil, fmt.Errorf("rules need to be fully-qualified, starting with a @")
}
remainingRule := wildCardRule[1:]
if strings.Contains(remainingRule, "@") {
return nil, fmt.Errorf("rules cannot contain more than one @")
}
parts := strings.Split(remainingRule, ":")
if len(parts) != 2 {
return nil, fmt.Errorf("expected exactly 2 parts in the rule, separated by `:`")
}
localPart := parts[0]
localPartPattern, err := getRegexPatternForPart(localPart)
if err != nil {
return nil, fmt.Errorf("failed to convert local part `%s` to regex: %s", localPart, err)
}
domainPart := parts[1]
domainPartPattern, err := getRegexPatternForPart(domainPart)
if err != nil {
return nil, fmt.Errorf("failed to convert domain part `%s` to regex: %s", domainPart, err)
}
finalPattern := fmt.Sprintf("^@%s:%s$", localPartPattern, domainPartPattern)
regex, err := regexp.Compile(finalPattern)
if err != nil {
return nil, fmt.Errorf("failed to compile regex `%s`: %s", finalPattern, err)
}
return regex, nil
}
func getRegexPatternForPart(part string) (string, error) {
if part == "" {
return "", fmt.Errorf("rejecting empty part")
}
var pattern strings.Builder
for _, rune := range part {
if rune == '*' {
// We match everything except for `:` and `@`, because that would be an invalid MXID anyway.
//
// If the whole part is `*` (only) instead of merely containing `*` within it,
// we may also consider replacing it with `([^:@]+)` (+, instead of *).
// See parseMXIDWildcard for notes about this.
pattern.WriteString("([^:@]*)")
continue
}
pattern.WriteString(regexp.QuoteMeta(string(rune)))
}
return pattern.String(), nil
}

View File

@@ -1,221 +0,0 @@
package utils
import "testing"
func TestRuleToRegex(t *testing.T) {
type testDataDefinition struct {
name string
checkedValue string
expectedResult string
expectedError bool
}
tests := []testDataDefinition{
{
name: "simple pattern without wildcards succeeds",
checkedValue: "@someone:example.com",
expectedResult: `^@someone:example\.com$`,
expectedError: false,
},
{
name: "pattern with wildcard as the whole local part succeeds",
checkedValue: "@*:example.com",
expectedResult: `^@([^:@]*):example\.com$`,
expectedError: false,
},
{
name: "pattern with wildcard within the local part succeeds",
checkedValue: "@bot.*.something:example.com",
expectedResult: `^@bot\.([^:@]*)\.something:example\.com$`,
expectedError: false,
},
{
name: "pattern with wildcard as the whole domain part succeeds",
checkedValue: "@someone:*",
expectedResult: `^@someone:([^:@]*)$`,
expectedError: false,
},
{
name: "pattern with wildcard within the domain part succeeds",
checkedValue: "@someone:*.organization.com",
expectedResult: `^@someone:([^:@]*)\.organization\.com$`,
expectedError: false,
},
{
name: "pattern with wildcard in both parts succeeds",
checkedValue: "@*:*",
expectedResult: `^@([^:@]*):([^:@]*)$`,
expectedError: false,
},
{
name: "pattern that does not appear fully-qualified fails",
checkedValue: "someone:example.com",
expectedResult: ``,
expectedError: true,
},
{
name: "pattern that does not appear fully-qualified fails",
checkedValue: "@someone",
expectedResult: ``,
expectedError: true,
},
{
name: "pattern with empty domain part fails",
checkedValue: "@someone:",
expectedResult: ``,
expectedError: true,
},
{
name: "pattern with empty local part fails",
checkedValue: "@:example.com",
expectedResult: ``,
expectedError: true,
},
{
name: "pattern with multiple @ fails",
checkedValue: "@someone@someone:example.com",
expectedResult: ``,
expectedError: true,
},
{
name: "pattern with multiple : fails",
checkedValue: "@someone:someone:example.com",
expectedResult: ``,
expectedError: true,
},
}
for _, testData := range tests {
func(testData testDataDefinition) {
t.Run(testData.name, func(t *testing.T) {
actualResult, err := parseMXIDWildcard(testData.checkedValue)
if testData.expectedError {
if err != nil {
return
}
t.Errorf("expected an error, but did not get one")
}
if err != nil {
t.Errorf("did not expect an error, but got one: %s", err)
}
if actualResult.String() == testData.expectedResult {
return
}
t.Errorf(
"Expected `%s` to yield `%s`, not `%s`",
testData.checkedValue,
testData.expectedResult,
actualResult.String(),
)
})
}(testData)
}
}
func TestMatch(t *testing.T) {
type testDataDefinition struct {
name string
checkedValue string
allowedUsers []string
expectedResult bool
}
tests := []testDataDefinition{
{
name: "Empty allowed users allows no one",
checkedValue: "@someone:example.com",
allowedUsers: []string{},
expectedResult: false,
},
{
name: "Direct full mxid match is allowed",
checkedValue: "@someone:example.com",
allowedUsers: []string{"@someone:example.com"},
expectedResult: true,
},
{
name: "Direct full mxid match later on is allowed",
checkedValue: "@someone:example.com",
allowedUsers: []string{"@another:example.com", "@someone:example.com"},
expectedResult: true,
},
{
name: "No mxid match is not allowed",
checkedValue: "@someone:example.com",
allowedUsers: []string{"@another:example.com"},
expectedResult: false,
},
{
name: "mxid localpart only wildcard match is allowed",
checkedValue: "@someone:example.com",
allowedUsers: []string{"@*:example.com"},
expectedResult: true,
},
{
name: "mxid localpart with wildcard match is allowed",
checkedValue: "@bot.abc:example.com",
allowedUsers: []string{"@bot.*:example.com"},
expectedResult: true,
},
{
name: "mxid localpart with wildcard match is not allowed when it does not match",
checkedValue: "@bot.abc:example.com",
allowedUsers: []string{"@employee.*:example.com"},
expectedResult: false,
},
{
name: "mxid localpart wildcard for another domain is not allowed",
checkedValue: "@someone:example.com",
allowedUsers: []string{"@*:another.com"},
expectedResult: false,
},
{
name: "mxid domainpart with only wildcard match is allowed",
checkedValue: "@someone:example.com",
allowedUsers: []string{"@someone:*"},
expectedResult: true,
},
{
name: "mxid domainpart with wildcard match is allowed",
checkedValue: "@someone:example.organization.com",
allowedUsers: []string{"@someone:*.organization.com"},
expectedResult: true,
},
{
name: "mxid domainpart with wildcard match is not allowed when it does not match",
checkedValue: "@someone:example.another.com",
allowedUsers: []string{"@someone:*.organization.com"},
expectedResult: false,
},
}
for _, testData := range tests {
func(testData testDataDefinition) {
t.Run(testData.name, func(t *testing.T) {
allowedUserRegexes, err := WildcardMXIDsToRegexes(testData.allowedUsers)
if err != nil {
t.Error(err)
}
actualResult := Match(testData.checkedValue, allowedUserRegexes)
if actualResult == testData.expectedResult {
return
}
t.Errorf(
"Expected `%s` compared against `%v` to yield `%v`, not `%v`",
testData.checkedValue,
testData.allowedUsers,
testData.expectedResult,
actualResult,
)
})
}(testData)
}
}