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

Allow appservice users to /login #8320

Merged
merged 12 commits into from
Sep 18, 2020
1 change: 1 addition & 0 deletions changelog.d/8320.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `uk.half-shot.msc2778.login.application_service` login type to allow appservices to login.
24 changes: 22 additions & 2 deletions synapse/rest/client/v1/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from synapse.api.errors import Codes, LoginError, SynapseError
from synapse.api.ratelimiting import Ratelimiter
from synapse.appservice import ApplicationService
from synapse.handlers.auth import (
convert_client_dict_legacy_fields_to_identifier,
login_id_phone_to_thirdparty,
Expand All @@ -39,6 +40,7 @@

class LoginRestServlet(RestServlet):
PATTERNS = client_patterns("/login$", v1=True)
APPSERVICE_TYPE = "uk.half-shot.msc2778.login.application_service"
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved
CAS_TYPE = "m.login.cas"
SSO_TYPE = "m.login.sso"
TOKEN_TYPE = "m.login.token"
Expand All @@ -61,6 +63,8 @@ def __init__(self, hs):
self.cas_enabled = hs.config.cas_enabled
self.oidc_enabled = hs.config.oidc_enabled

self.auth = hs.get_auth()

self.auth_handler = self.hs.get_auth_handler()
self.registration_handler = hs.get_registration_handler()
self.handlers = hs.get_handlers()
Expand Down Expand Up @@ -116,6 +120,11 @@ async def on_POST(self, request: SynapseRequest):
self._address_ratelimiter.ratelimit(request.getClientIP())

login_submission = parse_json_object_from_request(request)

appservice = None
if self.auth.has_access_token(request):
appservice = self.auth.get_appservice_by_req(request)

try:
if self.jwt_enabled and (
login_submission["type"] == LoginRestServlet.JWT_TYPE
Expand All @@ -125,7 +134,7 @@ async def on_POST(self, request: SynapseRequest):
elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE:
result = await self._do_token_login(login_submission)
else:
result = await self._do_other_login(login_submission)
result = await self._do_other_login(login_submission, appservice)
except KeyError:
raise SynapseError(400, "Missing JSON keys.")

Expand All @@ -134,7 +143,9 @@ async def on_POST(self, request: SynapseRequest):
result["well_known"] = well_known_data
return 200, result

async def _do_other_login(self, login_submission: JsonDict) -> Dict[str, str]:
async def _do_other_login(
self, login_submission: JsonDict, appservice: Optional[ApplicationService]
) -> Dict[str, str]:
"""Handle non-token/saml/jwt logins

Args:
Expand Down Expand Up @@ -229,6 +240,15 @@ async def _do_other_login(self, login_submission: JsonDict) -> Dict[str, str]:
else:
qualified_user_id = UserID(identifier["user"], self.hs.hostname).to_string()

if login_submission["type"] == LoginRestServlet.APPSERVICE_TYPE:
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved
if appservice is None or not appservice.is_interested_in_user(
qualified_user_id
):
raise LoginError(403, "Invalid access_token", errcode=Codes.FORBIDDEN)

result = await self._complete_login(qualified_user_id, login_submission)
return result
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved

# Check if we've hit the failed ratelimit (but don't update it)
self._failed_attempts_ratelimiter.ratelimit(
qualified_user_id.lower(), update=False
Expand Down
74 changes: 73 additions & 1 deletion tests/rest/client/v1/test_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
import jwt

import synapse.rest.admin
from synapse.appservice import ApplicationService
from synapse.rest.client.v1 import login, logout
from synapse.rest.client.v2_alpha import devices
from synapse.rest.client.v2_alpha import devices, register
from synapse.rest.client.v2_alpha.account import WhoamiRestServlet

from tests import unittest
Expand Down Expand Up @@ -748,3 +749,74 @@ def test_login_jwt_invalid_signature(self):
channel.json_body["error"],
"JWT validation failed: Signature verification failed",
)


AS_USER = "as_user_alice"


class AppserviceLoginRestServletTestCase(unittest.HomeserverTestCase):
servlets = [
login.register_servlets,
register.register_servlets,
lambda hs, http_server: WhoamiRestServlet(hs).register(http_server),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this gets registered with the rest of the account ones, did you do the lambda just to avoid registering all of them?

I think the code looks fine, I'd like to see some tests for the error scenarioes. The main one I can think of is:

  • Trying to login as a user that the appservice isn't interested in.

I suspect the other cases are all pretty covered already?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this gets registered with the rest of the account ones, did you do the lambda just to avoid registering all of them?

Heh, no I copy and pasted it without thinking. Fixed.

I'd like to see some tests for the error scenarios.

Added tests for all the other scenarios I could think of.

]

def register_as_user(self, username):
request, channel = self.make_request(
b"POST",
f"/_matrix/client/r0/register?access_token={self.service.token}",
clokep marked this conversation as resolved.
Show resolved Hide resolved
{"username": username},
)
self.render(request)

def make_homeserver(self, reactor, clock):
self.hs = self.setup_test_homeserver()

self.service = ApplicationService(
id="unique_identifier",
token="some_token",
hostname="example.com",
sender="@asbot:example.com",
namespaces={
ApplicationService.NS_USERS: [
{"regex": r"@as_user.*", "exclusive": False}
],
ApplicationService.NS_ROOMS: [],
ApplicationService.NS_ALIASES: [],
},
)

self.hs.get_datastore().services_cache.append(self.service)
return self.hs

def test_login_appservice_user(self):
"""Test that an appservice user can use /login
"""
self.register_as_user(AS_USER)

params = {
"type": login.LoginRestServlet.APPSERVICE_TYPE,
"identifier": {"type": "m.id.user", "user": AS_USER},
}
request, channel = self.make_request(
b"POST", LOGIN_URL, params, access_token=self.service.token
)

self.render(request)
self.assertEquals(channel.result["code"], b"200", channel.result)

def test_login_appservice_user_bot(self):
"""Test that the appservice bot can use /login
"""
self.register_as_user(AS_USER)

params = {
"type": login.LoginRestServlet.APPSERVICE_TYPE,
"identifier": {"type": "m.id.user", "user": self.service.sender},
}
request, channel = self.make_request(
b"POST", LOGIN_URL, params, access_token=self.service.token
)

self.render(request)
self.assertEquals(channel.result["code"], b"200", channel.result)