Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

unicef-security #3312

Open
wants to merge 1 commit into
base: update-reqs
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
37 changes: 36 additions & 1 deletion Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

89 changes: 1 addition & 88 deletions src/etools/applications/core/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -13,59 +12,14 @@
)
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

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
Expand Down Expand Up @@ -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)
Expand Down
21 changes: 12 additions & 9 deletions src/etools/applications/core/tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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": "[email protected]"}
Expand All @@ -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": "[email protected]"}
Expand All @@ -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"]})


Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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(
Expand All @@ -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(
Expand Down
12 changes: 7 additions & 5 deletions src/etools/config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion src/etools/config/settings/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
)

Expand Down
1 change: 1 addition & 0 deletions src/etools/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')),
Expand Down