mirror of
https://github.com/GSA/notifications-api.git
synced 2026-02-01 23:55:58 -05:00
Merge branch 'master' into remove-initial-update-sms-sender
This commit is contained in:
@@ -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"):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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", ...}
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
|
||||
149
app/user/rest.py
149
app/user/rest.py
@@ -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
41
app/user/users_schema.py
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user