Merge branch 'master' into remove-initial-update-sms-sender

This commit is contained in:
Rebecca Law
2017-11-09 11:53:29 +00:00
19 changed files with 634 additions and 179 deletions

View File

@@ -7,6 +7,7 @@ from sqlalchemy.exc import DataError
from sqlalchemy.orm.exc import NoResultFound
from app.dao.services_dao import dao_fetch_service_by_id_with_api_keys
from flask import jsonify
class AuthError(Exception):
@@ -44,11 +45,55 @@ def requires_no_auth():
pass
def restrict_ip_sms():
def check_route_secret():
# Check route of inbound sms (Experimental)
# Temporary custom header for route security
if request.headers.get("X-Custom-forwarder"):
current_app.logger.info("X-Custom-forwarder received")
# Custom header for route security
auth_error_msg = ''
if request.headers.get("X-Custom-Forwarder"):
route_secret_key = request.headers.get("X-Custom-Forwarder")
if route_secret_key is None:
# Not blocking at the moment
# raise AuthError('invalid secret key', 403)
auth_error_msg = auth_error_msg + 'invalid secret key, '
else:
key_1 = current_app.config.get('ROUTE_SECRET_KEY_1')
key_2 = current_app.config.get('ROUTE_SECRET_KEY_2')
if key_1 == '' and key_2 == '':
# Not blocking at the moment
# raise AuthError('X-Custom-Forwarder, no secret was set on server', 503)
auth_error_msg = auth_error_msg + 'no secret was set on server, '
else:
key_used = None
route_allowed = False
if route_secret_key == key_1:
key_used = 1
route_allowed = True
elif route_secret_key == key_2:
key_used = 2
route_allowed = True
if not key_used:
# Not blocking at the moment
# raise AuthError('X-Custom-Forwarder, wrong secret', 403)
auth_error_msg = auth_error_msg + 'wrong secret'
current_app.logger.info({
'message': 'X-Custom-Forwarder',
'log_contents': {
'passed': route_allowed,
'key_used': key_used,
'error': auth_error_msg
}
})
return jsonify(key_used=key_used), 200
def restrict_ip_sms():
check_route_secret()
# Check IP of SMS providers
if request.headers.get("X-Forwarded-For"):

View File

@@ -45,6 +45,8 @@ def extract_notify_config(notify_config):
os.environ['SECRET_KEY'] = notify_config['credentials']['secret_key']
os.environ['DANGEROUS_SALT'] = notify_config['credentials']['dangerous_salt']
os.environ['SMS_INBOUND_WHITELIST'] = json.dumps(notify_config['credentials']['allow_ip_inbound_sms'])
os.environ['ROUTE_SECRET_KEY_1'] = notify_config['credentials']['route_secret_key_1']
os.environ['ROUTE_SECRET_KEY_2'] = notify_config['credentials']['route_secret_key_2']
def extract_performance_platform_config(performance_platform_config):

View File

@@ -125,7 +125,8 @@ class Config(object):
NOTIFY_USER_ID = '6af522d0-2915-4e52-83a3-3690455a5fe6'
INVITATION_EMAIL_TEMPLATE_ID = '4f46df42-f795-4cc4-83bb-65ca312f49cc'
SMS_CODE_TEMPLATE_ID = '36fb0730-6259-4da1-8a80-c8de22ad4246'
EMAIL_VERIFY_CODE_TEMPLATE_ID = 'ece42649-22a8-4d06-b87f-d52d5d3f0a27'
EMAIL_2FA_TEMPLATE_ID = '299726d2-dba6-42b8-8209-30e1d66ea164'
NEW_USER_EMAIL_VERIFICATION_TEMPLATE_ID = 'ece42649-22a8-4d06-b87f-d52d5d3f0a27'
PASSWORD_RESET_TEMPLATE_ID = '474e9242-823b-4f99-813d-ed392e7f1201'
ALREADY_REGISTERED_EMAIL_TEMPLATE_ID = '0880fbb1-a0c6-46f0-9a8e-36c986381ceb'
CHANGE_EMAIL_CONFIRMATION_TEMPLATE_ID = 'eb4d9930-87ab-4aef-9bce-786762687884'
@@ -287,6 +288,8 @@ class Config(object):
FREE_SMS_TIER_FRAGMENT_COUNT = 250000
SMS_INBOUND_WHITELIST = json.loads(os.environ.get('SMS_INBOUND_WHITELIST', '[]'))
ROUTE_SECRET_KEY_1 = os.environ.get('ROUTE_SECRET_KEY_1', '')
ROUTE_SECRET_KEY_2 = os.environ.get('ROUTE_SECRET_KEY_2', '')
# Format is as follows:
# {"dataset_1": "token_1", ...}

