From 6b5ee76a562c60605b54ceca58c3a3fa0a3cde61 Mon Sep 17 00:00:00 2001 From: "Andrew Morgan (https://amorgan.xyz)" Date: Sat, 20 Jan 2018 00:55:37 -0800 Subject: [PATCH] Validate loaded Application Services * Ensure no two app services have the same token or ID * Check namespaces are valid regex * Ensure users can't register inside an exclusive app service namespace * Ensure exclusive app service namespaces are exclusive with each other * Precompile application service namespace regexes so we don't need to do so every time a user is registered Signed-off-by: Andrew Morgan (https://amorgan.xyz) --- .../auth/storage/accounts/accounts_table.go | 4 +- .../dendrite/clientapi/routing/register.go | 86 ++++++++------ .../dendrite/common/config/appservice.go | 105 +++++++++++++++++- .../dendrite/common/config/config.go | 8 ++ 4 files changed, 166 insertions(+), 37 deletions(-) diff --git a/src/github.com/matrix-org/dendrite/clientapi/auth/storage/accounts/accounts_table.go b/src/github.com/matrix-org/dendrite/clientapi/auth/storage/accounts/accounts_table.go index f56b1b188..a3961a305 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/auth/storage/accounts/accounts_table.go +++ b/src/github.com/matrix-org/dendrite/clientapi/auth/storage/accounts/accounts_table.go @@ -34,7 +34,7 @@ CREATE TABLE IF NOT EXISTS account_accounts ( -- The password hash for this account. Can be NULL if this is a passwordless account. password_hash TEXT, -- Identifies which Application Service this account belongs to. - appservice_id TEXT, + appservice_id TEXT -- TODO: -- is_guest, is_admin, upgraded_ts, devices, any email reset stuff? ); @@ -84,7 +84,7 @@ func (s *accountsStatements) insertAccount( ) (*authtypes.Account, error) { createdTimeMS := time.Now().UnixNano() / 1000000 stmt := s.insertAccountStmt - if _, err := stmt.ExecContext(ctx, localpart, createdTimeMS, hash); err != nil { + if _, err := stmt.ExecContext(ctx, localpart, createdTimeMS, hash, appserviceID); err != nil { return nil, err } return &authtypes.Account{ diff --git a/src/github.com/matrix-org/dendrite/clientapi/routing/register.go b/src/github.com/matrix-org/dendrite/clientapi/routing/register.go index 2cfe3b547..71921026c 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/routing/register.go +++ b/src/github.com/matrix-org/dendrite/clientapi/routing/register.go @@ -245,40 +245,49 @@ func UsernameIsWithinApplicationServiceNamespace( cfg *config.Dendrite, username string, appservice *config.ApplicationService, -) (bool, *util.JSONResponse) { +) bool { if appservice != nil { // Loop through given Application Service's namespaces and see if any match - for _, namespace := range appservice.Namespaces["users"] { - match, err := regexp.MatchString(namespace.Regex, username) - if err != nil { - return false, &util.JSONResponse{ - Code: 401, - JSON: jsonerror.BadJSON("Supplied Application Service namespace" + namespace.Regex + "is invalid."), - } - } - if match { - return true, nil + for _, namespace := range appservice.NamespaceMap["users"] { + // AS namespaces are checked for validity in config + if namespace.RegexpObject.MatchString(username) { + return true } } - return false, nil + return false } // Loop through all known Application Service's namespaces and see if any match for _, knownAppservice := range cfg.Derived.ApplicationServices { - for _, namespace := range knownAppservice.Namespaces["users"] { - match, err := regexp.MatchString(namespace.Regex, username) - if err != nil { - return false, &util.JSONResponse{ - Code: 401, - JSON: jsonerror.BadJSON("Supplied Application Service namespace" + namespace.Regex + "is invalid."), - } - } - if match { - return true, nil + for _, namespace := range knownAppservice.NamespaceMap["users"] { + // AS namespaces are checked for validity in config + if namespace.RegexpObject.MatchString(username) { + return true } } } - return false, nil + return false +} + +// UsernameMatchesMultipleExclusiveNamespaces will check if a given username matches +// more than one exclusive namespace. More than one is not allowed +func UsernameMatchesMultipleExclusiveNamespaces( + cfg *config.Dendrite, + username string, +) bool { + // Check namespaces and see if more than one match + matchCount := 0 + for _, appservice := range cfg.Derived.ApplicationServices { + for _, namespaceSlice := range appservice.NamespaceMap { + for _, namespace := range namespaceSlice { + // Check if we have a match on this username + if namespace.RegexpObject.MatchString(username) { + matchCount++ + } + } + } + } + return matchCount > 1 } // validateApplicationService checks if a provided application service token @@ -308,13 +317,8 @@ func validateApplicationService( } // Ensure the desired username is within at least one of the application service's namespaces. - matched, err := UsernameIsWithinApplicationServiceNamespace(cfg, username, matchedApplicationService) - if err != nil { - return "", err - } - - // If we didn't find any matches, return M_EXCLUSIVE - if !matched { + if !UsernameIsWithinApplicationServiceNamespace(cfg, username, matchedApplicationService) { + // If we didn't find any matches, return M_EXCLUSIVE return "", &util.JSONResponse{ Code: 401, JSON: jsonerror.Exclusive("Supplied username " + username + @@ -322,11 +326,21 @@ func validateApplicationService( } } + // Check this user does not fit multiple application service namespaces + if UsernameMatchesMultipleExclusiveNamespaces(cfg, username) { + return "", &util.JSONResponse{ + Code: 401, + JSON: jsonerror.Exclusive("Supplied username " + username + + " matches multiple exclusive application service namespaces. Only 1 match allowed"), + } + } + // No errors, registration valid return matchedApplicationService.ID, nil } -// Register processes a /register request. http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#post-matrix-client-unstable-register +// Register processes a /register request. +// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#post-matrix-client-unstable-register func Register( req *http.Request, accountDB *accounts.Database, @@ -366,6 +380,16 @@ func Register( return *resErr } + // Make sure normal user isn't registering under an exclusive application + // service namespace + if r.Auth.Type != "m.login.application_service" && + cfg.Derived.ExclusiveApplicationServicesUsernameRegexp.MatchString(r.Username) { + return util.JSONResponse{ + Code: 400, + JSON: jsonerror.Exclusive("This username is registered by an application service."), + } + } + logger := util.GetLogger(req.Context()) logger.WithFields(log.Fields{ "username": r.Username, diff --git a/src/github.com/matrix-org/dendrite/common/config/appservice.go b/src/github.com/matrix-org/dendrite/common/config/appservice.go index 8f89aa308..72efafaca 100644 --- a/src/github.com/matrix-org/dendrite/common/config/appservice.go +++ b/src/github.com/matrix-org/dendrite/common/config/appservice.go @@ -15,8 +15,11 @@ package config import ( + "fmt" "io/ioutil" "path/filepath" + "regexp" + "strings" "gopkg.in/yaml.v2" ) @@ -28,6 +31,8 @@ type ApplicationServiceNamespace struct { Exclusive bool `yaml:"exclusive"` // A regex pattern that represents the namespace Regex string `yaml:"regex"` + // Regex object representing our pattern. Saves having to recompile every time + RegexpObject *regexp.Regexp } // ApplicationService represents a Matrix application service. @@ -44,7 +49,7 @@ type ApplicationService struct { // Localpart of application service user SenderLocalpart string `yaml:"sender_localpart"` // Information about an application service's namespaces - Namespaces map[string][]ApplicationServiceNamespace `yaml:"namespaces"` + NamespaceMap map[string][]ApplicationServiceNamespace `yaml:"namespaces"` } // loadAppservices iterates through all application service config files @@ -80,14 +85,106 @@ func loadAppservices(config *Dendrite) error { return checkErrors(config) } +// setupRegexps will create regex objects for exclusive and non-exclusive +// usernames, aliases and rooms of all application services, so that other +// methods can quickly check if a particular string matches any of them. +func setupRegexps(cfg *Dendrite) { + // Combine all exclusive namespaces for later string checking + var exclusiveUsernameStrings, exclusiveAliasStrings, exclusiveRoomStrings []string + + // If an application service's regex is marked as exclusive, add + // it's contents to the overall exlusive regex string + for _, appservice := range cfg.Derived.ApplicationServices { + for key, namespaceSlice := range appservice.NamespaceMap { + switch key { + case "users": + appendExclusiveNamespaceRegexs(&exclusiveUsernameStrings, namespaceSlice) + case "aliases": + appendExclusiveNamespaceRegexs(&exclusiveAliasStrings, namespaceSlice) + case "rooms": + appendExclusiveNamespaceRegexs(&exclusiveRoomStrings, namespaceSlice) + } + } + } + + // Join the regexes together into one big regex. + // i.e. "app1.*", "app2.*" -> "(app1.*)|(app2.*)" + // Later we can check if a username or some other string matches any exclusive + // regex and deny access if it isn't from an application service + exclusiveUsernames := strings.Join(exclusiveUsernameStrings, "|") + + // TODO: Aliases and rooms. Needed? + //exclusiveAliases := strings.Join(exclusiveAliasStrings, "|") + //exclusiveRooms := strings.Join(exclusiveRoomStrings, "|") + + cfg.Derived.ExclusiveApplicationServicesUsernameRegexp, _ = regexp.Compile(exclusiveUsernames) +} + +// concatenateExclusiveNamespaces takes a slice of strings and a slice of +// namespaces and will append the regexes of only the exclusive namespaces +// into the string slice +func appendExclusiveNamespaceRegexs( + exclusiveStrings *[]string, namespaces []ApplicationServiceNamespace, +) { + for _, namespace := range namespaces { + if namespace.Exclusive { + // We append parenthesis to later separate each regex when we compile + // i.e. "app1.*", "app2.*" -> "(app1.*)|(app2.*)" + *exclusiveStrings = append(*exclusiveStrings, "("+namespace.Regex+")") + } + + // Compile this regex into a Regexp object for later use + namespace.RegexpObject, _ = regexp.Compile(namespace.Regex) + } +} + // checkErrors checks for any configuration errors amongst the loaded // application services according to the application service spec. func checkErrors(config *Dendrite) error { - // TODO: Check that no two app services have the same as_token or id + var idMap = make(map[string]bool) + var tokenMap = make(map[string]bool) - // TODO: Check that namespace(s) are valid regex + // Check that no two application services have the same as_token or id + for _, appservice := range config.Derived.ApplicationServices { + // Check if we've already seen this ID + if idMap[appservice.ID] { + return Error{[]string{fmt.Sprintf( + "Application Service ID %s must be unique", appservice.ID, + )}} + } + if tokenMap[appservice.ASToken] { + return Error{[]string{fmt.Sprintf( + "Application Service Token %s must be unique", appservice.ASToken, + )}} + } - // TODO: Check that exclusive namespaces are actually exclusive + // Add the id/token to their respective maps if we haven't already + // seen them. + idMap[appservice.ID] = true + tokenMap[appservice.ID] = true + } + + // Check that namespace(s) are valid regex + for _, appservice := range config.Derived.ApplicationServices { + for _, namespaceSlice := range appservice.NamespaceMap { + for _, namespace := range namespaceSlice { + if !IsValidRegex(namespace.Regex) { + return Error{[]string{fmt.Sprintf( + "Invalid regex string for Application Service %s", appservice.ID, + )}} + } + } + } + } + setupRegexps(config) return nil } + +// IsValidRegex returns true or false based on whether the +// given string is valid regex or not +func IsValidRegex(regexString string) bool { + _, err := regexp.Compile(regexString) + + return err == nil +} diff --git a/src/github.com/matrix-org/dendrite/common/config/config.go b/src/github.com/matrix-org/dendrite/common/config/config.go index 7a7f2b48d..1e2374a84 100644 --- a/src/github.com/matrix-org/dendrite/common/config/config.go +++ b/src/github.com/matrix-org/dendrite/common/config/config.go @@ -22,6 +22,7 @@ import ( "io" "io/ioutil" "path/filepath" + "regexp" "strings" "time" @@ -230,6 +231,13 @@ type Dendrite struct { // Application Services parsed from their config files // The paths of which were given above in the main config file ApplicationServices []ApplicationService + + // A meta-regex compiled from all exclusive Application Service + // Regexes. When a user registers, we check that their username + // does not match any exclusive Application Service namespaces + ExclusiveApplicationServicesUsernameRegexp *regexp.Regexp + + // TODO: Exclusive alias, room regexp's } `yaml:"-"` }