aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKuhnChris <kuhnchris@kuhnchris.eu>2023-11-24 22:34:13 +0100
committerGitHub <noreply@github.com>2023-11-24 22:34:13 +0100
commit4f943771fa9c26f5f2a0d17f5c477a3c9b704b6e (patch)
tree285c83cb6a7ab35a3639d5b6efcf3b2d70586ef6
parentb8f91485b47ac6e92a90988b394e8f3611735250 (diff)
Appservice Login (2nd attempt) (#3078)
Rebase of #2936 as @vijfhoek wrote he got no time to work on this, and I kind of needed it for my experiments. I checked the tests, and it is working with my example code (i.e. impersonating, registering, creating channel, invite people, write messages). I'm not a huge `go` pro, and still learning, but I tried to fix and/or integrate the changes as best as possible with the current `main` branch changes. If there is anything left, let me know and I'll try to figure it out. Signed-off-by: `Kuhn Christopher <kuhnchris+git@kuhnchris.eu>` --------- Signed-off-by: Sijmen <me@sijman.nl> Signed-off-by: Sijmen Schoon <me@sijman.nl> Co-authored-by: Sijmen Schoon <me@sijman.nl> Co-authored-by: Sijmen Schoon <me@vijf.life> Co-authored-by: Till <2353100+S7evinK@users.noreply.github.com>
-rw-r--r--clientapi/auth/login.go26
-rw-r--r--clientapi/auth/login_application_service.go55
-rw-r--r--clientapi/auth/login_test.go129
-rw-r--r--clientapi/auth/user_interactive.go2
-rw-r--r--clientapi/routing/login.go22
-rw-r--r--clientapi/routing/login_test.go38
-rw-r--r--clientapi/routing/receipt.go7
-rw-r--r--clientapi/routing/register.go14
-rw-r--r--clientapi/routing/sendevent.go2
-rw-r--r--internal/validate.go136
-rw-r--r--internal/validate_test.go134
11 files changed, 530 insertions, 35 deletions
diff --git a/clientapi/auth/login.go b/clientapi/auth/login.go
index 77835614..58a27e59 100644
--- a/clientapi/auth/login.go
+++ b/clientapi/auth/login.go
@@ -15,7 +15,6 @@
package auth
import (
- "context"
"encoding/json"
"io"
"net/http"
@@ -32,8 +31,13 @@ import (
// called after authorization has completed, with the result of the authorization.
// If the final return value is non-nil, an error occurred and the cleanup function
// is nil.
-func LoginFromJSONReader(ctx context.Context, r io.Reader, useraccountAPI uapi.UserLoginAPI, userAPI UserInternalAPIForLogin, cfg *config.ClientAPI) (*Login, LoginCleanupFunc, *util.JSONResponse) {
- reqBytes, err := io.ReadAll(r)
+func LoginFromJSONReader(
+ req *http.Request,
+ useraccountAPI uapi.UserLoginAPI,
+ userAPI UserInternalAPIForLogin,
+ cfg *config.ClientAPI,
+) (*Login, LoginCleanupFunc, *util.JSONResponse) {
+ reqBytes, err := io.ReadAll(req.Body)
if err != nil {
err := &util.JSONResponse{
Code: http.StatusBadRequest,
@@ -65,6 +69,20 @@ func LoginFromJSONReader(ctx context.Context, r io.Reader, useraccountAPI uapi.U
UserAPI: userAPI,
Config: cfg,
}
+ case authtypes.LoginTypeApplicationService:
+ token, err := ExtractAccessToken(req)
+ if err != nil {
+ err := &util.JSONResponse{
+ Code: http.StatusForbidden,
+ JSON: spec.MissingToken(err.Error()),
+ }
+ return nil, nil, err
+ }
+
+ typ = &LoginTypeApplicationService{
+ Config: cfg,
+ Token: token,
+ }
default:
err := util.JSONResponse{
Code: http.StatusBadRequest,
@@ -73,7 +91,7 @@ func LoginFromJSONReader(ctx context.Context, r io.Reader, useraccountAPI uapi.U
return nil, nil, &err
}
- return typ.LoginFromJSON(ctx, reqBytes)
+ return typ.LoginFromJSON(req.Context(), reqBytes)
}
// UserInternalAPIForLogin contains the aspects of UserAPI required for logging in.
diff --git a/clientapi/auth/login_application_service.go b/clientapi/auth/login_application_service.go
new file mode 100644
index 00000000..dd4a9cbb
--- /dev/null
+++ b/clientapi/auth/login_application_service.go
@@ -0,0 +1,55 @@
+// 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 auth
+
+import (
+ "context"
+
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/dendrite/clientapi/httputil"
+ "github.com/matrix-org/dendrite/internal"
+ "github.com/matrix-org/dendrite/setup/config"
+ "github.com/matrix-org/util"
+)
+
+// LoginTypeApplicationService describes how to authenticate as an
+// application service
+type LoginTypeApplicationService struct {
+ Config *config.ClientAPI
+ Token string
+}
+
+// Name implements Type
+func (t *LoginTypeApplicationService) Name() string {
+ return authtypes.LoginTypeApplicationService
+}
+
+// LoginFromJSON implements Type
+func (t *LoginTypeApplicationService) LoginFromJSON(
+ ctx context.Context, reqBytes []byte,
+) (*Login, LoginCleanupFunc, *util.JSONResponse) {
+ var r Login
+ if err := httputil.UnmarshalJSON(reqBytes, &r); err != nil {
+ return nil, nil, err
+ }
+
+ _, err := internal.ValidateApplicationServiceRequest(t.Config, r.Identifier.User, t.Token)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ cleanup := func(ctx context.Context, j *util.JSONResponse) {}
+ return &r, cleanup, nil
+}
diff --git a/clientapi/auth/login_test.go b/clientapi/auth/login_test.go
index 93d3e271..a2c2a719 100644
--- a/clientapi/auth/login_test.go
+++ b/clientapi/auth/login_test.go
@@ -17,7 +17,9 @@ package auth
import (
"context"
"net/http"
+ "net/http/httptest"
"reflect"
+ "regexp"
"strings"
"testing"
@@ -33,8 +35,9 @@ func TestLoginFromJSONReader(t *testing.T) {
ctx := context.Background()
tsts := []struct {
- Name string
- Body string
+ Name string
+ Body string
+ Token string
WantUsername string
WantDeviceID string
@@ -62,6 +65,30 @@ func TestLoginFromJSONReader(t *testing.T) {
WantDeviceID: "adevice",
WantDeletedTokens: []string{"atoken"},
},
+ {
+ Name: "appServiceWorksUserID",
+ Body: `{
+ "type": "m.login.application_service",
+ "identifier": { "type": "m.id.user", "user": "@alice:example.com" },
+ "device_id": "adevice"
+ }`,
+ Token: "astoken",
+
+ WantUsername: "@alice:example.com",
+ WantDeviceID: "adevice",
+ },
+ {
+ Name: "appServiceWorksLocalpart",
+ Body: `{
+ "type": "m.login.application_service",
+ "identifier": { "type": "m.id.user", "user": "alice" },
+ "device_id": "adevice"
+ }`,
+ Token: "astoken",
+
+ WantUsername: "alice",
+ WantDeviceID: "adevice",
+ },
}
for _, tst := range tsts {
t.Run(tst.Name, func(t *testing.T) {
@@ -72,11 +99,35 @@ func TestLoginFromJSONReader(t *testing.T) {
ServerName: serverName,
},
},
+ Derived: &config.Derived{
+ ApplicationServices: []config.ApplicationService{
+ {
+ ID: "anapplicationservice",
+ ASToken: "astoken",
+ NamespaceMap: map[string][]config.ApplicationServiceNamespace{
+ "users": {
+ {
+ Exclusive: true,
+ Regex: "@alice:example.com",
+ RegexpObject: regexp.MustCompile("@alice:example.com"),
+ },
+ },
+ },
+ },
+ },
+ },
}
- login, cleanup, err := LoginFromJSONReader(ctx, strings.NewReader(tst.Body), &userAPI, &userAPI, cfg)
- if err != nil {
- t.Fatalf("LoginFromJSONReader failed: %+v", err)
+
+ req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tst.Body))
+ if tst.Token != "" {
+ req.Header.Add("Authorization", "Bearer "+tst.Token)
}
+
+ login, cleanup, jsonErr := LoginFromJSONReader(req, &userAPI, &userAPI, cfg)
+ if jsonErr != nil {
+ t.Fatalf("LoginFromJSONReader failed: %+v", jsonErr)
+ }
+
cleanup(ctx, &util.JSONResponse{Code: http.StatusOK})
if login.Username() != tst.WantUsername {
@@ -104,8 +155,9 @@ func TestBadLoginFromJSONReader(t *testing.T) {
ctx := context.Background()
tsts := []struct {
- Name string
- Body string
+ Name string
+ Body string
+ Token string
WantErrCode spec.MatrixErrorCode
}{
@@ -142,6 +194,45 @@ func TestBadLoginFromJSONReader(t *testing.T) {
}`,
WantErrCode: spec.ErrorInvalidParam,
},
+ {
+ Name: "noASToken",
+ Body: `{
+ "type": "m.login.application_service",
+ "identifier": { "type": "m.id.user", "user": "@alice:example.com" },
+ "device_id": "adevice"
+ }`,
+ WantErrCode: "M_MISSING_TOKEN",
+ },
+ {
+ Name: "badASToken",
+ Token: "badastoken",
+ Body: `{
+ "type": "m.login.application_service",
+ "identifier": { "type": "m.id.user", "user": "@alice:example.com" },
+ "device_id": "adevice"
+ }`,
+ WantErrCode: "M_UNKNOWN_TOKEN",
+ },
+ {
+ Name: "badASNamespace",
+ Token: "astoken",
+ Body: `{
+ "type": "m.login.application_service",
+ "identifier": { "type": "m.id.user", "user": "@bob:example.com" },
+ "device_id": "adevice"
+ }`,
+ WantErrCode: "M_EXCLUSIVE",
+ },
+ {
+ Name: "badASUserID",
+ Token: "astoken",
+ Body: `{
+ "type": "m.login.application_service",
+ "identifier": { "type": "m.id.user", "user": "@alice:wrong.example.com" },
+ "device_id": "adevice"
+ }`,
+ WantErrCode: "M_INVALID_USERNAME",
+ },
}
for _, tst := range tsts {
t.Run(tst.Name, func(t *testing.T) {
@@ -152,8 +243,30 @@ func TestBadLoginFromJSONReader(t *testing.T) {
ServerName: serverName,
},
},
+ Derived: &config.Derived{
+ ApplicationServices: []config.ApplicationService{
+ {
+ ID: "anapplicationservice",
+ ASToken: "astoken",
+ NamespaceMap: map[string][]config.ApplicationServiceNamespace{
+ "users": {
+ {
+ Exclusive: true,
+ Regex: "@alice:example.com",
+ RegexpObject: regexp.MustCompile("@alice:example.com"),
+ },
+ },
+ },
+ },
+ },
+ },
}
- _, cleanup, errRes := LoginFromJSONReader(ctx, strings.NewReader(tst.Body), &userAPI, &userAPI, cfg)
+ req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tst.Body))
+ if tst.Token != "" {
+ req.Header.Add("Authorization", "Bearer "+tst.Token)
+ }
+
+ _, cleanup, errRes := LoginFromJSONReader(req, &userAPI, &userAPI, cfg)
if errRes == nil {
cleanup(ctx, nil)
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)
diff --git a/clientapi/auth/user_interactive.go b/clientapi/auth/user_interactive.go
index 92d83ad2..9831450c 100644
--- a/clientapi/auth/user_interactive.go
+++ b/clientapi/auth/user_interactive.go
@@ -55,7 +55,7 @@ type LoginCleanupFunc func(context.Context, *util.JSONResponse)
// https://matrix.org/docs/spec/client_server/r0.6.1#identifier-types
type LoginIdentifier struct {
Type string `json:"type"`
- // when type = m.id.user
+ // when type = m.id.user or m.id.application_service
User string `json:"user"`
// when type = m.id.thirdparty
Medium string `json:"medium"`
diff --git a/clientapi/routing/login.go b/clientapi/routing/login.go
index bc38b834..0f55c881 100644
--- a/clientapi/routing/login.go
+++ b/clientapi/routing/login.go
@@ -19,6 +19,7 @@ import (
"net/http"
"github.com/matrix-org/dendrite/clientapi/auth"
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/clientapi/userutil"
"github.com/matrix-org/dendrite/setup/config"
userapi "github.com/matrix-org/dendrite/userapi/api"
@@ -40,28 +41,25 @@ type flow struct {
Type string `json:"type"`
}
-func passwordLogin() flows {
- f := flows{}
- s := flow{
- Type: "m.login.password",
- }
- f.Flows = append(f.Flows, s)
- return f
-}
-
// Login implements GET and POST /login
func Login(
req *http.Request, userAPI userapi.ClientUserAPI,
cfg *config.ClientAPI,
) util.JSONResponse {
if req.Method == http.MethodGet {
- // TODO: support other forms of login other than password, depending on config options
+ loginFlows := []flow{{Type: authtypes.LoginTypePassword}}
+ if len(cfg.Derived.ApplicationServices) > 0 {
+ loginFlows = append(loginFlows, flow{Type: authtypes.LoginTypeApplicationService})
+ }
+ // TODO: support other forms of login, depending on config options
return util.JSONResponse{
Code: http.StatusOK,
- JSON: passwordLogin(),
+ JSON: flows{
+ Flows: loginFlows,
+ },
}
} else if req.Method == http.MethodPost {
- login, cleanup, authErr := auth.LoginFromJSONReader(req.Context(), req.Body, userAPI, userAPI, cfg)
+ login, cleanup, authErr := auth.LoginFromJSONReader(req, userAPI, userAPI, cfg)
if authErr != nil {
return *authErr
}
diff --git a/clientapi/routing/login_test.go b/clientapi/routing/login_test.go
index 1c628b19..3f893449 100644
--- a/clientapi/routing/login_test.go
+++ b/clientapi/routing/login_test.go
@@ -114,6 +114,44 @@ func TestLogin(t *testing.T) {
ctx := context.Background()
+ // Inject a dummy application service, so we have a "m.login.application_service"
+ // in the login flows
+ as := &config.ApplicationService{}
+ cfg.AppServiceAPI.Derived.ApplicationServices = []config.ApplicationService{*as}
+
+ t.Run("Supported log-in flows are returned", func(t *testing.T) {
+ req := test.NewRequest(t, http.MethodGet, "/_matrix/client/v3/login")
+ rec := httptest.NewRecorder()
+ routers.Client.ServeHTTP(rec, req)
+ if rec.Code != http.StatusOK {
+ t.Fatalf("failed to get log-in flows: %s", rec.Body.String())
+ }
+
+ t.Logf("response: %s", rec.Body.String())
+ resp := flows{}
+ if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
+ t.Fatal(err)
+ }
+
+ appServiceFound := false
+ passwordFound := false
+ for _, flow := range resp.Flows {
+ if flow.Type == "m.login.password" {
+ passwordFound = true
+ } else if flow.Type == "m.login.application_service" {
+ appServiceFound = true
+ } else {
+ t.Fatalf("got unknown login flow: %s", flow.Type)
+ }
+ }
+ if !appServiceFound {
+ t.Fatal("m.login.application_service missing from login flows")
+ }
+ if !passwordFound {
+ t.Fatal("m.login.password missing from login flows")
+ }
+ })
+
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req := test.NewRequest(t, http.MethodPost, "/_matrix/client/v3/login", test.WithJSONBody(t, map[string]interface{}{
diff --git a/clientapi/routing/receipt.go b/clientapi/routing/receipt.go
index be654297..1d7e3556 100644
--- a/clientapi/routing/receipt.go
+++ b/clientapi/routing/receipt.go
@@ -23,13 +23,12 @@ import (
"github.com/matrix-org/dendrite/clientapi/producers"
"github.com/matrix-org/gomatrixserverlib/spec"
- "github.com/matrix-org/dendrite/userapi/api"
userapi "github.com/matrix-org/dendrite/userapi/api"
"github.com/matrix-org/util"
"github.com/sirupsen/logrus"
)
-func SetReceipt(req *http.Request, userAPI api.ClientUserAPI, syncProducer *producers.SyncAPIProducer, device *userapi.Device, roomID, receiptType, eventID string) util.JSONResponse {
+func SetReceipt(req *http.Request, userAPI userapi.ClientUserAPI, syncProducer *producers.SyncAPIProducer, device *userapi.Device, roomID, receiptType, eventID string) util.JSONResponse {
timestamp := spec.AsTimestamp(time.Now())
logrus.WithFields(logrus.Fields{
"roomID": roomID,
@@ -54,13 +53,13 @@ func SetReceipt(req *http.Request, userAPI api.ClientUserAPI, syncProducer *prod
}
}
- dataReq := api.InputAccountDataRequest{
+ dataReq := userapi.InputAccountDataRequest{
UserID: device.UserID,
DataType: "m.fully_read",
RoomID: roomID,
AccountData: data,
}
- dataRes := api.InputAccountDataResponse{}
+ dataRes := userapi.InputAccountDataResponse{}
if err := userAPI.InputAccountData(req.Context(), &dataReq, &dataRes); err != nil {
util.GetLogger(req.Context()).WithError(err).Error("userAPI.InputAccountData failed")
return util.ErrorResponse(err)
diff --git a/clientapi/routing/register.go b/clientapi/routing/register.go
index 090a2fc2..558418a6 100644
--- a/clientapi/routing/register.go
+++ b/clientapi/routing/register.go
@@ -647,6 +647,16 @@ func handleGuestRegistration(
}
}
+// localpartMatchesExclusiveNamespaces will check if a given username matches any
+// application service's exclusive users namespace
+func localpartMatchesExclusiveNamespaces(
+ cfg *config.ClientAPI,
+ localpart string,
+) bool {
+ userID := userutil.MakeUserID(localpart, cfg.Matrix.ServerName)
+ return cfg.Derived.ExclusiveApplicationServicesUsernameRegexp.MatchString(userID)
+}
+
// handleRegistrationFlow will direct and complete registration flow stages
// that the client has requested.
// nolint: gocyclo
@@ -695,7 +705,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) {
+ localpartMatchesExclusiveNamespaces(cfg, r.Username) {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.ASExclusive("This username is reserved by an application service."),
@@ -772,7 +782,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 := internal.ValidateApplicationServiceRequest(
cfg, r.Username, accessToken,
)
if err != nil {
diff --git a/clientapi/routing/sendevent.go b/clientapi/routing/sendevent.go
index 69131966..44e82aed 100644
--- a/clientapi/routing/sendevent.go
+++ b/clientapi/routing/sendevent.go
@@ -224,7 +224,7 @@ func SendEvent(
req.Context(), rsAPI,
api.KindNew,
[]*types.HeaderedEvent{
- &types.HeaderedEvent{PDU: e},
+ {PDU: e},
},
device.UserDomain(),
domain,
diff --git a/internal/validate.go b/internal/validate.go
index 7f0d8b9e..da8b35cd 100644
--- a/internal/validate.go
+++ b/internal/validate.go
@@ -20,6 +20,9 @@ import (
"net/http"
"regexp"
+ "github.com/matrix-org/dendrite/clientapi/userutil"
+ "github.com/matrix-org/dendrite/setup/config"
+ "github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/gomatrixserverlib/spec"
"github.com/matrix-org/util"
)
@@ -100,10 +103,139 @@ func UsernameResponse(err error) *util.JSONResponse {
// ValidateApplicationServiceUsername returns an error if the username is invalid for an application service
func ValidateApplicationServiceUsername(localpart string, domain spec.ServerName) error {
- if id := fmt.Sprintf("@%s:%s", localpart, domain); len(id) > maxUsernameLength {
+ userID := userutil.MakeUserID(localpart, domain)
+ return ValidateApplicationServiceUserID(userID)
+}
+
+func ValidateApplicationServiceUserID(userID string) error {
+ if len(userID) > maxUsernameLength {
return ErrUsernameTooLong
- } else if !validUsernameRegex.MatchString(localpart) {
+ }
+
+ localpart, _, err := gomatrixserverlib.SplitID('@', userID)
+ if err != nil || !validUsernameRegex.MatchString(localpart) {
return ErrUsernameInvalid
}
+
return nil
}
+
+// 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 localpart, domain, err = gomatrixserverlib.SplitID('@', userID)
+ if err != nil {
+ // Not a valid userID
+ return false
+ }
+
+ if !cfg.Matrix.IsLocalServerName(domain) {
+ // This is a federated userID
+ return false
+ }
+
+ if localpart == appservice.SenderLocalpart {
+ // This is the application service bot userID
+ return true
+ }
+
+ // Loop through given application service's namespaces and see if any match
+ for _, namespace := range appservice.NamespaceMap["users"] {
+ // Application service 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 userIDMatchesMultipleExclusiveNamespaces(
+ cfg *config.ClientAPI,
+ userID string,
+) bool {
+ // 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
+}
+
+// ValidateApplicationServiceRequest checks if a provided application service
+// token corresponds to one that is registered, and, if so, checks if the
+// supplied userIDOrLocalpart is within that application service's namespace.
+//
+// As long as these two requirements are met, the matched application service
+// ID will be returned. Otherwise, it will return a JSON response with the
+// appropriate error message.
+func ValidateApplicationServiceRequest(
+ cfg *config.ClientAPI,
+ userIDOrLocalpart string,
+ accessToken string,
+) (string, *util.JSONResponse) {
+ localpart, domain, err := userutil.ParseUsernameParam(userIDOrLocalpart, cfg.Matrix)
+ if err != nil {
+ return "", &util.JSONResponse{
+ Code: http.StatusUnauthorized,
+ JSON: spec.InvalidUsername(err.Error()),
+ }
+ }
+
+ userID := userutil.MakeUserID(localpart, domain)
+
+ // 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: spec.UnknownToken("Supplied access_token does not match any known application service"),
+ }
+ }
+
+ // 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: spec.ASExclusive(fmt.Sprintf(
+ "Supplied username %s did not match any namespaces for application service ID: %s", userIDOrLocalpart, matchedApplicationService.ID)),
+ }
+ }
+
+ // Check this user does not fit multiple application service namespaces
+ if userIDMatchesMultipleExclusiveNamespaces(cfg, userID) {
+ return "", &util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: spec.ASExclusive(fmt.Sprintf(
+ "Supplied username %s matches multiple exclusive application service namespaces. Only 1 match allowed", userIDOrLocalpart)),
+ }
+ }
+
+ // Check username application service is trying to register is valid
+ if err := ValidateApplicationServiceUserID(userID); err != nil {
+ return "", UsernameResponse(err)
+ }
+
+ // No errors, registration valid
+ return matchedApplicationService.ID, nil
+}
diff --git a/internal/validate_test.go b/internal/validate_test.go
index e3a10178..cd262613 100644
--- a/internal/validate_test.go
+++ b/internal/validate_test.go
@@ -3,9 +3,11 @@ package internal
import (
"net/http"
"reflect"
+ "regexp"
"strings"
"testing"
+ "github.com/matrix-org/dendrite/setup/config"
"github.com/matrix-org/gomatrixserverlib/spec"
"github.com/matrix-org/util"
)
@@ -38,7 +40,7 @@ func Test_validatePassword(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
gotErr := ValidatePassword(tt.password)
if !reflect.DeepEqual(gotErr, tt.wantError) {
- t.Errorf("validatePassword() = %v, wantJSON %v", gotErr, tt.wantError)
+ t.Errorf("validatePassword() = %v, wantError %v", gotErr, tt.wantError)
}
if got := PasswordResponse(gotErr); !reflect.DeepEqual(got, tt.wantJSON) {
@@ -167,3 +169,133 @@ func Test_validateUsername(t *testing.T) {
})
}
}
+
+// This method tests validation of the provided Application Service token and
+// username that they're registering
+func TestValidateApplicationServiceRequest(t *testing.T) {
+ // Create a fake application service
+ regex := "@_appservice_.*"
+ fakeNamespace := config.ApplicationServiceNamespace{
+ Exclusive: true,
+ Regex: regex,
+ RegexpObject: regexp.MustCompile(regex),
+ }
+ fakeSenderLocalpart := "_appservice_bot"
+ fakeApplicationService := config.ApplicationService{
+ ID: "FakeAS",
+ URL: "null",
+ ASToken: "1234",
+ HSToken: "4321",
+ SenderLocalpart: fakeSenderLocalpart,
+ NamespaceMap: map[string][]config.ApplicationServiceNamespace{
+ "users": {fakeNamespace},
+ },
+ }
+
+ // Create a second fake application service where userIDs ending in
+ // "_overlap" overlap with the first.
+ regex = "@_.*_overlap"
+ fakeNamespace = config.ApplicationServiceNamespace{
+ Exclusive: true,
+ Regex: regex,
+ RegexpObject: regexp.MustCompile(regex),
+ }
+ fakeApplicationServiceOverlap := config.ApplicationService{
+ ID: "FakeASOverlap",
+ URL: fakeApplicationService.URL,
+ ASToken: fakeApplicationService.ASToken,
+ HSToken: fakeApplicationService.HSToken,
+ SenderLocalpart: "_appservice_bot_overlap",
+ NamespaceMap: map[string][]config.ApplicationServiceNamespace{
+ "users": {fakeNamespace},
+ },
+ }
+
+ // Set up a config
+ fakeConfig := &config.Dendrite{}
+ fakeConfig.Defaults(config.DefaultOpts{
+ Generate: true,
+ })
+ fakeConfig.Global.ServerName = "localhost"
+ fakeConfig.ClientAPI.Derived.ApplicationServices = []config.ApplicationService{fakeApplicationService, fakeApplicationServiceOverlap}
+
+ tests := []struct {
+ name string
+ localpart string
+ asToken string
+ wantError bool
+ wantASID string
+ }{
+ // Access token is correct, userID omitted so we are acting as SenderLocalpart
+ {
+ name: "correct access token but omitted userID",
+ localpart: fakeSenderLocalpart,
+ asToken: fakeApplicationService.ASToken,
+ wantError: false,
+ wantASID: fakeApplicationService.ID,
+ },
+ // Access token is incorrect, userID omitted so we are acting as SenderLocalpart
+ {
+ name: "incorrect access token but omitted userID",
+ localpart: fakeSenderLocalpart,
+ asToken: "xxxx",
+ wantError: true,
+ wantASID: "",
+ },
+ // Access token is correct, acting as valid userID
+ {
+ name: "correct access token and valid userID",
+ localpart: "_appservice_bob",
+ asToken: fakeApplicationService.ASToken,
+ wantError: false,
+ wantASID: fakeApplicationService.ID,
+ },
+ // Access token is correct, acting as invalid userID
+ {
+ name: "correct access token but invalid userID",
+ localpart: "_something_else",
+ asToken: fakeApplicationService.ASToken,
+ wantError: true,
+ wantASID: "",
+ },
+ // Access token is correct, acting as userID that matches two exclusive namespaces
+ {
+ name: "correct access token but non-exclusive userID",
+ localpart: "_appservice_overlap",
+ asToken: fakeApplicationService.ASToken,
+ wantError: true,
+ wantASID: "",
+ },
+ // Access token is correct, acting as matching userID that is too long
+ {
+ name: "correct access token but too long userID",
+ localpart: "_appservice_" + strings.Repeat("a", maxUsernameLength),
+ asToken: fakeApplicationService.ASToken,
+ wantError: true,
+ wantASID: "",
+ },
+ // Access token is correct, acting as userID that matches but is invalid
+ {
+ name: "correct access token and matching but invalid userID",
+ localpart: "@_appservice_bob::",
+ asToken: fakeApplicationService.ASToken,
+ wantError: true,
+ wantASID: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ gotASID, gotResp := ValidateApplicationServiceRequest(&fakeConfig.ClientAPI, tt.localpart, tt.asToken)
+ if tt.wantError && gotResp == nil {
+ t.Error("expected an error, but succeeded")
+ }
+ if !tt.wantError && gotResp != nil {
+ t.Errorf("expected success, but returned error: %v", *gotResp)
+ }
+ if gotASID != tt.wantASID {
+ t.Errorf("returned '%s', but expected '%s'", gotASID, tt.wantASID)
+ }
+ })
+ }
+}