mirror of
https://github.com/element-hq/dendrite.git
synced 2025-09-13 12:52:24 +03:00
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:
parent
b8f91485b4
commit
4f943771fa
11 changed files with 530 additions and 35 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue