mirror of
https://github.com/matrix-org/dendrite.git
synced 2025-12-12 09:23:09 -06:00
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) <andrew@amorgan.xyz>
This commit is contained in:
parent
8549d4b096
commit
6b5ee76a56
|
|
@ -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.
|
-- The password hash for this account. Can be NULL if this is a passwordless account.
|
||||||
password_hash TEXT,
|
password_hash TEXT,
|
||||||
-- Identifies which Application Service this account belongs to.
|
-- Identifies which Application Service this account belongs to.
|
||||||
appservice_id TEXT,
|
appservice_id TEXT
|
||||||
-- TODO:
|
-- TODO:
|
||||||
-- is_guest, is_admin, upgraded_ts, devices, any email reset stuff?
|
-- is_guest, is_admin, upgraded_ts, devices, any email reset stuff?
|
||||||
);
|
);
|
||||||
|
|
@ -84,7 +84,7 @@ func (s *accountsStatements) insertAccount(
|
||||||
) (*authtypes.Account, error) {
|
) (*authtypes.Account, error) {
|
||||||
createdTimeMS := time.Now().UnixNano() / 1000000
|
createdTimeMS := time.Now().UnixNano() / 1000000
|
||||||
stmt := s.insertAccountStmt
|
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 nil, err
|
||||||
}
|
}
|
||||||
return &authtypes.Account{
|
return &authtypes.Account{
|
||||||
|
|
|
||||||
|
|
@ -245,40 +245,49 @@ func UsernameIsWithinApplicationServiceNamespace(
|
||||||
cfg *config.Dendrite,
|
cfg *config.Dendrite,
|
||||||
username string,
|
username string,
|
||||||
appservice *config.ApplicationService,
|
appservice *config.ApplicationService,
|
||||||
) (bool, *util.JSONResponse) {
|
) bool {
|
||||||
if appservice != nil {
|
if appservice != nil {
|
||||||
// Loop through given Application Service's namespaces and see if any match
|
// Loop through given Application Service's namespaces and see if any match
|
||||||
for _, namespace := range appservice.Namespaces["users"] {
|
for _, namespace := range appservice.NamespaceMap["users"] {
|
||||||
match, err := regexp.MatchString(namespace.Regex, username)
|
// AS namespaces are checked for validity in config
|
||||||
if err != nil {
|
if namespace.RegexpObject.MatchString(username) {
|
||||||
return false, &util.JSONResponse{
|
return true
|
||||||
Code: 401,
|
|
||||||
JSON: jsonerror.BadJSON("Supplied Application Service namespace" + namespace.Regex + "is invalid."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if match {
|
|
||||||
return true, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false, nil
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop through all known Application Service's namespaces and see if any match
|
// Loop through all known Application Service's namespaces and see if any match
|
||||||
for _, knownAppservice := range cfg.Derived.ApplicationServices {
|
for _, knownAppservice := range cfg.Derived.ApplicationServices {
|
||||||
for _, namespace := range knownAppservice.Namespaces["users"] {
|
for _, namespace := range knownAppservice.NamespaceMap["users"] {
|
||||||
match, err := regexp.MatchString(namespace.Regex, username)
|
// AS namespaces are checked for validity in config
|
||||||
if err != nil {
|
if namespace.RegexpObject.MatchString(username) {
|
||||||
return false, &util.JSONResponse{
|
return true
|
||||||
Code: 401,
|
|
||||||
JSON: jsonerror.BadJSON("Supplied Application Service namespace" + namespace.Regex + "is invalid."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if match {
|
|
||||||
return true, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
// 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.
|
// Ensure the desired username is within at least one of the application service's namespaces.
|
||||||
matched, err := UsernameIsWithinApplicationServiceNamespace(cfg, username, matchedApplicationService)
|
if !UsernameIsWithinApplicationServiceNamespace(cfg, username, matchedApplicationService) {
|
||||||
if err != nil {
|
// If we didn't find any matches, return M_EXCLUSIVE
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we didn't find any matches, return M_EXCLUSIVE
|
|
||||||
if !matched {
|
|
||||||
return "", &util.JSONResponse{
|
return "", &util.JSONResponse{
|
||||||
Code: 401,
|
Code: 401,
|
||||||
JSON: jsonerror.Exclusive("Supplied username " + username +
|
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
|
// No errors, registration valid
|
||||||
return matchedApplicationService.ID, nil
|
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(
|
func Register(
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
accountDB *accounts.Database,
|
accountDB *accounts.Database,
|
||||||
|
|
@ -366,6 +380,16 @@ func Register(
|
||||||
return *resErr
|
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 := util.GetLogger(req.Context())
|
||||||
logger.WithFields(log.Fields{
|
logger.WithFields(log.Fields{
|
||||||
"username": r.Username,
|
"username": r.Username,
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,11 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
@ -28,6 +31,8 @@ type ApplicationServiceNamespace struct {
|
||||||
Exclusive bool `yaml:"exclusive"`
|
Exclusive bool `yaml:"exclusive"`
|
||||||
// A regex pattern that represents the namespace
|
// A regex pattern that represents the namespace
|
||||||
Regex string `yaml:"regex"`
|
Regex string `yaml:"regex"`
|
||||||
|
// Regex object representing our pattern. Saves having to recompile every time
|
||||||
|
RegexpObject *regexp.Regexp
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplicationService represents a Matrix application service.
|
// ApplicationService represents a Matrix application service.
|
||||||
|
|
@ -44,7 +49,7 @@ type ApplicationService struct {
|
||||||
// Localpart of application service user
|
// Localpart of application service user
|
||||||
SenderLocalpart string `yaml:"sender_localpart"`
|
SenderLocalpart string `yaml:"sender_localpart"`
|
||||||
// Information about an application service's namespaces
|
// 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
|
// loadAppservices iterates through all application service config files
|
||||||
|
|
@ -80,14 +85,106 @@ func loadAppservices(config *Dendrite) error {
|
||||||
return checkErrors(config)
|
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
|
// checkErrors checks for any configuration errors amongst the loaded
|
||||||
// application services according to the application service spec.
|
// application services according to the application service spec.
|
||||||
func checkErrors(config *Dendrite) error {
|
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
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -230,6 +231,13 @@ type Dendrite struct {
|
||||||
// Application Services parsed from their config files
|
// Application Services parsed from their config files
|
||||||
// The paths of which were given above in the main config file
|
// The paths of which were given above in the main config file
|
||||||
ApplicationServices []ApplicationService
|
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:"-"`
|
} `yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue