From 8fc52bc56a0813d40741f45164caa3230d3e00ec Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Fri, 2 Oct 2015 17:13:51 -0500 Subject: Allow synapse's useragent to be customized This will allow me to write tests which verify which server made HTTP requests in a federation context. --- synapse/config/server.py | 1 + 1 file changed, 1 insertion(+) (limited to 'synapse/config') diff --git a/synapse/config/server.py b/synapse/config/server.py index 4d12d49857..50c4afdcfe 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -26,6 +26,7 @@ class ServerConfig(Config): self.soft_file_limit = config["soft_file_limit"] self.daemonize = config.get("daemonize") self.print_pidfile = config.get("print_pidfile") + self.user_agent_override = config.get("user_agent_override") self.use_frozen_dicts = config.get("use_frozen_dicts", True) self.listeners = config.get("listeners", []) -- cgit 1.5.1 From b28c7da0a4507825440fd801e1b6dbc5e6a454a7 Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Mon, 5 Oct 2015 20:49:39 -0500 Subject: Preserve version string in user agent --- synapse/config/server.py | 2 +- synapse/http/client.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'synapse/config') diff --git a/synapse/config/server.py b/synapse/config/server.py index 50c4afdcfe..5c2d6bfeab 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -26,7 +26,7 @@ class ServerConfig(Config): self.soft_file_limit = config["soft_file_limit"] self.daemonize = config.get("daemonize") self.print_pidfile = config.get("print_pidfile") - self.user_agent_override = config.get("user_agent_override") + self.user_agent_suffix = config.get("user_agent_suffix") self.use_frozen_dicts = config.get("use_frozen_dicts", True) self.listeners = config.get("listeners", []) diff --git a/synapse/http/client.py b/synapse/http/client.py index 6adf35c7bf..5017801773 100644 --- a/synapse/http/client.py +++ b/synapse/http/client.py @@ -67,9 +67,9 @@ class SimpleHttpClient(object): connectTimeout=15, contextFactory=hs.get_http_client_context_factory() ) - self.user_agent = hs.config.user_agent_override - if self.user_agent is None: - self.user_agent = hs.version_string + self.user_agent = hs.version_string + if hs.config.user_agent_suffix: + self.user_agent += " - " + hs.config.user_agent_suffix def request(self, method, uri, *args, **kwargs): # A small wrapper around self.agent.request() so we can easily attach -- cgit 1.5.1 From c33f5c1a2414632f21183f41ecd4aef00e46a437 Mon Sep 17 00:00:00 2001 From: Steven Hammerton Date: Wed, 7 Oct 2015 14:45:57 +0100 Subject: Provide ability to login using CAS --- synapse/config/cas.py | 39 +++++++++++++++++++++++++ synapse/config/homeserver.py | 3 +- synapse/handlers/auth.py | 31 ++++++++++++++++++++ synapse/rest/client/v1/login.py | 64 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 synapse/config/cas.py (limited to 'synapse/config') diff --git a/synapse/config/cas.py b/synapse/config/cas.py new file mode 100644 index 0000000000..81d034e8f0 --- /dev/null +++ b/synapse/config/cas.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 OpenMarket Ltd +# +# 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. + +from ._base import Config + + +class CasConfig(Config): + """Cas Configuration + + cas_server_url: URL of CAS server + """ + + def read_config(self, config): + cas_config = config.get("cas_config", None) + if cas_config: + self.cas_enabled = True + self.cas_server_url = cas_config["server_url"] + else: + self.cas_enabled = False + self.cas_server_url = None + + def default_config(self, config_dir_path, server_name, **kwargs): + return """ + # Enable CAS for registration and login. + #cas_config: + # server_url: "https://cas-server.com" + """ diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py index d77f045406..3039f3c0bf 100644 --- a/synapse/config/homeserver.py +++ b/synapse/config/homeserver.py @@ -26,12 +26,13 @@ from .metrics import MetricsConfig from .appservice import AppServiceConfig from .key import KeyConfig from .saml2 import SAML2Config +from .cas import CasConfig class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig, RatelimitConfig, ContentRepositoryConfig, CaptchaConfig, VoipConfig, RegistrationConfig, MetricsConfig, - AppServiceConfig, KeyConfig, SAML2Config, ): + AppServiceConfig, KeyConfig, SAML2Config, CasConfig): pass diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 793b3fcd8b..0ad28c4948 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -295,6 +295,37 @@ class AuthHandler(BaseHandler): refresh_token = yield self.issue_refresh_token(user_id) defer.returnValue((user_id, access_token, refresh_token)) + @defer.inlineCallbacks + def login_with_cas_user_id(self, user_id): + """ + Authenticates the user with the given user ID, intended to have been captured from a CAS response + + Args: + user_id (str): User ID + Returns: + A tuple of: + The user's ID. + The access token for the user's session. + The refresh token for the user's session. + Raises: + StoreError if there was a problem storing the token. + LoginError if there was an authentication problem. + """ + user_id, ignored = yield self._find_user_id_and_pwd_hash(user_id) + + logger.info("Logging in user %s", user_id) + access_token = yield self.issue_access_token(user_id) + refresh_token = yield self.issue_refresh_token(user_id) + defer.returnValue((user_id, access_token, refresh_token)) + + @defer.inlineCallbacks + def does_user_exist(self, user_id): + try: + yield self._find_user_id_and_pwd_hash(user_id) + defer.returnValue(True) + except LoginError: + defer.returnValue(False) + @defer.inlineCallbacks def _find_user_id_and_pwd_hash(self, user_id): """Checks to see if a user with the given id exists. Will check case diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index e580f71964..56e5cf79fe 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -15,7 +15,7 @@ from twisted.internet import defer -from synapse.api.errors import SynapseError +from synapse.api.errors import SynapseError, LoginError, Codes from synapse.types import UserID from base import ClientV1RestServlet, client_path_pattern @@ -27,6 +27,9 @@ from saml2 import BINDING_HTTP_POST from saml2 import config from saml2.client import Saml2Client +import xml.etree.ElementTree as ET +import requests + logger = logging.getLogger(__name__) @@ -35,16 +38,23 @@ class LoginRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/login$") PASS_TYPE = "m.login.password" SAML2_TYPE = "m.login.saml2" + CAS_TYPE = "m.login.cas" def __init__(self, hs): super(LoginRestServlet, self).__init__(hs) self.idp_redirect_url = hs.config.saml2_idp_redirect_url self.saml2_enabled = hs.config.saml2_enabled + self.cas_enabled = hs.config.cas_enabled + + self.cas_server_url = hs.config.cas_server_url + self.servername = hs.config.server_name def on_GET(self, request): flows = [{"type": LoginRestServlet.PASS_TYPE}] if self.saml2_enabled: flows.append({"type": LoginRestServlet.SAML2_TYPE}) + if self.cas_enabled: + flows.append({"type": LoginRestServlet.CAS_TYPE}) return (200, {"flows": flows}) def on_OPTIONS(self, request): @@ -67,6 +77,12 @@ class LoginRestServlet(ClientV1RestServlet): "uri": "%s%s" % (self.idp_redirect_url, relay_state) } defer.returnValue((200, result)) + elif self.cas_enabled and (login_submission["type"] == LoginRestServlet.CAS_TYPE): + url = "%s/proxyValidate" % (self.cas_server_url) + parameters = {"ticket": login_submission["ticket"], "service": login_submission["service"]} + response = requests.get(url, verify=False, params=parameters) + result = yield self.do_cas_login(response.text) + defer.returnValue(result) else: raise SynapseError(400, "Bad login type.") except KeyError: @@ -100,6 +116,41 @@ class LoginRestServlet(ClientV1RestServlet): defer.returnValue((200, result)) + @defer.inlineCallbacks + def do_cas_login(self, cas_response_body): + root = ET.fromstring(cas_response_body) + if not root.tag.endswith("serviceResponse"): + raise LoginError(401, "Invalid CAS response", errcode=Codes.UNAUTHORIZED) + if not root[0].tag.endswith("authenticationSuccess"): + raise LoginError(401, "Unsuccessful CAS response", errcode=Codes.UNAUTHORIZED) + for child in root[0]: + if child.tag.endswith("user"): + user = child.text + user_id = "@%s:%s" % (user, self.servername) + auth_handler = self.handlers.auth_handler + user_exists = yield auth_handler.does_user_exist(user_id) + if user_exists: + user_id, access_token, refresh_token = yield auth_handler.login_with_cas_user_id(user_id) + result = { + "user_id": user_id, # may have changed + "access_token": access_token, + "refresh_token": refresh_token, + "home_server": self.hs.hostname, + } + + else: + user_id, access_token = yield self.handlers.registration_handler.register(localpart=user) + result = { + "user_id": user_id, # may have changed + "access_token": access_token, + "home_server": self.hs.hostname, + } + + defer.returnValue((200, result)) + + + raise LoginError(401, "Invalid CAS response", errcode=Codes.UNAUTHORIZED) + class LoginFallbackRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/login/fallback$") @@ -173,6 +224,15 @@ class SAML2RestServlet(ClientV1RestServlet): defer.returnValue(None) defer.returnValue((200, {"status": "not_authenticated"})) +class CasRestServlet(ClientV1RestServlet): + PATTERN = client_path_pattern("/login/cas") + + def __init__(self, hs): + super(CasRestServlet, self).__init__(hs) + self.cas_server_url = hs.config.cas_server_url + + def on_GET(self, request): + return (200, {"serverUrl": self.cas_server_url}) def _parse_json(request): try: @@ -188,4 +248,6 @@ def register_servlets(hs, http_server): LoginRestServlet(hs).register(http_server) if hs.config.saml2_enabled: SAML2RestServlet(hs).register(http_server) + if hs.config.cas_enabled: + CasRestServlet(hs).register(http_server) # TODO PasswordResetRestServlet(hs).register(http_server) -- cgit 1.5.1 From 76421c496d1ee4ba5ea97fb24466156d0ddc0723 Mon Sep 17 00:00:00 2001 From: Steven Hammerton Date: Mon, 12 Oct 2015 11:11:49 +0100 Subject: Allow optional config params for a required attribute and it's value, if specified any CAS user must have the given attribute and the value must equal --- synapse/config/cas.py | 15 +++++++++++++++ synapse/rest/client/v1/login.py | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) (limited to 'synapse/config') diff --git a/synapse/config/cas.py b/synapse/config/cas.py index 81d034e8f0..4d1dd8cc7b 100644 --- a/synapse/config/cas.py +++ b/synapse/config/cas.py @@ -27,13 +27,28 @@ class CasConfig(Config): if cas_config: self.cas_enabled = True self.cas_server_url = cas_config["server_url"] + + if "required_attribute" in cas_config: + self.cas_required_attribute = cas_config["required_attribute"] + else: + self.cas_required_attribute = None + + if "required_attribute_value" in cas_config: + self.cas_required_attribute_value = cas_config["required_attribute_value"] + else: + self.cas_required_attribute_value = None + else: self.cas_enabled = False self.cas_server_url = None + self.cas_required_attribute = None + self.cas_required_attribute_value = None def default_config(self, config_dir_path, server_name, **kwargs): return """ # Enable CAS for registration and login. #cas_config: # server_url: "https://cas-server.com" + # #required_attribute: something + # #required_attribute_value: true """ diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index 0e12880ab5..1e62beaff8 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -45,8 +45,9 @@ class LoginRestServlet(ClientV1RestServlet): self.idp_redirect_url = hs.config.saml2_idp_redirect_url self.saml2_enabled = hs.config.saml2_enabled self.cas_enabled = hs.config.cas_enabled - self.cas_server_url = hs.config.cas_server_url + self.cas_required_attribute = hs.config.cas_required_attribute + self.cas_required_attribute_value = hs.config.cas_required_attribute_value self.servername = hs.config.server_name def on_GET(self, request): @@ -126,6 +127,19 @@ class LoginRestServlet(ClientV1RestServlet): @defer.inlineCallbacks def do_cas_login(self, cas_response_body): (user, attributes) = self.parse_cas_response(cas_response_body) + + if self.cas_required_attribute is not None: + # If required attribute was not in CAS Response - Forbidden + if self.cas_required_attribute not in attributes: + raise LoginError(401, "Unauthorized", errcode=Codes.UNAUTHORIZED) + + # Also need to check value + if self.cas_required_attribute_value is not None: + actualValue = attributes[self.cas_required_attribute] + # If required attribute value does not match expected - Forbidden + if self.cas_required_attribute_value != actualValue: + raise LoginError(401, "Unauthorized", errcode=Codes.UNAUTHORIZED) + user_id = UserID.create(user, self.hs.hostname).to_string() auth_handler = self.handlers.auth_handler user_exists = yield auth_handler.does_user_exist(user_id) -- cgit 1.5.1 From 01a5f1991c8e54d0762cf1647c941d00c938f994 Mon Sep 17 00:00:00 2001 From: Steven Hammerton Date: Mon, 12 Oct 2015 14:43:17 +0100 Subject: Support multiple required attributes in CAS response, and in a nicer config format too --- synapse/config/cas.py | 19 ++++--------------- synapse/rest/client/v1/login.py | 13 ++++++------- 2 files changed, 10 insertions(+), 22 deletions(-) (limited to 'synapse/config') diff --git a/synapse/config/cas.py b/synapse/config/cas.py index 4d1dd8cc7b..e884d03fe6 100644 --- a/synapse/config/cas.py +++ b/synapse/config/cas.py @@ -27,28 +27,17 @@ class CasConfig(Config): if cas_config: self.cas_enabled = True self.cas_server_url = cas_config["server_url"] - - if "required_attribute" in cas_config: - self.cas_required_attribute = cas_config["required_attribute"] - else: - self.cas_required_attribute = None - - if "required_attribute_value" in cas_config: - self.cas_required_attribute_value = cas_config["required_attribute_value"] - else: - self.cas_required_attribute_value = None - + self.cas_required_attributes = cas_config.get("required_attributes", None) else: self.cas_enabled = False self.cas_server_url = None - self.cas_required_attribute = None - self.cas_required_attribute_value = None + self.cas_required_attributes = {} def default_config(self, config_dir_path, server_name, **kwargs): return """ # Enable CAS for registration and login. #cas_config: # server_url: "https://cas-server.com" - # #required_attribute: something - # #required_attribute_value: true + # #required_attributes: + # # name: value """ diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index 1e62beaff8..84774e61aa 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -46,8 +46,7 @@ class LoginRestServlet(ClientV1RestServlet): self.saml2_enabled = hs.config.saml2_enabled self.cas_enabled = hs.config.cas_enabled self.cas_server_url = hs.config.cas_server_url - self.cas_required_attribute = hs.config.cas_required_attribute - self.cas_required_attribute_value = hs.config.cas_required_attribute_value + self.cas_required_attributes = hs.config.cas_required_attributes self.servername = hs.config.server_name def on_GET(self, request): @@ -128,16 +127,16 @@ class LoginRestServlet(ClientV1RestServlet): def do_cas_login(self, cas_response_body): (user, attributes) = self.parse_cas_response(cas_response_body) - if self.cas_required_attribute is not None: + for required_attribute in self.cas_required_attributes: # If required attribute was not in CAS Response - Forbidden - if self.cas_required_attribute not in attributes: + if required_attribute not in attributes: raise LoginError(401, "Unauthorized", errcode=Codes.UNAUTHORIZED) # Also need to check value - if self.cas_required_attribute_value is not None: - actualValue = attributes[self.cas_required_attribute] + if self.cas_required_attributes[required_attribute] is not None: + actualValue = attributes[required_attribute] # If required attribute value does not match expected - Forbidden - if self.cas_required_attribute_value != actualValue: + if self.cas_required_attributes[required_attribute] != actualValue: raise LoginError(401, "Unauthorized", errcode=Codes.UNAUTHORIZED) user_id = UserID.create(user, self.hs.hostname).to_string() -- cgit 1.5.1 From ab7f9bb861791b9415d80f0e71d7b4b867b0a445 Mon Sep 17 00:00:00 2001 From: Steven Hammerton Date: Mon, 12 Oct 2015 14:58:59 +0100 Subject: Default cas_required_attributes to empty dictionary --- synapse/config/cas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse/config') diff --git a/synapse/config/cas.py b/synapse/config/cas.py index e884d03fe6..d268680729 100644 --- a/synapse/config/cas.py +++ b/synapse/config/cas.py @@ -27,7 +27,7 @@ class CasConfig(Config): if cas_config: self.cas_enabled = True self.cas_server_url = cas_config["server_url"] - self.cas_required_attributes = cas_config.get("required_attributes", None) + self.cas_required_attributes = cas_config.get("required_attributes", {}) else: self.cas_enabled = False self.cas_server_url = None -- cgit 1.5.1