add hipb api client

This commit is contained in:
uzairr
2021-09-01 10:45:16 +05:00
parent 03382ae400
commit c83750ff58
6 changed files with 207 additions and 27 deletions

View File

@@ -0,0 +1,60 @@
"""
Wrapper to use pwnedpassword Service
"""
import logging
import requests
from requests.exceptions import ReadTimeout
from rest_framework.status import HTTP_408_REQUEST_TIMEOUT
from openedx.core.djangoapps.user_authn.config.waffle import ENABLE_PWNED_PASSWORD_API
log = logging.getLogger(__name__)
def convert_password_tuple(value):
"""
a conversion function used to convert a string to a tuple
"""
signature, count = value.split(":")
return (signature, int(count))
class PwnedPasswordsAPI:
"""
WrapperClass on pwned password service
to fetch similar password signatures
along with their count
"""
API_URL = "https://api.pwnedpasswords.com"
@staticmethod
def range(password):
"""
Returns a dict containing hashed password signatures along with their count
**Argument(s):
password: a sha-1-hashed string against which pwnedservice is invoked
**Returns:
{
"7ecd77ecd7": 341,
"7ecd77ecd77ecd7": 12,
}
"""
range_url = PwnedPasswordsAPI.API_URL + '/range/{}'.format(password[:5])
if ENABLE_PWNED_PASSWORD_API.is_enabled():
try:
response = requests.get(range_url, timeout=5)
entries = dict(map(convert_password_tuple, response.text.split("\r\n")))
return entries
except ReadTimeout:
log.warning('Request timed out for {}'.format(password))
return HTTP_408_REQUEST_TIMEOUT
except Exception as exc: # pylint: disable=W0703
log.exception(f"Unable to range the password: {exc}")

View File

@@ -0,0 +1,54 @@
"""
Test 'have i been pwned' password service
"""
from unittest.mock import Mock, patch
from django.test import TestCase
from edx_toggles.toggles.testutils import override_waffle_switch
from requests.exceptions import ReadTimeout
from testfixtures import LogCapture
from openedx.core.djangoapps.password_policy.hibp import PwnedPasswordsAPI, log
from openedx.core.djangoapps.user_authn.config.waffle import ENABLE_PWNED_PASSWORD_API
class PwnedPasswordsAPITest(TestCase):
"""
Tests pwned password service
"""
@override_waffle_switch(ENABLE_PWNED_PASSWORD_API, True)
@patch('requests.get')
def test_matched_pwned_passwords(self, mock_get):
"""
Test that pwned service returns pwned passwords dict
"""
response_string = "7ecd77ecd7:341\r\n7ecd77ecd77ecd7:12"
pwned_password = {
"7ecd77ecd7": 341,
"7ecd77ecd77ecd7": 12,
}
response = Mock()
response.text = response_string
mock_get.return_value = response
response = PwnedPasswordsAPI.range('7ecd7')
self.assertEqual(response, pwned_password)
@override_waffle_switch(ENABLE_PWNED_PASSWORD_API, True)
@patch('requests.get', side_effect=ReadTimeout)
def test_warning_log_on_timeout(self, mock_get): # pylint: disable=unused-argument
"""
Test that captures the warning log on timeout
"""
with LogCapture(log.name) as log_capture:
PwnedPasswordsAPI.range('7ecd7')
log_capture.check_present(
(
log.name,
'WARNING',
'Request timed out for 7ecd7'
)
)
assert 'Request timed out for 7ecd7' in log_capture.records[0].getMessage()

View File

@@ -23,3 +23,17 @@ ENABLE_LOGIN_USING_THIRDPARTY_AUTH_ONLY = LegacyWaffleSwitch(
'enable_login_using_thirdparty_auth_only',
__name__
)
# .. toggle_name: user_authn.enable_pwned_password_api
# .. toggle_implementation: WaffleSwitch
# .. toggle_default: False
# .. toggle_description: When enabled, user password's vulnerability would be checked via pwned password database
# .. toggle_use_cases: temporary
# .. toggle_creation_date: 2021-09-22
# .. toggle_target_removal_date: 2021-12-31
# .. toggle_tickets: VAN-664
ENABLE_PWNED_PASSWORD_API = LegacyWaffleSwitch(
_WAFFLE_SWITCH_NAMESPACE,
'enable_pwned_password_api',
__name__
)

