Skip to content

Commit

Permalink
Merge branch 'dmr/test-group-api-sort' into sr/sortable-exact
Browse files Browse the repository at this point in the history
  • Loading branch information
dannyroberts committed Aug 4, 2020
2 parents 651b240 + ede5519 commit 2eccc6f
Show file tree
Hide file tree
Showing 124 changed files with 3,503 additions and 4,706 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,4 @@ docker/data
*.lock
!yarn.lock
fab/config.py
extensions/
7 changes: 6 additions & 1 deletion corehq/apps/accounting/enterprise.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import re
from datetime import datetime, timedelta

from couchdbkit import ResourceNotFound

from django.utils.translation import ugettext as _

from memoized import memoized
Expand Down Expand Up @@ -147,7 +149,10 @@ def _get_role_name(role):
return role
else:
role_id = role[len('user-role:'):]
return UserRole.get(role_id).name
try:
return UserRole.get(role_id).name
except ResourceNotFound:
return _('Unknown Role')
else:
return 'N/A'
rows = []
Expand Down
86 changes: 65 additions & 21 deletions corehq/apps/api/resources/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,31 @@
from corehq.apps.domain.auth import BASIC, determine_authtype_from_header
from corehq.apps.domain.decorators import (
api_key_auth,
api_key_auth_no_domain,
basic_auth,
basic_auth_no_domain,
basic_auth_or_try_api_key_auth,
digest_auth,
digest_auth_no_domain,
login_or_api_key,
login_or_basic,
login_or_digest,
login_or_oauth2, oauth2_auth)
login_or_oauth2,
oauth2_auth,
oauth2_auth_no_domain,
)
from corehq.apps.users.decorators import (
require_permission,
require_permission_raw,
)
from corehq.toggles import API_THROTTLE_WHITELIST, IS_CONTRACTOR


