"""
The helper module contains various methods for api security and for downloading files
"""
from hashlib import sha1
from uuid import uuid4
from urllib import unquote, quote
from urlparse import urlparse
import logging
from random import randint
import time
from zope.component import getUtility
from zope.globalrequest import getRequest
from zope.i18nmessageid import MessageFactory
from twilio.rest import TwilioRestClient
from plone.registry.interfaces import IRegistry
from plone import api
from ska import sign_url, validate_signed_request_data
import rebus
from collective.smsauthenticator.browser.controlpanel import ISMSAuthenticatorSettings
_ = MessageFactory('collective.smsauthenticator')
logger = logging.getLogger("collective.smsauthenticator")
# ******************************************
[docs]def get_app_settings():
"""
Gets the SMS Authenticator settings.
"""
registry = getUtility(IRegistry)
settings = registry.forInterface(ISMSAuthenticatorSettings)
return settings
[docs]def get_user(username):
"""
Get user by username given and return member object.
"""
return api.user.get(username=username)
[docs]def get_username(user=None):
"""
Gets the username of the user.
:param user: If given, used to extract the user. Otherwise, ``plone.api.user.get_current`` is used.
:return string:
"""
if user is None:
user = api.user.get_current()
if user:
return user.getUserName()
[docs]def get_base_url(request=None):
"""
Gets domain name (with HTTP).
:param ZPublisher.HTTPRequest request:
:return string:
"""
if request is None:
request = getRequest()
parsed_uri = urlparse(request.base)
return "{0}://{1}/".format(parsed_uri.scheme, parsed_uri.netloc)
[docs]def get_domain_name(request=None):
"""
Gets domain name (without HTTP).
:param ZPublisher.HTTPRequest request:
:return string:
"""
if request is None:
request = getRequest()
parsed_uri = urlparse(request.base)
return parsed_uri.netloc
[docs]def generate_secret(user):
"""
Generates secret for the user.
:param Products.PlonePAS.tools.memberdata user:
"""
secret = rebus.b32encode(str(uuid4()))
user.setMemberProperties(mapping={'two_step_verification_secret': secret,})
return secret
[docs]def get_secret(user=None, hashed=False):
"""
Gets users' secret code. If ``hashed`` is set to True, returned hashed.
:param Products.PlonePAS.tools.memberdata user:
:param bool hashed: If set to True, hashed version is returned.
:return string:
"""
#TODO: Return hashed version if ``hashed`` is set to True.
if user is None:
user = api.user.get_current()
if user:
secret = user.getProperty('two_step_verification_secret')
# If string returned, then it's likely a set string
if isinstance(secret, basestring) and secret:
return secret
[docs]def get_or_create_secret(user, overwrite=False):
"""
Gets or creates token secret for the user given. Checks first if user given has a ``secret`` generated.
If not, generate it for him and save it in his profile (``two_step_verification_secret``).
:param Products.PlonePAS.tools.memberdata user: If provided, used. Otherwise ``plone.api.user.get_current``
is used to obtain the user.
:return string:
"""
#TODO: Return hashed version if ``hashed`` is set to True.
if user is None:
user = api.user.get_current()
if overwrite:
return generate_secret(user)
secret = user.getProperty('two_step_verification_secret')
if isinstance(secret, basestring) and secret:
return secret
else:
return generate_secret(user)
[docs]def generate_code(user, length=6):
"""
Gets a random token to reset the mobile number (time based) + random char.
:param Products.PlonePAS.tools.memberdata user:
:param int length:
:return string:
"""
secret = get_or_create_secret(user)
return sha1("{0}{1}{2}".format(str(uuid4()), str(randint(0, 9)), secret)).hexdigest()[:8]
[docs]def validate_code(code, prop, user=None):
"""
Validates the given code by matching it with one stored in users' profile.
:param string code:
:param string prop:
:param Products.PlonePAS.tools.memberdata user:
:return bool:
"""
if user is None:
user = api.user.get_current()
stored_code = user.getProperty(prop)
return stored_code == code
[docs]def validate_mobile_number_reset_code(code, user=None):
return validate_code(code=code, prop='mobile_number_reset_code', user=user)
[docs]def validate_mobile_number_authentication_code(code, user=None):
return validate_code(code=code, prop='mobile_number_authentication_code', user=user)
[docs]def get_browser_hash(request=None):
"""
Gets browser hash. Adds an extra security layer, since browser version is unlikely to be changed.
:param ZPublisher.HTTPRequest request:
:return string:
"""
if request is None:
request = getRequest()
try:
return sha1(request.get('HTTP_USER_AGENT')).hexdigest()
except Exception as e:
logger.debug(str(e))
return ''
[docs]def get_ska_secret_key(request=None, user=None, use_browser_hash=True):
"""
Gets the `secret_key` to be used in `ska` package.
- Value of the ``two_step_verification_secret`` (from users' profile).
- Browser info (hash of)
- The SECRET set for the `ska` (use `plone.app.registry`).
:param ZPublisher.HTTPRequest request:
:param Products.PlonePAS.tools.memberdata user:
:param bool use_browser_hash: If set to True, browser hash is used. Otherwise - not. Defaults to True.
:return string:
"""
if request is None:
request = getRequest()
if user is None:
user = api.user.get_current()
settings = get_app_settings()
ska_secret_key = settings.ska_secret_key
user_secret = user.getProperty('two_step_verification_secret')
if use_browser_hash:
browser_hash = get_browser_hash(request=request)
else:
browser_hash = ''
return "{0}{1}{2}".format(user_secret, browser_hash, ska_secret_key)
[docs]def is_two_step_verification_globally_enabled():
"""
Checks if the two-step verification is globally enabled.
:return bool:
"""
settings = get_app_settings()
return settings.globally_enabled
[docs]def get_white_listed_ip_addresses():
"""
Gets list of white-listed IP addresses.
:return list:
"""
settings = get_app_settings()
ip_addresses = settings.ip_addresses_whitelist
ip_addresses_list = ip_addresses.split('\n')
return ip_addresses_list
[docs]def get_ska_token_lifetime(settings=None):
"""
Gets the `ska` token lifetime (in seconds) from settings.
:return int:
"""
if settings is None:
settings = get_app_settings()
return settings.ska_token_lifetime
[docs]def sign_user_data(request=None, user=None, url='@@sms-authenticator-token'):
"""
Signs the user data with `ska` package. The secret key is `secret_key` to be used with `ska` is a
combination of:
- Value of the ``two_step_verification_secret`` (from users' profile).
- Browser info (hash of)
- The SECRET set for the `ska` (use `plone.app.registry`).
:param ZPublisher.HTTPRequest request:
:param Products.PlonePAS.tools.memberdata user:
:param string url:
:return string:
"""
if request is None:
request = getRequest()
if user is None:
user = api.user.get_current()
token_lifetime = get_ska_token_lifetime()
# Make sure the secret key always exists
get_or_create_secret(user)
secret_key = get_ska_secret_key(request=request, user=user)
signed_url = sign_url(
auth_user = user.getUserId(),
secret_key = secret_key,
url = url,
lifetime = token_lifetime
)
return signed_url
[docs]def validate_user_data(request, user, use_browser_hash=True):
"""
Validates the user data.
:param ZPublisher.HTTPRequest request:
:param Products.PlonePAS.tools.memberdata user:
:return ska.SignatureValidationResult:
"""
secret_key = get_ska_secret_key(request=request, user=user, use_browser_hash=use_browser_hash)
validation_result = validate_signed_request_data(
data = extract_request_data(request),
secret_key = secret_key
)
return validation_result
[docs]def has_enabled_two_step_verification(user):
"""
Checks if user has enabled the two-step verification.
:param Products.PlonePAS.tools.memberdata user:
:return bool:
"""
if bool(api.user.is_anonymous()) is True:
return None
try:
return user.getProperty('enable_two_step_verification', False)
except Exception as e:
return None
[docs]def enable_two_step_verification_for_users(users=[]):
"""
Enable two-step verification for the list of users given.
"""
if not users:
users = api.user.get_users()
for user in users:
try:
get_or_create_secret(user)
if not has_enabled_two_step_verification(user):
user.setMemberProperties(mapping={'enable_two_step_verification': True,})
except Exception as e:
logger.debug(str(e))
[docs]def disable_two_step_verification_for_users(users=[]):
"""
Disable two-step verification for the list of users given.
"""
if not users:
users = api.user.get_users()
for user in users:
try:
#get_or_create_secret(user)
if has_enabled_two_step_verification(user):
user.setMemberProperties(
mapping = {
'enable_two_step_verification': False,
'two_step_verification_secret': '',
'mobile_number_reset_token': '',
'mobile_number_reset_code': '',
#'authentication_token_valid_until': '',
'mobile_number_authentication_code': '',
}
)
except Exception as e:
logger.debug(str(e))
[docs]def get_ip_addresses_whitelist(request=None):
"""
Gets IP addresses white list.
:param ZPublisher.HTTPRequest request:
:return list:
"""
if not request:
request = getRequest()
settings = get_app_settings()
ip_addresses_whitelist = settings.ip_addresses_whitelist
if ip_addresses_whitelist:
try:
ip_addresses_whitelist = ip_addresses_whitelist.split('\n')
ip_addresses_whitelist = [ip_address.strip() for ip_address in ip_addresses_whitelist]
except Exception as e:
logger.debug(str(e))
ip_addresses_whitelist = []
return ip_addresses_whitelist or []
[docs]def is_whitelisted_client(request=None):
"""
Checks if client's IP address is whitelisted.
:param ZPublisher.HTTPRequest request:
:return bool:
"""
ip_addresses_whitelist = get_ip_addresses_whitelist(request=request)
ip_address = extract_ip_address_from_request(request=request)
if ip_address in ip_addresses_whitelist:
return True
return False
[docs]def send_sms(mobile_number, message):
"""
Sends an SMS to the monile number given for mobile number reset confirmation.
:param string mobile_number:
:param string message: Message.
:return bool: True on success and False on failure.
"""
settings = get_app_settings()
sms_client = TwilioRestClient(settings.twilio_account_sid, settings.twilio_auth_token)
try:
sms_client.sms.messages.create(
to = mobile_number,
from_ = settings.twilio_number,
body = message
)
return True
except Exception as e:
# Log in the error_log
logger.debug(e)
return False
[docs]def send_mobile_number_reset_confirmation_code_sms(mobile_number, code):
"""
Sends an SMS to the monile number given for mobile number reset confirmation.
:param string mobile_number:
:param string code:
:return bool: True on success and False on failure.
"""
message = _("Use this code to confirm your mobile phone reset: {0}".format(code))
return send_sms(mobile_number, message)
[docs]def send_login_code_sms(mobile_number, code):
"""
Sends an SMS to the monile number given for mobile number reset confirmation.
:param string mobile_number:
:param string code:
:return bool: True on success and False on failure.
"""
message = _("Use this code to login: {0}".format(code))
return send_sms(mobile_number, message)
[docs]def send_mobile_number_setup_confirmation_code_sms(mobile_number, code):
"""
Sends an SMS to the monile number given for mobile number setup confirmation.
:param string mobile_number:
:param string code:
:return bool: True on success and False on failure.
"""
message = _("Use this code to confirm your mobile phone setup: {0}".format(code))
return send_sms(mobile_number, message)
[docs]def get_updated_ips_for_member_properties_update(user, request=None):
"""
Save IP, from which user is logged in, into the system.
:param Products.PlonePAS.tools.memberdata user:
:param ZPublisher.HTTPRequest request:
:return bool: True on success and False on failure.
"""
ip = extract_ip_address_from_request(request)
existing_ips = user.getProperty('ips', '')
if existing_ips:
updated_ips = "{0}\n{1},{2}".format(existing_ips, ip, time.time())
else:
updated_ips = "{1},{2}".format(existing_ips, ip, time.time())
return {'ips': updated_ips}
[docs]def save_ip(user, request=None):
"""
Save IP, from which user is logged in, into the system.
:param Products.PlonePAS.tools.memberdata user:
:param ZPublisher.HTTPRequest request:
:return bool: True on success and False on failure.
"""
mapping = get_updated_ips_for_member_properties_update(user=user, request=request)
try:
user.setMemberProperties(mapping=mapping)
return True
except Exception as e:
return False