From 959c068b436a4d46c0254c6dcca6a1601deafa7c Mon Sep 17 00:00:00 2001 From: Ethan Soergel Date: Fri, 19 Nov 2021 14:53:41 -0500 Subject: [PATCH 1/4] sort and clean imports --- corehq/apps/domain/decorators.py | 18 ++++++++---------- corehq/apps/ota/decorators.py | 11 +++++------ corehq/apps/ota/views.py | 24 ++++++++++++++---------- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/corehq/apps/domain/decorators.py b/corehq/apps/domain/decorators.py index d8c7ba3e6c84..2b4192ea2766 100644 --- a/corehq/apps/domain/decorators.py +++ b/corehq/apps/domain/decorators.py @@ -3,7 +3,6 @@ from django.conf import settings from django.contrib import messages -from django.contrib.auth.decorators import permission_required from django.http import ( Http404, HttpRequest, @@ -21,16 +20,8 @@ from django_otp import match_token from django_prbac.utils import has_privilege from oauth2_provider.oauth2_backends import get_oauthlib_core - from tastypie.http import HttpUnauthorized -from corehq.apps.sso.utils.request_helpers import ( - is_request_blocked_from_viewing_domain_due_to_sso, - is_request_using_sso, -) -from corehq.apps.sso.utils.view_helpers import ( - render_untrusted_identity_provider_for_domain_view, -) from dimagi.utils.django.request import mutable_querydict from dimagi.utils.web import json_response @@ -41,16 +32,23 @@ DIGEST, FORMPLAYER, OAUTH2, + HQApiKeyAuthentication, basic_or_api_key, basicauth, determine_authtype_from_request, formplayer_as_user_auth, get_username_and_password_from_request, - HQApiKeyAuthentication, ) from corehq.apps.domain.models import Domain, DomainAuditRecordEntry from corehq.apps.domain.utils import normalize_domain_name from corehq.apps.hqwebapp.signals import clear_login_attempts +from corehq.apps.sso.utils.request_helpers import ( + is_request_blocked_from_viewing_domain_due_to_sso, + is_request_using_sso, +) +from corehq.apps.sso.utils.view_helpers import ( + render_untrusted_identity_provider_for_domain_view, +) from corehq.apps.users.models import CouchUser from corehq.toggles import ( DATA_MIGRATION, diff --git a/corehq/apps/ota/decorators.py b/corehq/apps/ota/decorators.py index 38dbb03bd117..4d919bdecfda 100644 --- a/corehq/apps/ota/decorators.py +++ b/corehq/apps/ota/decorators.py @@ -1,14 +1,13 @@ import logging +from functools import wraps -from corehq import toggles -from corehq.apps.users.decorators import require_permission -from corehq.apps.users.models import Permissions +from django.http import HttpResponseForbidden from dimagi.utils.couch.cache.cache_core import get_redis_client -from functools import wraps - -from django.http import HttpResponseForbidden +from corehq import toggles +from corehq.apps.users.decorators import require_permission +from corehq.apps.users.models import Permissions auth_logger = logging.getLogger("commcare_auth") diff --git a/corehq/apps/ota/views.py b/corehq/apps/ota/views.py index 9a9e084b9527..4917621e6d52 100644 --- a/corehq/apps/ota/views.py +++ b/corehq/apps/ota/views.py @@ -2,6 +2,7 @@ import os from datetime import datetime from distutils.version import LooseVersion +from urllib.parse import unquote from django.conf import settings from django.http import ( @@ -11,21 +12,18 @@ HttpResponseForbidden, HttpResponseNotFound, JsonResponse, - HttpResponseNotFound, - HttpResponseForbidden, ) -from django.utils.translation import ugettext as _, ngettext +from django.utils.translation import ngettext +from django.utils.translation import ugettext as _ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_GET, require_POST from couchdbkit import ResourceConflict from iso8601 import iso8601 from tastypie.http import HttpTooManyRequests -from urllib.parse import unquote from casexml.apps.case.cleanup import claim_case, get_first_claim from casexml.apps.case.fixtures import CaseDBFixture -from casexml.apps.case.models import CommCareCase from casexml.apps.phone.restore import ( RestoreCacheSettings, RestoreConfig, @@ -42,6 +40,7 @@ ) from corehq.apps.app_manager.models import GlobalAppConfig from corehq.apps.builds.utils import get_default_build_spec +from corehq.apps.case_search.const import COMMCARE_PROJECT from corehq.apps.case_search.exceptions import CaseSearchUserError from corehq.apps.case_search.utils import get_case_search_results from corehq.apps.domain.decorators import ( @@ -50,11 +49,15 @@ mobile_auth_or_formplayer, ) from corehq.apps.domain.models import Domain -from corehq.apps.locations.permissions import location_safe, location_safe_bypass -from corehq.apps.ota.decorators import require_mobile_access -from corehq.apps.ota.rate_limiter import rate_limit_restore +from corehq.apps.locations.permissions import ( + location_safe, + location_safe_bypass, +) +from corehq.apps.registry.exceptions import ( + RegistryAccessException, + RegistryNotFound, +) from corehq.apps.registry.helper import DataRegistryHelper -from corehq.apps.registry.exceptions import RegistryNotFound, RegistryAccessException from corehq.apps.users.models import CouchUser, UserReportingMetadataStaging from corehq.const import ONE_DAY, OPENROSA_VERSION_MAP from corehq.form_processor.exceptions import CaseNotFound @@ -62,14 +65,15 @@ from corehq.middleware import OPENROSA_VERSION_HEADER from corehq.util.quickcache import quickcache +from .decorators import require_mobile_access from .models import DeviceLogRequest, MobileRecoveryMeasure, SerialIdBucket +from .rate_limiter import rate_limit_restore from .utils import ( demo_user_restore_response, get_restore_user, handle_401_response, is_permitted_to_restore, ) -from ..case_search.const import COMMCARE_PROJECT PROFILE_PROBABILITY = float(os.getenv('COMMCARE_PROFILE_RESTORE_PROBABILITY', 0)) PROFILE_LIMIT = os.getenv('COMMCARE_PROFILE_RESTORE_LIMIT') From 0a2e07efdd63f8c660da2f412e6d55b7fde5653e Mon Sep 17 00:00:00 2001 From: Ethan Soergel Date: Fri, 19 Nov 2021 15:19:41 -0500 Subject: [PATCH 2/4] Move mobile auth decorators to ota app --- corehq/apps/domain/decorators.py | 17 ++--------------- corehq/apps/mobile_auth/views.py | 7 ++----- corehq/apps/ota/decorators.py | 18 ++++++++++++++++++ corehq/apps/ota/views.py | 7 ++----- 4 files changed, 24 insertions(+), 25 deletions(-) diff --git a/corehq/apps/domain/decorators.py b/corehq/apps/domain/decorators.py index 2b4192ea2766..9d9204bae7d8 100644 --- a/corehq/apps/domain/decorators.py +++ b/corehq/apps/domain/decorators.py @@ -351,7 +351,7 @@ def login_or_oauth2_ex(allow_cc_users=False, allow_sessions=True, require_domain ) -def _get_multi_auth_decorator(default, allow_formplayer=False): +def get_multi_auth_decorator(default, allow_formplayer=False): """ :param allow_formplayer: If True this will allow one additional auth mechanism which is used by Formplayer: @@ -391,22 +391,9 @@ def wrapped_view(*args, **kwargs): return wraps(view_func, assigned=available_attrs(view_func))(wrapped_view) -# This decorator should be used for any endpoints used by CommCare mobile -# It supports basic, session, and apikey auth, but not digest -# Endpoints with this decorator will not enforce two factor authentication -def mobile_auth(view_func): - return _get_multi_auth_decorator(default=BASIC)(two_factor_exempt(view_func)) - - -# This decorator is used only for anonymous web apps and SMS forms -# Endpoints with this decorator will not enforce two factor authentication -def mobile_auth_or_formplayer(view_func): - return _get_multi_auth_decorator(default=BASIC, allow_formplayer=True)(two_factor_exempt(view_func)) - - # Use this decorator to allow any auth type - # basic, digest, session, or apikey -api_auth = _get_multi_auth_decorator(default=DIGEST) +api_auth = get_multi_auth_decorator(default=DIGEST) # Use these decorators on views to allow sesson-auth or an extra authorization method login_or_digest = login_or_digest_ex() diff --git a/corehq/apps/mobile_auth/views.py b/corehq/apps/mobile_auth/views.py index ef421f768150..35c040ee6469 100644 --- a/corehq/apps/mobile_auth/views.py +++ b/corehq/apps/mobile_auth/views.py @@ -6,17 +6,14 @@ from dimagi.utils.parsing import string_to_datetime from corehq.apps.domain.auth import BASIC -from corehq.apps.domain.decorators import ( - api_auth, - domain_admin_required, - mobile_auth, -) +from corehq.apps.domain.decorators import api_auth, domain_admin_required from corehq.apps.mobile_auth.models import MobileAuthKeyRecord from corehq.apps.mobile_auth.utils import ( bump_expiry, get_mobile_auth_payload, new_key_record, ) +from corehq.apps.ota.decorators import mobile_auth from corehq.apps.users.models import CommCareUser from corehq.apps.users.util import update_device_meta diff --git a/corehq/apps/ota/decorators.py b/corehq/apps/ota/decorators.py index 4d919bdecfda..ff21b6ae2eff 100644 --- a/corehq/apps/ota/decorators.py +++ b/corehq/apps/ota/decorators.py @@ -6,6 +6,11 @@ from dimagi.utils.couch.cache.cache_core import get_redis_client from corehq import toggles +from corehq.apps.domain.auth import BASIC +from corehq.apps.domain.decorators import ( + get_multi_auth_decorator, + two_factor_exempt, +) from corehq.apps.users.decorators import require_permission from corehq.apps.users.models import Permissions @@ -44,3 +49,16 @@ def _test_token_valid(origin_token): return test_result.decode("UTF-8") == '"valid"' return False + + +# This decorator should be used for any endpoints used by CommCare mobile +# It supports basic, session, and apikey auth, but not digest +# Endpoints with this decorator will not enforce two factor authentication +def mobile_auth(view_func): + return get_multi_auth_decorator(default=BASIC)(two_factor_exempt(view_func)) + + +# This decorator is used only for anonymous web apps and SMS forms +# Endpoints with this decorator will not enforce two factor authentication +def mobile_auth_or_formplayer(view_func): + return get_multi_auth_decorator(default=BASIC, allow_formplayer=True)(two_factor_exempt(view_func)) diff --git a/corehq/apps/ota/views.py b/corehq/apps/ota/views.py index 4917621e6d52..8af80f4f22cb 100644 --- a/corehq/apps/ota/views.py +++ b/corehq/apps/ota/views.py @@ -43,16 +43,13 @@ from corehq.apps.case_search.const import COMMCARE_PROJECT from corehq.apps.case_search.exceptions import CaseSearchUserError from corehq.apps.case_search.utils import get_case_search_results -from corehq.apps.domain.decorators import ( - check_domain_migration, - mobile_auth, - mobile_auth_or_formplayer, -) +from corehq.apps.domain.decorators import check_domain_migration from corehq.apps.domain.models import Domain from corehq.apps.locations.permissions import ( location_safe, location_safe_bypass, ) +from corehq.apps.ota.decorators import mobile_auth, mobile_auth_or_formplayer from corehq.apps.registry.exceptions import ( RegistryAccessException, RegistryNotFound, From 88ecc747222f29a76d2bbf1994caf9e3d2b2e028 Mon Sep 17 00:00:00 2001 From: Ethan Soergel Date: Fri, 19 Nov 2021 15:27:49 -0500 Subject: [PATCH 3/4] Check mobile access permission for all endpoints except form submission, as that handles things very differently --- corehq/apps/ota/decorators.py | 12 ++++++++++-- corehq/apps/ota/views.py | 2 -- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/corehq/apps/ota/decorators.py b/corehq/apps/ota/decorators.py index ff21b6ae2eff..55659bfa51b0 100644 --- a/corehq/apps/ota/decorators.py +++ b/corehq/apps/ota/decorators.py @@ -55,10 +55,18 @@ def _test_token_valid(origin_token): # It supports basic, session, and apikey auth, but not digest # Endpoints with this decorator will not enforce two factor authentication def mobile_auth(view_func): - return get_multi_auth_decorator(default=BASIC)(two_factor_exempt(view_func)) + return get_multi_auth_decorator(default=BASIC)( + two_factor_exempt( + require_mobile_access(view_func) + ) + ) # This decorator is used only for anonymous web apps and SMS forms # Endpoints with this decorator will not enforce two factor authentication def mobile_auth_or_formplayer(view_func): - return get_multi_auth_decorator(default=BASIC, allow_formplayer=True)(two_factor_exempt(view_func)) + return get_multi_auth_decorator(default=BASIC, allow_formplayer=True)( + two_factor_exempt( + require_mobile_access(view_func) + ) + ) diff --git a/corehq/apps/ota/views.py b/corehq/apps/ota/views.py index 8af80f4f22cb..99ec60a50718 100644 --- a/corehq/apps/ota/views.py +++ b/corehq/apps/ota/views.py @@ -62,7 +62,6 @@ from corehq.middleware import OPENROSA_VERSION_HEADER from corehq.util.quickcache import quickcache -from .decorators import require_mobile_access from .models import DeviceLogRequest, MobileRecoveryMeasure, SerialIdBucket from .rate_limiter import rate_limit_restore from .utils import ( @@ -80,7 +79,6 @@ @location_safe @handle_401_response @mobile_auth_or_formplayer -@require_mobile_access @check_domain_migration def restore(request, domain, app_id=None): """ From 906370e9dd6161f4ec4a53a5439ed855b193d072 Mon Sep 17 00:00:00 2001 From: Ethan Soergel Date: Mon, 22 Nov 2021 09:55:13 -0500 Subject: [PATCH 4/4] Protect form submission endpoints I'm not totally certain of the _noauth_post endpoint, but from looking at it, it's probably best to default to locking it down --- corehq/apps/receiverwrapper/views.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/corehq/apps/receiverwrapper/views.py b/corehq/apps/receiverwrapper/views.py index ed2ecb3bf716..df3f83baf9dc 100644 --- a/corehq/apps/receiverwrapper/views.py +++ b/corehq/apps/receiverwrapper/views.py @@ -33,6 +33,7 @@ two_factor_exempt, ) from corehq.apps.locations.permissions import location_safe +from corehq.apps.ota.decorators import require_mobile_access from corehq.apps.ota.utils import handle_401_response from corehq.apps.receiverwrapper.auth import ( AuthContext, @@ -230,6 +231,7 @@ def post(request, domain, app_id=None): ) +@require_mobile_access def _noauth_post(request, domain, app_id=None): """ This is explicitly called for a submission that has secure submissions enabled, but is manually @@ -301,6 +303,7 @@ def case_block_ok(case_updates): @login_or_digest_ex(allow_cc_users=True) @two_factor_exempt +@require_mobile_access @set_request_duration_reporting_threshold(60) def _secure_post_digest(request, domain, app_id=None): """only ever called from secure post""" @@ -316,6 +319,7 @@ def _secure_post_digest(request, domain, app_id=None): @handle_401_response @login_or_basic_ex(allow_cc_users=True) @two_factor_exempt +@require_mobile_access @set_request_duration_reporting_threshold(60) def _secure_post_basic(request, domain, app_id=None): """only ever called from secure post""" @@ -331,6 +335,7 @@ def _secure_post_basic(request, domain, app_id=None): @login_or_api_key_ex() @require_permission(Permissions.edit_data) @require_permission(Permissions.access_api) +@require_mobile_access @set_request_duration_reporting_threshold(60) def _secure_post_api_key(request, domain, app_id=None): """only ever called from secure post"""