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>
This commit is contained in:
KuhnChris 2023-11-24 22:34:13 +01:00 committed by GitHub
parent b8f91485b4
commit 4f943771fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 530 additions and 35 deletions

View file

@ -15,7 +15,6 @@
package auth package auth
import ( import (
"context"
"encoding/json" "encoding/json"
"io" "io"
"net/http" "net/http"
@ -32,8 +31,13 @@ import (
// called after authorization has completed, with the result of the authorization. // 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 // If the final return value is non-nil, an error occurred and the cleanup function
// is nil. // is nil.
func LoginFromJSONReader(ctx context.Context, r io.Reader, useraccountAPI uapi.UserLoginAPI, userAPI UserInternalAPIForLogin, cfg *config.ClientAPI) (*Login, LoginCleanupFunc, *util.JSONResponse) { func LoginFromJSONReader(
reqBytes, err := io.ReadAll(r) req *http.Request,
useraccountAPI uapi.UserLoginAPI,
userAPI UserInternalAPIForLogin,
cfg *config.ClientAPI,
) (*Login, LoginCleanupFunc, *util.JSONResponse) {
reqBytes, err := io.ReadAll(req.Body)
if err != nil { if err != nil {
err := &util.JSONResponse{ err := &util.JSONResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
@ -65,6 +69,20 @@ func LoginFromJSONReader(ctx context.Context, r io.Reader, useraccountAPI uapi.U
UserAPI: userAPI, UserAPI: userAPI,
Config: cfg, 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: default:
err := util.JSONResponse{ err := util.JSONResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
@ -73,7 +91,7 @@ func LoginFromJSONReader(ctx context.Context, r io.Reader, useraccountAPI uapi.U
return nil, nil, &err 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. // UserInternalAPIForLogin contains the aspects of UserAPI required for logging in.

View file

@ -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
}

View file

@ -17,7 +17,9 @@ package auth
import ( import (
"context" "context"
"net/http" "net/http"
"net/http/httptest"
"reflect" "reflect"
"regexp"
"strings" "strings"
"testing" "testing"
@ -33,8 +35,9 @@ func TestLoginFromJSONReader(t *testing.T) {
ctx := context.Background() ctx := context.Background()
tsts := []struct { tsts := []struct {
Name string Name string
Body string Body string
Token string
WantUsername string WantUsername string
WantDeviceID string WantDeviceID string
@ -62,6 +65,30 @@ func TestLoginFromJSONReader(t *testing.T) {
WantDeviceID: "adevice", WantDeviceID: "adevice",
WantDeletedTokens: []string{"atoken"}, 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 { for _, tst := range tsts {
t.Run(tst.Name, func(t *testing.T) { t.Run(tst.Name, func(t *testing.T) {
@ -72,11 +99,35 @@ func TestLoginFromJSONReader(t *testing.T) {
ServerName: serverName, 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 { req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tst.Body))
t.Fatalf("LoginFromJSONReader failed: %+v", err) 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}) cleanup(ctx, &util.JSONResponse{Code: http.StatusOK})
if login.Username() != tst.WantUsername { if login.Username() != tst.WantUsername {
@ -104,8 +155,9 @@ func TestBadLoginFromJSONReader(t *testing.T) {
ctx := context.Background() ctx := context.Background()
tsts := []struct { tsts := []struct {
Name string Name string
Body string Body string
Token string
WantErrCode spec.MatrixErrorCode WantErrCode spec.MatrixErrorCode
}{ }{
@ -142,6 +194,45 @@ func TestBadLoginFromJSONReader(t *testing.T) {
}`, }`,
WantErrCode: spec.ErrorInvalidParam, 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 { for _, tst := range tsts {
t.Run(tst.Name, func(t *testing.T) { t.Run(tst.Name, func(t *testing.T) {
@ -152,8 +243,30 @@ func TestBadLoginFromJSONReader(t *testing.T) {
ServerName: serverName, 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 { if errRes == nil {
cleanup(ctx, nil) cleanup(ctx, nil)
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode) t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)

View file

@ -55,7 +55,7 @@ type LoginCleanupFunc func(context.Context, *util.JSONResponse)
// https://matrix.org/docs/spec/client_server/r0.6.1#identifier-types // https://matrix.org/docs/spec/client_server/r0.6.1#identifier-types
type LoginIdentifier struct { type LoginIdentifier struct {
Type string `json:"type"` Type string `json:"type"`
// when type = m.id.user // when type = m.id.user or m.id.application_service
User string `json:"user"` User string `json:"user"`
// when type = m.id.thirdparty // when type = m.id.thirdparty
Medium string `json:"medium"` Medium string `json:"medium"`

View file

@ -19,6 +19,7 @@ import (
"net/http" "net/http"
"github.com/matrix-org/dendrite/clientapi/auth" "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/clientapi/userutil"
"github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/dendrite/setup/config"
userapi "github.com/matrix-org/dendrite/userapi/api" userapi "github.com/matrix-org/dendrite/userapi/api"
@ -40,28 +41,25 @@ type flow struct {
Type string `json:"type"` 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 // Login implements GET and POST /login
func Login( func Login(
req *http.Request, userAPI userapi.ClientUserAPI, req *http.Request, userAPI userapi.ClientUserAPI,
cfg *config.ClientAPI, cfg *config.ClientAPI,
) util.JSONResponse { ) util.JSONResponse {
if req.Method == http.MethodGet { 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{ return util.JSONResponse{
Code: http.StatusOK, Code: http.StatusOK,
JSON: passwordLogin(), JSON: flows{
Flows: loginFlows,
},
} }
} else if req.Method == http.MethodPost { } 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 { if authErr != nil {
return *authErr return *authErr
} }

View file

@ -114,6 +114,44 @@ func TestLogin(t *testing.T) {
ctx := context.Background() 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 { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
req := test.NewRequest(t, http.MethodPost, "/_matrix/client/v3/login", test.WithJSONBody(t, map[string]interface{}{ req := test.NewRequest(t, http.MethodPost, "/_matrix/client/v3/login", test.WithJSONBody(t, map[string]interface{}{

View file

@ -23,13 +23,12 @@ import (
"github.com/matrix-org/dendrite/clientapi/producers" "github.com/matrix-org/dendrite/clientapi/producers"
"github.com/matrix-org/gomatrixserverlib/spec" "github.com/matrix-org/gomatrixserverlib/spec"
"github.com/matrix-org/dendrite/userapi/api"
userapi "github.com/matrix-org/dendrite/userapi/api" userapi "github.com/matrix-org/dendrite/userapi/api"
"github.com/matrix-org/util" "github.com/matrix-org/util"
"github.com/sirupsen/logrus" "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()) timestamp := spec.AsTimestamp(time.Now())
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"roomID": roomID, "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, UserID: device.UserID,
DataType: "m.fully_read", DataType: "m.fully_read",
RoomID: roomID, RoomID: roomID,
AccountData: data, AccountData: data,
} }
dataRes := api.InputAccountDataResponse{} dataRes := userapi.InputAccountDataResponse{}
if err := userAPI.InputAccountData(req.Context(), &dataReq, &dataRes); err != nil { if err := userAPI.InputAccountData(req.Context(), &dataReq, &dataRes); err != nil {
util.GetLogger(req.Context()).WithError(err).Error("userAPI.InputAccountData failed") util.GetLogger(req.Context()).WithError(err).Error("userAPI.InputAccountData failed")
return util.ErrorResponse(err) return util.ErrorResponse(err)

View file

@ -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 // handleRegistrationFlow will direct and complete registration flow stages
// that the client has requested. // that the client has requested.
// nolint: gocyclo // nolint: gocyclo
@ -695,7 +705,7 @@ func handleRegistrationFlow(
// If an access token is provided, ignore this check this is an appservice // If an access token is provided, ignore this check this is an appservice
// request and we will validate in validateApplicationService // request and we will validate in validateApplicationService
if len(cfg.Derived.ApplicationServices) != 0 && if len(cfg.Derived.ApplicationServices) != 0 &&
UsernameMatchesExclusiveNamespaces(cfg, r.Username) { localpartMatchesExclusiveNamespaces(cfg, r.Username) {
return util.JSONResponse{ return util.JSONResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
JSON: spec.ASExclusive("This username is reserved by an application service."), 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. // Check application service register user request is valid.
// The application service's ID is returned if so. // The application service's ID is returned if so.
appserviceID, err := validateApplicationService( appserviceID, err := internal.ValidateApplicationServiceRequest(
cfg, r.Username, accessToken, cfg, r.Username, accessToken,
) )
if err != nil { if err != nil {

View file

@ -224,7 +224,7 @@ func SendEvent(
req.Context(), rsAPI, req.Context(), rsAPI,
api.KindNew, api.KindNew,
[]*types.HeaderedEvent{ []*types.HeaderedEvent{
&types.HeaderedEvent{PDU: e}, {PDU: e},
}, },
device.UserDomain(), device.UserDomain(),
domain, domain,

View file

@ -20,6 +20,9 @@ import (
"net/http" "net/http"
"regexp" "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/gomatrixserverlib/spec"
"github.com/matrix-org/util" "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 // ValidateApplicationServiceUsername returns an error if the username is invalid for an application service
func ValidateApplicationServiceUsername(localpart string, domain spec.ServerName) error { 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 return ErrUsernameTooLong
} else if !validUsernameRegex.MatchString(localpart) { }
localpart, _, err := gomatrixserverlib.SplitID('@', userID)
if err != nil || !validUsernameRegex.MatchString(localpart) {
return ErrUsernameInvalid return ErrUsernameInvalid
} }
return nil 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
}

View file

@ -3,9 +3,11 @@ package internal
import ( import (
"net/http" "net/http"
"reflect" "reflect"
"regexp"
"strings" "strings"
"testing" "testing"
"github.com/matrix-org/dendrite/setup/config"
"github.com/matrix-org/gomatrixserverlib/spec" "github.com/matrix-org/gomatrixserverlib/spec"
"github.com/matrix-org/util" "github.com/matrix-org/util"
) )
@ -38,7 +40,7 @@ func Test_validatePassword(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
gotErr := ValidatePassword(tt.password) gotErr := ValidatePassword(tt.password)
if !reflect.DeepEqual(gotErr, tt.wantError) { 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) { 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)
}
})
}
}