Files
matrix-synapse-rest-passwor…/rest_auth_provider.py
PeerD 25ebf58b7f replace 3pid option
Adds the option to replace 3pid from the external datasource in synapse. In this case, all 3pids that are not listed in the external store are deleted from synapse on login.
2019-02-21 16:58:09 +01:00

205 lines
7.5 KiB
Python

# -*- coding: utf-8 -*-
#
# REST endpoint Authentication module for Matrix synapse
# Copyright (C) 2017 Maxime Dor
#
# https://max.kamax.io/
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import logging
from twisted.internet import defer
import requests
import json
logger = logging.getLogger(__name__)
class RestAuthProvider(object):
def __init__(self, config, account_handler):
self.account_handler = account_handler
if not config.endpoint:
raise RuntimeError('Missing endpoint config')
self.endpoint = config.endpoint
self.regLower = config.regLower
self.config = config
logger.info('Endpoint: %s', self.endpoint)
logger.info('Enforce lowercase username during registration: %s', self.regLower)
@defer.inlineCallbacks
def check_password(self, user_id, password):
logger.info("Got password check for " + user_id)
data = {'user':{'id':user_id, 'password':password}}
r = requests.post(self.endpoint + '/_matrix-internal/identity/v1/check_credentials', json = data)
r.raise_for_status()
r = r.json()
if not r["auth"]:
reason = "Invalid JSON data returned from REST endpoint"
logger.warning(reason)
raise RuntimeError(reason)
auth = r["auth"]
if not auth["success"]:
logger.info("User not authenticated")
defer.returnValue(False)
localpart = user_id.split(":", 1)[0][1:]
logger.info("User %s authenticated", user_id)
registration = False
if not (yield self.account_handler.check_user_exists(user_id)):
logger.info("User %s does not exist yet, creating...", user_id)
if localpart != localpart.lower() and self.regLower:
logger.info('User %s was cannot be created due to username lowercase policy', localpart)
defer.returnValue(False)
user_id, access_token = (yield self.account_handler.register(localpart=localpart))
registration = True
logger.info("Registration based on REST data was successful for %s", user_id)
else:
logger.info("User %s already exists, registration skipped", user_id)
if auth["profile"]:
logger.info("Handling profile data")
profile = auth["profile"]
store = yield self.account_handler.hs.get_profile_handler().store
if "display_name" in profile and ((registration and self.config.setNameOnRegister) or (self.config.setNameOnLogin)):
display_name = profile["display_name"]
logger.info("Setting display name to '%s' based on profile data", display_name)
yield store.set_profile_displayname(localpart, display_name)
else:
logger.info("Display name was not set because it was not given or policy restricted it")
if (self.config.updateThreepid):
if "three_pids" in profile:
logger.info("Handling 3PIDs")
external_3pids = []
for threepid in profile["three_pids"]:
medium = threepid["medium"].lower()
address = threepid["address"].lower()
external_3pids.append({"medium": medium, "address": address})
logger.info("Looking for 3PID %s:%s in user profile", medium, address)
validated_at = self.account_handler.hs.get_clock().time_msec()
if not (yield store.get_user_id_by_threepid(medium, address)):
logger.info("3PID is not present, adding")
yield store.user_add_threepid(
user_id,
medium,
address,
validated_at,
validated_at
)
else:
logger.info("3PID is present, skipping")
if (self.config.replaceThreepid):
for threepid in (yield store.user_get_threepids(user_id)):
medium = threepid["medium"].lower()
address = threepid["address"].lower()
if {"medium": medium, "address": address} not in external_3pids:
logger.info("3PID is not present in external datastore, deleting")
yield store.user_delete_threepid(
user_id,
medium,
address
)
else:
logger.info("3PIDs were not updated due to policy")
else:
logger.info("No profile data")
defer.returnValue(True)
@staticmethod
def parse_config(config):
# verify config sanity
_require_keys(config, ["endpoint"])
class _RestConfig(object):
endpoint = ''
regLower = True
setNameOnRegister = True
setNameOnLogin = False
updateThreepid = True
replaceThreepid = False
rest_config = _RestConfig()
rest_config.endpoint = config["endpoint"]
try:
rest_config.regLower = config['policy']['registration']['username']['enforceLowercase']
except TypeError:
# we don't care
pass
except KeyError:
# we don't care
pass
try:
rest_config.setNameOnRegister = config['policy']['registration']['profile']['name']
except TypeError:
# we don't care
pass
except KeyError:
# we don't care
pass
try:
rest_config.setNameOnLogin = config['policy']['login']['profile']['name']
except TypeError:
# we don't care
pass
except KeyError:
# we don't care
pass
try:
rest_config.updateThreepid = config['policy']['all']['threepid']['update']
except TypeError:
# we don't care
pass
except KeyError:
# we don't care
pass
try:
rest_config.replaceThreepid = config['policy']['all']['threepid']['replace']
except TypeError:
# we don't care
pass
except KeyError:
# we don't care
pass
return rest_config
def _require_keys(config, required):
missing = [key for key in required if key not in config]
if missing:
raise Exception(
"REST Auth enabled but missing required config values: {}".format(
", ".join(missing)
)
)