diff --git a/admin/base/urls.py b/admin/base/urls.py index 139dd8d1453..3b3da1a4314 100644 --- a/admin/base/urls.py +++ b/admin/base/urls.py @@ -17,7 +17,7 @@ url(r'^brands/', include('admin.brands.urls', namespace='brands')), url(r'^spam/', include('admin.spam.urls', namespace='spam')), url(r'^institutions/', include('admin.institutions.urls', namespace='institutions')), - url(r'^entitlements/', include('admin.entitlements.urls', namespace='entitlements')), + url(r'^login_access_control/', include('admin.login_access_control.urls', namespace='login_access_control')), url(r'^quota_recalc/', include('admin.quota_recalc.urls', namespace='quota_recalc')), url(r'^preprint_providers/', include('admin.preprint_providers.urls', namespace='preprint_providers')), url(r'^collection_providers/', include('admin.collection_providers.urls', namespace='collection_providers')), diff --git a/admin/entitlements/urls.py b/admin/entitlements/urls.py deleted file mode 100644 index df75e0fc51c..00000000000 --- a/admin/entitlements/urls.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.conf.urls import url -from . import views - -app_name = 'admin' - -urlpatterns = [ - # url(r'^$', views.InstitutionEntitlementList.as_view(), name='list'), - url(r'^bulk_add/$', views.BulkAddInstitutionEntitlement.as_view(), name='bulk_add'), - # url(r'^(?P[0-9]+)/toggle/$', views.ToggleInstitutionEntitlement.as_view(http_method_names=['post']), name='toggle'), - # url(r'^(?P[0-9]+)/delete/$', views.DeleteInstitutionEntitlement.as_view(http_method_names=['post']), name='delete'), -] diff --git a/admin/entitlements/views.py b/admin/entitlements/views.py deleted file mode 100644 index f90d3f8e618..00000000000 --- a/admin/entitlements/views.py +++ /dev/null @@ -1,229 +0,0 @@ -from __future__ import unicode_literals - -import logging -from urllib.parse import urlencode - -from django.core.exceptions import PermissionDenied -from django.shortcuts import redirect -from django.urls import reverse -from django.views.generic import ListView, View - -from admin.rdm.utils import RdmPermissionMixin -from osf.models import Institution -from osf.models import InstitutionEntitlement -from django.contrib.auth.mixins import UserPassesTestMixin -from django.http import Http404 -from admin.base.utils import render_bad_request_response - -logger = logging.getLogger(__name__) - - -class InstitutionEntitlementList(RdmPermissionMixin, UserPassesTestMixin, ListView): - paginate_by = 25 - template_name = 'entitlements/list.html' - raise_exception = True - model = InstitutionEntitlement - institution_id = None - page = None - - def dispatch(self, request, *args, **kwargs): - - # login check - if not self.is_authenticated: - return self.handle_no_permission() - try: - self.institution_id = self.request.GET.get('institution_id') - if self.institution_id: - self.institution_id = int(self.institution_id) - return super(InstitutionEntitlementList, self).dispatch(request, *args, **kwargs) - except ValueError: - return render_bad_request_response(request=request, error_msgs='institution_id must be a integer') - - def test_func(self): - """check user permissions""" - if not self.institution_id: - # superuser or admin has an institution - return self.is_super_admin or self.is_institutional_admin - else: - # institution not exist - if not Institution.objects.filter(id=self.institution_id).exists(): - raise Http404( - 'Institution with id "{}" not found.'.format( - self.institution_id - )) - # superuser or institutional admin has permission - return self.is_super_admin or \ - (self.is_admin and self.is_affiliated_institution(self.institution_id)) - - def get_queryset(self): - return InstitutionEntitlement.objects.order_by('entitlement') - - def get_context_data(self, **kwargs): - user = self.request.user - # superuser - if self.is_super_admin: - institutions = Institution.objects.all().order_by('name') - # institution administrator - elif self.is_admin and user.affiliated_institutions.first(): - institutions = Institution.objects.filter(pk__in=user.affiliated_institutions.all()).order_by('name') - else: - raise PermissionDenied('Not authorized to view the entitlements.') - - selected_id = institutions.first().id - - institution_id = int(self.kwargs.get('institution_id', self.institution_id or selected_id)) - object_list = self.object_list.filter(institution_id=institution_id) - - page_size = self.get_paginate_by(object_list) - paginator, page, query_set, is_paginated = self.paginate_queryset(object_list, page_size) - - kwargs.setdefault('institutions', institutions) - kwargs.setdefault('institution_id', institution_id) - kwargs.setdefault('selected_id', institution_id) - kwargs.setdefault('entitlements', query_set) - kwargs.setdefault('page', page) - - return super(InstitutionEntitlementList, self).get_context_data(**kwargs) - - -class BulkAddInstitutionEntitlement(RdmPermissionMixin, UserPassesTestMixin, View): - raise_exception = True - institution_id = None - - def dispatch(self, request, *args, **kwargs): - """Initialize attributes shared by all view methods.""" - # login check - if not self.is_authenticated: - return self.handle_no_permission() - try: - self.institution_id = self.request.POST.get('institution_id') - if self.institution_id: - self.institution_id = int(self.institution_id) - else: - return render_bad_request_response(request=request, error_msgs='institution_id is required') - return super(BulkAddInstitutionEntitlement, self).dispatch(request, *args, **kwargs) - except ValueError: - return render_bad_request_response(request=request, error_msgs='institution_id must be a integer') - - def test_func(self): - """check user permissions""" - # institution not exist - if not Institution.objects.filter(id=self.institution_id, is_deleted=False).exists(): - raise Http404( - 'Institution with id "{}" not found.'.format( - self.institution_id - )) - # superuser or institutional admin has permission - return self.is_super_admin or \ - (self.is_admin and self.is_affiliated_institution(self.institution_id)) - - def post(self, request): - entitlements = request.POST.getlist('entitlements') - login_availability_list = request.POST.getlist('login_availability') - - existing_set = InstitutionEntitlement.objects.filter( - institution_id=self.institution_id, entitlement__in=entitlements) - existing_list = existing_set.values_list('entitlement', flat=True) - for idx, entitlement in enumerate(entitlements): - if entitlement not in existing_list: - InstitutionEntitlement.objects.create(institution_id=self.institution_id, - entitlement=entitlement, - login_availability=login_availability_list[idx] == 'on', - modifier=request.user) - base_url = reverse('institutions:entitlements') - query_string = urlencode({'institution_id': self.institution_id}) - return redirect('{}?{}'.format(base_url, query_string)) - - -class ToggleInstitutionEntitlement(RdmPermissionMixin, UserPassesTestMixin, View): - raise_exception = True - institution_id = None - entitlement_id = None - - def test_func(self): - """check user permissions""" - # login check - if not self.is_authenticated: - return False - - self.institution_id = int(self.kwargs.get('institution_id')) - self.entitlement_id = int(self.kwargs.get('entitlement_id')) - - # institution not exist - if not Institution.objects.filter(id=self.institution_id, is_deleted=False).exists(): - raise Http404( - 'Institution with id "{}" not found.'.format( - self.institution_id - )) - - # superuser or institutional admin has permission - has_auth = self.is_super_admin or \ - (self.is_admin and self.is_affiliated_institution(self.institution_id)) - if not has_auth: - return False - - entitlement = InstitutionEntitlement.objects.filter(id=self.entitlement_id).first() - if not entitlement: - raise Http404( - 'Entitlement with id "{}" not found.'.format( - self.entitlement_id - )) - else: - # entitlement same institution of admin user - return entitlement.institution_id == self.institution_id - - def post(self, request, *args, **kwargs): - entitlement = InstitutionEntitlement.objects.get(id=self.entitlement_id) - entitlement.login_availability = not entitlement.login_availability - entitlement.modifier = request.user - entitlement.save() - - base_url = reverse('institutions:entitlements') - query_string = urlencode({'institution_id': self.institution_id, 'page': request.GET.get('page', 1)}) - return redirect('{}?{}'.format(base_url, query_string)) - - -class DeleteInstitutionEntitlement(RdmPermissionMixin, UserPassesTestMixin, View): - raise_exception = True - institution_id = None - entitlement_id = None - - def test_func(self): - """check user permissions""" - # login check - if not self.is_authenticated: - return False - - self.institution_id = int(self.kwargs.get('institution_id')) - self.entitlement_id = int(self.kwargs.get('entitlement_id')) - - # superuser and institution not exist - if not Institution.objects.filter(id=self.institution_id, is_deleted=False).exists(): - raise Http404( - 'Institution with id "{}" not found.'.format( - self.institution_id - )) - - # superuser or institutional admin has permission - has_auth = self.is_super_admin or \ - (self.is_admin and self.is_affiliated_institution(self.institution_id)) - if not has_auth: - return False - - entitlement = InstitutionEntitlement.objects.filter(id=self.entitlement_id).first() - if not entitlement: - raise Http404( - 'Entitlement with id "{}" not found.'.format( - self.entitlement_id - )) - else: - # entitlement same institution of admin user - return entitlement.institution_id == self.institution_id - - def post(self, request, *args, **kwargs): - entitlement = InstitutionEntitlement.objects.get(id=self.kwargs['entitlement_id']) - entitlement.delete() - - base_url = reverse('institutions:entitlements') - query_string = urlencode({'institution_id': self.institution_id, 'page': request.GET.get('page', 1)}) - return redirect('{}?{}'.format(base_url, query_string)) diff --git a/admin/institutions/urls.py b/admin/institutions/urls.py index f33153b072e..7eafcf3f9c3 100644 --- a/admin/institutions/urls.py +++ b/admin/institutions/urls.py @@ -1,6 +1,5 @@ from django.conf.urls import url from . import views -from admin.entitlements.views import InstitutionEntitlementList, ToggleInstitutionEntitlement, DeleteInstitutionEntitlement app_name = 'admin' @@ -9,10 +8,6 @@ url(r'^institution_list/$', views.InstitutionUserList.as_view(), name='institution_list'), url(r'^create/$', views.CreateInstitution.as_view(), name='create'), url(r'^import/$', views.ImportInstitution.as_view(), name='import'), - url(r'^entitlements/$', InstitutionEntitlementList.as_view(), name='entitlements'), - # url(r'^(?P[0-9]+)/entitlements/$', InstitutionEntitlementList.as_view(), name='inst_entitlements'), - url(r'^(?P[0-9]+)/entitlements/(?P[0-9]+)/toggle/$', ToggleInstitutionEntitlement.as_view(), name='entitlement_toggle'), - url(r'^(?P[0-9]+)/entitlements/(?P[0-9]+)/delete/$', DeleteInstitutionEntitlement.as_view(), name='entitlement_delete'), url(r'^(?P[0-9]+)/$', views.InstitutionDetail.as_view(), name='detail'), url(r'^(?P[0-9]+)/export/$', views.InstitutionExport.as_view(), name='export'), url(r'^(?P[0-9]+)/delete/$', views.DeleteInstitution.as_view(), name='delete'), diff --git a/admin/entitlements/__init__.py b/admin/login_access_control/__init__.py similarity index 100% rename from admin/entitlements/__init__.py rename to admin/login_access_control/__init__.py diff --git a/admin/login_access_control/urls.py b/admin/login_access_control/urls.py new file mode 100644 index 00000000000..2da444d13a7 --- /dev/null +++ b/admin/login_access_control/urls.py @@ -0,0 +1,16 @@ +from django.conf.urls import url +from . import views + +app_name = 'admin' + +urlpatterns = [ + url(r'^$', views.LoginAccessControlListView.as_view(), name='list'), + url(r'^login_availability_default$', views.UpdateLoginAvailabilityDefaultView.as_view(), name='update_login_availability_default'), + url(r'^authentication_attribute/save$', views.SaveAuthenticationAttributeListView.as_view(), name='save_authentication_attribute_list'), + url(r'^authentication_attribute/update$', views.UpdateAuthenticationAttributeView.as_view(), name='update_authentication_attribute'), + url(r'^authentication_attribute/delete$', views.DeleteAuthenticationAttributeView.as_view(), name='delete_authentication_attribute'), + url(r'^authentication_attribute/logic_condition$', views.UpdateLoginLogicConditionView.as_view(), name='update_login_logic_condition'), + url(r'^mail_address/save$', views.SaveMailAddressListView.as_view(), name='save_mail_address_list'), + url(r'^mail_address/update$', views.UpdateMailAddressView.as_view(), name='update_mail_address'), + url(r'^mail_address/delete$', views.DeleteMailAddressView.as_view(), name='delete_mail_address'), +] diff --git a/admin/login_access_control/utils.py b/admin/login_access_control/utils.py new file mode 100644 index 00000000000..4deb9acee55 --- /dev/null +++ b/admin/login_access_control/utils.py @@ -0,0 +1,75 @@ +from osf.models import Institution + + +def validate_integer(value, name): + """ Check if value is an integer """ + if not value: + return f'{name} is required.' + if not isinstance(value, int): + return f'{name} is invalid.' + return None + + +def validate_boolean(value, name): + """ Check if value is an integer """ + if value is None: + return f'{name} is required.' + if not isinstance(value, bool): + return f'{name} is invalid.' + return None + + +def validate_institution_id(institution_id): + """ Check if value is a ID for existing institution """ + integer_error_message = validate_integer(institution_id, 'institution_id') + if integer_error_message is not None: + return integer_error_message + if not Institution.objects.filter(id=institution_id, is_deleted=False).exists(): + return 'institution_id is invalid.' + return None + + +def validate_logic_condition(logic_condition): + """Validate logic condition expression + + :param str logic_condition: logic condition + :return bool: logic condition is valid or not + """ + if not logic_condition: + # If logic condition is None or empty, return True + return True + + if not isinstance(logic_condition, str) or has_invalid_character(logic_condition): + # If logic condition is not a string or has at least one invalid character, return False + return False + + # Convert operator characters into their respective readable counterpart + expression = logic_condition. \ + replace('&&', ' and '). \ + replace('||', ' or '). \ + replace('!', ' not ') + + # If converted expression still have & or | then return False + if expression.find('&') >= 0 or expression.find('|') >= 0: + return False + + try: + # Try to evaluate expression + if not (type(eval(expression)) == int or type(eval(expression)) == bool): + # If expression is invalid then return False + return False + except (SyntaxError, NameError): + # Fail to evaluate expression, return False + return False + + # The expression is valid, return True + return True + + +def has_invalid_character(expression): + """ Check if expression has at least one invalid character """ + valid_characters = [' ', '!', '(', ')', '|', '&'] + for item in expression: + if not (item.isdigit() or item in valid_characters): + return True + return False diff --git a/admin/login_access_control/views.py b/admin/login_access_control/views.py new file mode 100644 index 00000000000..df7bebe32e4 --- /dev/null +++ b/admin/login_access_control/views.py @@ -0,0 +1,524 @@ +from __future__ import unicode_literals + +import json +import logging +import re + +from django.core.exceptions import PermissionDenied +from django.http import JsonResponse +from django.views.generic import ListView, View +from rest_framework import status as http_status + +from admin.base.settings import ATTRIBUTE_NAME_LIST +from admin.base.utils import render_bad_request_response +from admin.login_access_control import utils +from admin.rdm.utils import RdmPermissionMixin +from osf.models import Institution, LoginControlAuthenticationAttribute, LoginControlMailAddress +from django.contrib.auth.mixins import UserPassesTestMixin +from django.http import Http404 + +logger = logging.getLogger(__name__) + + +class LoginAccessControlListView(RdmPermissionMixin, UserPassesTestMixin, ListView): + """ Login Access Control page """ + paginate_by = 10 + template_name = 'login_access_control/list.html' + raise_exception = True + ordering = '-id' + + def test_func(self): + """check user permissions""" + # login check + if not self.is_authenticated: + self.raise_exception = False + return False + + # Superuser or institutional admin has permission + return self.is_super_admin or self.is_institutional_admin + + def get_queryset(self): + # Return None queryset as get_context_data function already has querysets + return LoginControlAuthenticationAttribute.objects.none() + + def get(self, request, *args, **kwargs): + institution_id = self.request.GET.get('institution_id') + if institution_id is not None: + # If institution_id query param has value, validate institution_id + try: + institution_id = int(institution_id) + if not Institution.objects.filter(id=institution_id, is_deleted=False).exists(): + # institution not found, redirect to 404 page + raise Http404(f'Institution with id "{institution_id}" not found.') + except (ValueError, TypeError): + # Invalid institution_id, redirect to 404 page + return render_bad_request_response(self.request, 'institution_id is invalid.') + + return super(LoginAccessControlListView, self).get(request, args, kwargs) + + def get_context_data(self, **kwargs): + institution_id = self.request.GET.get('institution_id') + user = self.request.user + # Get institution list + institutions = Institution.objects.filter(is_deleted=False).order_by('name') + if not self.is_super_admin and self.is_institutional_admin: + # If user is an institution administrator, get institution list that user is affiliated + institutions = user.affiliated_institutions.filter(is_deleted=False) + if institution_id is not None and not institutions.filter(id=institution_id).exists(): + # If user is not affiliated with institution with institution_id query params, return HTTP 403 page + raise PermissionDenied() + + if institution_id is None: + # If the url does not have institution_id param, select the first institution + selected_institution = institutions.first() + else: + # Otherwise, select the institution with institution_id + selected_institution = institutions.filter(id=institution_id).first() + + if not selected_institution: + raise Http404() + + # Get login control authentication attribute and login control mail address lists + login_control_authentication_attribute_list = LoginControlAuthenticationAttribute.objects.filter( + institution=selected_institution, is_deleted=False + ).order_by('attribute_name', '-id') + login_control_mail_address_list = LoginControlMailAddress.objects.filter( + institution=selected_institution, is_deleted=False + ).order_by('-id') + + # Pagination + self.page_kwarg = 'attribute_page_number' + attribute_page_size = self.get_paginate_by(login_control_authentication_attribute_list) + _, attribute_page, attribute_query_set, _ = self.paginate_queryset(login_control_authentication_attribute_list, attribute_page_size) + attribute_list = list(attribute_query_set) + + self.page_kwarg = 'mail_address_page_number' + mail_address_page_size = self.get_paginate_by(login_control_mail_address_list) + _, mail_address_page, mail_address_query_set, _ = self.paginate_queryset(login_control_mail_address_list, mail_address_page_size) + mail_address_list = list(mail_address_query_set) + + # Return data + return { + 'is_admin': self.is_institutional_admin, + 'institutions': institutions, + 'selected_institution': selected_institution, + 'attributes': ATTRIBUTE_NAME_LIST, + 'login_control_authentication_attribute_list': attribute_list, + 'login_control_authentication_attribute_page': attribute_page, + 'login_control_mail_address_list': mail_address_list, + 'login_control_mail_address_page': mail_address_page, + } + + +class BaseLogicAccessControlUpdateView(RdmPermissionMixin, UserPassesTestMixin, View): + """ Base view for create/update/delete API views in logic access control page """ + raise_exception = True + + def test_func(self): + """check user permissions""" + # login check + if not self.is_authenticated: + self.raise_exception = False + return False + + # superuser or institutional admin has permission + return self.is_super_admin or self.is_institutional_admin + + def parse_json_request(self, request): + """ Parse JSON request body """ + try: + data = json.loads(request.body) + except Exception: + return None, JsonResponse({'error_message': 'The request body data is invalid'}, status=http_status.HTTP_400_BAD_REQUEST) + + if not data: + return None, JsonResponse({'error_message': 'The request body data is required'}, status=http_status.HTTP_400_BAD_REQUEST) + + return data, None + + def is_affiliated_with_not_deleted_institution(self, institution_id): + """ Check if user is an institution administrator that is affiliated with institution_id """ + return self.request.user.affiliated_institutions.filter(pk=institution_id, is_deleted=False).exists() + + +class UpdateLoginAvailabilityDefaultView(BaseLogicAccessControlUpdateView): + """ Update institution's login availability default view """ + def post(self, request, *args, **kwargs): + # Get request body + request_body, error_response = self.parse_json_request(request) + if error_response: + return error_response + institution_id = request_body.get('institution_id') + login_availability_default = request_body.get('login_availability_default') + + # Validate request data + institution_id_error_message = utils.validate_institution_id(institution_id) + if institution_id_error_message is not None: + return JsonResponse({'error_message': institution_id_error_message}, status=http_status.HTTP_400_BAD_REQUEST) + + login_availability_default_error_message = utils.validate_boolean(login_availability_default, 'login_availability_default') + if login_availability_default_error_message is not None: + return JsonResponse({'error_message': login_availability_default_error_message}, status=http_status.HTTP_400_BAD_REQUEST) + + # If user is institution administrator and not affiliated with institution_id, return 403 response + if self.is_admin and not self.is_affiliated_with_not_deleted_institution(institution_id): + return JsonResponse({'error_message': 'You do not have permission to setting login access control of the other institution.'}, + status=http_status.HTTP_403_FORBIDDEN) + + # Update institution's login_availability default + institution = Institution.objects.filter(id=institution_id, is_deleted=False).first() + if institution: + institution.login_availability_default = login_availability_default + institution.save() + + return JsonResponse({}, status=http_status.HTTP_204_NO_CONTENT) + + +class SaveAuthenticationAttributeListView(BaseLogicAccessControlUpdateView): + """ Add institution's login authentication attributes view """ + def post(self, request): + # Get request body + request_body, error_response = self.parse_json_request(request) + if error_response: + return error_response + institution_id = request_body.get('institution_id') + attribute_data_list = request_body.get('attribute_data', []) + + # Validate institution_id + institution_id_error_message = utils.validate_institution_id(institution_id) + if institution_id_error_message is not None: + return JsonResponse({'error_message': institution_id_error_message}, status=http_status.HTTP_400_BAD_REQUEST) + + # Validate list of attribute name, value pair + if not attribute_data_list: + return JsonResponse({'error_message': 'Group (attribute name, attribute value) is required.'}, status=http_status.HTTP_400_BAD_REQUEST) + + current_attribute_list = LoginControlAuthenticationAttribute.objects.filter(institution_id=institution_id, is_deleted=False).values_list( + 'attribute_name', 'attribute_value') + validation_list = list(current_attribute_list) + new_attribute_list = [] + + for attribute_data in attribute_data_list: + attribute_name = (attribute_data.get('attribute_name') or '').strip() + attribute_value = (attribute_data.get('attribute_value') or '').strip() + + # Validate attribute name + if not attribute_name: + return JsonResponse({'error_message': 'Attribute name is required.'}, status=http_status.HTTP_400_BAD_REQUEST) + + if attribute_name not in ATTRIBUTE_NAME_LIST: + return JsonResponse({'error_message': 'Attribute name is not exist in config.'}, status=http_status.HTTP_400_BAD_REQUEST) + + # Validate attribute value + if not attribute_value: + return JsonResponse({'error_message': 'Attribute value is required.'}, status=http_status.HTTP_400_BAD_REQUEST) + + if len(attribute_value) > 255: + return JsonResponse({'error_message': 'Length of attribute value > 255 characters.'}, status=http_status.HTTP_400_BAD_REQUEST) + + validation_list.append((attribute_name, attribute_value,)) + + # Add name, value pair to bulk insert queryset + new_attribute = LoginControlAuthenticationAttribute(institution_id=institution_id, attribute_name=attribute_name, attribute_value=attribute_value) + new_attribute_list.append(new_attribute) + + if len(validation_list) != len(set(validation_list)): + # If there are duplicate records, return not unique error message + return JsonResponse({'error_message': 'Group (attribute name, attribute value) MUST be unique.'}, status=http_status.HTTP_400_BAD_REQUEST) + + # If user is institution administrator and not affiliated with institution_id, return 403 response + if self.is_admin and not self.is_affiliated_with_not_deleted_institution(institution_id): + return JsonResponse({'error_message': 'You do not have permission to setting login access control of the other institution.'}, + status=http_status.HTTP_403_FORBIDDEN) + + # Insert new attribute data into DB + LoginControlAuthenticationAttribute.objects.bulk_create(new_attribute_list) + + return JsonResponse({}, status=http_status.HTTP_201_CREATED) + + +class UpdateAuthenticationAttributeView(BaseLogicAccessControlUpdateView): + """ Update is_availability of a login authentication attribute view """ + def post(self, request, *args, **kwargs): + # Get request body + request_body, error_response = self.parse_json_request(request) + if error_response: + return error_response + institution_id = request_body.get('institution_id') + attribute_id = request_body.get('attribute_id') + is_availability = request_body.get('is_availability') + + # Validate data + institution_id_error_message = utils.validate_institution_id(institution_id) + if institution_id_error_message is not None: + return JsonResponse({'error_message': institution_id_error_message}, status=http_status.HTTP_400_BAD_REQUEST) + + attribute_id_error_message = utils.validate_integer(attribute_id, 'attribute_id') + if attribute_id_error_message is not None: + return JsonResponse({'error_message': attribute_id_error_message}, status=http_status.HTTP_400_BAD_REQUEST) + + attribute_queryset = LoginControlAuthenticationAttribute.objects.filter(id=attribute_id, is_deleted=False) + if not attribute_queryset.exists(): + return JsonResponse({'error_message': 'attribute_id is invalid.'}, status=http_status.HTTP_400_BAD_REQUEST) + + is_availability_error_message = utils.validate_boolean(is_availability, 'is_availability') + if is_availability_error_message is not None: + return JsonResponse({'error_message': is_availability_error_message}, status=http_status.HTTP_400_BAD_REQUEST) + + # If user is institution administrator and not affiliated with institution_id, return 403 response + if self.is_admin and not self.is_affiliated_with_not_deleted_institution(institution_id): + return JsonResponse({'error_message': 'You do not have permission to setting login access control of the other institution.'}, + status=http_status.HTTP_403_FORBIDDEN) + + # Get login control authentication attribute by institution_id + attribute = attribute_queryset.filter(institution_id=institution_id).first() + if not attribute: + return JsonResponse({'error_message': 'Can not setting login access control of the institution into the other institution.'}, + status=http_status.HTTP_403_FORBIDDEN) + + # Check if attribute is set in institution's login_logic_condition + institution = Institution.objects.filter(id=institution_id, is_deleted=False).first() + if institution.login_logic_condition: + logic_condition_attribute_id_list = map(int, re.findall('\\d+', institution.login_logic_condition)) + if attribute_id in logic_condition_attribute_id_list: + return JsonResponse({'error_message': 'Can not switch toggle the attribute element due to it is using in the logic condition.'}, + status=http_status.HTTP_400_BAD_REQUEST) + + # Update authentication attribute's is_availability + attribute.is_availability = is_availability + attribute.save() + + return JsonResponse({}, status=http_status.HTTP_204_NO_CONTENT) + + +class DeleteAuthenticationAttributeView(BaseLogicAccessControlUpdateView): + """ Delete a login authentication attribute view """ + def post(self, request, *args, **kwargs): + # Get request body + request_body, error_response = self.parse_json_request(request) + if error_response: + return error_response + institution_id = request_body.get('institution_id') + attribute_id = request_body.get('attribute_id') + + # Validate data + institution_id_error_message = utils.validate_institution_id(institution_id) + if institution_id_error_message is not None: + return JsonResponse({'error_message': institution_id_error_message}, status=http_status.HTTP_400_BAD_REQUEST) + + attribute_id_error_message = utils.validate_integer(attribute_id, 'attribute_id') + if attribute_id_error_message is not None: + return JsonResponse({'error_message': attribute_id_error_message}, status=http_status.HTTP_400_BAD_REQUEST) + + attribute_queryset = LoginControlAuthenticationAttribute.objects.filter(id=attribute_id, is_deleted=False) + if not attribute_queryset.exists(): + return JsonResponse({'error_message': 'attribute_id is invalid.'}, status=http_status.HTTP_400_BAD_REQUEST) + + # If user is institution administrator and not affiliated with institution_id, return 403 response + if self.is_admin and not self.is_affiliated_with_not_deleted_institution(institution_id): + return JsonResponse({'error_message': 'You do not have permission to setting login access control of the other institution.'}, + status=http_status.HTTP_403_FORBIDDEN) + + # Get login control authentication attribute by institution_id + attribute = attribute_queryset.filter(institution_id=institution_id).first() + if not attribute: + return JsonResponse({'error_message': 'Can not setting login access control of the institution into the other institution.'}, + status=http_status.HTTP_403_FORBIDDEN) + + # Check if attribute is set in institution's login_logic_condition + institution = Institution.objects.filter(id=institution_id, is_deleted=False).first() + if institution.login_logic_condition: + logic_condition_attribute_id_list = map(int, re.findall('\\d+', institution.login_logic_condition)) + if attribute_id in logic_condition_attribute_id_list: + return JsonResponse({'error_message': 'Can not delete the attribute element due to it is using in the logic condition.'}, + status=http_status.HTTP_400_BAD_REQUEST) + + # Update authentication attribute's is_deleted to True + attribute.is_deleted = True + attribute.save() + + return JsonResponse({}, status=http_status.HTTP_204_NO_CONTENT) + + +class UpdateLoginLogicConditionView(BaseLogicAccessControlUpdateView): + """ Update institution's login logic condition expression view """ + def post(self, request, *args, **kwargs): + # Get request body + request_body, error_response = self.parse_json_request(request) + if error_response: + return error_response + institution_id = request_body.get('institution_id') + logic_condition = (request_body.get('logic_condition') or '').strip() + + # Validate data + institution_id_error_message = utils.validate_institution_id(institution_id) + if institution_id_error_message is not None: + return JsonResponse({'error_message': institution_id_error_message}, status=http_status.HTTP_400_BAD_REQUEST) + + if not utils.validate_logic_condition(logic_condition): + return JsonResponse({'error_message': 'Logic condition is invalid.'}, status=http_status.HTTP_400_BAD_REQUEST) + + # If user is institution administrator and not affiliated with institution_id, return 403 response + if self.is_admin and not self.is_affiliated_with_not_deleted_institution(institution_id): + return JsonResponse({'error_message': 'You do not have permission to setting login access control of the other institution.'}, + status=http_status.HTTP_403_FORBIDDEN) + + institution = Institution.objects.filter(id=institution_id, is_deleted=False).first() + # Check if there is a index that does not exist for institution + logic_condition_attribute_id_list = re.findall('\\d+', logic_condition) + logic_condition_attribute_unique_id_list = set(logic_condition_attribute_id_list) + current_attribute_list = LoginControlAuthenticationAttribute.objects.filter( + institution=institution, id__in=logic_condition_attribute_unique_id_list, is_availability=True, is_deleted=False) + if len(logic_condition_attribute_unique_id_list) != current_attribute_list.count(): + return JsonResponse({'error_message': 'Index number does not exist.'}, status=http_status.HTTP_400_BAD_REQUEST) + + # Update institution's login logic condition + institution.login_logic_condition = logic_condition + institution.save() + + return JsonResponse({}, status=http_status.HTTP_204_NO_CONTENT) + + +class SaveMailAddressListView(BaseLogicAccessControlUpdateView): + """ Add institution's logic mail addresses view """ + def post(self, request, *args, **kwargs): + # Get request body + request_body, error_response = self.parse_json_request(request) + if error_response: + return error_response + institution_id = request_body.get('institution_id') + mail_address_list = request_body.get('mail_address_list', []) + + # Validate institution_id + institution_id_error_message = utils.validate_institution_id(institution_id) + if institution_id_error_message is not None: + return JsonResponse({'error_message': institution_id_error_message}, status=http_status.HTTP_400_BAD_REQUEST) + + # Validate mail address list + if not mail_address_list: + return JsonResponse({'error_message': 'Mail address is required.'}, status=http_status.HTTP_400_BAD_REQUEST) + + mail_address_regex = r'^([\w\-\.]+@|@|\.|)(?!\d+\.)+(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9]{0,62}[a-z0-9]$' + current_mail_address_list = LoginControlMailAddress.objects.filter(institution_id=institution_id, is_deleted=False).values_list('mail_address', flat=True) + validation_list = list(current_mail_address_list) + new_mail_list = [] + + for mail_address in mail_address_list: + # Trim mail address input + mail_address = (mail_address or '').strip() + # Validate mail_address + if not mail_address: + return JsonResponse({'error_message': 'Mail address is required.'}, status=http_status.HTTP_400_BAD_REQUEST) + + if len(mail_address) > 320: + return JsonResponse({'error_message': 'Length of mail address > 320 characters.'}, status=http_status.HTTP_400_BAD_REQUEST) + + if not re.match(mail_address_regex, mail_address): + return JsonResponse({'error_message': 'Mail address format is invalid.'}, status=http_status.HTTP_400_BAD_REQUEST) + + validation_list.append(mail_address) + + # Add mail address to bulk insert queryset + new_mail_address = LoginControlMailAddress(institution_id=institution_id, mail_address=mail_address) + new_mail_list.append(new_mail_address) + + if len(validation_list) != len(set(validation_list)): + # If there are duplicate records, return not unique error message + return JsonResponse({'error_message': 'Mail address MUST be unique.'}, status=http_status.HTTP_400_BAD_REQUEST) + + # If user is institution administrator and not affiliated with institution_id, return 403 response + if self.is_admin and not self.is_affiliated_with_not_deleted_institution(institution_id): + return JsonResponse({'error_message': 'You do not have permission to setting login access control of the other institution.'}, + status=http_status.HTTP_403_FORBIDDEN) + + # Insert new attribute data into DB + LoginControlMailAddress.objects.bulk_create(new_mail_list) + + return JsonResponse({}, status=http_status.HTTP_201_CREATED) + + +class UpdateMailAddressView(BaseLogicAccessControlUpdateView): + """ Update is_availability of a logic mail address view """ + def post(self, request, *args, **kwargs): + # Get request body + request_body, error_response = self.parse_json_request(request) + if error_response: + return error_response + institution_id = request_body.get('institution_id') + mail_address_id = request_body.get('mail_address_id') + is_availability = request_body.get('is_availability') + + # Validate data + institution_id_error_message = utils.validate_institution_id(institution_id) + if institution_id_error_message is not None: + return JsonResponse({'error_message': institution_id_error_message}, status=http_status.HTTP_400_BAD_REQUEST) + + mail_address_id_error_message = utils.validate_integer(mail_address_id, 'mail_address_id') + if mail_address_id_error_message is not None: + return JsonResponse({'error_message': mail_address_id_error_message}, status=http_status.HTTP_400_BAD_REQUEST) + + mail_address_control_query = LoginControlMailAddress.objects.filter(id=mail_address_id, is_deleted=False) + if not mail_address_control_query.exists(): + return JsonResponse({'error_message': 'mail_address_id is invalid.'}, status=http_status.HTTP_400_BAD_REQUEST) + + is_availability_error_message = utils.validate_boolean(is_availability, 'is_availability') + if is_availability_error_message is not None: + return JsonResponse({'error_message': is_availability_error_message}, status=http_status.HTTP_400_BAD_REQUEST) + + # If user is institution administrator and not affiliated with institution_id, return 403 response + if self.is_admin and not self.is_affiliated_with_not_deleted_institution(institution_id): + return JsonResponse({'error_message': 'You do not have permission to setting login access control of the other institution.'}, + status=http_status.HTTP_403_FORBIDDEN) + + # Get login control mail address by institution_id + mail_address_control = mail_address_control_query.filter(institution_id=institution_id).first() + if not mail_address_control: + return JsonResponse({'error_message': 'Can not setting login access control of the institution into the other institution.'}, + status=http_status.HTTP_403_FORBIDDEN) + + # Update login control mail address's is_availability + mail_address_control.is_availability = is_availability + mail_address_control.save() + + return JsonResponse({}, status=http_status.HTTP_204_NO_CONTENT) + + +class DeleteMailAddressView(BaseLogicAccessControlUpdateView): + """ Delete a logic mail address view """ + def post(self, request, *args, **kwargs): + # Get request body + request_body, error_response = self.parse_json_request(request) + if error_response: + return error_response + institution_id = request_body.get('institution_id') + mail_address_id = request_body.get('mail_address_id') + + # Validate data + institution_id_error_message = utils.validate_institution_id(institution_id) + if institution_id_error_message is not None: + return JsonResponse({'error_message': institution_id_error_message}, status=http_status.HTTP_400_BAD_REQUEST) + + mail_address_id_error_message = utils.validate_integer(mail_address_id, 'mail_address_id') + if mail_address_id_error_message is not None: + return JsonResponse({'error_message': mail_address_id_error_message}, status=http_status.HTTP_400_BAD_REQUEST) + + mail_address_control_query = LoginControlMailAddress.objects.filter(id=mail_address_id, is_deleted=False) + if not mail_address_control_query.exists(): + return JsonResponse({'error_message': 'mail_address_id is invalid.'}, status=http_status.HTTP_400_BAD_REQUEST) + + # If user is institution administrator and not affiliated with institution_id, return 403 response + if self.is_admin and not self.is_affiliated_with_not_deleted_institution(institution_id): + return JsonResponse({'error_message': 'You do not have permission to setting login access control of the other institution.'}, + status=http_status.HTTP_403_FORBIDDEN) + + # Get login control mail address by institution_id + mail_address_control = mail_address_control_query.filter(institution_id=institution_id).first() + if not mail_address_control: + return JsonResponse({'error_message': 'Can not setting login access control of the institution into the other institution.'}, + status=http_status.HTTP_403_FORBIDDEN) + + # Update login control mail address's is_deleted to True + mail_address_control.is_deleted = True + mail_address_control.save() + + return JsonResponse({}, status=http_status.HTTP_204_NO_CONTENT) diff --git a/admin/static/js/login_access_control/login-access-control.js b/admin/static/js/login_access_control/login-access-control.js new file mode 100644 index 00000000000..3341e9f7e63 --- /dev/null +++ b/admin/static/js/login_access_control/login-access-control.js @@ -0,0 +1,216 @@ +'use strict'; + +var $ = require('jquery'); +var $osf = require('js/osfHelpers'); +var _ = require('js/rdmGettext')._; + +var csrftoken = $('[name=csrfmiddlewaretoken]').val(); + +function csrfSafeMethod(method) { + return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); +} + +$.ajaxSetup({ + crossDomain: false, + beforeSend: function (xhr, settings) { + if (!csrfSafeMethod(settings.type)) { + xhr.setRequestHeader('X-CSRFToken', csrftoken); + } + } +}); + +function postAJAX(url, params, callback) { + $.ajax({ + url: url, + type: 'POST', + data: JSON.stringify(params), + contentType: 'application/json; charset=utf-8', + timeout: 120000, + success: function (data) { + if (callback) { + callback(); + } else { + window.location.reload(); + } + }, + error: function (jqXHR) { + if (jqXHR.status === 500) { + // If status is 500, show error message 'A server error occurred. Please contact the administrator.' + $osf.growl(_('Error'), jqXHR.responseText, 'danger', 5000); + } else if (jqXHR.responseJSON != null && 'error_message' in jqXHR.responseJSON) { + $osf.growl(_('Error'), _(jqXHR.responseJSON.error_message), 'danger', 5000); + } else { + $osf.growl(_('Error'), _('A server error occurred. Please contact the administrator.'), 'danger', 5000); + } + } + }); +} + +function getNumberId(id, prefix) { + return parseInt(id.replace(prefix, '')); +} + +function setInvalidMessageForAttributeName(event) { + event.target.setCustomValidity(_('Attribute name is required.')); +} + +function setInvalidMessageForAttributeValue(event) { + var element = event.target; + element.setCustomValidity(''); + if (!!element.validity.valueMissing) { + element.setCustomValidity(_('Attribute value is required.')); + } else if (!!element.validity.tooLong) { + element.setCustomValidity(_('Length of attribute value > 255 characters.')); + } +} + +function setInvalidMessageForMailAddress(event) { + var element = event.target; + element.setCustomValidity(''); + if (!!element.validity.valueMissing) { + element.setCustomValidity(_('Mail address is required.')); + } else if (!!element.validity.tooLong) { + element.setCustomValidity(_('Length of mail address > 320 characters.')); + } else if (!!element.validity.patternMismatch) { + element.setCustomValidity(_('Mail address format is invalid.')); + } +} + +$(document).ready(function() { + $('select[name="attribute_name"]').on('invalid', setInvalidMessageForAttributeName); + $('input[name="attribute_value_input"]').on('invalid', setInvalidMessageForAttributeValue); + $('input[name="mail_address_input"]').on('invalid', setInvalidMessageForMailAddress); +}) + +$('#loginAvailabilityDefaultForm').on('submit', function(event) { + event.preventDefault(); + $('#toggleLoginAvailabilityDefaultModal').modal('hide'); + var params = { + 'institution_id': window.contextVars.institution_id, + 'login_availability_default': document.getElementById('loginAvailability').checked, + }; + var url = './login_availability_default'; + postAJAX(url, params); +}); + +$('#authenticationAttributesForm').on('submit', function(event) { + event.preventDefault(); + var attributeData = []; + for (var i = 0; i < $("select[name='attribute_name']").length; i++) { + var attributeName = $($("select[name='attribute_name']")[i]); + var attributeNameInput = attributeName.val().trim(); + var attributeValue = $($("input[name='attribute_value_input']")[i]); + var attributeValueInput = attributeValue.val().trim(); + + attributeData.push({ + 'attribute_name': attributeNameInput, + 'attribute_value': attributeValueInput + }); + } + + var params = { + 'institution_id': window.contextVars.institution_id, + 'attribute_data': attributeData, + }; + var url = './authentication_attribute/save'; + postAJAX(url, params); +}); + +$('input[name="toggleAuthenticationAttribute"]').on('change', function(event) { + var params = { + 'institution_id': window.contextVars.institution_id, + 'attribute_id': getNumberId(event.target.id, 'toggle_authentication_attribute_'), + 'is_availability': event.target.checked, + }; + var url = './authentication_attribute/update'; + postAJAX(url, params); +}); + +$('a[name="deleteAuthenticationAttribute"]').on('click', function(event) { + var params = { + 'institution_id': window.contextVars.institution_id, + 'attribute_id': getNumberId(event.target.id, 'delete_authentication_attribute_'), + }; + var url = './authentication_attribute/delete'; + postAJAX(url, params, function() { + if (!!window.contextVars.attribute_length && window.contextVars.attribute_length <= 1) { + var redirectUrl = '/login_access_control?institution_id=' + window.contextVars.institution_id; + if (!!window.contextVars.authentication_attribute_page && window.contextVars.authentication_attribute_page > 1) { + redirectUrl += '&attribute_page_number=' + (window.contextVars.authentication_attribute_page - 1); + if (!!window.contextVars.mail_address_page) { + redirectUrl += '&mail_address_page_number=' + window.contextVars.mail_address_page; + } + window.location.href = redirectUrl; + return; + } + } + window.location.reload(); + }); +}); + +$('#logicConditionForm').on('submit', function(event) { + event.preventDefault(); + var params = { + 'institution_id': window.contextVars.institution_id, + 'logic_condition': $('#loginLogicCondition').val().trim(), + }; + var url = './authentication_attribute/logic_condition'; + postAJAX(url, params); +}); + +$('#mailAddressForm').on('submit', function(event) { + event.preventDefault(); + var mailAddressList = []; + for (var i = 0; i < $("[name='mail_address_input']").length; i++) { + var mailAddressElement = $($("[name='mail_address_input']")[i]); + var mailAddressInput = mailAddressElement.val().trim(); + mailAddressList.push(mailAddressInput); + } + var params = { + 'institution_id': window.contextVars.institution_id, + 'mail_address_list': mailAddressList, + }; + var url = './mail_address/save'; + postAJAX(url, params); +}); + +$('input[name="toggleMailAddress"]').on('change', function(event) { + var params = { + 'institution_id': window.contextVars.institution_id, + 'mail_address_id': getNumberId(event.target.id, 'toggle_mail_address_'), + 'is_availability': event.target.checked, + }; + var url = './mail_address/update'; + postAJAX(url, params); +}); + +$('a[name="deleteMailAddress"]').on('click', function(event) { + var params = { + 'institution_id': window.contextVars.institution_id, + 'mail_address_id': getNumberId(event.target.id, 'delete_mail_address_'), + }; + var url = './mail_address/delete'; + postAJAX(url, params, function() { + if (!!window.contextVars.mail_address_length && window.contextVars.mail_address_length <= 1) { + var redirectUrl = '/login_access_control?institution_id=' + window.contextVars.institution_id; + if (!!window.contextVars.mail_address_page && window.contextVars.mail_address_page > 1) { + if (!!window.contextVars.authentication_attribute_page) { + redirectUrl += '&attribute_page_number=' + window.contextVars.authentication_attribute_page; + } + redirectUrl += '&mail_address_page_number=' + (window.contextVars.mail_address_page - 1); + window.location.href = redirectUrl; + return; + } + } + window.location.reload(); + }); +}); + +$('#attributes_block').on('attribute_created', function() { + $('select[name="attribute_name"]').on('invalid', setInvalidMessageForAttributeName); + $('input[name="attribute_value_input"]').on('invalid', setInvalidMessageForAttributeValue); +}); + +$('#mail_addresses_block').on('mail_address_created', function() { + $('input[name="mail_address_input"]').on('invalid', setInvalidMessageForMailAddress); +}); diff --git a/admin/templates/400.html b/admin/templates/400.html index 4d7899f3b87..98a4517d3e3 100644 --- a/admin/templates/400.html +++ b/admin/templates/400.html @@ -6,5 +6,5 @@ {% block content %}

400

-

{{ exception }}

-{% endblock content %} \ No newline at end of file +

{{ exception | capfirst }}

+{% endblock content %} diff --git a/admin/templates/base.html b/admin/templates/base.html index a4d700ac2ac..e54d49fd465 100644 --- a/admin/templates/base.html +++ b/admin/templates/base.html @@ -129,8 +129,8 @@ {% if user.is_superuser or user.is_staff %}
  • - - {% trans "Login Availability Control" %} + + {% trans "Login Access Control" %}
  • {% endif %} diff --git a/admin/templates/entitlements/list.html b/admin/templates/entitlements/list.html deleted file mode 100644 index 4ad5fdaa3bf..00000000000 --- a/admin/templates/entitlements/list.html +++ /dev/null @@ -1,187 +0,0 @@ -{% extends "base.html" %} -{% load i18n %} -{% load render_bundle from webpack_loader %} -{% load spam_extras %} -{% load static %} - -{% block top_includes %} - - -{% endblock %} - -{% block title %} - {% trans "List of Entitlements" %} -{% endblock title %} - -{% block content %} -

    {% trans "Entitlements" %}

    - -
    -
    - {% if request.session.message %} -
    - {{ request.session.message }} -
    - {% endif %} -
    -
    -
    - {% csrf_token %} -
    - -
    - -
    -
    - -
    - -
    -
    -
    - -
    -
    - - -
    -
    -
    -
    -
    -
    -
    - - -
    -
    -
    - {% include "entitlements/pagination.html" with institution_id=selected_id items=page status=status %} - - - - - {% for entitlement in entitlements %} - - - - - - {% endfor %} - -
    {% autoescape on %}{{ entitlement.entitlement }}{% endautoescape %} - -
    {% csrf_token %}
    -
    - - {% trans 'Delete' %} - -
    {% csrf_token %}
    -
    - -{% endblock content %} - -{% block bottom_js %} - - - -{% endblock %} diff --git a/admin/templates/login_access_control/list.html b/admin/templates/login_access_control/list.html new file mode 100644 index 00000000000..3984a165f5f --- /dev/null +++ b/admin/templates/login_access_control/list.html @@ -0,0 +1,393 @@ +{% extends "base.html" %} +{% load i18n %} +{% load render_bundle from webpack_loader %} +{% load spam_extras %} +{% load static %} + +{% block top_includes %} + + +{% endblock %} + +{% block title %} + {% trans 'Login Access Control' %} +{% endblock title %} + +{% block content %} + +
    +
    + +
    + +
    +
    + +
    + +
    + +
    +
    +
    + +
    +

    {% trans '※ Set the default settings for whether users can log in to the target institution.' %}

    +

    {% trans '・If "Default login permission" is set to "Allow", users who meet the conditions of "Login access control by authentication attributes" or "Login access control by email address" will not be able to login.' %}

    +

    {% trans '・If "Default login permission" is set to "No", users who meet the conditions of "Login access control by authentication attributes" or "Login access control by email address" will be able to login.' %}

    +
    + + +

    {% trans 'Login access control by authentication attribute' %}

    +
    {% trans '※The authentication attributes configured in the IdP are available.' %}
    + +
    + {% csrf_token %} +
    + +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    + + + {% include "login_access_control/pagination.html" with institution_id=selected_institution.id items=login_control_authentication_attribute_page other_items=login_control_mail_address_page current_page_query_param='attribute_page_number' other_page_query_param='mail_address_page_number' %} + {% if login_control_authentication_attribute_list|length %} + {% regroup login_control_authentication_attribute_list by attribute_name as attribute_grouped_list %} + + + + + + + + + + + {% for attribute_name, grouped_list in attribute_grouped_list %} + {% for item in grouped_list %} + + {% ifchanged attribute_name %} + + {% endifchanged %} + + + + + {% endfor %} + {% endfor %} + +
    {% trans 'Authentication attribute' %}{% trans 'Index number' %}{% trans 'Value' %}{% trans 'Action' %}
    {{ attribute_name }}{{ item.id }}{{ item.attribute_value }} + + + {% trans 'Delete' %} + +
    + {% else %} +

    {% trans "No results found" %}

    + {% endif %} + + +
    + {% csrf_token %} +
    + +
    + + +
    +
    +
    + +
    {% trans '※Please set logical conditions such as "1", "1&&2", or "1||(2&&3)".' %}
    + + +

    {% trans 'Login access control by mail address' %}

    +
    {% trans "※Login access will be controlled based on whether the last part of the email address matches the user's email address." %}
    +
    + {% csrf_token %} +
    + +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    + + + {% include "login_access_control/pagination.html" with institution_id=selected_institution.id items=login_control_mail_address_page other_items=login_control_authentication_attribute_page current_page_query_param='mail_address_page_number' other_page_query_param='attribute_page_number' %} + {% if login_control_mail_address_list|length %} + + + + + + + + + {% for item in login_control_mail_address_list %} + + + + + {% endfor %} + +
    {% trans 'Mail address' %}{% trans 'Action' %}
    {{ item.mail_address }} + + + {% trans 'Delete' %} + +
    + {% else %} +

    {% trans "No results found" %}

    + {% endif %} + + + + +{% endblock content %} + +{% block bottom_js %} + {% render_bundle 'login-access-control' %} + +{% endblock %} diff --git a/admin/templates/entitlements/pagination.html b/admin/templates/login_access_control/pagination.html similarity index 66% rename from admin/templates/entitlements/pagination.html rename to admin/templates/login_access_control/pagination.html index 84de4396e42..ca3cc9c9975 100644 --- a/admin/templates/entitlements/pagination.html +++ b/admin/templates/login_access_control/pagination.html @@ -4,11 +4,11 @@ {% endif %} - {% if pagin %} - - 10 - 25 - 50 - - {% endif %} {% endblock content %} diff --git a/admin/translations/django.pot b/admin/translations/django.pot index 8202c292ede..2d19b3a7902 100644 --- a/admin/translations/django.pot +++ b/admin/translations/django.pot @@ -226,7 +226,7 @@ msgid "Schedule a banner" msgstr "" #: admin/templates/banners/create.html:29 -#: admin/templates/entitlements/list.html:91 +#: admin/templates/login_access_control/list.html:91 #: admin/templates/institutions/create.html:24 #: admin/templates/rdm_addons/addons/dataverse_credentials_modal.html:57 #: admin/templates/rdm_addons/addons/iqbrims_institution_settings.html:24 @@ -315,7 +315,7 @@ msgid "Menu" msgstr "" #: admin/templates/base.html:110 -msgid "Login Availability Control" +msgid "Login Access Control" msgstr "" #: admin/templates/base.html:116 admin/templates/base.html:121 @@ -725,55 +725,81 @@ msgstr "" msgid "-GakuNin RDM Admin" msgstr "" -#: admin/templates/entitlements/list.html:40 -msgid "List of Entitlements" +#: admin/templates/login_access_control/list.html:92 +msgid "Institutions" msgstr "" -#: admin/templates/entitlements/list.html:44 -#: admin/templates/entitlements/list.html:71 -msgid "Entitlements" +#: admin/templates/login_access_control/list.html:103 +#: admin/templates/login_access_control/list.html:149 +msgid "Login availability default" msgstr "" -#: admin/templates/entitlements/list.html:60 -msgid "Institutions" +#: admin/templates/login_access_control/list.html:113 +msgid "※ Set the default settings for whether users can log in to the target institution." msgstr "" -#: admin/templates/entitlements/list.html:75 -#: admin/templates/entitlements/list.html:145 -msgid "Enter new entitlement" +#: admin/templates/login_access_control/list.html:114 +msgid "・If "Default login permission" is set to "Allow", users who meet the conditions of "Login access control by authentication attributes" or "Login access control by email address" will not be able to login." msgstr "" -#: admin/templates/entitlements/list.html:82 -#: admin/templates/entitlements/list.html:149 -msgid "login availability" +#: admin/templates/login_access_control/list.html:115 +msgid "・If "Default login permission" is set to "No", users who meet the conditions of "Login access control by authentication attributes" or "Login access control by email address" will be able to login." msgstr "" -#: admin/templates/entitlements/list.html:90 -msgid "New entitlement" +#: admin/templates/login_access_control/list.html:119 +msgid "Login access control by authentication attribute" msgstr "" -#: admin/templates/entitlements/list.html:115 -msgid "Delete" +#: admin/templates/login_access_control/list.html:120 +msgid "※The authentication attributes configured in the IdP are available." +msgstr "" + +#: admin/templates/login_access_control/list.html:127 +msgid "Authentication attribute" +msgstr "" + +#: admin/templates/login_access_control/list.html:144 +#: admin/templates/login_access_control/list.html:226 +msgid "Add input field" +msgstr "" + +#: admin/templates/login_access_control/list.html:156 +msgid "Index number" msgstr "" -#: admin/templates/entitlements/list.html:165 -msgid "Entitlement must be not empty" +#: admin/templates/login_access_control/list.html:196 +msgid "Logic condition of authentication attribute" msgstr "" -#: admin/templates/entitlements/list.html:169 -#: admin/templates/entitlements/list.html:173 -msgid "Entitlement '" +#: admin/templates/login_access_control/list.html:204 +msgid "※Please set logical conditions such as "1", "1&&2", or "1||(2&&3)"." msgstr "" -#: admin/templates/entitlements/list.html:169 -msgid "' already exists" +#: admin/templates/login_access_control/list.html:207 +msgid "Login access control by mail address" msgstr "" -#: admin/templates/entitlements/list.html:173 -msgid "' have been inputted" +#: admin/templates/login_access_control/list.html:208 +msgid "※Login access will be controlled based on whether the last part of the email address matches the user's email address." +msgstr "" + +#: admin/templates/login_access_control/list.html:212 +#: admin/templates/login_access_control/list.html:237 +msgid "Mail address" +msgstr "" + +#: admin/templates/login_access_control/list.html:275 +msgid "Do you want to change setting of the login availability default for the target institution?" +msgstr "" + +#: admin/templates/login_access_control/list.html:276 +msgid "Please note that changing the above default settings may change the login availability of users who meet the conditions set for "login access control by authentication attributes" or "login access control by email address"." +msgstr "" + +#: admin/templates/login_access_control/list.html:115 +msgid "Delete" msgstr "" -#: admin/templates/entitlements/pagination.html:25 #: admin/templates/util/pagination.html:25 #, python-format msgid "Page %(itemsNumber)s of %(paginatorNumPage)s" diff --git a/admin/translations/en/LC_MESSAGES/django.po b/admin/translations/en/LC_MESSAGES/django.po index 08b8647a759..c4c5a0d64b9 100644 --- a/admin/translations/en/LC_MESSAGES/django.po +++ b/admin/translations/en/LC_MESSAGES/django.po @@ -232,7 +232,7 @@ msgid "Schedule a banner" msgstr "" #: admin/templates/banners/create.html:29 -#: admin/templates/entitlements/list.html:91 +#: admin/templates/login_access_control/list.html:91 #: admin/templates/institutions/create.html:24 #: admin/templates/rdm_addons/addons/dataverse_credentials_modal.html:57 #: admin/templates/rdm_addons/addons/iqbrims_institution_settings.html:24 @@ -322,7 +322,7 @@ msgstr "" # text of menuitem on left-side menu #: admin/templates/base.html:110 -msgid "Login Availability Control" +msgid "Login Access Control" msgstr "" #: admin/templates/base.html:116 admin/templates/base.html:121 @@ -731,60 +731,83 @@ msgstr "" msgid "-GakuNin RDM Admin" msgstr "" -# page title -#: admin/templates/entitlements/list.html:40 -msgid "List of Entitlements" -msgstr "List of eduPersonEntitlement" +#: admin/templates/login_access_control/list.html:92 +msgid "Institutions" +msgstr "" -# label for eduPersonEntitlement input box -#: admin/templates/entitlements/list.html:44 -#: admin/templates/entitlements/list.html:71 -msgid "Entitlements" -msgstr "eduPersonEntitlement" +# label for checkboxes +#: admin/templates/login_access_control/list.html:103 +#: admin/templates/login_access_control/list.html:149 +msgid "Login availability default" +msgstr "" -#: admin/templates/entitlements/list.html:60 -msgid "Institutions" +#: admin/templates/login_access_control/list.html:113 +msgid "※ Set the default settings for whether users can log in to the target institution." msgstr "" -# hint text of eduPersonEntitlement input box -#: admin/templates/entitlements/list.html:75 -#: admin/templates/entitlements/list.html:145 -msgid "Enter new entitlement" -msgstr "Enter new eduPersonEntitlement" +#: admin/templates/login_access_control/list.html:114 +msgid "・If "Default login permission" is set to "Allow", users who meet the conditions of "Login access control by authentication attributes" or "Login access control by email address" will not be able to login." +msgstr "" -# label for checkboxes -#: admin/templates/entitlements/list.html:82 -#: admin/templates/entitlements/list.html:149 -msgid "login availability" +#: admin/templates/login_access_control/list.html:115 +msgid "・If "Default login permission" is set to "No", users who meet the conditions of "Login access control by authentication attributes" or "Login access control by email address" will be able to login." msgstr "" -# text for button to add another eduPersonEntitlement input box -#: admin/templates/entitlements/list.html:90 -msgid "New entitlement" -msgstr "New eduPersonEntitlement" +#: admin/templates/login_access_control/list.html:119 +msgid "Login access control by authentication attribute" +msgstr "" -#: admin/templates/entitlements/list.html:115 -msgid "Delete" +#: admin/templates/login_access_control/list.html:120 +msgid "※The authentication attributes configured in the IdP are available." msgstr "" -#: admin/templates/entitlements/list.html:165 -msgid "Entitlement must be not empty" -msgstr "eduPersonEntitlement must be not empty" +#: admin/templates/login_access_control/list.html:127 +msgid "Authentication attribute" +msgstr "" -#: admin/templates/entitlements/list.html:169 -#: admin/templates/entitlements/list.html:173 -msgid "Entitlement '" -msgstr "eduPersonEntitlement '" +# text for button to add another input box +#: admin/templates/login_access_control/list.html:144 +#: admin/templates/login_access_control/list.html:226 +msgid "Add input field" +msgstr "" -#: admin/templates/entitlements/list.html:169 -msgid "' already exists" +#: admin/templates/login_access_control/list.html:156 +msgid "Index number" msgstr "" -#: admin/templates/entitlements/list.html:173 -msgid "' have been inputted" -msgstr "' is duplicated" +#: admin/templates/login_access_control/list.html:196 +msgid "Logic condition of authentication attribute" +msgstr "" + +#: admin/templates/login_access_control/list.html:204 +msgid "※Please set logical conditions such as "1", "1&&2", or "1||(2&&3)"." +msgstr "" + +#: admin/templates/login_access_control/list.html:207 +msgid "Login access control by mail address" +msgstr "" + +#: admin/templates/login_access_control/list.html:208 +msgid "※Login access will be controlled based on whether the last part of the email address matches the user's email address." +msgstr "" + +#: admin/templates/login_access_control/list.html:212 +#: admin/templates/login_access_control/list.html:237 +msgid "Mail address" +msgstr "" + +#: admin/templates/login_access_control/list.html:275 +msgid "Do you want to change setting of the login availability default for the target institution?" +msgstr "" + +#: admin/templates/login_access_control/list.html:276 +msgid "Please note that changing the above default settings may change the login availability of users who meet the conditions set for "login access control by authentication attributes" or "login access control by email address"." +msgstr "" + +#: admin/templates/login_access_control/list.html:115 +msgid "Delete" +msgstr "" -#: admin/templates/entitlements/pagination.html:25 #: admin/templates/util/pagination.html:25 #, python-format msgid "Page %(itemsNumber)s of %(paginatorNumPage)s" diff --git a/admin/translations/ja/LC_MESSAGES/django.po b/admin/translations/ja/LC_MESSAGES/django.po index a841c71b20d..4645e879841 100644 --- a/admin/translations/ja/LC_MESSAGES/django.po +++ b/admin/translations/ja/LC_MESSAGES/django.po @@ -232,7 +232,7 @@ msgid "Schedule a banner" msgstr "バナーをスケジュールする" #: admin/templates/banners/create.html:29 -#: admin/templates/entitlements/list.html:91 +#: admin/templates/login_access_control/list.html:91 #: admin/templates/institutions/create.html:24 #: admin/templates/rdm_addons/addons/dataverse_credentials_modal.html:57 #: admin/templates/rdm_addons/addons/iqbrims_institution_settings.html:24 @@ -322,8 +322,8 @@ msgstr "メニュー" # text of menuitem on left-side menu #: admin/templates/base.html:110 -msgid "Login Availability Control" -msgstr "ログイン可否設定" +msgid "Login Access Control" +msgstr "ログインアクセス制御" #: admin/templates/base.html:116 admin/templates/base.html:121 msgid "RDM Nodes" @@ -733,60 +733,83 @@ msgstr "ありがとうございます。" msgid "-GakuNin RDM Admin" msgstr "-GakuNin RDM 管理者" -# page title -#: admin/templates/entitlements/list.html:40 -msgid "List of Entitlements" -msgstr "eduPersonEntitlement の一覧" - -# label for eduPersonEntitlement input box -#: admin/templates/entitlements/list.html:44 -#: admin/templates/entitlements/list.html:71 -msgid "Entitlements" -msgstr "eduPersonEntitlement" - -#: admin/templates/entitlements/list.html:60 +#: admin/templates/login_access_control/list.html:92 msgid "Institutions" msgstr "機関" -# hint text of eduPersonEntitlement input box -#: admin/templates/entitlements/list.html:75 -#: admin/templates/entitlements/list.html:145 -msgid "Enter new entitlement" -msgstr "新しい eduPersonEntitlement" - # label for checkboxes -#: admin/templates/entitlements/list.html:82 -#: admin/templates/entitlements/list.html:149 -msgid "login availability" -msgstr "ログイン可否" - -# text for button to add another eduPersonEntitlement input box -#: admin/templates/entitlements/list.html:90 -msgid "New entitlement" +#: admin/templates/login_access_control/list.html:103 +#: admin/templates/login_access_control/list.html:149 +msgid "Login availability default" +msgstr "ログイン可否のデフォルト" + +#: admin/templates/login_access_control/list.html:113 +msgid "※ Set the default settings for whether users can log in to the target institution." +msgstr "※ 対象機関に対するユーザのログイン可否のデフォルトを設定します。" + +#: admin/templates/login_access_control/list.html:114 +msgid "・If "Default login permission" is set to "Allow", users who meet the conditions of "Login access control by authentication attributes" or "Login access control by email address" will not be able to login." +msgstr "・「デフォルトのログイン可否」を「可」に設定した場合、「認証属性によるログインアクセス制御」、もしくは、「メールアドレスによるログインアクセス制御」の条件を満たしたユーザがログイン不可となります。" + +#: admin/templates/login_access_control/list.html:115 +msgid "・If "Default login permission" is set to "No", users who meet the conditions of "Login access control by authentication attributes" or "Login access control by email address" will be able to login." +msgstr "・「デフォルトのログイン可否」を「否」に設定した場合、「認証属性によるログインアクセス制御」、もしくは、「メールアドレスによるログインアクセス制御」の条件を満たしたユーザがログイン可となります。" + +#: admin/templates/login_access_control/list.html:119 +msgid "Login access control by authentication attribute" +msgstr "認証属性によるログインアクセス制御" + +#: admin/templates/login_access_control/list.html:120 +msgid "※The authentication attributes configured in the IdP are available." +msgstr "※ IdP にて設定した認証属性が利用可能です。" + +#: admin/templates/login_access_control/list.html:127 +msgid "Authentication attribute" +msgstr "認証属性" + +# text for button to add another input box +#: admin/templates/login_access_control/list.html:144 +#: admin/templates/login_access_control/list.html:226 +msgid "Add input field" msgstr "入力欄を追加" -#: admin/templates/entitlements/list.html:115 -msgid "Delete" -msgstr "削除" +#: admin/templates/login_access_control/list.html:156 +msgid "Index number" +msgstr "項目" + +#: admin/templates/login_access_control/list.html:196 +msgid "Logic condition of authentication attribute" +msgstr "認証属性の論理条件" -#: admin/templates/entitlements/list.html:165 -msgid "Entitlement must be not empty" -msgstr "eduPersonEntitlement を入力してください。" +#: admin/templates/login_access_control/list.html:204 +msgid "※Please set logical conditions such as "1", "1&&2", or "1||(2&&3)"." +msgstr "※「1」や「1&&2」や「1||(2&&3)」などの論理条件を設定してください。" -#: admin/templates/entitlements/list.html:169 -#: admin/templates/entitlements/list.html:173 -msgid "Entitlement '" -msgstr "eduPersonEntitlement '" +#: admin/templates/login_access_control/list.html:207 +msgid "Login access control by mail address" +msgstr "メールアドレスによるログインアクセス制御" -#: admin/templates/entitlements/list.html:169 -msgid "' already exists" -msgstr "'は既に存在します。" +#: admin/templates/login_access_control/list.html:208 +msgid "※Login access will be controlled based on whether the last part of the email address matches the user's email address." +msgstr "※ユーザのメールアドレスと後方一致するか否かでログインアクセス制御を実施します。" -#: admin/templates/entitlements/list.html:173 -msgid "' have been inputted" -msgstr "'は重複しています。" +#: admin/templates/login_access_control/list.html:212 +#: admin/templates/login_access_control/list.html:237 +msgid "Mail address" +msgstr "メールアドレス" + +#: admin/templates/login_access_control/list.html:275 +msgid "Do you want to change setting of the login availability default for the target institution?" +msgstr "対象機関に対するユーザのログイン可否のデフォルト設定を変更しますか?" + +#: admin/templates/login_access_control/list.html:276 +msgid "Please note that changing the above default settings may change the login availability of users who meet the conditions set for "login access control by authentication attributes" or "login access control by email address"." +msgstr "上記のデフォルト設定の変更により、「認証属性によるログインアクセス制御」、もしくは、「メールアドレスによるログインアクセス制御」に設定した条件を満たすユーザのログイン可否が変わる可能性があるので、注意してください。" + +#: admin/templates/login_access_control/list.html:115 +msgid "Delete" +msgstr "削除" -#: admin/templates/entitlements/pagination.html:25 #: admin/templates/util/pagination.html:25 #, python-format msgid "Page %(itemsNumber)s of %(paginatorNumPage)s" diff --git a/admin/webpack.admin.config.js b/admin/webpack.admin.config.js index 129845dcc0b..43502903e95 100644 --- a/admin/webpack.admin.config.js +++ b/admin/webpack.admin.config.js @@ -38,6 +38,7 @@ var config = Object.assign({}, common, { 'admin-registration-edit-page': staticAdminPath('js/pages/admin-registration-edit-page.js'), 'dashboard': staticAdminPath('js/sales_analytics/dashboard.js'), 'metrics-page': staticAdminPath('js/pages/metrics-page.js'), + 'login-access-control': staticAdminPath('js/login_access_control/login-access-control.js'), 'banners': staticAdminPath('js/banners/banners.js'), 'brands': staticAdminPath('js/brands/brands.js'), 'maintenance': staticAdminPath('js/maintenance/maintenance.js'), diff --git a/admin_tests/base/test_utils.py b/admin_tests/base/test_utils.py index 37738383542..44667eecf3d 100644 --- a/admin_tests/base/test_utils.py +++ b/admin_tests/base/test_utils.py @@ -1,3 +1,4 @@ +import mock from nose.tools import * # noqa: F403 import datetime as datetime import pytest @@ -9,6 +10,8 @@ from django.contrib.admin.sites import AdminSite from django.forms.models import model_to_dict from django.http import QueryDict +from django.template.backends.django import DjangoTemplates, Template +from django.template import Template as BaseTemplate from admin.base.schemas.utils import from_json from tests.base import AdminTestCase @@ -17,7 +20,7 @@ from osf.models import Subject, OSFUser, Collection from osf.models.provider import rules_to_subjects -from admin.base.utils import get_subject_rules, change_embargo_date +from admin.base.utils import get_subject_rules, change_embargo_date, render_bad_request_response from osf.admin import OSFUserAdmin @@ -255,3 +258,12 @@ def test_from_json(self): def test_from_json__file_not_found(self): with pytest.raises(Exception): from_json('file-info-schema2.json') + + +class TestRenderBadRequestResponse: + @mock.patch('django.template.loader.get_template') + def test_render_bad_request_response(self, mock_get_template): + mock_get_template.return_value = Template(BaseTemplate(''), DjangoTemplates({'NAME': '', 'DIRS': '', 'APP_DIRS': '', 'OPTIONS': {}})) + response = render_bad_request_response(RequestFactory(), 'test') + assert response is not None + mock_get_template.assert_called() diff --git a/admin_tests/entitlements/test_views.py b/admin_tests/entitlements/test_views.py deleted file mode 100644 index d2816018eaa..00000000000 --- a/admin_tests/entitlements/test_views.py +++ /dev/null @@ -1,841 +0,0 @@ -from urllib.parse import urlencode - -import mock -import pytest -from admin.entitlements import views -from admin_tests.utilities import setup_user_view -from django.contrib.auth.models import Permission -from django.core.exceptions import PermissionDenied -from django.db.models.query import QuerySet -from django.test import RequestFactory -from django.urls import reverse -from nose import tools as nt -from osf.models.institution_entitlement import InstitutionEntitlement -from osf_tests.factories import ( - AuthUserFactory, - InstitutionFactory, - InstitutionEntitlementFactory, -) -from tests.base import AdminTestCase -from django.contrib.auth.models import AnonymousUser -from django.http import Http404 -from django.http import HttpResponseBadRequest - -pytestmark = pytest.mark.django_db - - -class TestInstitutionEntitlementList(AdminTestCase): - def setUp(self): - self.institution01 = InstitutionFactory(name='inst01') - self.institution02 = InstitutionFactory(name='inst02') - - self.change_permission = Permission.objects.get(codename='admin_institution_entitlement') - - # Not login user - self.anon = AnonymousUser() - - # Admin user - self.institution01_admin = AuthUserFactory(fullname='admin001_inst01') - self.institution01_admin.is_staff = True - self.institution01_admin.affiliated_institutions.add(self.institution01) - self.institution01_admin.user_permissions.add(self.change_permission) - self.institution01_admin.save() - - self.institution02_admin = AuthUserFactory(fullname='admin001_inst02') - self.institution02_admin.is_staff = True - self.institution02_admin.affiliated_institutions.add(self.institution02) - self.institution02_admin.user_permissions.add(self.change_permission) - self.institution02_admin.save() - - # Super user - self.user_2 = AuthUserFactory(fullname='Jeff Hardy') - self.user_2_alternate_email = 'brothernero@delapidatedboat.com' - self.user_2.emails.create(address=self.user_2_alternate_email) - self.user_2.is_superuser = True - self.user_2.save() - - self.user = AuthUserFactory() - self.user.save() - - self.request = RequestFactory().get('/fake_path') - - self.view = views.InstitutionEntitlementList() - - def test_get_context_data_is_super_admin(self): - self.institution = InstitutionFactory() - self.institution.name = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAA' - self.institutionEntitlement = InstitutionEntitlementFactory(institution=self.institution, - login_availability=True, modifier=self.user_2) - self.institution.save() - - request = RequestFactory().get('/fake_path', kwargs={'institution_id': self.institution.id}) - - self.view.request = request - self.view.request.user = self.user_2 - self.view.kwargs = {'institution_id': self.institution.id} - self.view.object_list = self.view.get_queryset() - res = self.view.get_context_data() - - nt.assert_is_instance(res, dict) - nt.assert_equal(res['institutions'][0].id, self.institution.id) - nt.assert_equal(res['selected_id'], self.institution.id) - nt.assert_is_instance(res['entitlements'], QuerySet) - nt.assert_is_instance(res['entitlements'][0], InstitutionEntitlement) - nt.assert_equal(res['entitlements'][0], self.institutionEntitlement) - - def test_get_context_data_is_admin_and_has_affiliated_institutions(self): - self.institution = InstitutionFactory() - self.institution.name = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAA' - self.institutionEntitlement = InstitutionEntitlementFactory(institution=self.institution, - login_availability=True, modifier=self.user_2) - self.institution.save() - - self.user_2.is_staff = True - self.user_2.is_superuser = False - self.user_2.affiliated_institutions.add(self.institution) - self.user_2.save() - - request = RequestFactory().get('/fake_path', kwargs={'institution_id': self.institution.id}) - self.view.request = request - self.view.request.user = self.user_2 - self.view.kwargs = {'institution_id': self.institution.id} - self.view.object_list = self.view.get_queryset() - res = self.view.get_context_data() - - nt.assert_is_instance(res, dict) - nt.assert_equal(res['institutions'][0].id, self.institution.id) - nt.assert_equal(res['selected_id'], self.institution.id) - nt.assert_is_instance(res['entitlements'], QuerySet) - nt.assert_is_instance(res['entitlements'][0], InstitutionEntitlement) - nt.assert_equal(res['entitlements'][0], self.institutionEntitlement) - - def test_get_context_data_raise_PermissionDenied(self): - self.institution = InstitutionFactory() - self.institution.name = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAA' - self.institutionEntitlement = InstitutionEntitlementFactory(institution=self.institution, - login_availability=True, modifier=self.user_2) - self.institution.save() - - self.user_2.is_staff = False - self.user_2.is_superuser = False - self.user_2.save() - - request = RequestFactory().get('/fake_path', kwargs={'institution_id': self.institution.id}) - self.view.request = request - self.view.request.user = self.user_2 - self.view.kwargs = {'institution_id': self.institution.id} - self.view.object_list = self.view.get_queryset() - - with self.assertRaises(PermissionDenied): - self.view.get_context_data() - - def test_get_queryset(self): - institution1 = InstitutionFactory() - institution2 = InstitutionFactory() - institution_entitlement1 = InstitutionEntitlementFactory(institution=institution1, login_availability=True) - institution_entitlement2 = InstitutionEntitlementFactory(institution=institution2, login_availability=True) - self.view = setup_user_view(self.view, self.request, user=self.user) - - institution_entitlements = list(self.view.get_queryset()) - - institution_entitlement_list = [institution_entitlement1, institution_entitlement2] - # Create a list ordered by entitlement - institution_entitlement_list = sorted(institution_entitlement_list, key=lambda item: item.entitlement) - - nt.assert_equals(set(institution_entitlements), set(institution_entitlement_list)) - nt.assert_is_instance(institution_entitlements[0], InstitutionEntitlement) - nt.assert_equal(len(self.view.get_queryset()), 2) - - def test_InstitutionEntitlementList_correct_view_permissions(self): - user = AuthUserFactory() - user.is_superuser = True - - self.institution = InstitutionFactory() - self.institution.name = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAA' - self.institutionEntitlement = InstitutionEntitlementFactory(institution=self.institution, - login_availability=True, modifier=user) - self.institution.save() - - self.view_permission = views.InstitutionEntitlementList - - change_permission = Permission.objects.get(codename='admin_institution_entitlement') - user.user_permissions.add(change_permission) - user.save() - - request = RequestFactory().get(reverse('institutions:entitlements')) - request.user = user - - response = self.view_permission.as_view()(request) - self.assertEqual(response.status_code, 200) - - def test_InstitutionEntitlementList_no_user_permissions_raises_error(self): - user = AuthUserFactory() - request = RequestFactory().get(reverse('institutions:entitlements')) - request.user = user - - with self.assertRaises(PermissionDenied): - views.InstitutionEntitlementList.as_view()(request) - - def test_InstitutionEntitlementList_anonymous(self): - request = RequestFactory().get(reverse('institutions:entitlements')) - request.user = self.anon - - with self.assertRaises(PermissionDenied): - views.InstitutionEntitlementList.as_view()(request) - - def test_InstitutionEntitlementList_admin_with_permission(self): - self.view_permission = views.InstitutionEntitlementList - request = RequestFactory().get(reverse('institutions:entitlements') - + '?institution_id=' + str(self.institution01.id)) - request.user = self.institution01_admin - - response = self.view_permission.as_view()(request) - self.assertEqual(response.status_code, 200) - - def test_InstitutionEntitlementList_admin_without_permission(self): - # institution_id not same institution of login user - request = RequestFactory().get(reverse('institutions:entitlements') - + '?institution_id=' + str(self.institution01.id)) - request.user = self.institution02_admin - - with self.assertRaises(PermissionDenied): - views.InstitutionEntitlementList.as_view()(request) - - # admin not in institution - request = RequestFactory().get(reverse('institutions:entitlements') - + '?institution_id=' + str(self.institution01.id)) - self.institution02_admin.affiliated_institutions = [] - request.user = self.institution02_admin - - with self.assertRaises(PermissionDenied): - views.InstitutionEntitlementList.as_view()(request) - - def test_InstitutionEntitlementList_not_exist_institution(self): - request = RequestFactory().get(reverse('institutions:entitlements') + '?institution_id=1234') - request.user = self.user_2 - - with self.assertRaises(Http404): - views.InstitutionEntitlementList.as_view()(request) - - @mock.patch('admin.entitlements.views.render_bad_request_response') - def test_InstitutionEntitlementList_not_valid_institution(self, mock_render): - mock_render.return_value = HttpResponseBadRequest(content='fake') - request = RequestFactory().get(reverse('institutions:entitlements') + '?institution_id=fake_id') - request.user = self.user_2 - - response = views.InstitutionEntitlementList.as_view()(request) - self.assertEqual(response.status_code, 400) - - -class TestBulkAddInstitutionEntitlement(AdminTestCase): - - def setUp(self): - self.institution01 = InstitutionFactory(name='inst01') - self.institution02 = InstitutionFactory(name='inst02') - self.entitlement_1 = InstitutionEntitlementFactory(institution=self.institution01) - self.entitlement_2 = InstitutionEntitlementFactory(institution=self.institution02) - - self.anon = AnonymousUser() - - self.change_permission = Permission.objects.get(codename='admin_institution_entitlement') - - self.normal_user = AuthUserFactory(fullname='normal_user') - self.normal_user.is_staff = False - self.normal_user.is_superuser = False - - self.superuser = AuthUserFactory(fullname='superuser') - self.superuser.is_staff = True - self.superuser.is_superuser = True - self.superuser.user_permissions.add(self.change_permission) - self.superuser.save() - - self.institution01_admin = AuthUserFactory(fullname='admin001_inst01') - self.institution01_admin.is_staff = True - self.institution01_admin.affiliated_institutions.add(self.institution01) - self.institution01_admin.user_permissions.add(self.change_permission) - self.institution01_admin.save() - - self.institution02_admin = AuthUserFactory(fullname='admin001_inst02') - self.institution02_admin.is_staff = True - self.institution02_admin.affiliated_institutions.add(self.institution02) - self.institution02_admin.user_permissions.add(self.change_permission) - self.institution02_admin.save() - - self.view = views.BulkAddInstitutionEntitlement.as_view() - self.view_permission = views.BulkAddInstitutionEntitlement - - @mock.patch('admin.entitlements.views.InstitutionEntitlement.objects.create') - def test_post_entitlement_find(self, mockApi): - request = RequestFactory().post( - reverse('entitlements:bulk_add'), { - 'institution_id': self.institution01.id, - 'entitlements': ['gkn1-ent111'], - 'login_availability': ['on'] - } - ) - - self.entitlement_1.institution_id = self.institution01.id - self.entitlement_1.entitlement = 'gkn1-ent111' - self.entitlement_1.save() - - request.user = self.institution01_admin - response = self.view(request) - - mockApi.assert_not_called() - nt.assert_equal(response.status_code, 302) - - @mock.patch('admin.entitlements.views.InstitutionEntitlement.objects.create') - def test_post_entitlement_not_found(self, mockApi): - request = RequestFactory().post( - reverse('entitlements:bulk_add'), { - 'institution_id': self.institution01.id, - 'entitlements': ['gkn1-ent11', 'gkn1-ent12', 'gkn1-ent13'], - 'login_availability': ['on', 'on', 'on'] - } - ) - request.user = self.institution01_admin - - response = self.view(request) - - mockApi.assert_called() - nt.assert_equal(response.status_code, 302) - - @pytest.mark.skip - def test_BulkAddInstitutionEntitlement_no_user_permissions_raises_error(self): - user = AuthUserFactory() - - request = RequestFactory().post(reverse('entitlements:bulk_add')) - request.user = user - - with self.assertRaises(PermissionDenied): - self.view_permission.as_view()(request) - - def test_BulkAddInstitutionEntitlement_correct_view_permissions(self): - institution = InstitutionFactory() - user = AuthUserFactory() - user.affiliated_institutions.add(institution) - user.is_staff = True - user.save() - - change_permission = Permission.objects.get(codename='admin_institution_entitlement') - user.user_permissions.add(change_permission) - user.save() - data = {'institution_id': institution.id, - 'entitlements': 'demo', - 'login_availability': 'on' - } - request = RequestFactory().post(reverse('entitlements:bulk_add'), data=data) - request.user = user - - response = self.view_permission.as_view()(request) - self.assertEqual(response.status_code, 302) - - def test_permission_anonymous(self): - request = RequestFactory().post(reverse('entitlements:bulk_add')) - request.user = self.anon - - with self.assertRaises(PermissionDenied): - self.view_permission.as_view()(request) - - def test_permission_normal_user(self): - data = {'institution_id': self.institution01.id, - 'entitlements': 'demo super', - 'login_availability': 'on' - } - request = RequestFactory().post(reverse('entitlements:bulk_add'), data=data) - request.user = self.normal_user - - with self.assertRaises(PermissionDenied): - self.view_permission.as_view()(request) - - def test_permission_super(self): - data = {'institution_id': self.institution01.id, - 'entitlements': 'demo super', - 'login_availability': 'on' - } - request = RequestFactory().post(reverse('entitlements:bulk_add'), data=data) - request.user = self.superuser - response = self.view_permission.as_view()(request) - self.assertEqual(response.status_code, 302) - - def test_permission_admin_with_permission(self): - data = {'institution_id': self.institution01.id, - 'entitlements': 'demo super', - 'login_availability': 'on' - } - request = RequestFactory().post(reverse('entitlements:bulk_add'), data=data) - request.user = self.institution01_admin - response = self.view_permission.as_view()(request) - self.assertEqual(response.status_code, 302) - - def test_permission_admin_without_permission(self): - data = {'institution_id': self.institution01.id, - 'entitlements': 'demo super', - 'login_availability': 'on' - } - request = RequestFactory().post(reverse('entitlements:bulk_add'), data=data) - request.user = self.institution02_admin - - with self.assertRaises(PermissionDenied): - self.view_permission.as_view()(request) - - def test_permission_not_exist_inst(self): - data = {'institution_id': 0, - 'entitlements': 'demo super', - 'login_availability': 'on' - } - request = RequestFactory().post(reverse('entitlements:bulk_add'), data=data) - request.user = self.superuser - with self.assertRaises(Http404): - self.view_permission.as_view()(request) - - @mock.patch('admin.entitlements.views.render_bad_request_response') - def test__institution_id_none(self, mock_render): - mock_render.return_value = HttpResponseBadRequest(content='fake') - data = {'entitlements': 'demo super', - 'login_availability': 'on' - } - request = RequestFactory().post(reverse('entitlements:bulk_add'), data=data) - request.user = self.superuser - result = self.view_permission.as_view()(request) - self.assertEqual(result.status_code, 400) - - @mock.patch('admin.entitlements.views.render_bad_request_response') - def test__institution_id_invalid_format(self, mock_render): - mock_render.return_value = HttpResponseBadRequest(content='fake') - data = {'institution_id': 'fake_id', - 'entitlements': 'demo super', - 'login_availability': 'on' - } - request = RequestFactory().post(reverse('entitlements:bulk_add'), data=data) - request.user = self.superuser - result = self.view_permission.as_view()(request) - self.assertEqual(result.status_code, 400) - - @mock.patch('admin.entitlements.views.render_bad_request_response') - def test__institution_id_not_exist(self, mock_render): - mock_render.return_value = HttpResponseBadRequest(content='fake') - data = {'institution_id': 0, - 'entitlements': 'demo super', - 'login_availability': 'on' - } - request = RequestFactory().post(reverse('entitlements:bulk_add'), data=data) - request.user = self.superuser - with self.assertRaises(Http404): - self.view_permission.as_view()(request) - - -class TestToggleInstitutionEntitlement(AdminTestCase): - - def setUp(self): - self.institution01 = InstitutionFactory(name='inst01') - self.institution02 = InstitutionFactory(name='inst02') - self.entitlement_1 = InstitutionEntitlementFactory(institution=self.institution01) - self.entitlement_2 = InstitutionEntitlementFactory(institution=self.institution02) - - self.anon = AnonymousUser() - - self.change_permission = Permission.objects.get(codename='admin_institution_entitlement') - - self.normal_user = AuthUserFactory(fullname='normal_user') - self.normal_user.is_staff = False - self.normal_user.is_superuser = False - - self.superuser = AuthUserFactory(fullname='superuser') - self.superuser.is_staff = True - self.superuser.is_superuser = True - self.superuser.user_permissions.add(self.change_permission) - self.superuser.save() - - self.institution01_admin = AuthUserFactory(fullname='admin001_inst01') - self.institution01_admin.is_staff = True - self.institution01_admin.affiliated_institutions.add(self.institution01) - self.institution01_admin.user_permissions.add(self.change_permission) - self.institution01_admin.save() - - self.institution02_admin = AuthUserFactory(fullname='admin001_inst02') - self.institution02_admin.is_staff = True - self.institution02_admin.affiliated_institutions.add(self.institution02) - self.institution02_admin.user_permissions.add(self.change_permission) - self.institution02_admin.save() - - self.view = views.ToggleInstitutionEntitlement.as_view() - self.view_permission = views.ToggleInstitutionEntitlement - - def test_post_method(self): - url = reverse('institutions:entitlement_toggle', - kwargs={'institution_id': self.institution01.id, 'entitlement_id': self.entitlement_1.id}) - request = RequestFactory().post(url) - request.user = self.superuser - - response = self.view(request, - institution_id=self.institution01.id, - entitlement_id=self.entitlement_1.id) - - base_url = reverse('institutions:entitlements') - query_string = urlencode({'institution_id': self.institution01.id, 'page': 1}) - - nt.assert_equal(response.status_code, 302) - nt.assert_equal(response.url, '{}?{}'.format(base_url, query_string)) - - @pytest.mark.skip - def test_ToggleInstitutionEntitlement_no_user_permissions_raises_error(self): - user = AuthUserFactory() - request = RequestFactory().post( - reverse('institutions:entitlement_toggle', - kwargs={'institution_id': self.institution01.id, 'entitlement_id': self.entitlement_1.id}) - ) - request.user = user - - with self.assertRaises(PermissionDenied): - self.view_permission.as_view()(request) - - def test_ToggleInstitutionEntitlement_correct_view_permissions(self): - user = AuthUserFactory() - - change_permission = Permission.objects.get(codename='admin_institution_entitlement') - user.user_permissions.add(change_permission) - user.is_staff = True - user.affiliated_institutions.add(self.institution01) - user.save() - - request = RequestFactory().post( - reverse('institutions:entitlement_toggle', - kwargs={'institution_id': self.institution01.id, 'entitlement_id': self.entitlement_1.id}) - ) - request.user = user - - response = self.view_permission.as_view()(request, - institution_id=self.institution01.id, - entitlement_id=self.entitlement_1.id) - self.assertEqual(response.status_code, 302) - - def test_permission_anonymous(self): - request = RequestFactory().post( - reverse('institutions:entitlement_toggle', - kwargs={'institution_id': self.institution01.id, 'entitlement_id': self.entitlement_1.id}) - ) - request.user = self.anon - - with self.assertRaises(PermissionDenied): - self.view_permission.as_view()(request, - institution_id=self.institution01.id, - entitlement_id=self.entitlement_1.id) - - def test_permission_normal_user(self): - request = RequestFactory().post( - reverse('institutions:entitlement_toggle', - kwargs={'institution_id': self.institution01.id, 'entitlement_id': self.entitlement_1.id}) - ) - request.user = self.normal_user - - with self.assertRaises(PermissionDenied): - self.view_permission.as_view()(request, - institution_id=self.institution01.id, - entitlement_id=self.entitlement_1.id) - - def test_permission_super(self): - request = RequestFactory().post( - reverse('institutions:entitlement_toggle', - kwargs={'institution_id': self.institution01.id, 'entitlement_id': self.entitlement_1.id}) - ) - request.user = self.superuser - response = self.view_permission.as_view()(request, - institution_id=self.institution01.id, - entitlement_id=self.entitlement_1.id) - self.assertEqual(response.status_code, 302) - - def test_permission_admin_with_permission(self): - request = RequestFactory().post( - reverse('institutions:entitlement_toggle', - kwargs={'institution_id': self.institution01.id, 'entitlement_id': self.entitlement_1.id}) - ) - request.user = self.institution01_admin - response = self.view_permission.as_view()(request, - institution_id=self.institution01.id, - entitlement_id=self.entitlement_1.id) - self.assertEqual(response.status_code, 302) - - def test_permission_admin_without_permission(self): - # institution_id not same institution of login user - request = RequestFactory().post( - reverse('institutions:entitlement_toggle', - kwargs={'institution_id': self.institution01.id, 'entitlement_id': self.entitlement_1.id}) - ) - request.user = self.institution02_admin - - with self.assertRaises(PermissionDenied): - self.view_permission.as_view()(request, - institution_id=self.institution01.id, - entitlement_id=self.entitlement_1.id) - - # admin not in institution - request = RequestFactory().post( - reverse('institutions:entitlement_toggle', - kwargs={'institution_id': self.institution01.id, 'entitlement_id': self.entitlement_1.id}) - ) - self.institution02_admin.affiliated_institutions = [] - request.user = self.institution02_admin - - with self.assertRaises(PermissionDenied): - self.view_permission.as_view()(request, - institution_id=self.institution01.id, - entitlement_id=self.entitlement_1.id) - - def test_permission_super_not_exist_inst(self): - request = RequestFactory().post( - reverse('institutions:entitlement_toggle', - kwargs={'institution_id': 0, 'entitlement_id': self.entitlement_1.id}) - ) - request.user = self.superuser - with self.assertRaises(Http404): - self.view_permission.as_view()(request, - institution_id=0, entitlement_id=self.entitlement_1.id) - - def test_permission_super_not_exist_entitlement(self): - request = RequestFactory().post( - reverse('institutions:entitlement_toggle', - kwargs={'institution_id': self.institution01.id, 'entitlement_id': 0}) - ) - request.user = self.superuser - with self.assertRaises(Http404): - self.view_permission.as_view()(request, - institution_id=self.institution01.id, entitlement_id=0) - - def test_permission_with_entitlement_not_in_institution(self): - request = RequestFactory().post( - reverse('institutions:entitlement_toggle', - kwargs={'institution_id': self.institution01.id, 'entitlement_id': self.entitlement_2.id}) - ) - request.user = self.superuser - with self.assertRaises(PermissionDenied): - self.view_permission.as_view()(request, - institution_id=self.institution01.id, - entitlement_id=self.entitlement_2.id) - - def test_permission_admin_not_exist_inst(self): - request = RequestFactory().post( - reverse('institutions:entitlement_toggle', - kwargs={'institution_id': 0, 'entitlement_id': self.entitlement_1.id}) - ) - request.user = self.institution01_admin - with self.assertRaises(Http404): - self.view_permission.as_view()(request, institution_id=0, - entitlement_id=self.entitlement_1.id) - - def test_permission_admin_not_exist_entitlement(self): - request = RequestFactory().post( - reverse('institutions:entitlement_toggle', - kwargs={'institution_id': self.institution01.id, 'entitlement_id': 0}) - ) - request.user = self.institution01_admin - with self.assertRaises(Http404): - self.view_permission.as_view()(request, - institution_id=self.institution01.id, entitlement_id=0) - - -class TestDeleteInstitutionEntitlement(AdminTestCase): - - def setUp(self): - self.institution01 = InstitutionFactory(name='inst01') - self.institution02 = InstitutionFactory(name='inst02') - self.entitlement_1 = InstitutionEntitlementFactory(institution=self.institution01) - self.entitlement_2 = InstitutionEntitlementFactory(institution=self.institution02) - - self.anon = AnonymousUser() - - self.change_permission = Permission.objects.get(codename='admin_institution_entitlement') - - self.normal_user = AuthUserFactory(fullname='normal_user') - self.normal_user.is_staff = False - self.normal_user.is_superuser = False - - self.superuser = AuthUserFactory(fullname='superuser') - self.superuser.is_staff = True - self.superuser.is_superuser = True - self.superuser.user_permissions.add(self.change_permission) - self.superuser.save() - - self.institution01_admin = AuthUserFactory(fullname='admin001_inst01') - self.institution01_admin.is_staff = True - self.institution01_admin.affiliated_institutions.add(self.institution01) - self.institution01_admin.user_permissions.add(self.change_permission) - self.institution01_admin.save() - - self.institution02_admin = AuthUserFactory(fullname='admin001_inst02') - self.institution02_admin.is_staff = True - self.institution02_admin.affiliated_institutions.add(self.institution02) - self.institution02_admin.user_permissions.add(self.change_permission) - self.institution02_admin.save() - - self.view = views.DeleteInstitutionEntitlement.as_view() - self.view_permission = views.DeleteInstitutionEntitlement - - @pytest.mark.skip - def test_DeleteInstitutionEntitlement_no_user_permissions_raises_error(self): - user = AuthUserFactory() - request = RequestFactory().get('/fake_path') - request.user = user - - with self.assertRaises(PermissionDenied): - self.view_permission.as_view()(request, institution_id=self.institution01.id, - entitlement_id=self.entitlement_1.id) - - def test_DeleteInstitutionEntitlement_correct_view_permissions(self): - user = AuthUserFactory() - - change_permission = Permission.objects.get(codename='admin_institution_entitlement') - user.user_permissions.add(change_permission) - user.affiliated_institutions.add(self.institution01) - user.is_staff = True - user.save() - - request = RequestFactory().post(reverse('institutions:entitlement_delete', - kwargs={'institution_id': self.institution01.id, - 'entitlement_id': self.entitlement_1.id}) - ) - request.user = user - - response = self.view_permission.as_view()(request, - institution_id=self.institution01.id, - entitlement_id=self.entitlement_1.id) - self.assertEqual(response.status_code, 302) - - def test_post_method(self): - url = reverse('institutions:entitlement_delete', - kwargs={'institution_id': self.institution01.id, 'entitlement_id': self.entitlement_1.id}) - request = RequestFactory().post(url) - request.user = self.superuser - - response = self.view(request, - institution_id=self.institution01.id, - entitlement_id=self.entitlement_1.id) - - base_url = reverse('institutions:entitlements') - query_string = urlencode({'institution_id': self.institution01.id, 'page': 1}) - - nt.assert_equal(response.status_code, 302) - nt.assert_equal(response.url, '{}?{}'.format(base_url, query_string)) - - def test_permission_anonymous(self): - request = RequestFactory().post( - reverse('institutions:entitlement_delete', - kwargs={'institution_id': self.institution01.id, 'entitlement_id': self.entitlement_1.id}) - ) - request.user = self.anon - - with self.assertRaises(PermissionDenied): - self.view_permission.as_view()(request, - institution_id=self.institution01.id, - entitlement_id=self.entitlement_1.id) - - def test_permission_normal_user(self): - request = RequestFactory().post( - reverse('institutions:entitlement_delete', - kwargs={'institution_id': self.institution01.id, 'entitlement_id': self.entitlement_1.id}) - ) - request.user = self.normal_user - - with self.assertRaises(PermissionDenied): - self.view_permission.as_view()(request, - institution_id=self.institution01.id, - entitlement_id=self.entitlement_1.id) - - def test_permission_super(self): - request = RequestFactory().post( - reverse('institutions:entitlement_delete', - kwargs={'institution_id': self.institution01.id, 'entitlement_id': self.entitlement_1.id}) - ) - request.user = self.superuser - response = self.view_permission.as_view()(request, - institution_id=self.institution01.id, - entitlement_id=self.entitlement_1.id) - self.assertEqual(response.status_code, 302) - - def test_permission_admin_with_permission(self): - request = RequestFactory().post( - reverse('institutions:entitlement_delete', - kwargs={'institution_id': self.institution01.id, 'entitlement_id': self.entitlement_1.id}) - ) - request.user = self.institution01_admin - response = self.view_permission.as_view()(request, - institution_id=self.institution01.id, - entitlement_id=self.entitlement_1.id) - self.assertEqual(response.status_code, 302) - - def test_permission_admin_without_permission(self): - # institution_id not same institution of login user - request = RequestFactory().post( - reverse('institutions:entitlement_delete', - kwargs={'institution_id': self.institution01.id, 'entitlement_id': self.entitlement_1.id}) - ) - request.user = self.institution02_admin - - with self.assertRaises(PermissionDenied): - self.view_permission.as_view()(request, - institution_id=self.institution01.id, - entitlement_id=self.entitlement_1.id) - - # admin not in institution - request = RequestFactory().post( - reverse('institutions:entitlement_delete', - kwargs={'institution_id': self.institution01.id, 'entitlement_id': self.entitlement_1.id}) - ) - self.institution02_admin.affiliated_institutions = [] - request.user = self.institution02_admin - - with self.assertRaises(PermissionDenied): - self.view_permission.as_view()(request, - institution_id=self.institution01.id, - entitlement_id=self.entitlement_1.id) - - def test_permission_super_not_exist_inst(self): - request = RequestFactory().post( - reverse('institutions:entitlement_delete', - kwargs={'institution_id': 0, 'entitlement_id': self.entitlement_1.id}) - ) - request.user = self.superuser - with self.assertRaises(Http404): - self.view_permission.as_view()(request, institution_id=0, - entitlement_id=self.entitlement_1.id) - - def test_permission_super_not_exist_entitlement(self): - request = RequestFactory().post( - reverse('institutions:entitlement_delete', - kwargs={'institution_id': self.institution01.id, 'entitlement_id': 0}) - ) - request.user = self.superuser - with self.assertRaises(Http404): - self.view_permission.as_view()(request, - institution_id=self.institution01.id, entitlement_id=0) - - def test_permission_with_entitlement_not_in_institution(self): - request = RequestFactory().post( - reverse('institutions:entitlement_delete', - kwargs={'institution_id': self.institution01.id, 'entitlement_id': self.entitlement_2.id}) - ) - request.user = self.superuser - with self.assertRaises(PermissionDenied): - self.view_permission.as_view()(request, - institution_id=self.institution01.id, - entitlement_id=self.entitlement_2.id) - - def test_permission_admin_not_exist_inst(self): - request = RequestFactory().post( - reverse('institutions:entitlement_delete', - kwargs={'institution_id': 0, 'entitlement_id': self.entitlement_1.id}) - ) - request.user = self.institution01_admin - with self.assertRaises(Http404): - self.view_permission.as_view()(request, institution_id=0, - entitlement_id=self.entitlement_1.id) - - def test_permission_admin_not_exist_entitlement(self): - request = RequestFactory().post( - reverse('institutions:entitlement_delete', - kwargs={'institution_id': self.institution01.id, 'entitlement_id': 0}) - ) - request.user = self.institution01_admin - with self.assertRaises(Http404): - self.view_permission.as_view()(request, - institution_id=self.institution01.id, entitlement_id=0) diff --git a/admin_tests/entitlements/__init__.py b/admin_tests/login_access_control/__init__.py similarity index 100% rename from admin_tests/entitlements/__init__.py rename to admin_tests/login_access_control/__init__.py diff --git a/admin_tests/login_access_control/test_utils.py b/admin_tests/login_access_control/test_utils.py new file mode 100644 index 00000000000..359cbbf8a3c --- /dev/null +++ b/admin_tests/login_access_control/test_utils.py @@ -0,0 +1,49 @@ +import pytest +from nose import tools as nt +from admin.login_access_control import utils +from osf_tests.factories import InstitutionFactory + + +class TestValidateValue: + def test_validate_integer(self): + name = 'attribute_id' + nt.assert_is_none(utils.validate_integer(1, name)) + nt.assert_is_not_none(utils.validate_integer(None, name)) + nt.assert_is_not_none(utils.validate_integer('test', name)) + + def test_validate_boolean(self): + name = 'is_availability' + nt.assert_is_none(utils.validate_boolean(True, name)) + nt.assert_is_none(utils.validate_boolean(False, name)) + nt.assert_is_not_none(utils.validate_boolean(None, name)) + nt.assert_is_not_none(utils.validate_boolean('test', name)) + + @pytest.mark.django_db + def test_validate_institution_id(self): + institution = InstitutionFactory() + nt.assert_is_none(utils.validate_institution_id(institution.id)) + nt.assert_is_not_none(utils.validate_institution_id(None)) + nt.assert_is_not_none(utils.validate_institution_id('test')) + nt.assert_is_not_none(utils.validate_institution_id(0)) + + def test_validate_logic_condition(self): + nt.assert_true(utils.validate_logic_condition('1&&(2||!3)')) + nt.assert_true(utils.validate_logic_condition('')) + nt.assert_true(utils.validate_logic_condition(None)) + nt.assert_true(utils.validate_logic_condition('12')) + nt.assert_true(utils.validate_logic_condition('!((2))')) + + nt.assert_false(utils.validate_logic_condition(1234)) + nt.assert_false(utils.validate_logic_condition('!a')) + nt.assert_false(utils.validate_logic_condition('1@@2++3--4<5?6>')) + nt.assert_false(utils.validate_logic_condition('1&&&&2')) + nt.assert_false(utils.validate_logic_condition('1|||2')) + nt.assert_false(utils.validate_logic_condition('!((2)')) + nt.assert_false(utils.validate_logic_condition('()')) + + def test_has_invalid_character(self): + nt.assert_true(utils.has_invalid_character('1@@2++3--4<5?6>')) + nt.assert_true(utils.has_invalid_character('a||b')) + nt.assert_false(utils.has_invalid_character('1&&(2||!3)')) + nt.assert_false(utils.has_invalid_character('')) + nt.assert_false(utils.has_invalid_character('12')) diff --git a/admin_tests/login_access_control/test_views.py b/admin_tests/login_access_control/test_views.py new file mode 100644 index 00000000000..33325e4129f --- /dev/null +++ b/admin_tests/login_access_control/test_views.py @@ -0,0 +1,1751 @@ +import json +import mock +import pytest +from admin.login_access_control import views +from django.core.exceptions import PermissionDenied +from django.test import RequestFactory +from django.urls import reverse +from nose import tools as nt + +from admin.login_access_control.views import BaseLogicAccessControlUpdateView +from osf.models import LoginControlAuthenticationAttribute, LoginControlMailAddress +from osf_tests.factories import ( + AuthUserFactory, + InstitutionFactory, + LoginControlAuthenticationAttributeFactory, LoginControlMailAddressFactory, +) +from tests.base import AdminTestCase +from django.contrib.auth.models import AnonymousUser +from django.http import Http404, HttpResponseBadRequest + +pytestmark = pytest.mark.django_db + + +class TestLoginAccessControlListView(AdminTestCase): + def setUp(self): + self.institution01 = InstitutionFactory(name='inst01') + self.institution02 = InstitutionFactory(name='inst02') + + # Not login user + self.anon = AnonymousUser() + + # Admin user + self.institution01_admin = AuthUserFactory(fullname='admin001_inst01') + self.institution01_admin.is_staff = True + self.institution01_admin.affiliated_institutions.add(self.institution01) + self.institution01_admin.save() + + self.institution02_admin = AuthUserFactory(fullname='admin001_inst02') + self.institution02_admin.is_staff = True + self.institution02_admin.affiliated_institutions.add(self.institution02) + self.institution02_admin.save() + + # Super user + self.superuser = AuthUserFactory(fullname='Jeff Hardy') + self.superuser.is_superuser = True + self.superuser.save() + + self.user = AuthUserFactory() + self.user.save() + + self.request = RequestFactory().get('/fake_path') + + self.view = views.LoginAccessControlListView() + + def test_permission__anonymous(self): + request = RequestFactory().get(reverse('login_access_control:list')) + request.user = self.anon + + response = views.LoginAccessControlListView.as_view()(request) + self.assertEqual(response.status_code, 302) + + def test_permission__admin_with_permission(self): + request = RequestFactory().get(reverse('login_access_control:list') + '?institution_id=' + str(self.institution01.id)) + request.user = self.institution01_admin + + response = views.LoginAccessControlListView.as_view()(request) + self.assertEqual(response.status_code, 200) + + def test_permission__admin_without_permission(self): + # institution_id not same institution of login user + request = RequestFactory().get(reverse('login_access_control:list') + '?institution_id=' + str(self.institution01.id)) + request.user = self.institution02_admin + + with self.assertRaises(PermissionDenied): + views.LoginAccessControlListView.as_view()(request) + + # admin not in institution + request = RequestFactory().get(reverse('login_access_control:list') + '?institution_id=' + str(self.institution01.id)) + self.institution02_admin.affiliated_institutions = [] + request.user = self.institution02_admin + + with self.assertRaises(PermissionDenied): + views.LoginAccessControlListView.as_view()(request) + + def test_permission__institution_not_exist(self): + request = RequestFactory().get(reverse('login_access_control:list') + '?institution_id=1234') + request.user = self.superuser + + with self.assertRaises(Http404): + views.LoginAccessControlListView.as_view()(request) + + def test_get_queryset(self): + queryset = self.view.get_queryset() + nt.assert_false(queryset.exists()) + + def test_get(self): + request = RequestFactory().get(f'/fake_path') + + self.view.request = request + self.view.request.user = self.superuser + self.view.kwargs = {} + self.view.object_list = self.view.get_queryset() + response = self.view.get(request) + self.assertEqual(response.status_code, 200) + + @mock.patch('admin.login_access_control.views.render_bad_request_response') + def test_get__invalid_institution_id(self, mock_bad_request_response): + mock_bad_request_response.return_value = HttpResponseBadRequest(content='institution_id is invalid.') + request = RequestFactory().get('/fake_path?institution_id=test') + self.view.request = request + self.view.request.user = self.superuser + self.view.kwargs = {'institution_id': 'test'} + self.view.object_list = self.view.get_queryset() + response = self.view.get(request) + self.assertEqual(response.status_code, 400) + + def test_get__institution_id_not_exist(self): + request = RequestFactory().get(f'/fake_path?institution_id=-1') + self.view.request = request + self.view.request.user = self.superuser + self.view.kwargs = {'institution_id': -1} + self.view.object_list = self.view.get_queryset() + + with self.assertRaises(Http404): + self.view.get(request) + + def test_get_context_data__super_admin(self): + authentication_attribute = LoginControlAuthenticationAttributeFactory(institution=self.institution01) + + request = RequestFactory().get(f'/fake_path') + + self.view.request = request + self.view.request.user = self.superuser + self.view.kwargs = {} + self.view.object_list = self.view.get_queryset() + res = self.view.get_context_data() + + nt.assert_is_instance(res, dict) + nt.assert_equal(res['is_admin'], False) + nt.assert_equal(len(res['institutions']), 2) + nt.assert_equal(res['selected_institution'], self.institution01) + nt.assert_equal(res['login_control_authentication_attribute_list'], [authentication_attribute]) + + def test_get_context_data__super_admin_with_institution_id(self): + institution = InstitutionFactory(name='test') + authentication_attribute = LoginControlAuthenticationAttributeFactory(institution=institution) + + request = RequestFactory().get(f'/fake_path?institution_id={institution.id}') + + self.view.request = request + self.view.request.user = self.superuser + self.view.kwargs = {'institution_id': institution.id} + self.view.object_list = self.view.get_queryset() + res = self.view.get_context_data() + + nt.assert_is_instance(res, dict) + nt.assert_equal(res['is_admin'], False) + nt.assert_equal(len(res['institutions']), 3) + nt.assert_equal(res['selected_institution'], institution) + nt.assert_equal(res['login_control_authentication_attribute_list'], [authentication_attribute]) + + def test_get_context_data__admin(self): + authentication_attribute = LoginControlAuthenticationAttributeFactory(institution=self.institution01) + + request = RequestFactory().get(f'/fake_path') + self.view.request = request + self.view.request.user = self.institution01_admin + self.view.kwargs = {} + self.view.object_list = self.view.get_queryset() + res = self.view.get_context_data() + + nt.assert_is_instance(res, dict) + nt.assert_equal(res['is_admin'], True) + nt.assert_equal(len(res['institutions']), 1) + nt.assert_equal(res['selected_institution'], self.institution01) + nt.assert_equal(res['login_control_authentication_attribute_list'], [authentication_attribute]) + + def test_get_context_data__admin_with_institution_id(self): + institution = InstitutionFactory(name='test') + authentication_attribute = LoginControlAuthenticationAttributeFactory(institution=institution) + + self.institution01_admin.affiliated_institutions.add(institution) + self.institution01_admin.save() + + request = RequestFactory().get(f'/fake_path?institution_id={institution.id}') + self.view.request = request + self.view.request.user = self.institution01_admin + self.view.kwargs = {'institution_id': institution.id} + self.view.object_list = self.view.get_queryset() + res = self.view.get_context_data() + + nt.assert_is_instance(res, dict) + nt.assert_equal(res['is_admin'], True) + nt.assert_equal(len(res['institutions']), 2) + nt.assert_equal(res['selected_institution'], institution) + nt.assert_equal(res['login_control_authentication_attribute_list'], [authentication_attribute]) + + def test_get_context_data__no_institutions(self): + self.institution01.is_deleted = True + self.institution01.save() + + request = RequestFactory().get(f'/fake_path?institution_id={self.institution01.id}') + self.view.request = request + self.view.request.user = self.superuser + self.view.kwargs = {'institution_id': self.institution01.id} + self.view.object_list = self.view.get_queryset() + + with self.assertRaises(Http404): + self.view.get_context_data() + + def test_get_context_data__raise_permission_denied(self): + institution = InstitutionFactory(name='test') + + request = RequestFactory().get(f'/fake_path?institution_id={institution.id}') + self.view.request = request + self.view.request.user = self.institution02_admin + self.view.kwargs = {'institution_id': institution.id} + self.view.object_list = self.view.get_queryset() + + with self.assertRaises(PermissionDenied): + self.view.get_context_data() + + +class TestBaseLogicAccessControlUpdateView(AdminTestCase): + def setUp(self): + self.institution = InstitutionFactory(name='inst01') + + self.anon = AnonymousUser() + + self.normal_user = AuthUserFactory(fullname='normal_user') + + self.superuser = AuthUserFactory(fullname='superuser') + self.superuser.is_superuser = True + self.superuser.save() + + self.admin = AuthUserFactory(fullname='admin001') + self.admin.is_staff = True + self.admin.save() + + self.institution_admin = AuthUserFactory(fullname='admin001_inst01') + self.institution_admin.is_staff = True + self.institution_admin.affiliated_institutions.add(self.institution) + self.institution_admin.save() + + self.view = BaseLogicAccessControlUpdateView() + self.request = RequestFactory().post(f'/fake_path') + self.view.request = self.request + + def test_permission__anonymous(self): + self.view.request.user = self.anon + result = self.view.test_func() + nt.assert_false(result) + nt.assert_false(self.view.raise_exception) + + def test_permission__normal_user(self): + self.view.request.user = self.normal_user + result = self.view.test_func() + nt.assert_false(result) + nt.assert_true(self.view.raise_exception) + + def test_permission__super_admin(self): + self.view.request.user = self.superuser + result = self.view.test_func() + nt.assert_true(result) + + def test_permission__admin_with_no_institution(self): + self.view.request.user = self.admin + result = self.view.test_func() + nt.assert_false(result) + nt.assert_true(self.view.raise_exception) + + def test_permission__admin(self): + self.view.request.user = self.institution_admin + result = self.view.test_func() + nt.assert_true(result) + + def test_parse_json_request(self): + test_json = json.dumps({'institution_id': 1}) + request = RequestFactory().post( + '/fake_path', + json.dumps(test_json), + content_type='application/json' + ) + data, error = self.view.parse_json_request(request) + nt.assert_is_not_none(data) + nt.assert_is_none(error) + + def test_parse_json_request__invalid_json(self): + test_invalid_json = '{a}' + request = RequestFactory().post( + '/fake_path', + test_invalid_json, + content_type='application/json' + ) + data, error = self.view.parse_json_request(request) + nt.assert_is_none(data) + nt.assert_is_not_none(error) + + def test_parse_json_request__empty_json(self): + request = RequestFactory().post( + '/fake_path', + None, + content_type='application/json' + ) + data, error = self.view.parse_json_request(request) + nt.assert_is_none(data) + nt.assert_is_not_none(error) + + def test_is_affiliated_with_not_deleted_institution(self): + self.view.request.user = self.institution_admin + result = self.view.is_affiliated_with_not_deleted_institution(self.institution.id) + nt.assert_true(result) + + def test_is_affiliated_with_not_deleted_institution__false(self): + self.view.request.user = self.admin + result = self.view.is_affiliated_with_not_deleted_institution(self.institution.id) + nt.assert_false(result) + + +class TestUpdateLoginAvailabilityDefaultView(AdminTestCase): + def setUp(self): + self.institution = InstitutionFactory(name='inst01') + self.user = AuthUserFactory(fullname='superuser', is_superuser=True) + self.view = views.UpdateLoginAvailabilityDefaultView.as_view() + + def test_post_invalid_body_request(self): + request = RequestFactory().post( + reverse('login_access_control:update_login_availability_default'), + None, + content_type='application/json' + ) + + request.user = self.user + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + + def test_post_invalid_institution_id(self): + params = { + 'institution_id': 'test', + 'login_availability_default': False, + } + request = RequestFactory().post( + reverse('login_access_control:update_login_availability_default'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.user + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "institution_id is invalid."}') + + def test_post_invalid_login_availability_default(self): + params = { + 'institution_id': self.institution.id, + 'login_availability_default': 'test', + } + request = RequestFactory().post( + reverse('login_access_control:update_login_availability_default'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.user + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "login_availability_default is invalid."}') + + def test_post_admin_not_affiliated(self): + self.user.is_staff = True + self.user.is_superuser = False + self.user.affiliated_institutions.add(self.institution) + self.user.save() + + institution = InstitutionFactory(name='inst02') + + params = { + 'institution_id': institution.id, + 'login_availability_default': False, + } + request = RequestFactory().post( + reverse('login_access_control:update_login_availability_default'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.user + response = self.view(request) + + nt.assert_equal(response.status_code, 403) + nt.assert_equal(response.content, b'{"error_message": "You do not have permission to setting login access control of the other institution."}') + + def test_post(self): + params = { + 'institution_id': self.institution.id, + 'login_availability_default': False, + } + request = RequestFactory().post( + reverse('login_access_control:update_login_availability_default'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.user + response = self.view(request) + + nt.assert_equal(response.status_code, 204) + + +class TestSaveAuthenticationAttributeListView(AdminTestCase): + + def setUp(self): + self.institution01 = InstitutionFactory(name='inst01') + self.institution02 = InstitutionFactory(name='inst02') + + self.superuser = AuthUserFactory(fullname='superuser') + self.superuser.is_superuser = True + self.superuser.save() + + self.institution01_admin = AuthUserFactory(fullname='admin001_inst01') + self.institution01_admin.is_staff = True + self.institution01_admin.affiliated_institutions.add(self.institution01) + self.institution01_admin.save() + + self.institution02_admin = AuthUserFactory(fullname='admin001_inst02') + self.institution02_admin.is_staff = True + self.institution02_admin.affiliated_institutions.add(self.institution02) + self.institution02_admin.save() + + self.view = views.SaveAuthenticationAttributeListView.as_view() + self.view_permission = views.SaveAuthenticationAttributeListView + + def test_post_invalid_body_request(self): + request = RequestFactory().post( + reverse('login_access_control:save_authentication_attribute_list'), + None, + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + + def test_post__invalid_institution_id(self): + params = { + 'institution_id': 'test', + 'attribute_data': [{ + 'attribute_name': 'mail', + 'attribute_value': 'test.com', + }] + } + request = RequestFactory().post( + reverse('login_access_control:save_authentication_attribute_list'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "institution_id is invalid."}') + + def test_post__admin_not_affiliated(self): + params = { + 'institution_id': self.institution01.id, + 'attribute_data': [{ + 'attribute_name': 'mail', + 'attribute_value': 'test.com', + }] + } + request = RequestFactory().post( + reverse('login_access_control:save_authentication_attribute_list'), + json.dumps(params), + content_type='application/json' + ) + request.user = self.institution02_admin + response = self.view(request) + + nt.assert_equal(response.status_code, 403) + nt.assert_equal(response.content, b'{"error_message": "You do not have permission to setting login access control of the other institution."}') + + def test_post__empty_attribute_data(self): + params = { + 'institution_id': self.institution01.id, + 'attribute_data': [] + } + request = RequestFactory().post( + reverse('login_access_control:save_authentication_attribute_list'), + json.dumps(params), + content_type='application/json' + ) + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "Group (attribute name, attribute value) is required."}') + + def test_post__attribute_name_is_none(self): + params = { + 'institution_id': self.institution01.id, + 'attribute_data': [{ + 'attribute_name': None, + 'attribute_value': 'test.com', + }] + } + request = RequestFactory().post( + reverse('login_access_control:save_authentication_attribute_list'), + json.dumps(params), + content_type='application/json' + ) + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "Attribute name is required."}') + + def test_post__attribute_name_invalid(self): + params = { + 'institution_id': self.institution01.id, + 'attribute_data': [{ + 'attribute_name': 'test_invalid_attribute', + 'attribute_value': 'test.com', + }] + } + request = RequestFactory().post( + reverse('login_access_control:save_authentication_attribute_list'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "Attribute name is not exist in config."}') + + def test_post__attribute_value_is_none(self): + params = { + 'institution_id': self.institution01.id, + 'attribute_data': [{ + 'attribute_name': 'mail', + 'attribute_value': None, + }] + } + request = RequestFactory().post( + reverse('login_access_control:save_authentication_attribute_list'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "Attribute value is required."}') + + def test_post__attribute_name_too_long(self): + params = { + 'institution_id': self.institution01.id, + 'attribute_data': [{ + 'attribute_name': 'ou', + 'attribute_value': 'X' * 257, + }] + } + request = RequestFactory().post( + reverse('login_access_control:save_authentication_attribute_list'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "Length of attribute value > 255 characters."}') + + def test_post__attributes_are_not_unique(self): + params = { + 'institution_id': self.institution01.id, + 'attribute_data': [{ + 'attribute_name': 'mail', + 'attribute_value': 'test.com', + }, { + 'attribute_name': 'mail', + 'attribute_value': 'test.com', + }] + } + request = RequestFactory().post( + reverse('login_access_control:save_authentication_attribute_list'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "Group (attribute name, attribute value) MUST be unique."}') + + def test_post(self): + params = { + 'institution_id': self.institution01.id, + 'attribute_data': [{ + 'attribute_name': 'mail', + 'attribute_value': 'test.com', + }] + } + request = RequestFactory().post( + reverse('login_access_control:save_authentication_attribute_list'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 201) + attribute_list = LoginControlAuthenticationAttribute.objects.filter(institution=self.institution01) + nt.assert_true(len(attribute_list), 1) + first_attribute = attribute_list.first() + nt.assert_true(first_attribute.attribute_name, 'mail') + nt.assert_true(first_attribute.attribute_value, 'test.com') + + +class TestUpdateAuthenticationAttributeView(AdminTestCase): + + def setUp(self): + self.institution01 = InstitutionFactory(name='inst01') + self.institution02 = InstitutionFactory(name='inst02') + self.attribute_1 = LoginControlAuthenticationAttributeFactory(institution=self.institution01) + self.attribute_2 = LoginControlAuthenticationAttributeFactory(institution=self.institution02) + + self.superuser = AuthUserFactory(fullname='superuser') + self.superuser.is_superuser = True + self.superuser.save() + + self.institution01_admin = AuthUserFactory(fullname='admin001_inst01') + self.institution01_admin.is_staff = True + self.institution01_admin.affiliated_institutions.add(self.institution01) + self.institution01_admin.save() + + self.institution02_admin = AuthUserFactory(fullname='admin001_inst02') + self.institution02_admin.is_staff = True + self.institution02_admin.affiliated_institutions.add(self.institution02) + self.institution02_admin.save() + + self.view = views.UpdateAuthenticationAttributeView.as_view() + self.view_permission = views.UpdateAuthenticationAttributeView + + def test_post_invalid_body_request(self): + request = RequestFactory().post( + reverse('login_access_control:update_authentication_attribute'), + None, + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + + def test_post__missing_institution_id(self): + params = { + 'attribute_id': self.attribute_1.id, + 'is_availability': False, + } + request = RequestFactory().post( + reverse('login_access_control:update_authentication_attribute'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "institution_id is required."}') + + def test_post__invalid_institution_id(self): + params = { + 'institution_id': 'test', + 'attribute_id': self.attribute_1.id, + 'is_availability': False, + } + request = RequestFactory().post( + reverse('login_access_control:update_authentication_attribute'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "institution_id is invalid."}') + + def test_post__missing_attribute_id(self): + params = { + 'institution_id': self.institution01.id, + 'is_availability': False, + } + request = RequestFactory().post( + reverse('login_access_control:update_authentication_attribute'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "attribute_id is required."}') + + def test_post__invalid_attribute_id(self): + params = { + 'institution_id': self.institution01.id, + 'attribute_id': 'test', + 'is_availability': False, + } + request = RequestFactory().post( + reverse('login_access_control:update_authentication_attribute'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "attribute_id is invalid."}') + + def test_post__attribute_id_not_exist(self): + params = { + 'institution_id': self.institution01.id, + 'attribute_id': 0, + 'is_availability': False, + } + request = RequestFactory().post( + reverse('login_access_control:update_authentication_attribute'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "attribute_id is required."}') + + def test_post__missing_is_availability(self): + params = { + 'institution_id': self.institution01.id, + 'attribute_id': self.attribute_1.id, + } + request = RequestFactory().post( + reverse('login_access_control:update_authentication_attribute'), + json.dumps(params), + content_type='application/json' + ) + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "is_availability is required."}') + + def test_post__invalid_is_availability(self): + params = { + 'institution_id': self.institution01.id, + 'attribute_id': self.attribute_1.id, + 'is_availability': 'test', + } + request = RequestFactory().post( + reverse('login_access_control:update_authentication_attribute'), + json.dumps(params), + content_type='application/json' + ) + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "is_availability is invalid."}') + + def test_post__admin_not_affiliated(self): + params = { + 'institution_id': self.institution01.id, + 'attribute_id': self.attribute_1.id, + 'is_availability': False, + } + request = RequestFactory().post( + reverse('login_access_control:update_authentication_attribute'), + json.dumps(params), + content_type='application/json' + ) + request.user = self.institution02_admin + response = self.view(request) + + nt.assert_equal(response.status_code, 403) + nt.assert_equal(response.content, b'{"error_message": "You do not have permission to setting login access control of the other institution."}') + + def test_post__attribute_not_match_with_institution(self): + params = { + 'institution_id': self.institution01.id, + 'attribute_id': self.attribute_2.id, + 'is_availability': False, + } + request = RequestFactory().post( + reverse('login_access_control:update_authentication_attribute'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 403) + nt.assert_equal(response.content, b'{"error_message": "Can not setting login access control of the institution into the other institution."}') + + def test_post__attribute_set_in_logic_condition(self): + self.institution01.login_logic_condition = f'!{self.attribute_1.id}' + self.institution01.save() + params = { + 'institution_id': self.institution01.id, + 'attribute_id': self.attribute_1.id, + 'is_availability': False, + } + request = RequestFactory().post( + reverse('login_access_control:update_authentication_attribute'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "Can not switch toggle the attribute element due to it is using in the logic condition."}') + + def test_post(self): + params = { + 'institution_id': self.institution01.id, + 'attribute_id': self.attribute_1.id, + 'is_availability': False, + } + request = RequestFactory().post( + reverse('login_access_control:update_authentication_attribute'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 204) + attribute_list = LoginControlAuthenticationAttribute.objects.filter(institution=self.institution01) + nt.assert_true(len(attribute_list), 1) + first_attribute = attribute_list.first() + nt.assert_false(first_attribute.is_availability) + + +class TestDeleteAuthenticationAttributeView(AdminTestCase): + + def setUp(self): + self.institution01 = InstitutionFactory(name='inst01') + self.institution02 = InstitutionFactory(name='inst02') + self.attribute_1 = LoginControlAuthenticationAttributeFactory(institution=self.institution01) + self.attribute_2 = LoginControlAuthenticationAttributeFactory(institution=self.institution02) + + self.superuser = AuthUserFactory(fullname='superuser') + self.superuser.is_superuser = True + self.superuser.save() + + self.institution01_admin = AuthUserFactory(fullname='admin001_inst01') + self.institution01_admin.is_staff = True + self.institution01_admin.affiliated_institutions.add(self.institution01) + self.institution01_admin.save() + + self.institution02_admin = AuthUserFactory(fullname='admin001_inst02') + self.institution02_admin.is_staff = True + self.institution02_admin.affiliated_institutions.add(self.institution02) + self.institution02_admin.save() + + self.view = views.DeleteAuthenticationAttributeView.as_view() + self.view_permission = views.DeleteAuthenticationAttributeView + + def test_post_invalid_body_request(self): + request = RequestFactory().post( + reverse('login_access_control:delete_authentication_attribute'), + None, + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + + def test_post__missing_institution_id(self): + params = { + 'attribute_id': self.attribute_1.id, + } + request = RequestFactory().post( + reverse('login_access_control:delete_authentication_attribute'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "institution_id is required."}') + + def test_post__invalid_institution_id(self): + params = { + 'institution_id': 'test', + 'attribute_id': self.attribute_1.id, + } + request = RequestFactory().post( + reverse('login_access_control:delete_authentication_attribute'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "institution_id is invalid."}') + + def test_post__missing_attribute_id(self): + params = { + 'institution_id': self.institution01.id, + } + request = RequestFactory().post( + reverse('login_access_control:delete_authentication_attribute'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "attribute_id is required."}') + + def test_post__invalid_attribute_id(self): + params = { + 'institution_id': self.institution01.id, + 'attribute_id': 'test', + } + request = RequestFactory().post( + reverse('login_access_control:delete_authentication_attribute'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "attribute_id is invalid."}') + + def test_post__attribute_id_not_exist(self): + params = { + 'institution_id': self.institution01.id, + 'attribute_id': 0, + } + request = RequestFactory().post( + reverse('login_access_control:delete_authentication_attribute'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "attribute_id is required."}') + + def test_post__admin_not_affiliated(self): + params = { + 'institution_id': self.institution01.id, + 'attribute_id': self.attribute_1.id, + } + request = RequestFactory().post( + reverse('login_access_control:delete_authentication_attribute'), + json.dumps(params), + content_type='application/json' + ) + request.user = self.institution02_admin + response = self.view(request) + + nt.assert_equal(response.status_code, 403) + nt.assert_equal(response.content, b'{"error_message": "You do not have permission to setting login access control of the other institution."}') + + def test_post__attribute_not_match_with_institution(self): + params = { + 'institution_id': self.institution01.id, + 'attribute_id': self.attribute_2.id, + } + request = RequestFactory().post( + reverse('login_access_control:delete_authentication_attribute'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 403) + nt.assert_equal(response.content, b'{"error_message": "Can not setting login access control of the institution into the other institution."}') + + def test_post__attribute_set_in_logic_condition(self): + self.institution01.login_logic_condition = f'!{self.attribute_1.id}' + self.institution01.save() + params = { + 'institution_id': self.institution01.id, + 'attribute_id': self.attribute_1.id, + } + request = RequestFactory().post( + reverse('login_access_control:delete_authentication_attribute'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "Can not delete the attribute element due to it is using in the logic condition."}') + + def test_post(self): + params = { + 'institution_id': self.institution01.id, + 'attribute_id': self.attribute_1.id, + } + request = RequestFactory().post( + reverse('login_access_control:delete_authentication_attribute'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 204) + attribute_list = LoginControlAuthenticationAttribute.objects.filter(institution=self.institution01) + nt.assert_true(len(attribute_list), 1) + first_attribute = attribute_list.first() + nt.assert_true(first_attribute.is_deleted) + + +class TestUpdateLoginLogicConditionView(AdminTestCase): + def setUp(self): + self.institution = InstitutionFactory(name='inst01') + self.user = AuthUserFactory(fullname='superuser', is_superuser=True) + self.view = views.UpdateLoginLogicConditionView.as_view() + + def test_post_invalid_body_request(self): + request = RequestFactory().post( + reverse('login_access_control:update_login_logic_condition'), + None, + content_type='application/json' + ) + + request.user = self.user + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + + def test_post_missing_institution_id(self): + params = { + 'logic_condition': '1&&2', + } + request = RequestFactory().post( + reverse('login_access_control:update_login_logic_condition'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.user + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "institution_id is required."}') + + def test_post_invalid_institution_id(self): + params = { + 'institution_id': 'test', + 'logic_condition': '1&&2', + } + request = RequestFactory().post( + reverse('login_access_control:update_login_logic_condition'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.user + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "institution_id is invalid."}') + + def test_post_invalid_login_condition(self): + params = { + 'institution_id': self.institution.id, + 'logic_condition': 'test', + } + request = RequestFactory().post( + reverse('login_access_control:update_login_logic_condition'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.user + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "Logic condition is invalid."}') + + def test_post_admin_not_affiliated(self): + self.user.is_staff = True + self.user.is_superuser = False + self.user.affiliated_institutions.add(self.institution) + self.user.save() + + institution = InstitutionFactory(name='inst02') + + params = { + 'institution_id': institution.id, + 'logic_condition': '1&&2', + } + request = RequestFactory().post( + reverse('login_access_control:update_login_logic_condition'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.user + response = self.view(request) + + nt.assert_equal(response.status_code, 403) + nt.assert_equal(response.content, b'{"error_message": "You do not have permission to setting login access control of the other institution."}') + + def test_post__attribute_index_not_exist(self): + params = { + 'institution_id': self.institution.id, + 'logic_condition': '1&&2', + } + request = RequestFactory().post( + reverse('login_access_control:update_login_logic_condition'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.user + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "Index number does not exist."}') + + def test_post(self): + attribute = LoginControlAuthenticationAttributeFactory(institution=self.institution) + params = { + 'institution_id': self.institution.id, + 'logic_condition': f'!{attribute.id}', + } + request = RequestFactory().post( + reverse('login_access_control:update_login_logic_condition'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.user + response = self.view(request) + + nt.assert_equal(response.status_code, 204) + + +class TestSaveMailAddressListView(AdminTestCase): + def setUp(self): + self.institution01 = InstitutionFactory(name='inst01') + self.institution02 = InstitutionFactory(name='inst02') + + self.superuser = AuthUserFactory(fullname='superuser') + self.superuser.is_superuser = True + self.superuser.save() + + self.institution01_admin = AuthUserFactory(fullname='admin001_inst01') + self.institution01_admin.is_staff = True + self.institution01_admin.affiliated_institutions.add(self.institution01) + self.institution01_admin.save() + + self.institution02_admin = AuthUserFactory(fullname='admin001_inst02') + self.institution02_admin.is_staff = True + self.institution02_admin.affiliated_institutions.add(self.institution02) + self.institution02_admin.save() + + self.view = views.SaveMailAddressListView.as_view() + + def test_post_invalid_body_request(self): + request = RequestFactory().post( + reverse('login_access_control:save_mail_address_list'), + None, + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + + def test_post__missing_institution_id(self): + params = { + 'mail_address_list': ['test.com', '@test2.com'], + } + request = RequestFactory().post( + reverse('login_access_control:save_mail_address_list'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "institution_id is required."}') + + def test_post__invalid_institution_id(self): + params = { + 'institution_id': 'test', + 'mail_address_list': ['test.com', '@test2.com'], + } + request = RequestFactory().post( + reverse('login_access_control:save_mail_address_list'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "institution_id is invalid."}') + + def test_post__admin_not_affiliated(self): + params = { + 'institution_id': self.institution01.id, + 'mail_address_list': ['test.com', '@test2.com'], + } + request = RequestFactory().post( + reverse('login_access_control:save_mail_address_list'), + json.dumps(params), + content_type='application/json' + ) + request.user = self.institution02_admin + response = self.view(request) + + nt.assert_equal(response.status_code, 403) + nt.assert_equal(response.content, b'{"error_message": "You do not have permission to setting login access control of the other institution."}') + + def test_post__empty_mail_address_list(self): + params = { + 'institution_id': self.institution01.id, + 'mail_address_list': [], + } + request = RequestFactory().post( + reverse('login_access_control:save_mail_address_list'), + json.dumps(params), + content_type='application/json' + ) + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "Mail address is required."}') + + def test_post__mail_address_is_empty(self): + params = { + 'institution_id': self.institution01.id, + 'mail_address_list': [''], + } + request = RequestFactory().post( + reverse('login_access_control:save_mail_address_list'), + json.dumps(params), + content_type='application/json' + ) + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "Mail address is required."}') + + def test_post__mail_address_too_long(self): + params = { + 'institution_id': self.institution01.id, + 'mail_address_list': [f'{"X"* 320}@test.com'], + } + request = RequestFactory().post( + reverse('login_access_control:save_mail_address_list'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "Length of mail address > 320 characters."}') + + def test_post__mail_address_invalid(self): + params = { + 'institution_id': self.institution01.id, + 'mail_address_list': ['test'], + } + request = RequestFactory().post( + reverse('login_access_control:save_mail_address_list'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "Mail address format is invalid."}') + + def test_post__mail_address_not_unique(self): + params = { + 'institution_id': self.institution01.id, + 'mail_address_list': ['test.com', 'test.com'], + } + request = RequestFactory().post( + reverse('login_access_control:save_mail_address_list'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "Mail address MUST be unique."}') + + def test_post(self): + params = { + 'institution_id': self.institution01.id, + 'mail_address_list': ['test.com', '@test2.com'], + } + request = RequestFactory().post( + reverse('login_access_control:save_mail_address_list'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 201) + mail_address = LoginControlMailAddress.objects.filter(institution=self.institution01) + nt.assert_true(len(mail_address), 2) + nt.assert_true(mail_address.first().mail_address, 'test.com') + nt.assert_true(mail_address.last().mail_address, '@test2.com') + + +class TestUpdateMailAddressView(AdminTestCase): + def setUp(self): + self.institution01 = InstitutionFactory(name='inst01') + self.institution02 = InstitutionFactory(name='inst02') + self.mail_address_1 = LoginControlMailAddressFactory(institution=self.institution01) + self.mail_address_2 = LoginControlMailAddressFactory(institution=self.institution02) + + self.superuser = AuthUserFactory(fullname='superuser') + self.superuser.is_superuser = True + self.superuser.save() + + self.institution01_admin = AuthUserFactory(fullname='admin001_inst01') + self.institution01_admin.is_staff = True + self.institution01_admin.affiliated_institutions.add(self.institution01) + self.institution01_admin.save() + + self.institution02_admin = AuthUserFactory(fullname='admin001_inst02') + self.institution02_admin.is_staff = True + self.institution02_admin.affiliated_institutions.add(self.institution02) + self.institution02_admin.save() + + self.view = views.UpdateMailAddressView.as_view() + + def test_post_invalid_body_request(self): + request = RequestFactory().post( + reverse('login_access_control:update_mail_address'), + None, + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + + def test_post__missing_institution_id(self): + params = { + 'mail_address_id': self.mail_address_1.id, + 'is_availability': False, + } + request = RequestFactory().post( + reverse('login_access_control:update_mail_address'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "institution_id is required."}') + + def test_post__invalid_institution_id(self): + params = { + 'institution_id': 'test', + 'mail_address_id': self.mail_address_1.id, + 'is_availability': False, + } + request = RequestFactory().post( + reverse('login_access_control:update_mail_address'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "institution_id is invalid."}') + + def test_post__missing_mail_address_id(self): + params = { + 'institution_id': self.institution01.id, + 'is_availability': False, + } + request = RequestFactory().post( + reverse('login_access_control:update_mail_address'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "mail_address_id is required."}') + + def test_post__invalid_mail_address_id(self): + params = { + 'institution_id': self.institution01.id, + 'mail_address_id': 'test', + 'is_availability': False, + } + request = RequestFactory().post( + reverse('login_access_control:update_mail_address'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "mail_address_id is invalid."}') + + def test_post__mail_address_id_not_exist(self): + params = { + 'institution_id': self.institution01.id, + 'mail_address_id': 0, + 'is_availability': False, + } + request = RequestFactory().post( + reverse('login_access_control:update_mail_address'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "mail_address_id is required."}') + + def test_post__missing_is_availability(self): + params = { + 'institution_id': self.institution01.id, + 'mail_address_id': self.mail_address_1.id, + } + request = RequestFactory().post( + reverse('login_access_control:update_mail_address'), + json.dumps(params), + content_type='application/json' + ) + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "is_availability is required."}') + + def test_post__invalid_is_availability(self): + params = { + 'institution_id': self.institution01.id, + 'mail_address_id': self.mail_address_1.id, + 'is_availability': 'test', + } + request = RequestFactory().post( + reverse('login_access_control:update_mail_address'), + json.dumps(params), + content_type='application/json' + ) + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "is_availability is invalid."}') + + def test_post__admin_not_affiliated(self): + params = { + 'institution_id': self.institution01.id, + 'mail_address_id': self.mail_address_1.id, + 'is_availability': False, + } + request = RequestFactory().post( + reverse('login_access_control:update_mail_address'), + json.dumps(params), + content_type='application/json' + ) + request.user = self.institution02_admin + response = self.view(request) + + nt.assert_equal(response.status_code, 403) + nt.assert_equal(response.content, b'{"error_message": "You do not have permission to setting login access control of the other institution."}') + + def test_post__mail_address_not_match_with_institution(self): + params = { + 'institution_id': self.institution01.id, + 'mail_address_id': self.mail_address_2.id, + 'is_availability': False, + } + request = RequestFactory().post( + reverse('login_access_control:update_mail_address'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 403) + nt.assert_equal(response.content, b'{"error_message": "Can not setting login access control of the institution into the other institution."}') + + def test_post(self): + params = { + 'institution_id': self.institution01.id, + 'mail_address_id': self.mail_address_1.id, + 'is_availability': False, + } + request = RequestFactory().post( + reverse('login_access_control:update_mail_address'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 204) + mail_address = LoginControlMailAddress.objects.filter(institution=self.institution01) + nt.assert_true(len(mail_address), 1) + nt.assert_false(mail_address.first().is_availability) + + +class TestDeleteMailAddressView(AdminTestCase): + def setUp(self): + self.institution01 = InstitutionFactory(name='inst01') + self.institution02 = InstitutionFactory(name='inst02') + self.mail_address_1 = LoginControlMailAddressFactory(institution=self.institution01) + self.mail_address_2 = LoginControlMailAddressFactory(institution=self.institution02) + + self.superuser = AuthUserFactory(fullname='superuser') + self.superuser.is_superuser = True + self.superuser.save() + + self.institution01_admin = AuthUserFactory(fullname='admin001_inst01') + self.institution01_admin.is_staff = True + self.institution01_admin.affiliated_institutions.add(self.institution01) + self.institution01_admin.save() + + self.institution02_admin = AuthUserFactory(fullname='admin001_inst02') + self.institution02_admin.is_staff = True + self.institution02_admin.affiliated_institutions.add(self.institution02) + self.institution02_admin.save() + + self.view = views.DeleteMailAddressView.as_view() + + def test_post_invalid_body_request(self): + request = RequestFactory().post( + reverse('login_access_control:delete_mail_address'), + None, + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + + def test_post__missing_institution_id(self): + params = { + 'mail_address_id': self.mail_address_1.id, + } + request = RequestFactory().post( + reverse('login_access_control:delete_mail_address'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "institution_id is required."}') + + def test_post__invalid_institution_id(self): + params = { + 'institution_id': 'test', + 'mail_address_id': self.mail_address_1.id, + } + request = RequestFactory().post( + reverse('login_access_control:delete_mail_address'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "institution_id is invalid."}') + + def test_post__missing_mail_address_id(self): + params = { + 'institution_id': self.institution01.id, + } + request = RequestFactory().post( + reverse('login_access_control:delete_mail_address'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "mail_address_id is required."}') + + def test_post__invalid_mail_address_id(self): + params = { + 'institution_id': self.institution01.id, + 'mail_address_id': 'test', + } + request = RequestFactory().post( + reverse('login_access_control:delete_mail_address'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "mail_address_id is invalid."}') + + def test_post__mail_address_id_not_exist(self): + params = { + 'institution_id': self.institution01.id, + 'mail_address_id': 0, + } + request = RequestFactory().post( + reverse('login_access_control:delete_mail_address'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 400) + nt.assert_equal(response.content, b'{"error_message": "mail_address_id is required."}') + + def test_post__admin_not_affiliated(self): + params = { + 'institution_id': self.institution01.id, + 'mail_address_id': self.mail_address_1.id, + } + request = RequestFactory().post( + reverse('login_access_control:delete_mail_address'), + json.dumps(params), + content_type='application/json' + ) + request.user = self.institution02_admin + response = self.view(request) + + nt.assert_equal(response.status_code, 403) + nt.assert_equal(response.content, b'{"error_message": "You do not have permission to setting login access control of the other institution."}') + + def test_post__mail_address_not_match_with_institution(self): + params = { + 'institution_id': self.institution01.id, + 'mail_address_id': self.mail_address_2.id, + } + request = RequestFactory().post( + reverse('login_access_control:delete_mail_address'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 403) + nt.assert_equal(response.content, b'{"error_message": "Can not setting login access control of the institution into the other institution."}') + + def test_post(self): + params = { + 'institution_id': self.institution01.id, + 'mail_address_id': self.mail_address_1.id, + } + request = RequestFactory().post( + reverse('login_access_control:delete_mail_address'), + json.dumps(params), + content_type='application/json' + ) + + request.user = self.superuser + response = self.view(request) + + nt.assert_equal(response.status_code, 204) + mail_address = LoginControlMailAddress.objects.filter(institution=self.institution01) + nt.assert_true(len(mail_address), 1) + nt.assert_true(mail_address.first().is_deleted) diff --git a/api/base/urls.py b/api/base/urls.py index 0fc32825291..15e9b58802a 100644 --- a/api/base/urls.py +++ b/api/base/urls.py @@ -49,7 +49,6 @@ url(r'^guids/', include('api.guids.urls', namespace='guids')), url(r'^identifiers/', include('api.identifiers.urls', namespace='identifiers')), url(r'^institutions/', include('api.institutions.urls', namespace='institutions')), - url(r'^entitlements/', include('api.entitlements.urls', namespace='entitlements')), url(r'^licenses/', include('api.licenses.urls', namespace='licenses')), url(r'^logs/', include('api.logs.urls', namespace='logs')), url(r'^metaschemas/', include('api.metaschemas.urls', namespace='metaschemas')), diff --git a/api/entitlements/__init__.py b/api/entitlements/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/api/entitlements/serializers.py b/api/entitlements/serializers.py deleted file mode 100644 index 6d63b226a60..00000000000 --- a/api/entitlements/serializers.py +++ /dev/null @@ -1,6 +0,0 @@ -from rest_framework import serializers as ser - - -class LoginAvailabilitySerializer(ser.Serializer): - institution_id = ser.CharField(required=True) - entitlements = ser.ListField(required=True, child=ser.CharField()) diff --git a/api/entitlements/urls.py b/api/entitlements/urls.py deleted file mode 100644 index b785c29f85f..00000000000 --- a/api/entitlements/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.conf.urls import url - -from api.entitlements import views - -app_name = 'osf' - -urlpatterns = [ - url(r'^login_availability/$', views.LoginAvailability.as_view(), name=views.LoginAvailability.view_name), -] diff --git a/api/entitlements/views.py b/api/entitlements/views.py deleted file mode 100644 index 045b4b4f312..00000000000 --- a/api/entitlements/views.py +++ /dev/null @@ -1,44 +0,0 @@ -import logging - -from rest_framework import status -from rest_framework.parsers import JSONParser -from rest_framework.response import Response -from rest_framework.views import APIView - -from api.base.permissions import TokenHasScope -from api.entitlements.serializers import ( - LoginAvailabilitySerializer, -) -from osf.models import InstitutionEntitlement, Institution - -logger = logging.getLogger(__name__) - - -class LoginAvailability(APIView): - view_category = 'institutions' - view_name = 'login_availability' - parser_classes = (JSONParser,) - permission_classes = (TokenHasScope,) - - def get_serializer_class(self): - return None - - def _get_embed_partial(self): - return None - - def post(self, request, *args, **kwargs): - serializer = LoginAvailabilitySerializer(data=request.data) - if serializer.is_valid(): - data = serializer.validated_data - institution_guid = data.get('institution_id') - institution = Institution.load(institution_guid) - if institution is None: - return Response({'login_availability': False}, status=status.HTTP_200_OK) - entitlements = data.get('entitlements') - entitlement_list = InstitutionEntitlement.objects.filter( - institution_id=institution, - entitlement__in=entitlements, - ) - login_availability = all(list(entitlement_list.values_list('login_availability', flat=True))) - return Response({'login_availability': login_availability}, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/api/institutions/authentication.py b/api/institutions/authentication.py index 4a6e5e8217e..bc41cb2a1cb 100644 --- a/api/institutions/authentication.py +++ b/api/institutions/authentication.py @@ -176,6 +176,8 @@ def get_next(obj, *args): organization_name_ja = get_next(p_user, 'jao', 'jaOrganizationName') # affiliation: 'jaou' is friendlyName organizational_unit_ja = get_next(p_user, 'jaou', 'jaOrganizationalUnitName') + # login_availability + login_availability = p_user.get('login_availability') # edu_person_affiliation: 'eduPersonAffiliation' is friendlyName edu_person_affiliation = get_next(p_user, 'edu_person_affiliation', 'eduPersonAffiliation') @@ -436,6 +438,7 @@ def get_next(obj, *args): 'organizational_unit': organizational_unit, 'organization_name_ja': organization_name_ja, 'organizational_unit_ja': organizational_unit_ja, + 'login_availability': login_availability, 'groups': groups, 'family_name': family_name, 'given_name': given_name, diff --git a/api/institutions/urls.py b/api/institutions/urls.py index 624f0c4a76c..a7bca6908eb 100644 --- a/api/institutions/urls.py +++ b/api/institutions/urls.py @@ -1,14 +1,13 @@ from django.conf.urls import url from api.institutions import views -from api.entitlements.views import LoginAvailability app_name = 'osf' urlpatterns = [ url(r'^$', views.InstitutionList.as_view(), name=views.InstitutionList.view_name), url(r'^auth/$', views.InstitutionAuth.as_view(), name=views.InstitutionAuth.view_name), - url(r'^login_availability/$', LoginAvailability.as_view(), name=LoginAvailability.view_name), + url(r'^login_availability/$', views.LoginAvailability.as_view(), name=views.LoginAvailability.view_name), url(r'^(?P\w+)/$', views.InstitutionDetail.as_view(), name=views.InstitutionDetail.view_name), url(r'^(?P\w+)/nodes/$', views.InstitutionNodeList.as_view(), name=views.InstitutionNodeList.view_name), url(r'^(?P\w+)/registrations/$', views.InstitutionRegistrationList.as_view(), name=views.InstitutionRegistrationList.view_name), diff --git a/api/institutions/views.py b/api/institutions/views.py index dcc8cefb53b..50a2c636ccc 100644 --- a/api/institutions/views.py +++ b/api/institutions/views.py @@ -1,15 +1,19 @@ +import re + from django.db.models import F from rest_framework import generics from rest_framework import permissions as drf_permissions from rest_framework import exceptions from rest_framework import status +from rest_framework.parsers import JSONParser from rest_framework.response import Response from rest_framework.settings import api_settings +from rest_framework.views import APIView from framework.auth.oauth_scopes import CoreScopes from osf.metrics import InstitutionProjectCounts -from osf.models import OSFUser, Node, Institution, Registration +from osf.models import OSFUser, Node, Institution, Registration, LoginControlAuthenticationAttribute, UserExtendedData from osf.metrics import UserInstitutionProjectCounts from osf.utils import permissions as osf_permissions @@ -520,3 +524,76 @@ def get_default_queryset(self): institution = self.get_institution() search = UserInstitutionProjectCounts.get_current_user_metrics(institution) return self._make_elasticsearch_results_filterable(search, id=institution._id, department=DEFAULT_ES_NULL_VALUE) + + +class LoginAvailability(APIView): + """ API that returns institution's login availability status """ + view_category = 'institutions' + view_name = 'login_availability' + parser_classes = (JSONParser,) + permission_classes = (base_permissions.TokenHasScope,) + + def get_serializer_class(self): + return None + + def _get_embed_partial(self): + return None + + def post(self, request, *args, **kwargs): + data = request.data + institution_guid = data.get('institution_id') + institution = Institution.load(institution_guid) + if institution is None or institution.is_deleted is True: + # If institution GUID does not exist or institution is deleted, return HTTP 403 response + return Response({}, status=status.HTTP_403_FORBIDDEN) + logic_condition = institution.login_logic_condition + if not logic_condition: + # If institution's login logic condition is not set, return check mail address response + return Response({'login_availability': UserExtendedData.CHECK_MAIL_ADDRESS}, status=status.HTTP_200_OK) + + # Get attribute id list from login logic condition + logic_condition_attribute_id_list = map(int, re.findall('\\d+', logic_condition)) + logic_condition_attribute_unique_id_list = set(logic_condition_attribute_id_list) + for logic_condition_id in logic_condition_attribute_unique_id_list: + # Get login control authentication attribute by ID + login_attribute = LoginControlAuthenticationAttribute.objects.filter(id=logic_condition_id, is_deleted=False).first() + if login_attribute: + # Replace attribute id in logic condition to True or False + request_attribute_values = data.get(login_attribute.attribute_name) + if not request_attribute_values: + # If attribute is not found in request, replace it to False + logic_condition = re.sub(r'\b' + str(logic_condition_id) + r'\b', 'False', logic_condition) + if isinstance(request_attribute_values, str) and login_attribute.attribute_value == request_attribute_values: + # If attribute value in request is a string and it has a record in DB, replace it to True + logic_condition = re.sub(r'\b' + str(logic_condition_id) + r'\b', 'True', logic_condition) + elif isinstance(request_attribute_values, list) and login_attribute.attribute_value in request_attribute_values: + # If attribute value in request is a list and one of it has a record in DB, replace it to True + logic_condition = re.sub(r'\b' + str(logic_condition_id) + r'\b', 'True', logic_condition) + else: + # Otherwise, replace it to False (not match) + logic_condition = re.sub(r'\b' + str(logic_condition_id) + r'\b', 'False', logic_condition) + + # Convert operator characters into their respective readable counterpart + expression = logic_condition. \ + replace('&&', ' and '). \ + replace('||', ' or '). \ + replace('!', ' not ') + + try: + # Evaluate the converted expression + expression = eval(expression) + + # Check if expression is evaluated to be true + if type(expression) == bool and expression is True: + if institution.login_availability_default is True: + # If institution's login availability default is True, return forbidden response + return Response({}, status=status.HTTP_403_FORBIDDEN) + else: + # If institution's login availability default is False, return can login response + return Response({'login_availability': UserExtendedData.CAN_LOGIN}, status=status.HTTP_200_OK) + except (SyntaxError, NameError): + # Cannot evaluate the logic condition, do nothing here to return check mail address response below + pass + + # If expression cannot be evaluated or expression is evaluated to be false, return check mail address response + return Response({'login_availability': UserExtendedData.CHECK_MAIL_ADDRESS}, status=status.HTTP_200_OK) diff --git a/api_tests/base/test_views.py b/api_tests/base/test_views.py index ea2ce1b3878..94c29d4fa18 100644 --- a/api_tests/base/test_views.py +++ b/api_tests/base/test_views.py @@ -7,7 +7,7 @@ from nose import SkipTest from nose.tools import * # noqa: -from api.entitlements.views import LoginAvailability +from api.institutions.views import LoginAvailability from tests.base import ApiTestCase from osf_tests import factories from osf.utils.permissions import READ, WRITE diff --git a/api_tests/entitlements/__init__.py b/api_tests/entitlements/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/api_tests/entitlements/serializers/__init__.py b/api_tests/entitlements/serializers/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/api_tests/entitlements/serializers/test_serializers.py b/api_tests/entitlements/serializers/test_serializers.py deleted file mode 100644 index 8a63c97f063..00000000000 --- a/api_tests/entitlements/serializers/test_serializers.py +++ /dev/null @@ -1,20 +0,0 @@ -import pytest -from api.entitlements.serializers import LoginAvailabilitySerializer - - -@pytest.mark.django_db -class TestLoginAvailabilitySerializer: - - def test_serializer(self): - id_test = '1' - payload = { - 'institution_id': id_test, - 'entitlements': ['gkn1-ent1', 'gkn1-ent2', 'gkn1-ent1'] - } - data = LoginAvailabilitySerializer(data=payload) - assert data.is_valid() is True - - data = data.validated_data - institution_id = data.get('institution_id') - - assert institution_id == id_test diff --git a/api_tests/entitlements/views/__init__.py b/api_tests/entitlements/views/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/api_tests/entitlements/views/test_login_availability.py b/api_tests/entitlements/views/test_login_availability.py deleted file mode 100644 index c0533ffc4a2..00000000000 --- a/api_tests/entitlements/views/test_login_availability.py +++ /dev/null @@ -1,57 +0,0 @@ -import pytest -from api.base.settings.defaults import API_BASE -from nose import tools as nt -from osf_tests.factories import InstitutionFactory, AuthUserFactory, InstitutionEntitlementFactory - - -@pytest.mark.django_db -class TestLoginAvailability: - - def test_post_serializer_valid(self, app): - self.institution = InstitutionFactory() - self.institution.save() - - self.institution_entitlement1 = InstitutionEntitlementFactory(institution=self.institution) - self.institution_entitlement2 = InstitutionEntitlementFactory(institution=self.institution) - self.institution_entitlement3 = InstitutionEntitlementFactory(institution=self.institution) - - self.institution_entitlement1.save() - self.institution_entitlement2.save() - self.institution_entitlement3.save() - - self.user = AuthUserFactory() - self.user.save() - url = '/{0}institutions/login_availability/'.format(API_BASE) - data = { - 'institution_id': self.institution._id, - 'entitlements': [self.institution_entitlement1.entitlement, self.institution_entitlement2.entitlement, - self.institution_entitlement3.entitlement] - } - - res = app.simple_post_api(url, data, expect_errors=True) - res_data = res.json['login_availability'] - nt.assert_equal(res.status_code, 200) - nt.assert_equal(res_data, False) - - def test_post_serializer_invalid(self, app): - self.institution = InstitutionFactory() - self.institution.save() - - self.institution_entitlement1 = InstitutionEntitlementFactory(institution=self.institution) - self.institution_entitlement2 = InstitutionEntitlementFactory(institution=self.institution) - self.institution_entitlement3 = InstitutionEntitlementFactory(institution=self.institution) - - self.institution_entitlement1.save() - self.institution_entitlement2.save() - self.institution_entitlement3.save() - - self.user = AuthUserFactory() - self.user.save() - url = '/{0}institutions/login_availability/'.format(API_BASE) - data = { - 'institution_id': self.institution._id, - 'entitlements': [True, False, True] - } - - res = app.simple_post_api(url, data, expect_errors=True) - nt.assert_equal(res.status_code, 400) diff --git a/api_tests/institutions/views/test_institution_auth.py b/api_tests/institutions/views/test_institution_auth.py index 0b1192c6bf0..cf5507181f3 100644 --- a/api_tests/institutions/views/test_institution_auth.py +++ b/api_tests/institutions/views/test_institution_auth.py @@ -11,7 +11,7 @@ from framework.auth import signals, Auth from framework.auth.views import send_confirm_email -from osf.models import OSFUser +from osf.models import OSFUser, UserExtendedData from osf_tests.factories import InstitutionFactory, ProjectFactory, UserFactory from tests.base import capture_signals @@ -36,6 +36,7 @@ def make_payload( jaOrganizationalUnitName='', organizationalUnit='', organizationName='', + login_availability='', edu_person_affiliation='', edu_person_scoped_affiliation='', edu_person_targeted_id='', @@ -68,6 +69,7 @@ def make_payload( 'jaOrganizationalUnitName': jaOrganizationalUnitName, 'organizationalUnitName': organizationalUnit, 'organizationName': organizationName, + 'login_availability': login_availability, 'eduPersonAffiliation': edu_person_affiliation, 'eduPersonScopedAffiliation': edu_person_scoped_affiliation, 'eduPersonTargetedID': edu_person_targeted_id, @@ -542,6 +544,32 @@ def test_authenticate_OrganizationalUnitName_is_valid( assert user assert user.jobs[0]['department'] == organizationnameunit + def test_authenticate__check_user_extended_data(self, app, institution, url_auth_institution): + username, fullname, password = 'user_active@user.edu', 'Foo Bar', 'FuAsKeEr' + user = make_user(username, fullname) + user.set_password(password) + user.save() + + with capture_signals() as mock_signals: + res = app.post( + url_auth_institution, + make_payload( + institution, + username, + family_name='User', + given_name='Fake', + fullname='Fake User', + department='Fake Department', + login_availability='can login', + ) + ) + assert res.status_code == 204 + assert not mock_signals.signals_sent() + + # confirm login availability extended data + extended_data = UserExtendedData.objects.get(user=user) + assert extended_data.data.get('idp_attr', {}).get('login_availability') == 'can login' + @mock.patch('api.institutions.authentication.login_by_eppn') def test_with_new_attribute(self, mock, app, institution, url_auth_institution): mock.return_value = True diff --git a/api_tests/institutions/views/test_login_availability.py b/api_tests/institutions/views/test_login_availability.py new file mode 100644 index 00000000000..3127a41c9b7 --- /dev/null +++ b/api_tests/institutions/views/test_login_availability.py @@ -0,0 +1,101 @@ +import pytest +from api.base.settings.defaults import API_BASE +from nose import tools as nt + +from api.institutions.views import LoginAvailability +from osf_tests.factories import InstitutionFactory, AuthUserFactory, LoginControlAuthenticationAttributeFactory +from tests.base import ApiTestCase + + +@pytest.mark.django_db +class TestLoginAvailability(ApiTestCase): + def setUp(self): + super(TestLoginAvailability, self).setUp() + self.institution = InstitutionFactory() + self.institution.save() + + self.attribute1 = LoginControlAuthenticationAttributeFactory(institution=self.institution, attribute_name='mail', attribute_value='test.com') + self.attribute2 = LoginControlAuthenticationAttributeFactory(institution=self.institution, attribute_name='o', attribute_value='test_o') + self.attribute3 = LoginControlAuthenticationAttributeFactory(institution=self.institution, attribute_name='sn', attribute_value='test_sn') + + self.user = AuthUserFactory() + self.user.save() + + self.url = '/{0}institutions/login_availability/'.format(API_BASE) + self.view = LoginAvailability() + + def test_get_serializer_class(self): + nt.assert_is_none(self.view.get_serializer_class()) + + def test__get_embed_partial(self): + nt.assert_is_none(self.view._get_embed_partial()) + + def test_post__invalid_institution_guid(self): + payload = { + 'institution_id': '', + } + + res = self.app.simple_post_api(self.url, payload, expect_errors=True) + nt.assert_equal(res.status_code, 403) + + def test_post__institution_no_logic_condition(self): + payload = { + 'institution_id': self.institution.guid, + } + + res = self.app.simple_post_api(self.url, payload) + res_data = res.json['login_availability'] + nt.assert_equal(res.status_code, 200) + nt.assert_equal(res_data, 'check mail address') + + def test_post__match_logic_condition_and_login_availability_default(self): + self.institution.login_logic_condition = f'{self.attribute1.id}&&{self.attribute2.id}&&{self.attribute3.id}' + self.institution.save() + payload = { + 'institution_id': self.institution.guid, + 'mail': 'test.com', + 'o': [ + 'test_o', + 'test_o_2', + ], + 'sn': 'test_sn', + } + + res = self.app.simple_post_api(self.url, payload, expect_errors=True) + nt.assert_equal(res.status_code, 403) + + def test_post__match_logic_condition_and_not_login_availability_default(self): + self.institution.login_logic_condition = f'{self.attribute1.id}&&{self.attribute2.id}&&{self.attribute3.id}' + self.institution.login_availability_default = False + self.institution.save() + payload = { + 'institution_id': self.institution.guid, + 'mail': 'test.com', + 'o': [ + 'test_o', + 'test_o_2', + ], + 'sn': 'test_sn', + } + + res = self.app.simple_post_api(self.url, payload) + res_data = res.json['login_availability'] + nt.assert_equal(res.status_code, 200) + nt.assert_equal(res_data, 'can login') + + def test_post__not_match_logic_condition(self): + self.institution.login_logic_condition = f'{self.attribute1.id}&&{self.attribute2.id}&&{self.attribute3.id}' + self.institution.save() + payload = { + 'institution_id': self.institution.guid, + 'o': [ + 'test_o_1', + 'test_o_2', + ], + 'sn': 'test_sn_wrong', + } + + res = self.app.simple_post_api(self.url, payload) + res_data = res.json['login_availability'] + nt.assert_equal(res.status_code, 200) + nt.assert_equal(res_data, 'check mail address') diff --git a/framework/auth/decorators.py b/framework/auth/decorators.py index 06b87c29807..ae9f698d685 100644 --- a/framework/auth/decorators.py +++ b/framework/auth/decorators.py @@ -133,6 +133,8 @@ def _must_be_logged_in_factory(login=True, email=True, use_mapcore=True): def wrapper(func): @functools.wraps(func) def wrapped(*args, **kwargs): + from osf.models import UserExtendedData + from framework.auth.views import auth_logout auth = Auth.from_kwargs(request.args.to_dict(), kwargs) if login: # require auth @@ -149,6 +151,16 @@ def wrapped(*args, **kwargs): # for GakuNin mAP Core (API v2) response = mapcore_check_token(auth, None, use_mapcore=use_mapcore) + + # Get user's extended data + extended_data = UserExtendedData.objects.filter(user=auth.user).first() + if extended_data: + # check user's login availability by user's mail address + login_availability = extended_data.data.get('idp_attr', {}).get('login_availability') + if login_availability == UserExtendedData.CHECK_MAIL_ADDRESS and not auth.user.check_login_availability_by_mail_address(): + # If user is not allowed to log in by mail address, logout then redirect to top page + return auth_logout(redirect_url=web_url_for('index', login_not_available='true', _absolute=True)) + return response or func(*args, **kwargs) else: return redirect(web_url_for('user_account_email')) diff --git a/framework/auth/views.py b/framework/auth/views.py index d8c9e42ef9e..84db531c337 100644 --- a/framework/auth/views.py +++ b/framework/auth/views.py @@ -719,6 +719,7 @@ def unconfirmed_email_add(auth=None): """ user = auth.user json_body = request.get_json() + user_have_email = user.have_email try: token = json_body['token'] except KeyError: @@ -740,6 +741,14 @@ def unconfirmed_email_add(auth=None): }) user.save() + + # If user register new email for the first time, check user's login availability by user's mail address + if not user_have_email and not user.check_login_availability_by_mail_address(): + # If user is not allowed to log in by mail address, logout then redirect to top page + top_page_url = web_url_for('index', login_not_available='true', _absolute=True) + logout_url = web_url_for('auth_logout', redirect_url=top_page_url) + raise HTTPError(http_status.HTTP_401_UNAUTHORIZED, data=dict(redirect_url=logout_url)) + return { 'status': 'success', 'removed_email': json_body['address'] diff --git a/osf/migrations/0261_auto_20251218_0823.py b/osf/migrations/0261_auto_20251218_0823.py new file mode 100644 index 00000000000..4927d87d9da --- /dev/null +++ b/osf/migrations/0261_auto_20251218_0823.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.28 on 2025-12-18 08:23 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django_extensions.db.fields +import osf.models.base + + +class Migration(migrations.Migration): + + dependencies = [ + ('osf', '0260_merge_20251126_1230'), + ] + + operations = [ + migrations.CreateModel( + name='LoginControlAuthenticationAttribute', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')), + ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), + ('attribute_name', models.CharField(max_length=255)), + ('attribute_value', models.CharField(max_length=255)), + ('is_availability', models.BooleanField(default=True)), + ('is_deleted', models.BooleanField(default=False)), + ], + options={ + 'db_table': 'osf_login_control_authentication_attribute', + }, + bases=(models.Model, osf.models.base.QuerySetExplainMixin), + ), + migrations.CreateModel( + name='LoginControlMailAddress', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')), + ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), + ('mail_address', models.CharField(max_length=320)), + ('is_availability', models.BooleanField(default=True)), + ('is_deleted', models.BooleanField(default=False)), + ], + options={ + 'db_table': 'osf_login_control_mail_address', + }, + bases=(models.Model, osf.models.base.QuerySetExplainMixin), + ), + migrations.AlterUniqueTogether( + name='institutionentitlement', + unique_together=set([]), + ), + migrations.RemoveField( + model_name='institutionentitlement', + name='institution', + ), + migrations.RemoveField( + model_name='institutionentitlement', + name='modifier', + ), + migrations.AddField( + model_name='institution', + name='login_availability_default', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='institution', + name='login_logic_condition', + field=models.TextField(blank=True, null=True), + ), + migrations.DeleteModel( + name='InstitutionEntitlement', + ), + migrations.AddField( + model_name='logincontrolmailaddress', + name='institution', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='login_control_mail_addresses', to='osf.Institution'), + ), + migrations.AddField( + model_name='logincontrolauthenticationattribute', + name='institution', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='login_control_authentication_attributes', to='osf.Institution'), + ), + ] diff --git a/osf/models/__init__.py b/osf/models/__init__.py index d82b6d44124..f42e8efec50 100644 --- a/osf/models/__init__.py +++ b/osf/models/__init__.py @@ -63,10 +63,10 @@ from osf.models.user_quota import UserQuota # noqa from osf.models.project_storage_type import ProjectStorageType # noqa from osf.models.region_external_account import RegionExternalAccount # noqa -from osf.models.institution_entitlement import InstitutionEntitlement # noqa from osf.models.export_data_location import ExportDataLocation # noqa from osf.models.export_data import ExportData # noqa from osf.models.export_data_restore import ExportDataRestore # noqa +from osf.models.login_control import LoginControlAuthenticationAttribute, LoginControlMailAddress # noqa from osf.models.project_limit_number_default import ProjectLimitNumberDefault # noqa from osf.models.project_limit_number_template import ProjectLimitNumberTemplate # noqa from osf.models.project_limit_number_template_attribute import ProjectLimitNumberTemplateAttribute # noqa diff --git a/osf/models/institution.py b/osf/models/institution.py index caae48f4772..73f21a7dbb9 100644 --- a/osf/models/institution.py +++ b/osf/models/institution.py @@ -65,6 +65,9 @@ class Institution(DirtyFieldsMixin, Loggable, base.ObjectIDMixin, base.BaseModel related_name='institutions' ) + login_availability_default = models.BooleanField(default=True) + login_logic_condition = models.TextField(null=True, blank=True) + is_deleted = models.BooleanField(default=False, db_index=True) deleted = NonNaiveDateTimeField(null=True, blank=True) diff --git a/osf/models/institution_entitlement.py b/osf/models/institution_entitlement.py deleted file mode 100644 index 32ffd12c6f5..00000000000 --- a/osf/models/institution_entitlement.py +++ /dev/null @@ -1,29 +0,0 @@ -import logging - -from django.db import models - -from osf.models import base - -logger = logging.getLogger(__name__) - - -class InstitutionEntitlement(base.BaseModel): - institution = models.ForeignKey('Institution', on_delete=models.CASCADE) - entitlement = models.CharField(max_length=255) - login_availability = models.BooleanField(default=True) - modifier = models.ForeignKey('OSFUser', on_delete=models.CASCADE) - - class Meta: - unique_together = ('institution', 'entitlement') - # custom permissions for use in the GakuNin RDM Admin App - permissions = ( - ('view_institution_entitlement', 'Can view institution entitlement'), - ('admin_institution_entitlement', 'Can manage institution entitlement'), - ) - - def __init__(self, *args, **kwargs): - kwargs.pop('node', None) - super(InstitutionEntitlement, self).__init__(*args, **kwargs) - - def __unicode__(self): - return u'institution_{}:{}'.format(self.institution._id, self.entitlement) diff --git a/osf/models/login_control.py b/osf/models/login_control.py new file mode 100644 index 00000000000..90d87422c4f --- /dev/null +++ b/osf/models/login_control.py @@ -0,0 +1,23 @@ +from django.db import models +from osf.models import base + + +class LoginControlAuthenticationAttribute(base.BaseModel): + institution = models.ForeignKey('Institution', related_name='login_control_authentication_attributes') + attribute_name = models.CharField(max_length=255) + attribute_value = models.CharField(max_length=255) + is_availability = models.BooleanField(default=True) + is_deleted = models.BooleanField(default=False) + + class Meta: + db_table = 'osf_login_control_authentication_attribute' + + +class LoginControlMailAddress(base.BaseModel): + institution = models.ForeignKey('Institution', related_name='login_control_mail_addresses') + mail_address = models.CharField(max_length=320) + is_availability = models.BooleanField(default=True) + is_deleted = models.BooleanField(default=False) + + class Meta: + db_table = 'osf_login_control_mail_address' diff --git a/osf/models/user.py b/osf/models/user.py index 2725f9360be..aa405cc7263 100644 --- a/osf/models/user.py +++ b/osf/models/user.py @@ -44,6 +44,7 @@ from osf.models.base import BaseModel, GuidMixin, GuidMixinQuerySet from osf.models.contributor import Contributor, RecentlyAddedContributor from osf.models.institution import Institution +from osf.models.login_control import LoginControlMailAddress from osf.models.mixins import AddonModelMixin from osf.models.nodelog import NodeLog from osf.models.spam import SpamMixin @@ -131,6 +132,9 @@ def __unicode__(self): class UserExtendedData(BaseModel): + CAN_LOGIN = 'can login' + CHECK_MAIL_ADDRESS = 'check mail address' + user = models.OneToOneField('OSFUser', related_name='ext', on_delete=models.CASCADE) @@ -146,6 +150,7 @@ class UserExtendedData(BaseModel): # 'organization_name_ja': , # 'organizational_unit': , # 'organizational_unit_ja': , + # 'login_availability': , # }, # } @@ -2140,6 +2145,36 @@ def has_resources(self): return groups or nodes or quickfiles or preprints + def check_login_availability_by_mail_address(self): + """ Check user login availability by login control mail address """ + institution = self.affiliated_institutions.filter(is_deleted=False).first() + if not institution: + # If user does not have any affiliated institutions, return True + return True + + # Get login control mail address by user's username + has_login_control_mail_address = LoginControlMailAddress.objects.filter( + institution=institution, is_availability=True, is_deleted=False + ).annotate( + mail_address_domain=models.Value(self.username, output_field=models.CharField()) + ).filter( + mail_address_domain__endswith=models.F('mail_address') + ).exists() + + # If user has a login control mail address record in DB and institution's login_availability_default is True + # or user does not have a login control mail address record in DB and institution's login_availability_default is False + # then return False + if has_login_control_mail_address == institution.login_availability_default: + return False + + # Get user extended data or create if not have + extended_data, _ = UserExtendedData.objects.get_or_create(user=self) + # Update user extended data's login availability status + idp_attr = extended_data.data.get('idp_attr', {}) + extended_data.set_idp_attr({**idp_attr, 'login_availability': UserExtendedData.CAN_LOGIN}) + + return True + class Meta: # custom permissions for use in the GakuNin RDM Admin App permissions = ( diff --git a/osf_tests/factories.py b/osf_tests/factories.py index 7fe294ded72..e755a331383 100644 --- a/osf_tests/factories.py +++ b/osf_tests/factories.py @@ -18,6 +18,7 @@ from faker import Factory from waffle.models import Flag, Sample, Switch +from admin.base.settings import ATTRIBUTE_NAME_LIST from website.notifications.constants import NOTIFICATION_TYPES from osf.utils import permissions from website.archiver import ARCHIVER_SUCCESS @@ -265,20 +266,18 @@ class Meta: model = models.Institution -class InstitutionEntitlementFactory(DjangoModelFactory): - entitlement = factory.Faker('name') +class LoginControlAuthenticationAttributeFactory(DjangoModelFactory): + attribute_name = FuzzyChoice(choices=ATTRIBUTE_NAME_LIST) + attribute_value = factory.Faker('name') class Meta: - model = models.InstitutionEntitlement + model = models.LoginControlAuthenticationAttribute @classmethod - def _create(cls, target_class, institution=None, login_availability=None, modifier=None, *args, **kwargs): + def _create(cls, target_class, institution=None, *args, **kwargs): institution = institution or models.Institution.objects.first() or InstitutionFactory() - login_availability = login_availability or False - modifier = modifier or models.OSFUser.objects.first() or UserFactory() - return super(InstitutionEntitlementFactory, cls)._create(target_class, institution=institution, login_availability=login_availability, - modifier=modifier, *args, **kwargs) + return super(LoginControlAuthenticationAttributeFactory, cls)._create(target_class, institution=institution, *args, **kwargs) class NodeLicenseRecordFactory(DjangoModelFactory): @@ -1296,6 +1295,11 @@ class Meta: model = models.RdmFileTimestamptokenVerifyResult +class LoginControlMailAddressFactory(DjangoModelFactory): + class Meta: + model = models.LoginControlMailAddress + + class ProjectLimitNumberTemplateFactory(factory.django.DjangoModelFactory): class Meta: model = models.ProjectLimitNumberTemplate diff --git a/osf_tests/test_institution_entitlement.py b/osf_tests/test_institution_entitlement.py deleted file mode 100644 index cdde8808cbc..00000000000 --- a/osf_tests/test_institution_entitlement.py +++ /dev/null @@ -1,33 +0,0 @@ -from osf.models.institution_entitlement import InstitutionEntitlement -from nose import tools as nt -from .factories import InstitutionFactory, InstitutionEntitlementFactory, AuthUserFactory -import pytest - - -class TestInstitutionEntitlementModel: - - @pytest.mark.django_db - def test_factory(self): - institution = InstitutionFactory() - user = AuthUserFactory() - inst = InstitutionEntitlementFactory(institution=institution, login_availability=True, modifier=user) - nt.assert_equal(inst.institution, institution) - nt.assert_equal(inst.login_availability, True) - nt.assert_equal(inst.modifier, user) - - @pytest.mark.django_db - def test__init__(self): - institution = InstitutionFactory() - user = AuthUserFactory() - institution_entitlement = InstitutionEntitlement(institution=institution, login_availability=True, modifier=user) - nt.assert_equal(institution_entitlement.institution, institution) - nt.assert_equal(institution_entitlement.login_availability, True) - nt.assert_equal(institution_entitlement.modifier, user) - - @pytest.mark.django_db - def test__unitcode__(self): - institution = InstitutionFactory() - user = AuthUserFactory() - inst = InstitutionEntitlementFactory(institution=institution, login_availability=True, modifier=user) - expectedResult = u'institution_{}:{}'.format(inst.institution._id, inst.entitlement) - nt.assert_equal(inst.__unicode__(), expectedResult) diff --git a/osf_tests/test_login_control.py b/osf_tests/test_login_control.py new file mode 100644 index 00000000000..7990ce73233 --- /dev/null +++ b/osf_tests/test_login_control.py @@ -0,0 +1,40 @@ +from admin.base.settings import ATTRIBUTE_NAME_LIST +from osf.models.login_control import LoginControlAuthenticationAttribute, LoginControlMailAddress +from nose import tools as nt +from .factories import InstitutionFactory, LoginControlAuthenticationAttributeFactory, LoginControlMailAddressFactory +import pytest + + +class TestLoginControlAuthenticationAttributeModel: + + @pytest.mark.django_db + def test_factory(self): + institution = InstitutionFactory() + authentication_attribute = LoginControlAuthenticationAttributeFactory(institution=institution) + nt.assert_equal(authentication_attribute.institution, institution) + nt.assert_equal(authentication_attribute.is_availability, True) + nt.assert_in(authentication_attribute.attribute_name, ATTRIBUTE_NAME_LIST) + + @pytest.mark.django_db + def test__init__(self): + institution = InstitutionFactory() + authentication_attribute = LoginControlAuthenticationAttribute(institution=institution, attribute_name='ou', attribute_value='test') + nt.assert_equal(authentication_attribute.institution, institution) + nt.assert_equal(authentication_attribute.is_availability, True) + nt.assert_in(authentication_attribute.attribute_name, ATTRIBUTE_NAME_LIST) + + +class TestLoginControlMailAddressModel: + @pytest.mark.django_db + def test_factory(self): + institution = InstitutionFactory() + mail_address = LoginControlMailAddressFactory(institution=institution) + nt.assert_equal(mail_address.institution, institution) + nt.assert_equal(mail_address.is_availability, True) + + @pytest.mark.django_db + def test__init__(self): + institution = InstitutionFactory() + mail_address = LoginControlMailAddress(institution=institution) + nt.assert_equal(mail_address.institution, institution) + nt.assert_equal(mail_address.is_availability, True) diff --git a/osf_tests/test_user.py b/osf_tests/test_user.py index f71449c1118..80b27d56c6e 100644 --- a/osf_tests/test_user.py +++ b/osf_tests/test_user.py @@ -40,6 +40,7 @@ PreprintContributor, DraftRegistrationContributor, Institution, + UserExtendedData, ) from addons.github.tests.factories import GitHubAccountFactory from addons.osfstorage.models import Region @@ -71,6 +72,7 @@ PreprintFactory, ExportDataLocationFactory, RegionFactory, + LoginControlMailAddressFactory, ) from tests.base import OsfTestCase from tests.utils import run_celery_tasks @@ -2853,3 +2855,70 @@ def test_check_spam(self, mock_do_check_spam, user): with mock.patch('osf.models.OSFUser._get_spam_content', mock.Mock(return_value='some content!')): user.check_spam(saved_fields={'schools': ['one']}, request_headers=None) assert mock_do_check_spam.call_count == 1 + + +class TestUserLoginAvailability: + @pytest.fixture + def user(self): + return AuthUserFactory() + + def test_check_login_availability_by_mail_address__no_affiliated_institution(self, user): + is_valid = user.check_login_availability_by_mail_address() + assert is_valid is True + + def test_check_login_availability_by_mail_address__has_record_and_default(self, user): + institution = InstitutionFactory(login_availability_default=True) + user.affiliated_institutions.add(institution) + user.save() + LoginControlMailAddressFactory(institution=institution, mail_address=user.username) + is_valid = user.check_login_availability_by_mail_address() + assert is_valid is False + + extended_data = UserExtendedData.objects.filter(user=user).first() + assert extended_data is None + + def test_check_login_availability_by_mail_address__has_record_and_not_default(self, user): + institution = InstitutionFactory(login_availability_default=False) + user.affiliated_institutions.add(institution) + user.save() + LoginControlMailAddressFactory(institution=institution, mail_address=user.username) + is_valid = user.check_login_availability_by_mail_address() + assert is_valid is True + + extended_data = UserExtendedData.objects.filter(user=user).first() + assert extended_data is not None + assert extended_data.data.get('idp_attr', {}).get('login_availability') == 'can login' + + def test_check_login_availability_by_mail_address__has_record_and_update_idp_attr(self, user): + institution = InstitutionFactory(login_availability_default=False) + user.affiliated_institutions.add(institution) + user.save() + UserExtendedData.objects.get_or_create(user=user, data={'idp_attr': {'login_availability': 'check mail address'}}) + LoginControlMailAddressFactory(institution=institution, mail_address=user.username) + is_valid = user.check_login_availability_by_mail_address() + assert is_valid is True + + extended_data = UserExtendedData.objects.filter(user=user).first() + assert extended_data is not None + assert extended_data.data.get('idp_attr', {}).get('login_availability') == 'can login' + + def test_check_login_availability_by_mail_address__no_record_and_default(self, user): + institution = InstitutionFactory(login_availability_default=True) + user.affiliated_institutions.add(institution) + user.save() + is_valid = user.check_login_availability_by_mail_address() + assert is_valid is True + + extended_data = UserExtendedData.objects.filter(user=user).first() + assert extended_data is not None + assert extended_data.data.get('idp_attr', {}).get('login_availability') == 'can login' + + def test_check_login_availability_by_mail_address__no_record_and_not_default(self, user): + institution = InstitutionFactory(login_availability_default=False) + user.affiliated_institutions.add(institution) + user.save() + is_valid = user.check_login_availability_by_mail_address() + assert is_valid is False + + extended_data = UserExtendedData.objects.filter(user=user).first() + assert extended_data is None diff --git a/tests/test_auth.py b/tests/test_auth.py index 798402c2290..cc7dc460a86 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -22,7 +22,7 @@ from framework.auth import Auth from framework.auth.decorators import must_be_logged_in -from osf.models import OSFUser, Session +from osf.models import OSFUser, Session, UserExtendedData from osf.utils import permissions from website import mails from website import settings @@ -750,6 +750,21 @@ def test_must_have_permission_not_logged_in(self, mock_from_kwargs, mock_to_node thriller(node=project) assert_equal(ctx.exception.code, http_status.HTTP_401_UNAUTHORIZED) + @mock.patch('framework.auth.decorators.web_url_for') + @mock.patch('osf.models.user.OSFUser.check_login_availability_by_mail_address') + @mock.patch('framework.auth.decorators.Auth.from_kwargs') + def test_must_be_logged_in_decorator_login_not_available(self, mock_from_kwargs, mock_check_login, mock_web_url_for): + user = UserFactory() + UserExtendedData.objects.get_or_create(user=user, data={'idp_attr': {'login_availability': 'check mail address'}}) + mock_from_kwargs.return_value = Auth(user=user) + mock_check_login.return_value = False + mock_web_url_for.return_value = '/?login_not_available=true' + + protected() + mock_from_kwargs.assert_called() + mock_check_login.assert_called() + mock_web_url_for.assert_called() + def needs_addon_view(**kwargs): return 'openaddon' diff --git a/tests/test_views.py b/tests/test_views.py index c9c44ed5d44..146d6ca86b6 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -2064,6 +2064,32 @@ def test_user_update_temp_user_not_exist(self): expect_errors=True) assert_equal(res.status_code, http_status.HTTP_403_FORBIDDEN) + def test_user_update_email_redirect_to_top_page(self): + user1 = AuthUserFactory(fullname='fullname_1') + inst = InstitutionFactory(login_availability_default=False) + user1.affiliated_institutions.add(inst) + user1.save() + email_1 = 'andy@cos.vn' + email_2 = 'wis@csp.com' + user1.emails.create(address=email_1) + user1.emails.create(address=email_2) + + url = api_url_for('update_user', user1._id) + + header = { + 'id': user1._id, + 'emails': [ + {'address': email_1, 'confirmed': True, 'primary': True}, + {'address': email_2}, + {'address': user1.username}, + ] + } + + res = self.app.put_json(url, header, auth=user1.auth, + expect_errors=True) + assert_equal(res.status_code, http_status.HTTP_401_UNAUTHORIZED) + assert_is_not_none(res.json_body['redirect_url']) + def test_profile_view_has_temp_user(self): user1 = AuthUserFactory(fullname='fullname_1') user2 = AuthUserFactory(fullname='fullname_2') diff --git a/website/profile/views.py b/website/profile/views.py index 69943b878c0..6a512b21f6a 100644 --- a/website/profile/views.py +++ b/website/profile/views.py @@ -115,6 +115,7 @@ def update_user(auth): ########## # Emails # ########## + check_login_availability = False if 'emails' in data: @@ -219,6 +220,8 @@ def update_user(auth): # make sure the new username has already been confirmed if username and username != user.username and user.emails.filter(address=username).exists(): + # If primary_email is changed then check login availability by mail address later + check_login_availability = True mails.send_mail( user.username, @@ -257,6 +260,13 @@ def update_user(auth): if subscription: mailchimp_utils.subscribe_mailchimp(list_name, user._id) + # check user's login availability by user's mail address + if check_login_availability and not user.check_login_availability_by_mail_address(): + # If user is not allowed to log in by mail address, logout then redirect to top page + top_page_url = web_url_for('index', login_not_available='true', _absolute=True) + logout_url = web_url_for('auth_logout', redirect_url=top_page_url) + raise HTTPError(http_status.HTTP_401_UNAUTHORIZED, data=dict(redirect_url=logout_url)) + return _profile_view(user, is_profile=True) diff --git a/website/static/js/accountSettings.js b/website/static/js/accountSettings.js index 4bfa5faf040..9378abc2ed0 100644 --- a/website/static/js/accountSettings.js +++ b/website/static/js/accountSettings.js @@ -108,9 +108,15 @@ var UserProfileClient = oop.defclass({ ).done(function (data) { ret.resolve(this.unserialize(data, profile)); }.bind(this)).fail(function(xhr, status, error) { - if (xhr.status === 400) { + if (xhr.status === 401) { + if (xhr.responseJSON && xhr.responseJSON.redirect_url) { + window.location.href = xhr.responseJSON.redirect_url; + } else { + window.location.href = '/?login_not_available=true'; + } + return; + } else if (xhr.status === 400) { $osf.growl('Error', xhr.responseJSON.message_long); - } else { $osf.growl('Error', sprintf(_('User profile not updated. Please refresh the page and try again or contact %1$s if the problem persists.'),$osf.osfSupportLink()), 'danger'); } diff --git a/website/static/js/pages/base-page.js b/website/static/js/pages/base-page.js index b68d0f3afc1..4a56c47d00f 100644 --- a/website/static/js/pages/base-page.js +++ b/website/static/js/pages/base-page.js @@ -195,6 +195,14 @@ function confirmEmails(emailsToAdd) { $osf.growl('Success', confirmMessage, 'success', 3000); confirmEmails(emailsToAdd.slice(1)); }).fail(function (xhr, textStatus, error) { + if (xhr.status === 401) { + if (xhr.responseJSON && xhr.responseJSON.redirect_url) { + window.location.href = xhr.responseJSON.redirect_url; + } else { + window.location.href = '/?login_not_available=true'; + } + return; + } Raven.captureMessage(_('Could not add email'), { extra: { url: confirmedEmailURL, diff --git a/website/translations/en/LC_MESSAGES/js_messages.po b/website/translations/en/LC_MESSAGES/js_messages.po index a1bff9090cb..06483971cee 100644 --- a/website/translations/en/LC_MESSAGES/js_messages.po +++ b/website/translations/en/LC_MESSAGES/js_messages.po @@ -9270,3 +9270,88 @@ msgstr "" msgid "The Integrated Admin added ${contributors} as contributor(s) to ${node}" msgstr "" + +msgid "A server error occurred. Please contact the administrator." +msgstr "" + +# admin/static/js/login_access_control/login-access-control.js +msgid "institution_id is required." +msgstr "" + +msgid "institution_id is invalid." +msgstr "" + +msgid "is_availability is required." +msgstr "" + +msgid "is_availability is invalid." +msgstr "" + +msgid "attribute_id is required." +msgstr "" + +msgid "attribute_id is invalid." +msgstr "" + +msgid "mail_address_id is required." +msgstr "" + +msgid "mail_address_id is invalid." +msgstr "" + +msgid "login_availability_default is required." +msgstr "" + +msgid "login_availability_default is invalid." +msgstr "" + +msgid "Attribute name is not exist in config." +msgstr "" + +msgid "Attribute name is required." +msgstr "" + +msgid "Attribute value is required." +msgstr "" + +msgid "Length of attribute value > 255 characters." +msgstr "" + +msgid "Group (attribute name, attribute value) MUST be unique." +msgstr "" + +msgid "Logic condition is invalid." +msgstr "" + +msgid "Index number does not exist." +msgstr "" + +msgid "Length of mail address > 320 characters." +msgstr "" + +msgid "Mail address format is invalid." +msgstr "" + +msgid "Mail address is required." +msgstr "" + +msgid "Mail address MUST be unique." +msgstr "" + +msgid "Can not setting login access control of the institution into the other institution." +msgstr "" + +msgid "Can not switch toggle the attribute element due to it is using in the logic condition." +msgstr "" + +msgid "Can not delete the attribute element due to it is using in the logic condition." +msgstr "" + +msgid "You do not have permission to setting login access control of the other institution." +msgstr "" + +msgid "You do not have permission to get login access control of the other institution." +msgstr "" + +msgid "Login availability is not available." +msgstr "" diff --git a/website/translations/ja/LC_MESSAGES/js_messages.po b/website/translations/ja/LC_MESSAGES/js_messages.po index 39bcc9e353d..7b96857bfaf 100644 --- a/website/translations/ja/LC_MESSAGES/js_messages.po +++ b/website/translations/ja/LC_MESSAGES/js_messages.po @@ -10555,5 +10555,93 @@ msgstr "プロジェクト制限数が不正な値です。" msgid "The new project cannot be created due to the created project number is greater than or equal the project number can create." msgstr "作成したプロジェクト数が作成可能なプロジェクトの数の上限に達しているため、新規プロジェクトを作成できません。" +msgid "Cannot be restored because export data does not exist" +msgstr "エクスポートデータが存在しないため、復元できません。" + +msgid "A server error occurred. Please contact the administrator." +msgstr "サーバーエラーが発生しました。 管理者にお問い合わせください。" + +# admin/static/js/login_access_control/login-access-control.js +msgid "institution_id is required." +msgstr "institution_id が必須です。" + +msgid "institution_id is invalid." +msgstr "institution_id が不正です。" + +msgid "is_availability is required." +msgstr "is_availability が必須です。" + +msgid "is_availability is invalid." +msgstr "is_availability が不正です。" + +msgid "attribute_id is required." +msgstr "attribute_id が必須です。" + +msgid "attribute_id is invalid." +msgstr "attribute_id が不正です。" + +msgid "mail_address_id is required." +msgstr "mail_address_id が必須です。" + +msgid "mail_address_id is invalid." +msgstr "mail_address_id が不正です。" + +msgid "login_availability_default is required." +msgstr "login_availability_default が必須です。" + +msgid "login_availability_default is invalid." +msgstr "login_availability_default が不正です。" + +msgid "Attribute name is not exist in config." +msgstr "属性名が設定に存在しません。" + +msgid "Attribute name is required." +msgstr "属性名が必須です。" + +msgid "Attribute value is required." +msgstr "属性値が必須です。" + +msgid "Length of attribute value > 255 characters." +msgstr "属性値の長さが 255 文字を超えています。" + +msgid "Group (attribute name, attribute value) MUST be unique." +msgstr "(属性名、属性値) の組み合わせは一意である必要があります。" + +msgid "Logic condition is invalid." +msgstr "論理条件が不正です。" + +msgid "Index number does not exist." +msgstr "インデックス番号が存在しません。" + +msgid "Length of mail address > 320 characters." +msgstr "メールアドレスの長さが 320 文字を超えています。" + +msgid "Mail address format is invalid." +msgstr "メールアドレスの形式が不正です。" + +msgid "Mail address is required." +msgstr "メールアドレスが必須です。" + +msgid "Mail address MUST be unique." +msgstr "メールアドレスは一意である必要があります。" + +msgid "Can not setting login access control of the institution into the other institution." +msgstr "機関のログインアクセス制御を他機関に設定することができません。" + +msgid "Can not switch toggle the attribute element due to it is using in the logic condition." +msgstr "論理条件で使用されているため、認証属性の要素のトグルを切り替えることができません。" + +msgid "Can not delete the attribute element due to it is using in the logic condition." +msgstr "論理条件で使用されているため、認証属性の要素のトグルを削除ことができません。" + +msgid "You do not have permission to setting login access control of the other institution." +msgstr "他機関のログインアクセス制御を設定する権限がありません。" + +msgid "You do not have permission to get login access control of the other institution." +msgstr "他機関のログインアクセス制御を取得する権限がありません。" + +msgid "Login availability is not available." +msgstr "ログインできません。" + msgid "The Integrated Admin added ${contributors} as contributor(s) to ${node}" msgstr "統合管理者代理アカウントが${contributors}をコントリビューターとして${node}に追加しました" diff --git a/website/translations/js_messages.pot b/website/translations/js_messages.pot index a05c7aac7ca..d08dd35b04f 100644 --- a/website/translations/js_messages.pot +++ b/website/translations/js_messages.pot @@ -9164,6 +9164,90 @@ msgstr "" msgid "Cannot be restored because export data does not exist" msgstr "" +msgid "A server error occurred. Please contact the administrator." +msgstr "" + +# admin/static/js/login_access_control/login-access-control.js +msgid "institution_id is required." +msgstr "" + +msgid "institution_id is invalid." +msgstr "" + +msgid "is_availability is required." +msgstr "" + +msgid "is_availability is invalid." +msgstr "" + +msgid "attribute_id is required." +msgstr "" + +msgid "attribute_id is invalid." +msgstr "" + +msgid "mail_address_id is required." +msgstr "" + +msgid "mail_address_id is invalid." +msgstr "" + +msgid "login_availability_default is required." +msgstr "" + +msgid "login_availability_default is invalid." +msgstr "" + +msgid "Attribute name is not exist in config." +msgstr "" + +msgid "Attribute name is required." +msgstr "" + +msgid "Attribute value is required." +msgstr "" + +msgid "Length of attribute value > 255 characters." +msgstr "" + +msgid "Group (attribute name, attribute value) MUST be unique." +msgstr "" + +msgid "Logic condition is invalid." +msgstr "" + +msgid "Index number does not exist." +msgstr "" + +msgid "Length of mail address > 320 characters." +msgstr "" + +msgid "Mail address format is invalid." +msgstr "" + +msgid "Mail address is required." +msgstr "" + +msgid "Mail address MUST be unique." +msgstr "" + +msgid "Can not setting login access control of the institution into the other institution." +msgstr "" + +msgid "Can not switch toggle the attribute element due to it is using in the logic condition." +msgstr "" + +msgid "Can not delete the attribute element due to it is using in the logic condition." +msgstr "" + +msgid "You do not have permission to setting login access control of the other institution." +msgstr "" + +msgid "You do not have permission to get login access control of the other institution." +msgstr "" + +msgid "Login availability is not available." +msgstr "" msgid "Institution does not exist" msgstr ""