View File

@@ -47,12 +47,7 @@ def get_user_code(user, code, code_type):
codes = VerifyCode.query.filter_by(
user=user, code_type=code_type).order_by(
VerifyCode.created_at.desc())
retval = None
for x in codes:
if x.check_code(code):
retval = x
break
return retval
return next((x for x in codes if x.check_code(code)), None)
def delete_codes_older_created_more_than_a_day_ago():

View File

@@ -224,7 +224,7 @@ class Service(db.Model, Versioned):
_reply_to_email_address = db.Column("reply_to_email_address", db.Text, index=False, unique=False, nullable=True)
_letter_contact_block = db.Column('letter_contact_block', db.Text, index=False, unique=False, nullable=True)
sms_sender = db.Column(db.String(11), nullable=True)
prefix_sms = db.Column(db.Boolean, nullable=True)
prefix_sms = db.Column(db.Boolean, nullable=True, default=True)
organisation_id = db.Column(UUID(as_uuid=True), db.ForeignKey('organisation.id'), index=True, nullable=True)
free_sms_fragment_limit = db.Column(db.BigInteger, index=False, unique=False, nullable=True)
organisation = db.relationship('Organisation')

View File

@@ -366,14 +366,6 @@ class JobSchema(BaseSchema):
strict = True
class RequestVerifyCodeSchema(ma.Schema):
class Meta:
strict = True
to = fields.Str(required=False)
class NotificationSchema(ma.Schema):
class Meta:
@@ -653,7 +645,6 @@ api_key_schema = ApiKeySchema()
api_key_schema_load_json = ApiKeySchema(load_json=True)
job_schema = JobSchema()
job_schema_load_json = JobSchema(load_json=True)
request_verify_code_schema = RequestVerifyCodeSchema()
sms_admin_notification_schema = SmsAdminNotificationSchema()
sms_template_notification_schema = SmsTemplateNotificationSchema()
job_sms_template_notification_schema = JobSmsTemplateNotificationSchema()

View File