View File

@@ -2,8 +2,10 @@
This file contains celery tasks for sending email
"""
import hashlib
import logging
import math
from celery import shared_task
from celery.exceptions import MaxRetriesExceededError
from django.conf import settings
@@ -13,13 +15,54 @@ from edx_ace import ace
from edx_ace.errors import RecoverableChannelDeliveryError
from edx_ace.message import Message
from edx_django_utils.monitoring import set_code_owner_attribute
from rest_framework.status import HTTP_408_REQUEST_TIMEOUT
from common.djangoapps.track import segment
from openedx.core.djangoapps.password_policy.hibp import PwnedPasswordsAPI
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.lib.celery.task_utils import emulate_http_request
log = logging.getLogger('edx.celery.task')
def get_pwned_properties(pwned_response, password):
"""
Derive different pwned parameters for analytics
"""
properties = {}
if pwned_response == HTTP_408_REQUEST_TIMEOUT:
properties['vulnerability'] = 'unknown'
pwned_count = 0
else:
pwned_count = pwned_response.get(password, 0)
properties['vulnerability'] = 'yes' if pwned_count > 0 else 'no'
if pwned_count > 0:
properties['frequency'] = math.ceil(math.log10(pwned_count))
return properties
@shared_task
def check_pwned_password_and_send_track_event(user_id, password, internal_user=False):
"""
Check the Pwned Databases and send its event to Segment
"""
try:
password = hashlib.sha1(password.encode('utf-8')).hexdigest()
pwned_response = PwnedPasswordsAPI.range(password)
if pwned_response is not None:
properties = get_pwned_properties(pwned_response, password)
properties['internal_user'] = internal_user
segment.track(user_id, 'edx.bi.user.pwned.password.status', properties)
except Exception: # pylint: disable=W0703
log.exception(
'Unable to get response from pwned password api for user_id: "%s"',
user_id,
)
return None # lint-amnesty, pylint: disable=raise-missing-from
@shared_task(bind=True)
@set_code_owner_attribute
def send_activation_email(self, msg_string, from_address=None):

View File

@@ -4,18 +4,17 @@ Views for login / logout and associated functionality
Much of this file was broken out from views.py, previous history can be found there.
"""
import hashlib
import json
import logging
import hashlib
import re
import urllib
from django.conf import settings
from django.contrib.auth import authenticate
from django.contrib.auth import login as django_login
from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required
from django.contrib import admin
from django.contrib.auth import authenticate, get_user_model
from django.contrib.auth import login as django_login
from django.contrib.auth.decorators import login_required
from django.db.models import Q
from django.http import HttpRequest, HttpResponse, HttpResponseForbidden
from django.shortcuts import redirect
@@ -31,31 +30,33 @@ from rest_framework.views import APIView
from openedx_events.learning.data import UserData, UserPersonalData
from openedx_events.learning.signals import SESSION_LOGIN_COMPLETED
from common.djangoapps import third_party_auth
from common.djangoapps.edxmako.shortcuts import render_to_response
from openedx.core.djangoapps.password_policy import compliance as password_policy_compliance
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_authn.views.login_form import get_login_session_form
from openedx.core.djangoapps.user_authn.cookies import get_response_with_refreshed_jwt_cookies, set_logged_in_cookies
from openedx.core.djangoapps.user_authn.exceptions import AuthFailedError
from openedx.core.djangoapps.user_authn.toggles import should_redirect_to_authn_microfrontend
from openedx.core.djangoapps.util.user_messages import PageLevelMessages
from openedx.core.djangoapps.user_authn.views.password_reset import send_password_reset_email_for_user
from openedx.core.djangoapps.user_authn.views.utils import (
ENTERPRISE_ENROLLMENT_URL_REGEX, UUID4_REGEX, API_V1
)
from openedx.core.djangoapps.user_authn.toggles import is_require_third_party_auth_enabled
from openedx.core.djangoapps.user_authn.config.waffle import ENABLE_LOGIN_USING_THIRDPARTY_AUTH_ONLY
from openedx.core.djangolib.markup import HTML, Text
from openedx.core.lib.api.view_utils import require_post_params # lint-amnesty, pylint: disable=unused-import
from openedx.features.enterprise_support.api import activate_learner_enterprise, get_enterprise_learner_data_from_api
from common.djangoapps.student.helpers import get_next_url_for_login_page, get_redirect_url_with_host
from common.djangoapps.student.models import LoginFailures, AllowedAuthUser, UserProfile
from common.djangoapps.student.models import AllowedAuthUser, LoginFailures, UserProfile
from common.djangoapps.student.views import compose_and_send_activation_email
from common.djangoapps.third_party_auth import pipeline, provider
from common.djangoapps import third_party_auth
from common.djangoapps.track import segment
from common.djangoapps.util.json_request import JsonResponse
from common.djangoapps.util.password_policy_validators import normalize_password
from openedx.core.djangoapps.password_policy import compliance as password_policy_compliance
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_authn.config.waffle import ENABLE_LOGIN_USING_THIRDPARTY_AUTH_ONLY
from openedx.core.djangoapps.user_authn.cookies import get_response_with_refreshed_jwt_cookies, set_logged_in_cookies
from openedx.core.djangoapps.user_authn.exceptions import AuthFailedError
from openedx.core.djangoapps.user_authn.toggles import (
is_require_third_party_auth_enabled,
should_redirect_to_authn_microfrontend
)
from openedx.core.djangoapps.user_authn.views.login_form import get_login_session_form
from openedx.core.djangoapps.user_authn.views.password_reset import send_password_reset_email_for_user
from openedx.core.djangoapps.user_authn.views.utils import API_V1, ENTERPRISE_ENROLLMENT_URL_REGEX, UUID4_REGEX
from openedx.core.djangoapps.user_authn.tasks import check_pwned_password_and_send_track_event
from openedx.core.djangoapps.util.user_messages import PageLevelMessages
from openedx.core.djangolib.markup import HTML, Text
from openedx.core.lib.api.view_utils import require_post_params # lint-amnesty, pylint: disable=unused-import
from openedx.features.enterprise_support.api import activate_learner_enterprise, get_enterprise_learner_data_from_api
log = logging.getLogger("edx.student")
AUDIT_LOG = logging.getLogger("audit")
@@ -557,6 +558,8 @@ def login_user(request, api_version='v1'):
if possibly_authenticated_user and password_policy_compliance.should_enforce_compliance_on_login():
# Important: This call must be made AFTER the user was successfully authenticated.
_enforce_password_policy_compliance(request, possibly_authenticated_user)
is_internal_user = user.email.split('@')[1] == 'edx.org'
check_pwned_password_and_send_track_event.delay(user.id, request.POST.get('password'), is_internal_user)
if possibly_authenticated_user is None or not possibly_authenticated_user.is_active:
_handle_failed_authentication(user, possibly_authenticated_user)

View File

@@ -61,6 +61,7 @@ from openedx.core.djangoapps.user_authn.views.registration_form import (
RegistrationFormFactory,
get_registration_extension_form
)
from openedx.core.djangoapps.user_authn.tasks import check_pwned_password_and_send_track_event
from openedx.core.djangoapps.user_authn.toggles import is_require_third_party_auth_enabled
from common.djangoapps.student.helpers import (
AccountValidationError,
@@ -276,12 +277,17 @@ def create_account_with_params(request, params):
# TODO: there is no error checking here to see that the user actually logged in successfully,
# and is not yet an active user.
if new_user is not None:
AUDIT_LOG.info(f"Login success on new account creation - {new_user.username}")
is_new_user(request, new_user)
return new_user
def is_new_user(request, user):
if user is not None:
AUDIT_LOG.info(f"Login success on new account creation - {user.username}")
is_internal_user = user.email.split('@')[1] == 'edx.org'
check_pwned_password_and_send_track_event.delay(user.id, request.POST.get('password'), is_internal_user)
def _link_user_to_third_party_provider(
is_third_party_auth_enabled,
third_party_auth_credentials_in_api,