Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 68c7a69

Browse files
Half-Shotclokep
andauthored
Allow appservice users to /login (#8320)
Add ability for ASes to /login using the `uk.half-shot.msc2778.login.application_service` login `type`. Co-authored-by: Patrick Cloke <[email protected]>
1 parent 7c407ef commit 68c7a69

File tree

3 files changed

+173
-11
lines changed

3 files changed

+173
-11
lines changed

changelog.d/8320.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `uk.half-shot.msc2778.login.application_service` login type to allow appservices to login.

synapse/rest/client/v1/login.py

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from synapse.api.errors import Codes, LoginError, SynapseError
2020
from synapse.api.ratelimiting import Ratelimiter
21+
from synapse.appservice import ApplicationService
2122
from synapse.handlers.auth import (
2223
convert_client_dict_legacy_fields_to_identifier,
2324
login_id_phone_to_thirdparty,
@@ -44,6 +45,7 @@ class LoginRestServlet(RestServlet):
4445
TOKEN_TYPE = "m.login.token"
4546
JWT_TYPE = "org.matrix.login.jwt"
4647
JWT_TYPE_DEPRECATED = "m.login.jwt"
48+
APPSERVICE_TYPE = "uk.half-shot.msc2778.login.application_service"
4749

4850
def __init__(self, hs):
4951
super(LoginRestServlet, self).__init__()
@@ -61,6 +63,8 @@ def __init__(self, hs):
6163
self.cas_enabled = hs.config.cas_enabled
6264
self.oidc_enabled = hs.config.oidc_enabled
6365

66+
self.auth = hs.get_auth()
67+
6468
self.auth_handler = self.hs.get_auth_handler()
6569
self.registration_handler = hs.get_registration_handler()
6670
self.handlers = hs.get_handlers()
@@ -107,6 +111,8 @@ def on_GET(self, request: SynapseRequest):
107111
({"type": t} for t in self.auth_handler.get_supported_login_types())
108112
)
109113

114+
flows.append({"type": LoginRestServlet.APPSERVICE_TYPE})
115+
110116
return 200, {"flows": flows}
111117

112118
def on_OPTIONS(self, request: SynapseRequest):
@@ -116,8 +122,12 @@ async def on_POST(self, request: SynapseRequest):
116122
self._address_ratelimiter.ratelimit(request.getClientIP())
117123

118124
login_submission = parse_json_object_from_request(request)
125+
119126
try:
120-
if self.jwt_enabled and (
127+
if login_submission["type"] == LoginRestServlet.APPSERVICE_TYPE:
128+
appservice = self.auth.get_appservice_by_req(request)
129+
result = await self._do_appservice_login(login_submission, appservice)
130+
elif self.jwt_enabled and (
121131
login_submission["type"] == LoginRestServlet.JWT_TYPE
122132
or login_submission["type"] == LoginRestServlet.JWT_TYPE_DEPRECATED
123133
):
@@ -134,6 +144,33 @@ async def on_POST(self, request: SynapseRequest):
134144
result["well_known"] = well_known_data
135145
return 200, result
136146

147+
def _get_qualified_user_id(self, identifier):
148+
if identifier["type"] != "m.id.user":
149+
raise SynapseError(400, "Unknown login identifier type")
150+
if "user" not in identifier:
151+
raise SynapseError(400, "User identifier is missing 'user' key")
152+
153+
if identifier["user"].startswith("@"):
154+
return identifier["user"]
155+
else:
156+
return UserID(identifier["user"], self.hs.hostname).to_string()
157+
158+
async def _do_appservice_login(
159+
self, login_submission: JsonDict, appservice: ApplicationService
160+
):
161+
logger.info(
162+
"Got appservice login request with identifier: %r",
163+
login_submission.get("identifier"),
164+
)
165+
166+
identifier = convert_client_dict_legacy_fields_to_identifier(login_submission)
167+
qualified_user_id = self._get_qualified_user_id(identifier)
168+
169+
if not appservice.is_interested_in_user(qualified_user_id):
170+
raise LoginError(403, "Invalid access_token", errcode=Codes.FORBIDDEN)
171+
172+
return await self._complete_login(qualified_user_id, login_submission)
173+
137174
async def _do_other_login(self, login_submission: JsonDict) -> Dict[str, str]:
138175
"""Handle non-token/saml/jwt logins
139176
@@ -219,15 +256,7 @@ async def _do_other_login(self, login_submission: JsonDict) -> Dict[str, str]:
219256

220257
# by this point, the identifier should be an m.id.user: if it's anything
221258
# else, we haven't understood it.
222-
if identifier["type"] != "m.id.user":
223-
raise SynapseError(400, "Unknown login identifier type")
224-
if "user" not in identifier:
225-
raise SynapseError(400, "User identifier is missing 'user' key")
226-
227-
if identifier["user"].startswith("@"):
228-
qualified_user_id = identifier["user"]
229-
else:
230-
qualified_user_id = UserID(identifier["user"], self.hs.hostname).to_string()
259+
qualified_user_id = self._get_qualified_user_id(identifier)
231260

232261
# Check if we've hit the failed ratelimit (but don't update it)
233262
self._failed_attempts_ratelimiter.ratelimit(

tests/rest/client/v1/test_login.py

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
import jwt
88

99
import synapse.rest.admin
10+
from synapse.appservice import ApplicationService
1011
from synapse.rest.client.v1 import login, logout
11-
from synapse.rest.client.v2_alpha import devices
12+
from synapse.rest.client.v2_alpha import devices, register
1213
from synapse.rest.client.v2_alpha.account import WhoamiRestServlet
1314

1415
from tests import unittest
@@ -748,3 +749,134 @@ def test_login_jwt_invalid_signature(self):
748749
channel.json_body["error"],
749750
"JWT validation failed: Signature verification failed",
750751
)
752+
753+
754+
AS_USER = "as_user_alice"
755+
756+
757+
class AppserviceLoginRestServletTestCase(unittest.HomeserverTestCase):
758+
servlets = [
759+
login.register_servlets,
760+
register.register_servlets,
761+
]
762+
763+
def register_as_user(self, username):
764+
request, channel = self.make_request(
765+
b"POST",
766+
"/_matrix/client/r0/register?access_token=%s" % (self.service.token,),
767+
{"username": username},
768+
)
769+
self.render(request)
770+
771+
def make_homeserver(self, reactor, clock):
772+
self.hs = self.setup_test_homeserver()
773+
774+
self.service = ApplicationService(
775+
id="unique_identifier",
776+
token="some_token",
777+
hostname="example.com",
778+
sender="@asbot:example.com",
779+
namespaces={
780+
ApplicationService.NS_USERS: [
781+
{"regex": r"@as_user.*", "exclusive": False}
782+
],
783+
ApplicationService.NS_ROOMS: [],
784+
ApplicationService.NS_ALIASES: [],
785+
},
786+
)
787+
self.another_service = ApplicationService(
788+
id="another__identifier",
789+
token="another_token",
790+
hostname="example.com",
791+
sender="@as2bot:example.com",
792+
namespaces={
793+
ApplicationService.NS_USERS: [
794+
{"regex": r"@as2_user.*", "exclusive": False}
795+
],
796+
ApplicationService.NS_ROOMS: [],
797+
ApplicationService.NS_ALIASES: [],
798+
},
799+
)
800+
801+
self.hs.get_datastore().services_cache.append(self.service)
802+
self.hs.get_datastore().services_cache.append(self.another_service)
803+
return self.hs
804+
805+
def test_login_appservice_user(self):
806+
"""Test that an appservice user can use /login
807+
"""
808+
self.register_as_user(AS_USER)
809+
810+
params = {
811+
"type": login.LoginRestServlet.APPSERVICE_TYPE,
812+
"identifier": {"type": "m.id.user", "user": AS_USER},
813+
}
814+
request, channel = self.make_request(
815+
b"POST", LOGIN_URL, params, access_token=self.service.token
816+
)
817+
818+
self.render(request)
819+
self.assertEquals(channel.result["code"], b"200", channel.result)
820+
821+
def test_login_appservice_user_bot(self):
822+
"""Test that the appservice bot can use /login
823+
"""
824+
self.register_as_user(AS_USER)
825+
826+
params = {
827+
"type": login.LoginRestServlet.APPSERVICE_TYPE,
828+
"identifier": {"type": "m.id.user", "user": self.service.sender},
829+
}
830+
request, channel = self.make_request(
831+
b"POST", LOGIN_URL, params, access_token=self.service.token
832+
)
833+
834+
self.render(request)
835+
self.assertEquals(channel.result["code"], b"200", channel.result)
836+
837+
def test_login_appservice_wrong_user(self):
838+
"""Test that non-as users cannot login with the as token
839+
"""
840+
self.register_as_user(AS_USER)
841+
842+
params = {
843+
"type": login.LoginRestServlet.APPSERVICE_TYPE,
844+
"identifier": {"type": "m.id.user", "user": "fibble_wibble"},
845+
}
846+
request, channel = self.make_request(
847+
b"POST", LOGIN_URL, params, access_token=self.service.token
848+
)
849+
850+
self.render(request)
851+
self.assertEquals(channel.result["code"], b"403", channel.result)
852+
853+
def test_login_appservice_wrong_as(self):
854+
"""Test that as users cannot login with wrong as token
855+
"""
856+
self.register_as_user(AS_USER)
857+
858+
params = {
859+
"type": login.LoginRestServlet.APPSERVICE_TYPE,
860+
"identifier": {"type": "m.id.user", "user": AS_USER},
861+
}
862+
request, channel = self.make_request(
863+
b"POST", LOGIN_URL, params, access_token=self.another_service.token
864+
)
865+
866+
self.render(request)
867+
self.assertEquals(channel.result["code"], b"403", channel.result)
868+
869+
def test_login_appservice_no_token(self):
870+
"""Test that users must provide a token when using the appservice
871+
login method
872+
"""
873+
self.register_as_user(AS_USER)
874+
875+
params = {
876+
"type": login.LoginRestServlet.APPSERVICE_TYPE,
877+
"identifier": {"type": "m.id.user", "user": AS_USER},
878+
}
879+
request, channel = self.make_request(b"POST", LOGIN_URL, params)
880+
881+
self.render(request)
882+
self.assertEquals(channel.result["code"], b"401", channel.result)

0 commit comments

Comments
 (0)