From 795683382470e4a753fb87efe03abc05ae141b01 Mon Sep 17 00:00:00 2001 From: Domenico Date: Fri, 3 Jun 2022 21:47:43 -0500 Subject: [PATCH] unicef-security --- Pipfile | 1 + Pipfile.lock | 37 +++++++- src/etools/applications/core/auth.py | 89 +------------------ .../applications/core/tests/test_auth.py | 21 +++-- src/etools/config/settings/base.py | 12 +-- src/etools/config/settings/local.py | 2 +- src/etools/config/urls.py | 1 + 7 files changed, 59 insertions(+), 104 deletions(-) diff --git a/Pipfile b/Pipfile index 110404520..73c369dc5 100644 --- a/Pipfile +++ b/Pipfile @@ -77,6 +77,7 @@ unicef-djangolib = "==0.5.4" unicef-locations = "==4.0.1" unicef-notification = "==1.1" unicef-restlib = "==0.7" +unicef-security = "==1.1" unicef-snapshot = "==1.2" unicef-rest-export = "==0.6" xhtml2pdf = "==0.2.7" diff --git a/Pipfile.lock b/Pipfile.lock index e14710e16..3824a77e5 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e64cf0f85bde902ceb0e195b38d6178a885abc2c279d4d1de69f061475603bc7" + "sha256": "cf4250071c053c1de6898dcd29b11b9e6fafc2e0b976bd364ee7f1b78cf71e69" }, "pipfile-spec": 6, "requires": { @@ -281,6 +281,12 @@ "index": "pypi", "version": "==3.2.13" }, + "django-admin-extra-buttons": { + "hashes": [ + "sha256:893c84d8b1718e9ab62ee1436e7ab10a706a8959b1eb92e01b4c0a5d4d5df43b" + ], + "version": "==1.4.1" + }, "django-admin-extra-urls": { "hashes": [ "sha256:b896eebc24779081f5bb5015c41fb12a741c61d520eec88688749e4991f7cbf5" @@ -326,6 +332,13 @@ "index": "pypi", "version": "==2.3.1" }, + "django-constance": { + "hashes": [ + "sha256:758e190755f05b12e9729933239b87053aa1f790071a402cc02034cb23c8624e", + "sha256:c78eed0f75eb003836db84345fa7df0c529b47b2c4e04ac84fdf8558b3338a28" + ], + "version": "==2.9.0" + }, "django-contrib-comments": { "hashes": [ "sha256:2ca79060bbc8fc5b636981ef6e50f35ab83649af75fc1be47bf770636be3271c", @@ -342,6 +355,13 @@ "index": "pypi", "version": "==3.11.0" }, + "django-countries": { + "hashes": [ + "sha256:0df6d34193667c2343da8935cbfb8a2bd4fb0c97baf01ac10db4628ba1557a82", + "sha256:27fc8a0f66a87c9d839493f3926b4e0f4dd873ef66465aa8cd3e953f99758cc9" + ], + "version": "==7.3.2" + }, "django-debug-toolbar": { "hashes": [ "sha256:42c1c2e9dc05bb57b53d641e3a6d131fc031b92377b34ae32e604a1fe439bb83", @@ -437,6 +457,13 @@ "index": "pypi", "version": "==3.5" }, + "django-picklefield": { + "hashes": [ + "sha256:15ccba592ca953b9edf9532e64640329cd47b136b7f8f10f2939caa5f9ce4287", + "sha256:3c702a54fde2d322fe5b2f39b8f78d9f655b8f77944ab26f703be6c0ed335a35" + ], + "version": "==3.0.1" + }, "django-post-office": { "hashes": [ "sha256:495c62ab845b381811c3456acf4ddaa5326eb3ffc9813a256224a2de562ca984", @@ -1438,6 +1465,14 @@ "index": "pypi", "version": "==0.7" }, + "unicef-security": { + "hashes": [ + "sha256:475d649c6287d97005e12c853a10bac39fa02cab9e36bce93a5d682127b688a6", + "sha256:4aae14e907748e7f3715acfb48b19156b4ca19bbb86142c0e5d54a2fbb508623" + ], + "index": "pypi", + "version": "==1.1" + }, "unicef-snapshot": { "hashes": [ "sha256:4b7b1b409b57bf920c223e6ee9dd2caa258820ca4a8bd87b3b0e1c3dda475f74", diff --git a/src/etools/applications/core/auth.py b/src/etools/applications/core/auth.py index 615730abb..a37f918f1 100644 --- a/src/etools/applications/core/auth.py +++ b/src/etools/applications/core/auth.py @@ -3,7 +3,6 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.models import Group -from django.http import HttpResponseRedirect from rest_framework.authentication import ( BasicAuthentication, @@ -13,10 +12,7 @@ ) from rest_framework.exceptions import PermissionDenied from rest_framework_simplejwt.authentication import JWTAuthentication -from social_core.backends.azuread_b2c import AzureADB2COAuth2 -from social_core.exceptions import AuthCanceled, AuthMissingParameter -from social_core.pipeline import social_auth, user as social_core_user -from social_django.middleware import SocialAuthExceptionMiddleware +from social_core.pipeline import user as social_core_user from etools.applications.users.models import Country from etools.libraries.tenant_support.utils import set_country @@ -24,48 +20,6 @@ logger = logging.getLogger(__name__) -def social_details(backend, details, response, *args, **kwargs): - r = social_auth.social_details(backend, details, response, *args, **kwargs) - r['details']['idp'] = response.get('idp') - - if not r['details'].get('email'): - if not response.get('email'): - r['details']['email'] = response["signInNames.emailAddress"] - else: - r['details']['email'] = response.get('email') - - email = r['details'].get('email') - if isinstance(email, str): - r['details']['email'] = email.lower() - return r - - -def get_username(strategy, details, backend, user=None, *args, **kwargs): - return {'username': details.get('email')} - - -def create_user(strategy, details, backend, user=None, *args, **kwargs): - """ Overwrite social_account.user.create_user to only create new users if they're UNICEF""" - - if user: - return {'is_new': False} - - fields = dict((name, kwargs.get(name, details.get(name))) - for name in backend.setting('USER_FIELDS', social_core_user.USER_FIELDS)) - if not fields: - return - - response = kwargs.get('response') - if response: - email = response.get('email') or response.get("signInNames.emailAddress") - if not email.endswith("unicef.org"): - return - return { - 'is_new': True, - 'user': strategy.create_user(**fields) - } - - def user_details(strategy, details, backend, user=None, *args, **kwargs): # This is where we update the user # see what the property to map by is here @@ -96,47 +50,6 @@ def user_details(strategy, details, backend, user=None, *args, **kwargs): return social_core_user.user_details(strategy, details, backend, user, *args, **kwargs) -class CustomAzureADBBCOAuth2(AzureADB2COAuth2): - BASE_URL = 'https://{tenant_id}.b2clogin.com/{tenant_id}.onmicrosoft.com' - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.redirect_uri = settings.HOST + '/social/complete/azuread-b2c-oauth2/' - - -class CustomSocialAuthExceptionMiddleware(SocialAuthExceptionMiddleware): - - def process_exception(self, request, exception): - if isinstance(exception, (AuthCanceled, AuthMissingParameter)): - return HttpResponseRedirect(self.get_redirect_uri(request, exception)) - else: - raise exception - - def get_redirect_uri(self, request, exception): - error = request.GET.get('error', None) - - # This is what we should expect: - # ['AADB2C90118: The user has forgotten their password.\r\n - # Correlation ID: 7e8c3cf9-2fa7-47c7-8924-a1ea91137ba9\r\n - # Timestamp: 2018-11-13 11:37:56Z\r\n'] - error_description = request.GET.get('error_description', None) - - if error == "access_denied" and error_description is not None: - if 'AADB2C90118' in error_description: - auth_class = CustomAzureADBBCOAuth2() - redirect_home = auth_class.get_redirect_uri() - redirect_url = auth_class.base_url + '/oauth2/v2.0/' + \ - 'authorize?p=' + settings.SOCIAL_PASSWORD_RESET_POLICY + \ - '&client_id=' + settings.KEY + \ - '&nonce=defaultNonce&redirect_uri=' + redirect_home + \ - '&scope=openid+email&response_type=code' - - return redirect_url - - # TODO: In case of password reset the state can't be verified figure out a way to log the user in after reset - return settings.LOGIN_URL - - class DRFBasicAuthMixin(BasicAuthentication): def authenticate(self, request): super_return = super().authenticate(request) diff --git a/src/etools/applications/core/tests/test_auth.py b/src/etools/applications/core/tests/test_auth.py index 725fce772..c6aab1988 100644 --- a/src/etools/applications/core/tests/test_auth.py +++ b/src/etools/applications/core/tests/test_auth.py @@ -3,12 +3,15 @@ from django.contrib.auth import get_user_model from django.test import SimpleTestCase +from unicef_security import pipeline + from etools.applications.core import auth from etools.applications.core.tests.cases import BaseTenantTestCase from etools.applications.users.tests.factories import UserFactory -SOCIAL_AUTH_PATH = "etools.applications.core.auth.social_auth" -SOCIAL_USER_PATH = "etools.applications.core.auth.social_core_user" +SOCIAL_AUTH_PATH = "unicef_security.pipeline.social_auth" +SOCIAL_USER_PATH = "unicef_security.pipeline.social_core_user" +SOCIAL_ETOOLS_USER_PATH = "etools.applications.core.auth.social_core_user" class TestSocialDetails(SimpleTestCase): @@ -27,7 +30,7 @@ def test_details_missing_email(self): 'details': self.details } with patch(SOCIAL_AUTH_PATH, self.mock_social): - r = auth.social_details( + r = pipeline.social_details( None, {}, {"idp": "123", "email": "test@example.com"} @@ -42,7 +45,7 @@ def test_details(self): 'details': self.details } with patch(SOCIAL_AUTH_PATH, self.mock_social): - r = auth.social_details( + r = pipeline.social_details( None, {}, {"idp": "123", "email": "new@example.com"} @@ -63,7 +66,7 @@ def setUp(self): def test_user_exists(self): self.user = UserFactory(username=self.details["email"]) - r = auth.get_username(None, self.details, None) + r = pipeline.get_username(None, self.details, None) self.assertEqual(r, {"username": self.details["email"]}) @@ -81,7 +84,7 @@ def setUp(self): def test_no_user(self): with patch(SOCIAL_USER_PATH, self.mock_social): - r = auth.user_details("strategy", self.details, None, None) + r = pipeline.user_details("strategy", self.details, None, None) self.assertEqual(r, "Returned") self.mock_social.user_details.assert_called_with( "strategy", @@ -97,7 +100,7 @@ def test_no_update(self): ) self.details["business_area_code"] = user.profile.country.business_area_code with patch(SOCIAL_USER_PATH, self.mock_social): - r = auth.user_details("strategy", self.details, None, user) + r = pipeline.user_details("strategy", self.details, None, user) self.assertEqual(r, "Returned") self.mock_social.user_details.assert_called_with( "strategy", @@ -116,7 +119,7 @@ def test_no_profile_country(self): user.profile.country = None user.profile.save() self.assertIsNone(user.profile.country) - with patch(SOCIAL_USER_PATH, self.mock_social): + with patch(SOCIAL_ETOOLS_USER_PATH, self.mock_social): r = auth.user_details("strategy", self.details, None, user) self.assertEqual(r, "Returned") self.mock_social.user_details.assert_called_with( @@ -139,7 +142,7 @@ def test_is_staff_update(self): self.details["business_area_code"] = country.business_area_code self.details["idp"] = "UNICEF Azure AD" self.assertFalse(user.is_staff) - with patch(SOCIAL_USER_PATH, self.mock_social): + with patch(SOCIAL_ETOOLS_USER_PATH, self.mock_social): r = auth.user_details("strategy", self.details, None, user) self.assertEqual(r, "Returned") self.mock_social.user_details.assert_called_with( diff --git a/src/etools/config/settings/base.py b/src/etools/config/settings/base.py index 7df4b1c12..90ec6bed1 100644 --- a/src/etools/config/settings/base.py +++ b/src/etools/config/settings/base.py @@ -118,7 +118,7 @@ def get_from_secrets_or_env(var_name, default=None): # DJANGO: HTTP MIDDLEWARE = ( 'django.contrib.sessions.middleware.SessionMiddleware', - 'etools.applications.core.auth.CustomSocialAuthExceptionMiddleware', + 'unicef_security.middleware.UNICEFSocialAuthExceptionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -196,6 +196,7 @@ def get_from_secrets_or_env(var_name, default=None): 'etools.applications.tpm.tpmpartners', 'waffle', 'etools.applications.permissions2', + 'unicef_security', 'unicef_notification', 'etools_offline', 'etools.applications.offline', @@ -282,7 +283,7 @@ def get_from_secrets_or_env(var_name, default=None): # CONTRIB: AUTH AUTHENTICATION_BACKENDS = ( # 'social_core.backends.azuread_b2c.AzureADB2COAuth2', - 'etools.applications.core.auth.CustomAzureADBBCOAuth2', + 'unicef_security.backends.UNICEFAzureADB2COAuth2', 'django.contrib.auth.backends.ModelBackend', ) AUTH_USER_MODEL = 'users.User' @@ -509,6 +510,7 @@ def before_send(event, hint): SOCIAL_AUTH_JSONFIELD_ENABLED = True POLICY = os.getenv('AZURE_B2C_POLICY_NAME', "b2c_1A_UNICEF_PARTNERS_signup_signin") +TENANT_NAME = os.getenv('AZURE_B2C_TENANT_NAME', 'unicefpartners') TENANT_ID = os.getenv('AZURE_B2C_TENANT', 'unicefpartners') SCOPE = ['openid', 'email'] IGNORE_DEFAULT_SCOPE = True @@ -521,15 +523,15 @@ def before_send(event, hint): SOCIAL_PASSWORD_RESET_POLICY = os.getenv('AZURE_B2C_PASS_RESET_POLICY', "B2C_1_PasswordResetPolicy") SOCIAL_AUTH_PIPELINE = ( # 'social_core.pipeline.social_auth.social_details', - 'etools.applications.core.auth.social_details', + 'unicef_security.pipeline.social_details', 'social_core.pipeline.social_auth.social_uid', # allows based on emails being listed in 'WHITELISTED_EMAILS' or 'WHITELISTED_DOMAINS' 'social_core.pipeline.social_auth.auth_allowed', 'social_core.pipeline.social_auth.social_user', # 'social_core.pipeline.user.get_username', - 'etools.applications.core.auth.get_username', + 'unicef_security.pipeline.get_username', 'social_core.pipeline.social_auth.associate_by_email', - 'etools.applications.core.auth.create_user', + 'unicef_security.pipeline.create_user', 'social_core.pipeline.social_auth.associate_user', 'social_core.pipeline.social_auth.load_extra_data', 'social_core.pipeline.user.user_details', diff --git a/src/etools/config/settings/local.py b/src/etools/config/settings/local.py index 53f9528f5..5d065636e 100644 --- a/src/etools/config/settings/local.py +++ b/src/etools/config/settings/local.py @@ -25,7 +25,7 @@ AUTHENTICATION_BACKENDS = ( # 'social_core.backends.azuread_b2c.AzureADB2COAuth2', - 'etools.applications.core.auth.CustomAzureADBBCOAuth2', + 'unicef_security.backends.UNICEFAzureADB2COAuth2', 'django.contrib.auth.backends.ModelBackend', ) diff --git a/src/etools/config/urls.py b/src/etools/config/urls.py index 7769ffe70..a2f76c092 100644 --- a/src/etools/config/urls.py +++ b/src/etools/config/urls.py @@ -98,6 +98,7 @@ re_path(r'^api/v2/activity/', include('unicef_snapshot.urls')), re_path(r'^api/v2/environment/', include('etools.applications.environment.urls_v2')), re_path(r'^api/v2/attachments/', include('unicef_attachments.urls')), + re_path(r'^security/', include('unicef_security.urls')), # *************** API version 3 ****************** re_path(r'^api/v3/users/', include('etools.applications.users.urls_v3', namespace='users_v3')),