def api_auth(view_func):
def wrap_4xx_errors_for_apis(view_func):
@wraps(view_func)
def _inner(req, domain, *args, **kwargs):
def _inner(req, *args, **kwargs):
try:
return view_func(req, domain, *args, **kwargs)
return view_func(req, *args, **kwargs)
except Http404 as e:
if str(e):
return HttpResponse(json.dumps({"error": str(e)}),
Expand All @@ -40,7 +46,56 @@ def _inner(req, domain, *args, **kwargs):
return _inner


class LoginAndDomainAuthentication(Authentication):
class HQAuthenticationMixin:
decorator_map = {} # should be set by subclasses

def _get_auth_decorator(self, request):
return self.decorator_map[determine_authtype_from_header(request)]

def get_identifier(self, request):
username = request.couch_user.username
if API_THROTTLE_WHITELIST.enabled(username):
return username
return f"{getattr(request, 'domain', '')}_{username}"


class LoginAuthentication(HQAuthenticationMixin, Authentication):
"""
Just checks you are able to login. Does not check against any permissions/domains, etc.
"""
def __init__(self):
super().__init__()
self.decorator_map = {
'digest': digest_auth_no_domain,
'basic': basic_auth_no_domain,
'api_key': api_key_auth_no_domain,
'oauth2': oauth2_auth_no_domain,
}

def is_authenticated(self, request, **kwargs):
return self._auth_test(request, wrappers=[
self._get_auth_decorator(request),
wrap_4xx_errors_for_apis,
], **kwargs)

def _auth_test(self, request, wrappers, **kwargs):
PASSED_AUTH = object()

def dummy(request, **kwargs):
return PASSED_AUTH

wrapped_dummy = dummy
for wrapper in wrappers:
wrapped_dummy = wrapper(wrapped_dummy)

try:
response = wrapped_dummy(request, **kwargs)
return response is PASSED_AUTH
except PermissionDenied:
return False


class LoginAndDomainAuthentication(HQAuthenticationMixin, Authentication):

def __init__(self, allow_session_auth=False, *args, **kwargs):
"""
Expand All @@ -66,15 +121,10 @@ def __init__(self, allow_session_auth=False, *args, **kwargs):
def is_authenticated(self, request, **kwargs):
return self._auth_test(request, wrappers=[
self._get_auth_decorator(request),
api_auth,
wrap_4xx_errors_for_apis,
require_permission('access_api', login_decorator=self._get_auth_decorator(request)),
], **kwargs)

def _get_auth_decorator(self, request):
# the initial digest request doesn't have any authorization, so default to
# digest in order to send back
return self.decorator_map[determine_authtype_from_header(request)]

def _auth_test(self, request, wrappers, **kwargs):
PASSED_AUTH = 'is_authenticated'

Expand All @@ -98,12 +148,6 @@ def dummy(request, domain, **kwargs):
else:
return response

def get_identifier(self, request):
username = request.couch_user.username
if API_THROTTLE_WHITELIST.enabled(username) or not hasattr(request, 'domain'):
return username
return f"{request.domain}_{username}"


class RequirePermissionAuthentication(LoginAndDomainAuthentication):

Expand All @@ -114,7 +158,7 @@ def __init__(self, permission, *args, **kwargs):
def is_authenticated(self, request, **kwargs):
wrappers = [
require_permission(self.permission, login_decorator=self._get_auth_decorator(request)),
api_auth,
wrap_4xx_errors_for_apis,
]
return self._auth_test(request, wrappers=wrappers, **kwargs)

Expand All @@ -134,7 +178,7 @@ def is_authenticated(self, request, **kwargs):
odata_permissions_check,
self._get_auth_decorator(request)
),
api_auth,
wrap_4xx_errors_for_apis,
]
return self._auth_test(request, wrappers=wrappers, **kwargs)

Expand All @@ -148,7 +192,7 @@ def is_authenticated(self, request, **kwargs):
permission_check = lambda couch_user, domain: couch_user.is_domain_admin(domain)
wrappers = [
require_permission_raw(permission_check, login_decorator=self._get_auth_decorator(request)),
api_auth,
wrap_4xx_errors_for_apis,
]
return self._auth_test(request, wrappers=wrappers, **kwargs)

Expand All @@ -167,7 +211,7 @@ def is_authenticated(self, request, **kwargs):
self._permission_check,
login_decorator=self._get_auth_decorator(request)
)
wrappers = [decorator, api_auth]
wrappers = [decorator, wrap_4xx_errors_for_apis]
# passing the domain is a hack to work around non-domain-specific requests
# failing on auth
return self._auth_test(request, wrappers=wrappers, domain='dimagi', **kwargs)
8 changes: 4 additions & 4 deletions corehq/apps/api/resources/v0_5.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
AdminAuthentication,
ODataAuthentication,
RequirePermissionAuthentication,
)
LoginAuthentication)
from corehq.apps.api.resources.meta import CustomResourceMeta
from corehq.apps.api.util import get_obj
from corehq.apps.app_manager.models import Application
Expand Down Expand Up @@ -94,7 +94,7 @@
HqBaseResource,
v0_1,
v0_4,
)
CorsResourceMixin)
from .pagination import DoesNothingPaginator, NoCountingPaginator

MOCK_BULK_USER_ES = None
Expand Down Expand Up @@ -811,13 +811,13 @@ class Meta(CustomResourceMeta):
UserDomain.__new__.__defaults__ = ('', '')


class UserDomainsResource(Resource):
class UserDomainsResource(CorsResourceMixin, Resource):
domain_name = fields.CharField(attribute='domain_name')
project_name = fields.CharField(attribute='project_name')

class Meta(object):
resource_name = 'user_domains'
authentication = HQApiKeyAuthentication()
authentication = LoginAuthentication()
object_class = UserDomain
include_resource_uri = False

Expand Down
60 changes: 31 additions & 29 deletions corehq/apps/api/tests/group_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,36 +24,33 @@ def setUpClass(cls):

def test_get_list(self):

group = Group({"name": "test", "domain": self.domain.name})
group.save()
send_to_elasticsearch('groups', group.to_json())
self.es.indices.refresh(GROUP_INDEX_INFO.index)
self.addCleanup(group.delete)
self.addCleanup(lambda: send_to_elasticsearch('groups', group.to_json(), delete=True))
backend_id = group.get_id
# create groups in jumbled (non-alphabetical) order
group_b = self._add_group(Group({"name": "test_b", "domain": self.domain.name}), send_to_es=True)
group_d = self._add_group(Group({"name": "test_d", "domain": self.domain.name}), send_to_es=True)
group_c = self._add_group(Group({"name": "test_c", "domain": self.domain.name}), send_to_es=True)
group_a = self._add_group(Group({"name": "test_a", "domain": self.domain.name}), send_to_es=True)
groups_in_order = [group_a, group_b, group_c, group_d]

response = self._assert_auth_get_resource(self.list_endpoint)
self.assertEqual(response.status_code, 200)

api_groups = json.loads(response.content)['objects']
self.assertEqual(len(api_groups), 1)
self.assertEqual(api_groups[0]['id'], backend_id)
self.assertEqual(api_groups[0], {
'case_sharing': False,
'domain': 'qwerty',
'id': backend_id,
'metadata': {},
'name': 'test',
'reporting': True,
'resource_uri': '/a/qwerty/api/v0.5/group/{}/'.format(backend_id),
'users': [],
})
self.assertEqual(len(api_groups), 4)
for i, group in enumerate(groups_in_order):
self.assertEqual(api_groups[i]['id'], group.get_id, f"group_id for api_groups[{i}] is wrong")
self.assertEqual(api_groups[i], {
'case_sharing': False,
'domain': 'qwerty',
'id': group.get_id,
'metadata': {},
'name': group.name,
'reporting': True,
'resource_uri': '/a/qwerty/api/v0.5/group/{}/'.format(group.get_id),
'users': [],
})

def test_get_single(self):

group = Group({"name": "test", "domain": self.domain.name})
group.save()
self.addCleanup(group.delete)
group = self._add_group(Group({"name": "test", "domain": self.domain.name}))
backend_id = group.get_id

response = self._assert_auth_get_resource(self.single_endpoint(backend_id))
Expand Down Expand Up @@ -97,9 +94,7 @@ def test_create(self):

def test_update(self):

group = Group({"name": "test", "domain": self.domain.name})
group.save()
self.addCleanup(group.delete)
group = self._add_group(Group({"name": "test", "domain": self.domain.name}))

group_json = {
"case_sharing": True,
Expand All @@ -125,11 +120,18 @@ def test_update(self):

def test_delete_group(self):

group = Group({"name": "test", "domain": self.domain.name})
group.save()
self.addCleanup(group.delete)
group = self._add_group(Group({"name": "test", "domain": self.domain.name}))

backend_id = group._id
response = self._assert_auth_post_resource(self.single_endpoint(backend_id), '', method='DELETE')
self.assertEqual(response.status_code, 204, response.content)
self.assertEqual(0, len(Group.by_domain(self.domain.name)))

def _add_group(self, group, send_to_es=False):
group.save()
self.addCleanup(group.delete)
if send_to_es:
send_to_elasticsearch('groups', group.to_json())
self.addCleanup(lambda: send_to_elasticsearch('groups', group.to_json(), delete=True))
self.es.indices.refresh(GROUP_INDEX_INFO.index)
return group
10 changes: 0 additions & 10 deletions corehq/apps/case_search/admin.py

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Generated by Django 2.2.13 on 2020-07-31 19:17

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('case_search', '0008_auto_20180119_1716'),
]

operations = [
migrations.DeleteModel(
name='CaseSearchQueryAddition',
),
]
Loading

0 comments on commit 2eccc6f

Please sign in to comment.