diff --git a/bot/access.go b/bot/access.go index 18291f6..6bb987b 100644 --- a/bot/access.go +++ b/bot/access.go @@ -4,9 +4,8 @@ import ( "context" "regexp" + "gitlab.com/etke.cc/go/mxidwc" "maunium.net/go/mautrix/id" - - "gitlab.com/etke.cc/postmoogle/utils" ) func parseMXIDpatterns(patterns []string, defaultPattern string) ([]*regexp.Regexp, error) { @@ -14,12 +13,12 @@ func parseMXIDpatterns(patterns []string, defaultPattern string) ([]*regexp.Rege patterns = []string{defaultPattern} } - return utils.WildcardMXIDsToRegexes(patterns) + return mxidwc.ParsePatterns(patterns) } func (b *Bot) allowUsers(actorID id.UserID) bool { if len(b.allowedUsers) != 0 { - if !utils.Match(actorID.String(), b.allowedUsers) { + if !mxidwc.Match(actorID.String(), b.allowedUsers) { return false } } @@ -50,7 +49,7 @@ func (b *Bot) allowOwner(actorID id.UserID, targetRoomID id.RoomID) bool { } func (b *Bot) allowAdmin(actorID id.UserID, targetRoomID id.RoomID) bool { - return utils.Match(actorID.String(), b.allowedAdmins) + return mxidwc.Match(actorID.String(), b.allowedAdmins) } func (b *Bot) allowSend(actorID id.UserID, targetRoomID id.RoomID) bool { diff --git a/bot/sync.go b/bot/sync.go index 1f1e339..55d66b1 100644 --- a/bot/sync.go +++ b/bot/sync.go @@ -3,10 +3,9 @@ package bot import ( "context" + "gitlab.com/etke.cc/go/mxidwc" "maunium.net/go/mautrix" "maunium.net/go/mautrix/event" - - "gitlab.com/etke.cc/postmoogle/utils" ) func (b *Bot) initSync() { @@ -32,7 +31,7 @@ func (b *Bot) initSync() { // joinPermit is called by linkpearl when processing "invite" events and deciding if rooms should be auto-joined or not func (b *Bot) joinPermit(evt *event.Event) bool { - if !utils.Match(evt.Sender.String(), b.allowedUsers) { + if !mxidwc.Match(evt.Sender.String(), b.allowedUsers) { b.log.Debug("Rejecting room invitation from unallowed user: %s", evt.Sender) return false } diff --git a/go.mod b/go.mod index 774895f..572fa6d 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.14 gitlab.com/etke.cc/go/env v1.0.0 gitlab.com/etke.cc/go/logger v1.1.0 + gitlab.com/etke.cc/go/mxidwc v1.0.0 gitlab.com/etke.cc/go/secgen v1.1.1 gitlab.com/etke.cc/linkpearl v0.0.0-20220831124140-598117f26c77 golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b diff --git a/go.sum b/go.sum index 3fba51f..269295b 100644 --- a/go.sum +++ b/go.sum @@ -88,6 +88,8 @@ gitlab.com/etke.cc/go/env v1.0.0 h1:J98BwzOuELnjsVPFvz5wa79L7IoRV9CmrS41xLYXtSw= gitlab.com/etke.cc/go/env v1.0.0/go.mod h1:e1l4RM5MA1sc0R1w/RBDAESWRwgo5cOG9gx8BKUn2C4= gitlab.com/etke.cc/go/logger v1.1.0 h1:Yngp/DDLmJ0jJNLvLXrfan5Gi5QV+r7z6kCczTv8t4U= gitlab.com/etke.cc/go/logger v1.1.0/go.mod h1:8Vw5HFXlZQ5XeqvUs5zan+GnhrQyYtm/xe+yj8H/0zk= +gitlab.com/etke.cc/go/mxidwc v1.0.0 h1:6EAlJXvs3nU4RaMegYq6iFlyVvLw7JZYnZmNCGMYQP0= +gitlab.com/etke.cc/go/mxidwc v1.0.0/go.mod h1:E/0kh45SAN9+ntTG0cwkAEKdaPxzvxVmnjwivm9nmz8= gitlab.com/etke.cc/go/secgen v1.1.1 h1:RmKOki725HIhWJHzPtAc9X4YvBneczndchpMgoDkE8w= gitlab.com/etke.cc/go/secgen v1.1.1/go.mod h1:3pJqRGeWApzx7qXjABqz2o2SMCNpKSZao/gXVdasqE8= gitlab.com/etke.cc/linkpearl v0.0.0-20220831124140-598117f26c77 h1:O9t4Sw/nu0JDUX+3KYjaqBi887opyNZ0imE+i2sV+q8= diff --git a/utils/user.go b/utils/user.go deleted file mode 100644 index 3b93c49..0000000 --- a/utils/user.go +++ /dev/null @@ -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 -} diff --git a/utils/user_test.go b/utils/user_test.go deleted file mode 100644 index 28e87c9..0000000 --- a/utils/user_test.go +++ /dev/null @@ -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) - } -}