Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c130100a51 | |||
| 325c5d3741 | |||
| 7a467573a9 | |||
| 880005c3b7 | |||
| b55e8840e4 | |||
| 426049247b | |||
| 69a43d9974 | |||
| abce00fc6a | |||
| 0382b0ffee | |||
| 6d1e699b5c | |||
| ca7abb129c | |||
| 12f49b22ec | |||
| 0afcbc65cb | |||
| 843dd714cb | |||
| b56a224e22 | |||
| ab157e61a2 | |||
| 26b62796c2 | |||
| 028f674cd3 | |||
| 4914a88829 | |||
| 2decc92e2f | |||
| 8dc1eb6697 | |||
| 8e0cee90d2 | |||
| ccbc9e5e17 | |||
| d51ca9d9b3 | |||
| fe13bd52ac | |||
| a4cf2c1184 | |||
| aeb2263320 | |||
| deca87ddf2 | |||
| 83827c4922 | |||
| dc3e586938 |
@@ -0,0 +1,4 @@
|
||||
# One username per supported platform and one custom link
|
||||
patreon: matrixdotorg
|
||||
liberapay: matrixdotorg
|
||||
custom: https://paypal.me/matrixdotorg
|
||||
@@ -0,0 +1 @@
|
||||
Fix a bug where running synapse_port_db would cause the account validity feature to fail because it didn't set the type of the email_sent column to boolean.
|
||||
@@ -0,0 +1 @@
|
||||
Allow expired user to trigger renewal email sending manually.
|
||||
@@ -0,0 +1 @@
|
||||
Add a sponsor button to the repo.
|
||||
@@ -0,0 +1 @@
|
||||
Add a sponsor button to the repo.
|
||||
@@ -0,0 +1 @@
|
||||
Add --no-daemonize option to run synapse in the foreground, per issue #4130. Contributed by Soham Gumaste.
|
||||
@@ -0,0 +1 @@
|
||||
Fully support SAML2 authentication. Contributed by [Alexander Trost](https://github.com/galexrt) - thank you!
|
||||
@@ -54,6 +54,7 @@ BOOLEAN_COLUMNS = {
|
||||
"group_roles": ["is_public"],
|
||||
"local_group_membership": ["is_publicised", "is_admin"],
|
||||
"e2e_room_keys": ["is_verified"],
|
||||
"account_validity": ["email_sent"],
|
||||
}
|
||||
|
||||
|
||||
|
||||
+13
-2
@@ -184,11 +184,22 @@ class Auth(object):
|
||||
return event_auth.get_public_keys(invite_event)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_user_by_req(self, request, allow_guest=False, rights="access"):
|
||||
def get_user_by_req(
|
||||
self,
|
||||
request,
|
||||
allow_guest=False,
|
||||
rights="access",
|
||||
allow_expired=False,
|
||||
):
|
||||
""" Get a registered user's ID.
|
||||
|
||||
Args:
|
||||
request - An HTTP request with an access_token query parameter.
|
||||
allow_expired - Whether to allow the request through even if the account is
|
||||
expired. If true, Synapse will still require an access token to be
|
||||
provided but won't check if the account it belongs to has expired. This
|
||||
works thanks to /login delivering access tokens regardless of accounts'
|
||||
expiration.
|
||||
Returns:
|
||||
defer.Deferred: resolves to a ``synapse.types.Requester`` object
|
||||
Raises:
|
||||
@@ -229,7 +240,7 @@ class Auth(object):
|
||||
is_guest = user_info["is_guest"]
|
||||
|
||||
# Deny the request if the user account has expired.
|
||||
if self._account_validity.enabled:
|
||||
if self._account_validity.enabled and not allow_expired:
|
||||
user_id = user.to_string()
|
||||
expiration_ts = yield self.store.get_expiration_ts_for_user(user_id)
|
||||
if expiration_ts is not None and self.clock.time_msec() >= expiration_ts:
|
||||
|
||||
@@ -378,6 +378,7 @@ def setup(config_options):
|
||||
|
||||
logger.info("Database prepared in %s.", config.database_config['name'])
|
||||
|
||||
hs.samlreqs = {}
|
||||
hs.setup()
|
||||
hs.setup_master()
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
# 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 synapse.python_dependencies import DependencyException, check_requirements
|
||||
|
||||
from ._base import Config, ConfigError
|
||||
|
||||
@@ -25,6 +26,11 @@ class SAML2Config(Config):
|
||||
if not saml2_config or not saml2_config.get("enabled", True):
|
||||
return
|
||||
|
||||
try:
|
||||
check_requirements('saml2')
|
||||
except DependencyException as e:
|
||||
raise ConfigError(e.message)
|
||||
|
||||
self.saml2_enabled = True
|
||||
|
||||
import saml2.config
|
||||
|
||||
@@ -93,6 +93,7 @@ class LoginRestServlet(RestServlet):
|
||||
self.jwt_enabled = hs.config.jwt_enabled
|
||||
self.jwt_secret = hs.config.jwt_secret
|
||||
self.jwt_algorithm = hs.config.jwt_algorithm
|
||||
self.saml2_enabled = hs.config.saml2_enabled
|
||||
self.cas_enabled = hs.config.cas_enabled
|
||||
self.auth_handler = self.hs.get_auth_handler()
|
||||
self.registration_handler = hs.get_registration_handler()
|
||||
@@ -104,6 +105,9 @@ class LoginRestServlet(RestServlet):
|
||||
flows = []
|
||||
if self.jwt_enabled:
|
||||
flows.append({"type": LoginRestServlet.JWT_TYPE})
|
||||
if self.saml2_enabled:
|
||||
flows.append({"type": LoginRestServlet.SSO_TYPE})
|
||||
flows.append({"type": LoginRestServlet.TOKEN_TYPE})
|
||||
if self.cas_enabled:
|
||||
flows.append({"type": LoginRestServlet.SSO_TYPE})
|
||||
|
||||
@@ -370,28 +374,49 @@ class LoginRestServlet(RestServlet):
|
||||
defer.returnValue(result)
|
||||
|
||||
|
||||
class CasRedirectServlet(RestServlet):
|
||||
class BaseSsoRedirectServlet(RestServlet):
|
||||
"""Common base class for /login/sso/redirect impls"""
|
||||
PATTERNS = client_patterns("/login/(cas|sso)/redirect", v1=True)
|
||||
|
||||
def on_GET(self, request):
|
||||
args = request.args
|
||||
if b"redirectUrl" not in args:
|
||||
return 400, "Redirect URL not specified for SSO auth"
|
||||
client_redirect_url = args[b"redirectUrl"][0]
|
||||
sso_url = self.get_sso_url(client_redirect_url)
|
||||
request.redirect(sso_url)
|
||||
finish_request(request)
|
||||
|
||||
def get_sso_url(self, client_redirect_url):
|
||||
"""Get the URL to redirect to, to perform SSO auth
|
||||
|
||||
Args:
|
||||
client_redirect_url (bytes): the URL that we should redirect the
|
||||
client to when everything is done
|
||||
|
||||
Returns:
|
||||
bytes: URL to redirect to
|
||||
"""
|
||||
# to be implemented by subclasses
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class CasRedirectServlet(RestServlet):
|
||||
def __init__(self, hs):
|
||||
super(CasRedirectServlet, self).__init__()
|
||||
self.cas_server_url = hs.config.cas_server_url.encode('ascii')
|
||||
self.cas_service_url = hs.config.cas_service_url.encode('ascii')
|
||||
|
||||
def on_GET(self, request):
|
||||
args = request.args
|
||||
if b"redirectUrl" not in args:
|
||||
return (400, "Redirect URL not specified for CAS auth")
|
||||
def get_sso_url(self, client_redirect_url):
|
||||
client_redirect_url_param = urllib.parse.urlencode({
|
||||
b"redirectUrl": args[b"redirectUrl"][0]
|
||||
b"redirectUrl": client_redirect_url
|
||||
}).encode('ascii')
|
||||
hs_redirect_url = (self.cas_service_url +
|
||||
b"/_matrix/client/r0/login/cas/ticket")
|
||||
service_param = urllib.parse.urlencode({
|
||||
b"service": b"%s?%s" % (hs_redirect_url, client_redirect_url_param)
|
||||
}).encode('ascii')
|
||||
request.redirect(b"%s/login?%s" % (self.cas_server_url, service_param))
|
||||
finish_request(request)
|
||||
return b"%s/login?%s" % (self.cas_server_url, service_param)
|
||||
|
||||
|
||||
class CasTicketServlet(RestServlet):
|
||||
@@ -474,6 +499,28 @@ class CasTicketServlet(RestServlet):
|
||||
return user, attributes
|
||||
|
||||
|
||||
class SAMLRedirectServlet(BaseSsoRedirectServlet):
|
||||
PATTERNS = client_patterns("/login/sso/redirect", v1=True)
|
||||
|
||||
def __init__(self, hs):
|
||||
self._saml_client = hs.get_saml_client()
|
||||
self.samlreqs = hs.samlreqs
|
||||
|
||||
def get_sso_url(self, client_redirect_url):
|
||||
reqid, info = self._saml_client.prepare_for_authenticate(
|
||||
relay_state=client_redirect_url,
|
||||
)
|
||||
logger.info("prepared to auth - reqid: %r, info: %r, client redirect uri: %r", reqid, info, client_redirect_url)
|
||||
self.samlreqs[reqid] = client_redirect_url
|
||||
|
||||
for key, value in info['headers']:
|
||||
if key == 'Location':
|
||||
return value
|
||||
|
||||
# this shouldn't happen!
|
||||
raise Exception("prepare_for_authenticate didn't return a Location header")
|
||||
|
||||
|
||||
class SSOAuthHandler(object):
|
||||
"""
|
||||
Utility class for Resources and Servlets which handle the response from a SSO
|
||||
@@ -549,3 +596,5 @@ def register_servlets(hs, http_server):
|
||||
if hs.config.cas_enabled:
|
||||
CasRedirectServlet(hs).register(http_server)
|
||||
CasTicketServlet(hs).register(http_server)
|
||||
elif hs.config.saml2_enabled:
|
||||
SAMLRedirectServlet(hs).register(http_server)
|
||||
|
||||
@@ -79,7 +79,7 @@ class AccountValiditySendMailServlet(RestServlet):
|
||||
if not self.account_validity.renew_by_email_enabled:
|
||||
raise AuthError(403, "Account renewal via email is disabled on this server.")
|
||||
|
||||
requester = yield self.auth.get_user_by_req(request)
|
||||
requester = yield self.auth.get_user_by_req(request, allow_expired=True)
|
||||
user_id = requester.user.to_string()
|
||||
yield self.account_activity_handler.send_renewal_email_to_user(user_id)
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
import logging
|
||||
|
||||
import saml2
|
||||
from saml2.client import Saml2Client
|
||||
|
||||
from twisted.web.resource import Resource
|
||||
from twisted.web.server import NOT_DONE_YET
|
||||
@@ -36,9 +35,9 @@ class SAML2ResponseResource(Resource):
|
||||
|
||||
def __init__(self, hs):
|
||||
Resource.__init__(self)
|
||||
|
||||
self._saml_client = Saml2Client(hs.config.saml2_sp_config)
|
||||
self._saml_client = hs.get_saml_client()
|
||||
self._sso_auth_handler = SSOAuthHandler(hs)
|
||||
self.samlreqs = hs.samlreqs
|
||||
|
||||
def render_POST(self, request):
|
||||
self._async_render_POST(request)
|
||||
@@ -52,6 +51,7 @@ class SAML2ResponseResource(Resource):
|
||||
try:
|
||||
saml2_auth = self._saml_client.parse_authn_request_response(
|
||||
resp_bytes, saml2.BINDING_HTTP_POST,
|
||||
outstanding=self.samlreqs,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("Exception parsing SAML2 response", exc_info=1)
|
||||
@@ -62,12 +62,13 @@ class SAML2ResponseResource(Resource):
|
||||
if saml2_auth.not_signed:
|
||||
raise CodeMessageException(400, "SAML2 response was not signed")
|
||||
|
||||
if "uid" not in saml2_auth.ava:
|
||||
raise CodeMessageException(400, "uid not in SAML2 response")
|
||||
if "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn" not in saml2_auth.ava:
|
||||
logger.info("ava: %r", saml2_auth.ava)
|
||||
raise CodeMessageException(400, "upn not in SAML2 response")
|
||||
|
||||
username = saml2_auth.ava["uid"][0]
|
||||
username = saml2_auth.ava["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"][0]
|
||||
|
||||
displayName = saml2_auth.ava.get("displayName", [None])[0]
|
||||
displayName = saml2_auth.ava.get("http://schemas.auth0.com/nickname", [None])[0]
|
||||
return self._sso_auth_handler.on_successful_auth(
|
||||
username, request, relay_state,
|
||||
user_display_name=displayName,
|
||||
|
||||
@@ -189,6 +189,7 @@ class HomeServer(object):
|
||||
'registration_handler',
|
||||
'account_validity_handler',
|
||||
'event_client_serializer',
|
||||
'saml_client',
|
||||
]
|
||||
|
||||
REQUIRED_ON_MASTER_STARTUP = [
|
||||
@@ -522,6 +523,10 @@ class HomeServer(object):
|
||||
def build_event_client_serializer(self):
|
||||
return EventClientSerializer(self)
|
||||
|
||||
def build_saml_client(self):
|
||||
from saml2.client import Saml2Client
|
||||
return Saml2Client(self.config.saml2_sp_config)
|
||||
|
||||
def remove_pusher(self, app_id, push_key, user_id):
|
||||
return self.get_pusherpool().remove_pusher(app_id, push_key, user_id)
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ var show_login = function() {
|
||||
}
|
||||
|
||||
if (matrixLogin.serverAcceptsSso) {
|
||||
$("#sso_form").attr("action", "/_matrix/client/r0/login/sso/redirect");
|
||||
$("#sso_flow").show();
|
||||
} else if (matrixLogin.serverAcceptsCas) {
|
||||
$("#sso_form").attr("action", "/_matrix/client/r0/login/cas/redirect");
|
||||
@@ -79,7 +80,7 @@ var fetch_info = function(cb) {
|
||||
$.get(matrixLogin.endpoint, function(response) {
|
||||
var serverAcceptsPassword = false;
|
||||
var serverAcceptsCas = false;
|
||||
for (var i=0; i<response.flows.length; i++) {
|
||||
for (var i = 0; i < response.flows.length; i++) {
|
||||
var flow = response.flows[i];
|
||||
if ("m.login.cas" === flow.type) {
|
||||
matrixLogin.serverAcceptsCas = true;
|
||||
@@ -121,6 +122,7 @@ matrixLogin.onLogin = function(response) {
|
||||
// clobber this function
|
||||
console.log("onLogin - This function should be replaced to proceed.");
|
||||
console.log(response);
|
||||
alert("Login successful!");
|
||||
};
|
||||
|
||||
var parseQsFromUrl = function(query) {
|
||||
@@ -143,7 +145,7 @@ var try_token = function() {
|
||||
if (pos == -1) {
|
||||
return false;
|
||||
}
|
||||
var qs = parseQsFromUrl(window.location.href.substr(pos+1));
|
||||
var qs = parseQsFromUrl(window.location.href.substr(pos + 1));
|
||||
|
||||
var loginToken = qs.loginToken;
|
||||
|
||||
|
||||
@@ -69,10 +69,14 @@ def abort(message, colour=RED, stream=sys.stderr):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def start(configfile):
|
||||
def start(configfile, daemonize=True):
|
||||
write("Starting ...")
|
||||
args = SYNAPSE
|
||||
args.extend(["--daemonize", "-c", configfile])
|
||||
|
||||
if daemonize:
|
||||
args.extend(["--daemonize", "-c", configfile])
|
||||
else:
|
||||
args.extend(["-c", configfile])
|
||||
|
||||
try:
|
||||
subprocess.check_call(args)
|
||||
@@ -143,12 +147,21 @@ def main():
|
||||
help="start or stop all the workers in the given directory"
|
||||
" and the main synapse process",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-daemonize",
|
||||
action="store_false",
|
||||
help="Run synapse in the foreground for debugging. "
|
||||
"Will work only if the daemonize option is not set in the config."
|
||||
)
|
||||
|
||||
options = parser.parse_args()
|
||||
|
||||
if options.worker and options.all_processes:
|
||||
write('Cannot use "--worker" with "--all-processes"', stream=sys.stderr)
|
||||
sys.exit(1)
|
||||
if options.no_daemonize and options.all_processes:
|
||||
write('Cannot use "--no-daemonize" with "--all-processes"', stream=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
configfile = options.configfile
|
||||
|
||||
@@ -276,7 +289,7 @@ def main():
|
||||
# Check if synapse is already running
|
||||
if os.path.exists(pidfile) and pid_running(int(open(pidfile).read())):
|
||||
abort("synapse.app.homeserver already running")
|
||||
start(configfile)
|
||||
start(configfile, bool(options.no_daemonize))
|
||||
|
||||
for worker in workers:
|
||||
env = os.environ.copy()
|
||||
|
||||
@@ -427,6 +427,41 @@ class AccountValidityRenewalByEmailTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
self.assertEqual(len(self.email_attempts), 1)
|
||||
|
||||
def test_manual_email_send_expired_account(self):
|
||||
user_id = self.register_user("kermit", "monkey")
|
||||
tok = self.login("kermit", "monkey")
|
||||
|
||||
# We need to manually add an email address otherwise the handler will do
|
||||
# nothing.
|
||||
now = self.hs.clock.time_msec()
|
||||
self.get_success(
|
||||
self.store.user_add_threepid(
|
||||
user_id=user_id,
|
||||
medium="email",
|
||||
address="kermit@example.com",
|
||||
validated_at=now,
|
||||
added_at=now,
|
||||
)
|
||||
)
|
||||
|
||||
# Make the account expire.
|
||||
self.reactor.advance(datetime.timedelta(days=8).total_seconds())
|
||||
|
||||
# Ignore all emails sent by the automatic background task and only focus on the
|
||||
# ones sent manually.
|
||||
self.email_attempts = []
|
||||
|
||||
# Test that we're still able to manually trigger a mail to be sent.
|
||||
request, channel = self.make_request(
|
||||
b"POST",
|
||||
"/_matrix/client/unstable/account_validity/send_mail",
|
||||
access_token=tok,
|
||||
)
|
||||
self.render(request)
|
||||
self.assertEquals(channel.result["code"], b"200", channel.result)
|
||||
|
||||
self.assertEqual(len(self.email_attempts), 1)
|
||||
|
||||
|
||||
class AccountValidityBackgroundJobTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
|
||||
Reference in New Issue
Block a user