From 29bf74483279cf18f329979bf4146985fb132e24 Mon Sep 17 00:00:00 2001 From: Sijmen Schoon Date: Sat, 7 Jan 2023 02:04:27 +0100 Subject: [PATCH] Move ValidateApplicationService to a separate package for reuse Signed-off-by: Sijmen Signed-off-by: Sijmen Schoon --- appservice/validate/validate.go | 163 +++++++++++++++++++++++++++ appservice/validate/validate_test.go | 86 ++++++++++++++ clientapi/routing/register.go | 5 +- clientapi/routing/register_test.go | 3 - 4 files changed, 252 insertions(+), 5 deletions(-) create mode 100644 appservice/validate/validate.go create mode 100644 appservice/validate/validate_test.go diff --git a/appservice/validate/validate.go b/appservice/validate/validate.go new file mode 100644 index 000000000..cbda5f7bb --- /dev/null +++ b/appservice/validate/validate.go @@ -0,0 +1,163 @@ +// Copyright 2023 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package validate + +import ( + "fmt" + "net/http" + + "github.com/matrix-org/dendrite/clientapi/jsonerror" + "github.com/matrix-org/dendrite/clientapi/userutil" + "github.com/matrix-org/dendrite/internal" + "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/util" +) + +// UserIDIsWithinApplicationServiceNamespace checks to see if a given userID +// falls within any of the namespaces of a given Application Service. If no +// Application Service is given, it will check to see if it matches any +// Application Service's namespace. +func UserIDIsWithinApplicationServiceNamespace( + cfg *config.ClientAPI, + userID string, + appservice *config.ApplicationService, +) bool { + + var local, domain, err = gomatrixserverlib.SplitID('@', userID) + if err != nil { + // Not a valid userID + return false + } + + if !cfg.Matrix.IsLocalServerName(domain) { + return false + } + + if appservice != nil { + if appservice.SenderLocalpart == local { + return true + } + + // Loop through given application service's namespaces and see if any match + for _, namespace := range appservice.NamespaceMap["users"] { + // AS namespaces are checked for validity in config + if namespace.RegexpObject.MatchString(userID) { + return true + } + } + return false + } + + // Loop through all known application service's namespaces and see if any match + for _, knownAppService := range cfg.Derived.ApplicationServices { + if knownAppService.SenderLocalpart == local { + return true + } + for _, namespace := range knownAppService.NamespaceMap["users"] { + // AS namespaces are checked for validity in config + if namespace.RegexpObject.MatchString(userID) { + return true + } + } + } + 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.ClientAPI, + username string, +) bool { + userID := userutil.MakeUserID(username, cfg.Matrix.ServerName) + + // Check namespaces and see if more than one match + matchCount := 0 + for _, appservice := range cfg.Derived.ApplicationServices { + if appservice.OwnsNamespaceCoveringUserId(userID) { + if matchCount++; matchCount > 1 { + return true + } + } + } + return false +} + +// UsernameMatchesExclusiveNamespaces will check if a given username matches any +// application service's exclusive users namespace +func UsernameMatchesExclusiveNamespaces( + cfg *config.ClientAPI, + username string, +) bool { + userID := userutil.MakeUserID(username, cfg.Matrix.ServerName) + return cfg.Derived.ExclusiveApplicationServicesUsernameRegexp.MatchString(userID) +} + +// validateApplicationService checks if a provided application service token +// corresponds to one that is registered. If so, then it checks if the desired +// username is within that application service's namespace. As long as these +// two requirements are met, no error will be returned. +// TODO Move somewhere better +func ValidateApplicationService( + cfg *config.ClientAPI, + username string, + accessToken string, +) (string, *util.JSONResponse) { + // Check if the token if the application service is valid with one we have + // registered in the config. + var matchedApplicationService *config.ApplicationService + for _, appservice := range cfg.Derived.ApplicationServices { + if appservice.ASToken == accessToken { + matchedApplicationService = &appservice + break + } + } + if matchedApplicationService == nil { + return "", &util.JSONResponse{ + Code: http.StatusUnauthorized, + JSON: jsonerror.UnknownToken("Supplied access_token does not match any known application service"), + } + } + + userID := userutil.MakeUserID(username, cfg.Matrix.ServerName) + + // Ensure the desired username is within at least one of the application service's namespaces. + if !UserIDIsWithinApplicationServiceNamespace(cfg, userID, matchedApplicationService) { + // If we didn't find any matches, return M_EXCLUSIVE + return "", &util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.ASExclusive(fmt.Sprintf( + "Supplied username %s did not match any namespaces for application service ID: %s", username, matchedApplicationService.ID)), + } + } + + // Check this user does not fit multiple application service namespaces + if UsernameMatchesMultipleExclusiveNamespaces(cfg, userID) { + return "", &util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.ASExclusive(fmt.Sprintf( + "Supplied username %s matches multiple exclusive application service namespaces. Only 1 match allowed", username)), + } + } + + // Check username application service is trying to register is valid + if err := internal.ValidateApplicationServiceUsername(username, cfg.Matrix.ServerName); err != nil { + return "", internal.UsernameResponse(err) + } + + // No errors, registration valid + return matchedApplicationService.ID, nil +} diff --git a/appservice/validate/validate_test.go b/appservice/validate/validate_test.go new file mode 100644 index 000000000..dc3741c78 --- /dev/null +++ b/appservice/validate/validate_test.go @@ -0,0 +1,86 @@ +// Copyright 2023 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package validate + +import ( + "regexp" + "testing" + + "github.com/matrix-org/dendrite/setup/config" +) + +// This method tests validation of the provided Application Service token and +// username that they're registering +func TestValidationOfApplicationServices(t *testing.T) { + // Set up application service namespaces + regex := "@_appservice_.*" + regexp, err := regexp.Compile(regex) + if err != nil { + t.Errorf("Error compiling regex: %s", regex) + } + + fakeNamespace := config.ApplicationServiceNamespace{ + Exclusive: true, + Regex: regex, + RegexpObject: regexp, + } + + // Create a fake application service + fakeID := "FakeAS" + fakeSenderLocalpart := "_appservice_bot" + fakeApplicationService := config.ApplicationService{ + ID: fakeID, + URL: "null", + ASToken: "1234", + HSToken: "4321", + SenderLocalpart: fakeSenderLocalpart, + NamespaceMap: map[string][]config.ApplicationServiceNamespace{ + "users": {fakeNamespace}, + }, + } + + // Set up a config + fakeConfig := &config.Dendrite{} + fakeConfig.Defaults(config.DefaultOpts{ + Generate: true, + Monolithic: true, + }) + fakeConfig.Global.ServerName = "localhost" + fakeConfig.ClientAPI.Derived.ApplicationServices = []config.ApplicationService{fakeApplicationService} + + // Access token is correct, user_id omitted so we are acting as SenderLocalpart + asID, resp := ValidateApplicationService(&fakeConfig.ClientAPI, fakeSenderLocalpart, "1234") + if resp != nil || asID != fakeID { + t.Errorf("appservice should have validated and returned correct ID: %s", resp.JSON) + } + + // Access token is incorrect, user_id omitted so we are acting as SenderLocalpart + asID, resp = ValidateApplicationService(&fakeConfig.ClientAPI, fakeSenderLocalpart, "xxxx") + if resp == nil || asID == fakeID { + t.Errorf("access_token should have been marked as invalid") + } + + // Access token is correct, acting as valid user_id + asID, resp = ValidateApplicationService(&fakeConfig.ClientAPI, "_appservice_bob", "1234") + if resp != nil || asID != fakeID { + t.Errorf("access_token and user_id should've been valid: %s", resp.JSON) + } + + // Access token is correct, acting as invalid user_id + asID, resp = ValidateApplicationService(&fakeConfig.ClientAPI, "_something_else", "1234") + if resp == nil || asID == fakeID { + t.Errorf("user_id should not have been valid: @_something_else:localhost") + } +} diff --git a/clientapi/routing/register.go b/clientapi/routing/register.go index 565c41533..b3e617bdd 100644 --- a/clientapi/routing/register.go +++ b/clientapi/routing/register.go @@ -30,6 +30,7 @@ import ( "sync" "time" + asvalidate "github.com/matrix-org/dendrite/appservice/validate" "github.com/matrix-org/dendrite/internal" "github.com/tidwall/gjson" @@ -695,7 +696,7 @@ func handleRegistrationFlow( // If an access token is provided, ignore this check this is an appservice // request and we will validate in validateApplicationService if len(cfg.Derived.ApplicationServices) != 0 && - UsernameMatchesExclusiveNamespaces(cfg, r.Username) { + asvalidate.UsernameMatchesExclusiveNamespaces(cfg, r.Username) { return util.JSONResponse{ Code: http.StatusBadRequest, JSON: spec.ASExclusive("This username is reserved by an application service."), @@ -772,7 +773,7 @@ func handleApplicationServiceRegistration( // Check application service register user request is valid. // The application service's ID is returned if so. - appserviceID, err := validateApplicationService( + appserviceID, err := asvalidate.ValidateApplicationService( cfg, r.Username, accessToken, ) if err != nil { diff --git a/clientapi/routing/register_test.go b/clientapi/routing/register_test.go index 2a88ec380..c92f53fa9 100644 --- a/clientapi/routing/register_test.go +++ b/clientapi/routing/register_test.go @@ -22,7 +22,6 @@ import ( "net/http" "net/http/httptest" "reflect" - "regexp" "strings" "testing" "time" @@ -32,8 +31,6 @@ import ( "github.com/matrix-org/dendrite/internal/caching" "github.com/matrix-org/dendrite/internal/sqlutil" "github.com/matrix-org/dendrite/roomserver" - "github.com/matrix-org/dendrite/setup/config" - "github.com/matrix-org/dendrite/setup/jetstream" "github.com/matrix-org/dendrite/test" "github.com/matrix-org/dendrite/test/testrig" "github.com/matrix-org/dendrite/userapi"