@@ -1,8 +1,9 @@
import json
import uuid
from datetime import datetime
from urllib.parse import urlencode
from flask import (jsonify, request, Blueprint, current_app)
from flask import (jsonify, request, Blueprint, current_app, abort)
from app.config import QueueNames
from app.dao.users_dao import (
@@ -22,7 +23,7 @@ from app.dao.users_dao import (
from app.dao.permissions_dao import permission_dao
from app.dao.services_dao import dao_fetch_service_by_id
from app.dao.templates_dao import dao_get_template_by_id
from app.models import SMS_TYPE, KEY_TYPE_NORMAL, EMAIL_TYPE, Service
from app.models import KEY_TYPE_NORMAL, Service, SMS_TYPE, EMAIL_TYPE
from app.notifications.process_notifications import (
persist_notification,
send_notification_to_queue
@@ -30,7 +31,6 @@ from app.notifications.process_notifications import (
from app.schemas import (
email_data_request_schema,
user_schema,
request_verify_code_schema,
permission_schema,
user_schema_load_json,
user_update_schema_load_json,
@@ -41,6 +41,12 @@ from app.errors import (
InvalidRequest
)
from app.utils import url_with_token
from app.user.users_schema import (
post_verify_code_schema,
post_send_user_sms_code_schema,
post_send_user_email_code_schema,
)
from app.schema_validation import validate
user_blueprint = Blueprint('user', __name__)
register_errors(user_blueprint)
@@ -115,68 +121,99 @@ def verify_user_password(user_id):
@user_blueprint.route('/<uuid:user_id>/verify/code', methods=['POST'])
def verify_user_code(user_id):
data = request.get_json()
validate(data, post_verify_code_schema)
user_to_verify = get_user_by_id(user_id=user_id)
req_json = request.get_json()
verify_code = None
code_type = None
errors = {}
try:
verify_code = req_json['code']
except KeyError:
errors.update({'code': ['Required field missing data']})
try:
code_type = req_json['code_type']
except KeyError:
errors.update({'code_type': ['Required field missing data']})
if errors:
raise InvalidRequest(errors, status_code=400)
code = get_user_code(user_to_verify, verify_code, code_type)
code = get_user_code(user_to_verify, data['code'], data['code_type'])
if user_to_verify.failed_login_count >= current_app.config.get('MAX_VERIFY_CODE_COUNT'):
raise InvalidRequest("Code not found", status_code=404)
if not code:
# only relevant from sms
increment_failed_login_count(user_to_verify)
raise InvalidRequest("Code not found", status_code=404)
if datetime.utcnow() > code.expiry_datetime or code.code_used:
# sms and email
increment_failed_login_count(user_to_verify)
raise InvalidRequest("Code has expired", status_code=400)
if code_type == 'sms':
user_to_verify.current_session_id = str(uuid.uuid4())
user_to_verify.logged_in_at = datetime.utcnow()
user_to_verify.failed_login_count = 0
save_model_user(user_to_verify)
user_to_verify.current_session_id = str(uuid.uuid4())
user_to_verify.logged_in_at = datetime.utcnow()
user_to_verify.failed_login_count = 0
save_model_user(user_to_verify)
use_user_code(code.id)
return jsonify({}), 204
@user_blueprint.route('/<uuid:user_id>/sms-code', methods=['POST'])
def send_user_sms_code(user_id):
@user_blueprint.route('/<uuid:user_id>/<code_type>-code', methods=['POST'])
def send_user_2fa_code(user_id, code_type):
user_to_send_to = get_user_by_id(user_id=user_id)
verify_code, errors = request_verify_code_schema.load(request.get_json())
if count_user_verify_codes(user_to_send_to) >= current_app.config.get('MAX_VERIFY_CODE_COUNT'):
# Prevent more than `MAX_VERIFY_CODE_COUNT` active verify codes at a time
current_app.logger.warn('Max verify code has exceeded for user {}'.format(user_to_send_to.id))
return jsonify({}), 204
current_app.logger.warn('Too many verify codes created for user {}'.format(user_to_send_to.id))
else:
data = request.get_json()
if code_type == SMS_TYPE:
validate(data, post_send_user_sms_code_schema)
send_user_sms_code(user_to_send_to, data)
elif code_type == EMAIL_TYPE:
validate(data, post_send_user_email_code_schema)
send_user_email_code(user_to_send_to, data)
else:
abort(404)
return '{}', 204
def send_user_sms_code(user_to_send_to, data):
recipient = data.get('to') or user_to_send_to.mobile_number
secret_code = create_secret_code()
create_user_code(user_to_send_to, secret_code, SMS_TYPE)
personalisation = {'verify_code': secret_code}
mobile = user_to_send_to.mobile_number if verify_code.get('to', None) is None else verify_code.get('to')
sms_code_template_id = current_app.config['SMS_CODE_TEMPLATE_ID']
sms_code_template = dao_get_template_by_id(sms_code_template_id)
service = Service.query.get(current_app.config['NOTIFY_SERVICE_ID'])
create_2fa_code(
current_app.config['SMS_CODE_TEMPLATE_ID'],
user_to_send_to,
secret_code,
recipient,
personalisation
)
def send_user_email_code(user_to_send_to, data):
recipient = user_to_send_to.email_address
secret_code = str(uuid.uuid4())
personalisation = {
'name': user_to_send_to.name,
'url': _create_2fa_url(user_to_send_to, secret_code, data.get('next'))
}
create_2fa_code(
current_app.config['EMAIL_2FA_TEMPLATE_ID'],
user_to_send_to,
secret_code,
recipient,
personalisation
)
def create_2fa_code(template_id, user_to_send_to, secret_code, recipient, personalisation):
template = dao_get_template_by_id(template_id)
# save the code in the VerifyCode table
create_user_code(user_to_send_to, secret_code, template.template_type)
saved_notification = persist_notification(
template_id=sms_code_template_id,
template_version=sms_code_template.version,
recipient=mobile,
service=service,
personalisation={'verify_code': secret_code},
notification_type=SMS_TYPE,
template_id=template.id,
template_version=template.version,
recipient=recipient,
service=template.service,
personalisation=personalisation,
notification_type=template.template_type,
api_key_id=None,
key_type=KEY_TYPE_NORMAL
)
@@ -185,8 +222,6 @@ def send_user_sms_code(user_id):
# admin even if we're doing user research using this service:
send_notification_to_queue(saved_notification, False, queue=QueueNames.NOTIFY)
return jsonify({}), 204
@user_blueprint.route('/<uuid:user_id>/change-email-verification', methods=['POST'])
def send_user_confirm_new_email(user_id):
@@ -208,7 +243,7 @@ def send_user_confirm_new_email(user_id):
'url': _create_confirmation_url(user=user_to_send_to, email_address=email['email']),
'feedback_url': current_app.config['ADMIN_BASE_URL'] + '/support'
},
notification_type=EMAIL_TYPE,
notification_type=template.template_type,
api_key_id=None,
key_type=KEY_TYPE_NORMAL
)
@@ -218,12 +253,11 @@ def send_user_confirm_new_email(user_id):
@user_blueprint.route('/<uuid:user_id>/email-verification', methods=['POST'])
def send_user_email_verification(user_id):
def send_new_user_email_verification(user_id):
# when registering, we verify all users' email addresses using this function
user_to_send_to = get_user_by_id(user_id=user_id)
secret_code = create_secret_code()
create_user_code(user_to_send_to, secret_code, 'email')
template = dao_get_template_by_id(current_app.config['EMAIL_VERIFY_CODE_TEMPLATE_ID'])
template = dao_get_template_by_id(current_app.config['NEW_USER_EMAIL_VERIFICATION_TEMPLATE_ID'])
service = Service.query.get(current_app.config['NOTIFY_SERVICE_ID'])
saved_notification = persist_notification(
@@ -233,9 +267,9 @@ def send_user_email_verification(user_id):
service=service,
personalisation={
'name': user_to_send_to.name,
'url': _create_verification_url(user_to_send_to, secret_code)
'url': _create_verification_url(user_to_send_to)
},
notification_type=EMAIL_TYPE,
notification_type=template.template_type,
api_key_id=None,
key_type=KEY_TYPE_NORMAL
)
@@ -261,7 +295,7 @@ def send_already_registered_email(user_id):
'forgot_password_url': current_app.config['ADMIN_BASE_URL'] + '/forgot-password',
'feedback_url': current_app.config['ADMIN_BASE_URL'] + '/support'
},
notification_type=EMAIL_TYPE,
notification_type=template.template_type,
api_key_id=None,
key_type=KEY_TYPE_NORMAL
)
@@ -323,7 +357,7 @@ def send_user_reset_password():
'user_name': user_to_send_to.name,
'url': _create_reset_password_url(user_to_send_to.email_address)
},
notification_type=EMAIL_TYPE,
notification_type=template.template_type,
api_key_id=None,
key_type=KEY_TYPE_NORMAL
)
@@ -351,8 +385,8 @@ def _create_reset_password_url(email):
return url_with_token(data, url, current_app.config)
def _create_verification_url(user, secret_code):
data = json.dumps({'user_id': str(user.id), 'email': user.email_address, 'secret_code': secret_code})
def _create_verification_url(user):
data = json.dumps({'user_id': str(user.id), 'email': user.email_address})
url = '/verify-email/'
return url_with_token(data, url, current_app.config)
@@ -361,3 +395,12 @@ def _create_confirmation_url(user, email_address):
data = json.dumps({'user_id': str(user.id), 'email': email_address})
url = '/user-profile/email/confirm/'
return url_with_token(data, url, current_app.config)
def _create_2fa_url(user, secret_code, next_redir):
data = json.dumps({'user_id': str(user.id), 'secret_code': secret_code})
url = '/email-auth/'
ret = url_with_token(data, url, current_app.config)
if next_redir:
ret += '?{}'.format(urlencode({'next': next_redir}))
return ret

41
app/user/users_schema.py Normal file
View File

@@ -0,0 +1,41 @@
post_verify_code_schema = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'description': 'POST schema for verifying a 2fa code',
'type': 'object',
'properties': {
'code': {'type': 'string'},
'code_type': {'type': 'string'},
},
'required': ['code', 'code_type'],
'additionalProperties': False
}
post_send_user_email_code_schema = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'description': (
'POST schema for generating a 2fa email - "to" is required for legacy purposes. '
'"next" is an optional url to redirect to on sign in'
),
'type': 'object',
'properties': {
# doesn't need 'to' as we'll just grab user.email_address. but lets keep it
# as allowed to keep admin code cleaner, but only as null to prevent confusion
'to': {'type': 'null'},
'next': {'type': ['string', 'null']},
},
'required': [],
'additionalProperties': False
}
post_send_user_sms_code_schema = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'description': 'POST schema for generating a 2fa sms',
'type': 'object',
'properties': {
'to': {'type': ['string', 'null']},
},
'required': [],
'additionalProperties': False
}