From: Sofia Papagiannaki Date: Mon, 21 May 2012 12:27:40 +0000 (+0300) Subject: Provide api calls for permitting helpdesk users to access user information by user... X-Git-Tag: astakos/v0.6.0~7^2~13 X-Git-Url: https://code.grnet.gr/git/astakos/commitdiff_plain/30dc8c1a05c1b0862871a9a49bad1552311075f4?hp=e2447828a52b10c5101beb080ecab1bff4fad2ff Provide api calls for permitting helpdesk users to access user information by user email and by username Refs: #2414 --- diff --git a/snf-astakos-app/astakos/im/api.py b/snf-astakos-app/astakos/im/api.py index d47b8f3..98d2eb9 100644 --- a/snf-astakos-app/astakos/im/api.py +++ b/snf-astakos-app/astakos/im/api.py @@ -45,13 +45,15 @@ from django.http import HttpResponse from django.utils import simplejson as json from django.core.urlresolvers import reverse -from astakos.im.faults import BadRequest, Unauthorized, InternalServerError, Fault +from astakos.im.faults import BadRequest, Unauthorized, InternalServerError, \ +Fault, ItemNotFound, Forbidden from astakos.im.models import AstakosUser from astakos.im.settings import CLOUD_SERVICES, INVITATIONS_ENABLED, COOKIE_NAME, \ EMAILCHANGE_ENABLED from astakos.im.util import epoch logger = logging.getLogger(__name__) +format = ('%a, %d %b %Y %H:%M:%S GMT') def render_fault(request, fault): if isinstance(fault, InternalServerError) and settings.DEBUG: @@ -65,8 +67,10 @@ def render_fault(request, fault): response['Content-Length'] = len(response.content) return response -def api_method(http_method=None, token_required=False, perms=[]): +def api_method(http_method=None, token_required=False, perms=None): """Decorator function for views that implement an API method.""" + if not perms: + perms = [] def decorator(func): @wraps(func) @@ -81,7 +85,7 @@ def api_method(http_method=None, token_required=False, perms=[]): try: user = AstakosUser.objects.get(auth_token=x_auth_token) if not user.has_perms(perms): - raise Unauthorized('Unauthorized request') + raise Forbidden('Unauthorized request') except AstakosUser.DoesNotExist, e: raise Unauthorized('Invalid X-Auth-Token') kwargs['user'] = user @@ -221,46 +225,68 @@ def get_menu(request, with_extra_links=False, with_signout=True): return HttpResponse(content=data, mimetype=mimetype) -@api_method(http_method='GET', token_required=True, perms=['astakos.im.can_find_userid']) -def find_userid(request): - # Normal Response Codes: 204 +@api_method(http_method='GET', token_required=True, perms=['im.can_access_userinfo']) +def get_user_by_email(request, user=None): + # Normal Response Codes: 200 # Error Response Codes: internalServerError (500) # badRequest (400) # unauthorised (401) - email = request.GET.get('email') + # forbidden (403) + # itemNotFound (404) + email = request.GET.get('name') if not email: raise BadRequest('Email missing') try: - user = AstakosUser.objects.get(email = email, is_active=True) + user = AstakosUser.objects.get(email = email) except AstakosUser.DoesNotExist, e: - raise BadRequest('Invalid email') + raise ItemNotFound('Invalid email') + + if not user.is_active: + raise ItemNotFound('Inactive user') else: response = HttpResponse() - response.status=204 - user_info = {'userid':user.username} + response.status=200 + user_info = {'id':user.id, + 'username':user.username, + 'email':[user.email], + 'enabled':user.is_active, + 'name':user.realname, + 'auth_token_created':user.auth_token_created.strftime(format), + 'auth_token_expires':user.auth_token_expires.strftime(format), + 'has_credits':user.has_credits, + 'groups':[g.name for g in user.groups.all()], + 'user_permissions':[p.codename for p in user.user_permissions.all()], + 'group_permissions': list(user.get_group_permissions())} response.content = json.dumps(user_info) response['Content-Type'] = 'application/json; charset=UTF-8' response['Content-Length'] = len(response.content) return response -@api_method(http_method='GET', token_required=True, perms=['astakos.im.can_find_email']) -def find_email(request): - # Normal Response Codes: 204 +@api_method(http_method='GET', token_required=True, perms=['can_access_userinfo']) +def get_user_by_username(request, user_id, user=None): + # Normal Response Codes: 200 # Error Response Codes: internalServerError (500) # badRequest (400) # unauthorised (401) - userid = request.GET.get('userid') - if not userid: - raise BadRequest('Userid missing') + # forbidden (403) + # itemNotFound (404) try: - user = AstakosUser.objects.get(username = userid) + user = AstakosUser.objects.get(username = user_id) except AstakosUser.DoesNotExist, e: - raise BadRequest('Invalid userid') + raise ItemNotFound('Invalid userid') else: response = HttpResponse() - response.status=204 - user_info = {'userid':user.email} + response.status=200 + user_info = {'id':user.id, + 'username':user.username, + 'email':[user.email], + 'name':user.realname, + 'auth_token_created':user.auth_token_created.strftime(format), + 'auth_token_expires':user.auth_token_expires.strftime(format), + 'has_credits':user.has_credits, + 'enabled':user.is_active, + 'groups':[g.name for g in user.groups.all()]} response.content = json.dumps(user_info) response['Content-Type'] = 'application/json; charset=UTF-8' response['Content-Length'] = len(response.content) - return response + return response \ No newline at end of file diff --git a/snf-astakos-app/astakos/im/faults.py b/snf-astakos-app/astakos/im/faults.py index e5a5244..72408f1 100644 --- a/snf-astakos-app/astakos/im/faults.py +++ b/snf-astakos-app/astakos/im/faults.py @@ -49,3 +49,9 @@ class Unauthorized(Fault): class InternalServerError(Fault): code = 500 + +class Forbidden(Fault): + code = 403 + +class ItemNotFound(Fault): + code = 404 \ No newline at end of file diff --git a/snf-astakos-app/astakos/im/fixtures/groups.json b/snf-astakos-app/astakos/im/fixtures/groups.json index 472b56e..4b325cf 100644 --- a/snf-astakos-app/astakos/im/fixtures/groups.json +++ b/snf-astakos-app/astakos/im/fixtures/groups.json @@ -19,5 +19,12 @@ "fields": { "name": "shibboleth" } + }, + { + "model": "auth.group", + "pk": 4, + "fields": { + "name": "helpdesk" + } } ] diff --git a/snf-astakos-app/astakos/im/management/commands/_common.py b/snf-astakos-app/astakos/im/management/commands/_common.py index 73fe569..0771989 100644 --- a/snf-astakos-app/astakos/im/management/commands/_common.py +++ b/snf-astakos-app/astakos/im/management/commands/_common.py @@ -34,9 +34,12 @@ from datetime import datetime from django.utils.timesince import timesince, timeuntil +from django.contrib.auth.models import Permission +from django.contrib.contenttypes.models import ContentType from astakos.im.models import AstakosUser +content_type = None def get_user(email_or_id, **kwargs): try: @@ -59,3 +62,59 @@ def format_date(d): return timesince(d) + ' ago' else: return 'in ' + timeuntil(d) + +def get_astakosuser_content_type(): + if content_type: + return content_type + + try: + return ContentType.objects.get(app_label='im', + model='astakosuser') + except: + return content_type + +def add_user_permission(user, pname): + content_type = get_astakosuser_content_type() + if user.has_perm(pname): + return 0, None + p, created = Permission.objects.get_or_create(codename=pname, + name=pname.capitalize(), + content_type=content_type) + user.user_permissions.add(p) + return 1, created + +def add_group_permission(group, pname): + content_type = get_astakosuser_content_type() + if pname in [p.codename for p in group.permissions.all()]: + return 0, None + content_type = ContentType.objects.get(app_label='im', + model='astakosuser') + p, created = Permission.objects.get_or_create(codename=pname, + name=pname.capitalize(), + content_type=content_type) + group.permissions.add(p) + return 1, created + +def remove_user_permission(user, pname): + content_type = get_astakosuser_content_type() + if user.has_perm(pname): + return 0 + try: + p = Permission.objects.get(codename=pname, + content_type=content_type) + user.user_permissions.remove(p) + return 1 + except Permission.DoesNotExist, e: + return -1 + +def remove_group_permission(group, pname): + content_type = get_astakosuser_content_type() + if pname not in [p.codename for p in group.permissions.all()]: + return 0 + try: + p = Permission.objects.get(codename=pname, + content_type=content_type) + group.permissions.remove(p) + return 1 + except Permission.DoesNotExist, e: + return -1 \ No newline at end of file diff --git a/snf-astakos-app/astakos/im/management/commands/addgroup.py b/snf-astakos-app/astakos/im/management/commands/addgroup.py index b37f89f..25620da 100644 --- a/snf-astakos-app/astakos/im/management/commands/addgroup.py +++ b/snf-astakos-app/astakos/im/management/commands/addgroup.py @@ -39,15 +39,16 @@ from time import time from os.path import abspath from django.core.management.base import BaseCommand, CommandError - from django.contrib.auth.models import Group +from ._common import add_group_permission + class Command(BaseCommand): - args = "" + args = " [ ...]" help = "Insert group" def handle(self, *args, **options): - if len(args) != 1: + if len(args) < 1: raise CommandError("Invalid number of arguments") name = args[0].decode('utf8') @@ -58,6 +59,16 @@ class Command(BaseCommand): except Group.DoesNotExist, e: group = Group(name=name) group.save() - - msg = "Created group id %d" % (group.id,) - self.stdout.write(msg + '\n') + msg = "Created group id %d" % (group.id,) + self.stdout.write(msg + '\n') + try: + for pname in args[1:]: + r, created = add_group_permission(group, pname) + if created: + self.stdout.write('Permission: %s created successfully\n' % pname) + if r == 0: + self.stdout.write('Group has already permission: %s\n' % pname) + else: + self.stdout.write('Permission: %s added successfully\n' % pname) + except Exception, e: + raise CommandError(e) \ No newline at end of file diff --git a/snf-astakos-app/astakos/im/management/commands/addgrouppermissions.py b/snf-astakos-app/astakos/im/management/commands/addgrouppermissions.py new file mode 100644 index 0000000..6f6b9b9 --- /dev/null +++ b/snf-astakos-app/astakos/im/management/commands/addgrouppermissions.py @@ -0,0 +1,73 @@ +# Copyright 2012 GRNET S.A. All rights reserved. +# +# Redistribution and use in source and binary forms, with or +# without modification, are permitted provided that the following +# conditions are met: +# +# 1. Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# 2. Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials +# provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED +# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and +# documentation are those of the authors and should not be +# interpreted as representing official policies, either expressed +# or implied, of GRNET S.A. + +from optparse import make_option + +from django.core.management.base import BaseCommand, CommandError +from django.contrib.auth.models import Group, Permission +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ValidationError + +from astakos.im.models import AstakosUser +from ._common import add_group_permission + +class Command(BaseCommand): + args = " [ ...]" + help = "Add group permissions" + + def handle(self, *args, **options): + if len(args) < 2: + raise CommandError("Please provide a group name and at least one permission") + + group = None + try: + if args[0].isdigit(): + group = Group.objects.get(id=args[0]) + else: + group = Group.objects.get(name=args[0]) + except Group.DoesNotExist, e: + raise CommandError("Invalid group") + + try: + content_type = ContentType.objects.get(app_label='im', + model='astakosuser') + for pname in args[1:]: + r, created = add_group_permission(group, pname) + if created: + self.stdout.write('Permission: %s created successfully\n' % pname) + if r == 0: + self.stdout.write('Group has already permission: %s\n' % pname) + else: + self.stdout.write('Permission: %s added successfully\n' % pname) + except Exception, e: + raise CommandError(e) \ No newline at end of file diff --git a/snf-astakos-app/astakos/im/management/commands/createuser.py b/snf-astakos-app/astakos/im/management/commands/createuser.py index 9c21447..3691f4a 100644 --- a/snf-astakos-app/astakos/im/management/commands/createuser.py +++ b/snf-astakos-app/astakos/im/management/commands/createuser.py @@ -41,10 +41,14 @@ from uuid import uuid4 from django.core.management.base import BaseCommand, CommandError from django.core.validators import validate_email from django.core.exceptions import ValidationError +from django.contrib.auth.models import Group, Permission +from django.contrib.contenttypes.models import ContentType from astakos.im.models import AstakosUser from astakos.im.util import reserved_email +from ._common import add_user_permission + class Command(BaseCommand): args = " " help = "Create a user" @@ -63,7 +67,13 @@ class Command(BaseCommand): make_option('--password', dest='password', metavar='PASSWORD', - help="Set user's password") + help="Set user's password"), + make_option('--add-group', + dest='add-group', + help="Add user group"), + make_option('--add-permission', + dest='add-permission', + help="Add user permission") ) def handle(self, *args, **options): @@ -108,3 +118,25 @@ class Command(BaseCommand): if options['password'] is None: msg += " with password '%s'" % (password,) self.stdout.write(msg + '\n') + + groupname = options.get('add-group') + if groupname is not None: + try: + group = Group.objects.get(name=groupname) + user.groups.add(group) + self.stdout.write('Group: %s added successfully\n' % groupname) + except Group.DoesNotExist, e: + self.stdout.write('Group named %s does not exist\n' % groupname) + + pname = options.get('add-permission') + if pname is not None: + try: + r, created = add_user_permission(user, pname) + if created: + self.stdout.write('Permission: %s created successfully\n' % pname) + if r > 0: + self.stdout.write('Permission: %s added successfully\n' % pname) + elif r==0: + self.stdout.write('User has already permission: %s\n' % pname) + except Exception, e: + raise CommandError(e) \ No newline at end of file diff --git a/snf-astakos-app/astakos/im/management/commands/listgroups.py b/snf-astakos-app/astakos/im/management/commands/listgroups.py index 9559aaf..9846370 100644 --- a/snf-astakos-app/astakos/im/management/commands/listgroups.py +++ b/snf-astakos-app/astakos/im/management/commands/listgroups.py @@ -58,8 +58,8 @@ class Command(BaseCommand): groups = Group.objects.all() - labels = ('id', 'name') - columns = (1, 2) + labels = ('id', 'name', 'permissions') + columns = (3, 12, 50) if not options['csv']: line = ' '.join(l.rjust(w) for l, w in zip(labels, columns)) @@ -68,7 +68,8 @@ class Command(BaseCommand): self.stdout.write(sep + '\n') for group in groups: - fields = (str(group.id), group.name) + fields = (str(group.id), group.name, + ','.join(p.codename for p in group.permissions.all())) if options['csv']: line = '|'.join(fields) diff --git a/snf-astakos-app/astakos/im/management/commands/listusers.py b/snf-astakos-app/astakos/im/management/commands/listusers.py index 48e78b8..839689c 100644 --- a/snf-astakos-app/astakos/im/management/commands/listusers.py +++ b/snf-astakos-app/astakos/im/management/commands/listusers.py @@ -64,8 +64,8 @@ class Command(BaseCommand): if options['pending']: users = users.filter(is_active=False) - labels = ('id', 'email', 'real name', 'affiliation', 'active', 'admin', 'provider') - columns = (3, 24, 24, 12, 6, 5, 12) + labels = ('id', 'email', 'real name', 'active', 'admin', 'provider', 'groups') + columns = (3, 24, 24, 6, 5, 12, 24) if not options['csv']: line = ' '.join(l.rjust(w) for l, w in zip(labels, columns)) @@ -77,8 +77,8 @@ class Command(BaseCommand): id = str(user.id) active = format_bool(user.is_active) admin = format_bool(user.is_superuser) - fields = (id, user.email, user.realname, user.affiliation, active, - admin, user.provider) + fields = (id, user.email, user.realname, active, admin, user.provider, + ','.join([g.name for g in user.groups.all()])) if options['csv']: line = '|'.join(fields) diff --git a/snf-astakos-app/astakos/im/management/commands/modifyuser.py b/snf-astakos-app/astakos/im/management/commands/modifyuser.py index d50995e..45a5a8b 100644 --- a/snf-astakos-app/astakos/im/management/commands/modifyuser.py +++ b/snf-astakos-app/astakos/im/management/commands/modifyuser.py @@ -34,10 +34,12 @@ from optparse import make_option from django.core.management.base import BaseCommand, CommandError -from django.contrib.auth.models import Group +from django.contrib.auth.models import Group, Permission +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from astakos.im.models import AstakosUser +from ._common import remove_user_permission, add_user_permission class Command(BaseCommand): args = "" @@ -87,6 +89,12 @@ class Command(BaseCommand): make_option('--delete-group', dest='delete-group', help="Delete user group"), + make_option('--add-permission', + dest='add-permission', + help="Add user permission"), + make_option('--delete-permission', + dest='delete-permission', + help="Delete user permission"), ) def handle(self, *args, **options): @@ -121,7 +129,7 @@ class Command(BaseCommand): group = Group.objects.get(name=groupname) user.groups.add(group) except Group.DoesNotExist, e: - raise CommandError("Group named %s does not exist." % groupname) + self.stdout.write("Group named %s does not exist\n" % groupname) groupname = options.get('delete-group') if groupname is not None: @@ -129,7 +137,33 @@ class Command(BaseCommand): group = Group.objects.get(name=groupname) user.groups.remove(group) except Group.DoesNotExist, e: - raise CommandError("Group named %s does not exist." % groupname) + self.stdout.write("Group named %s does not exist\n" % groupname) + + pname = options.get('add-permission') + if pname is not None: + try: + r, created = add_user_permission(user, pname) + if created: + self.stdout.write('Permission: %s created successfully\n' % pname) + if r > 0: + self.stdout.write('Permission: %s added successfully\n' % pname) + elif r==0: + self.stdout.write('User has already permission: %s\n' % pname) + except Exception, e: + raise CommandError(e) + + pname = options.get('delete-permission') + if pname is not None and not user.has_perm(pname): + try: + r = remove_user_permission(user, pname) + if r < 0: + self.stdout.write('Invalid permission codename: %s\n' % pname) + elif r == 0: + self.stdout.write('User has not permission: %s\n' % pname) + elif r > 0: + self.stdout.write('Permission: %s removed successfully\n' % pname) + except Exception, e: + raise CommandError(e) level = options.get('level') if level is not None: diff --git a/snf-astakos-app/astakos/im/management/commands/removegrouppermissions.py b/snf-astakos-app/astakos/im/management/commands/removegrouppermissions.py new file mode 100644 index 0000000..5ead6b0 --- /dev/null +++ b/snf-astakos-app/astakos/im/management/commands/removegrouppermissions.py @@ -0,0 +1,70 @@ +# Copyright 2012 GRNET S.A. All rights reserved. +# +# Redistribution and use in source and binary forms, with or +# without modification, are permitted provided that the following +# conditions are met: +# +# 1. Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# 2. Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials +# provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED +# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and +# documentation are those of the authors and should not be +# interpreted as representing official policies, either expressed +# or implied, of GRNET S.A. + +from optparse import make_option + +from django.core.management.base import BaseCommand, CommandError +from django.contrib.auth.models import Group +from django.core.exceptions import ValidationError + +from astakos.im.models import AstakosUser +from ._common import remove_group_permission + +class Command(BaseCommand): + args = " [ ...]" + help = "Remove group permissions" + + def handle(self, *args, **options): + if len(args) < 2: + raise CommandError("Please provide a group name and at least one permission") + + group = None + try: + if args[0].isdigit(): + group = Group.objects.get(id=args[0]) + else: + group = Group.objects.get(name=args[0]) + except Group.DoesNotExist, e: + raise CommandError("Invalid group") + + try: + for pname in args[1:]: + r = remove_group_permission(group, pname) + if r < 0: + self.stdout.write('Invalid permission codename: %s\n' % pname) + elif r == 0: + self.stdout.write('Group has not permission: %s\n' % pname) + elif r > 0: + self.stdout.write('Permission: %s removed successfully\n' % pname) + except Exception, e: + raise CommandError(e) \ No newline at end of file diff --git a/snf-astakos-app/astakos/im/management/commands/showuser.py b/snf-astakos-app/astakos/im/management/commands/showuser.py index d149a76..1609687 100644 --- a/snf-astakos-app/astakos/im/management/commands/showuser.py +++ b/snf-astakos-app/astakos/im/management/commands/showuser.py @@ -67,7 +67,7 @@ class Command(BaseCommand): 'last login': format_date(user.last_login), 'date joined': format_date(user.date_joined), 'last update': format_date(user.updated), - 'token': user.auth_token, + #'token': user.auth_token, 'token expiration': format_date(user.auth_token_expires), 'invitations': user.invitations, 'invitation level': user.level, @@ -75,8 +75,11 @@ class Command(BaseCommand): 'verified': format_bool(user.is_verified), 'has_credits': format_bool(user.has_credits), 'groups': [elem.name for elem in user.groups.all()], + 'permissions': [elem.codename for elem in user.user_permissions.all()], + 'group_permissions': user.get_group_permissions(), 'third_party_identifier': user.third_party_identifier, - 'email_verified': format_bool(user.email_verified) + 'email_verified': format_bool(user.email_verified), + 'username': user.username } if get_latest_terms(): has_signed_terms = user.signed_terms() diff --git a/snf-astakos-app/astakos/im/urls.py b/snf-astakos-app/astakos/im/urls.py index 5317574..1b76b58 100644 --- a/snf-astakos-app/astakos/im/urls.py +++ b/snf-astakos-app/astakos/im/urls.py @@ -98,6 +98,6 @@ urlpatterns += patterns('astakos.im.api', url(r'^authenticate/v2/?$', 'authenticate'), url(r'^get_services/?$', 'get_services'), url(r'^get_menu/?$', 'get_menu'), - url(r'^find_userid/?$', 'find_userid'), - url(r'^find_email/?$', 'find_email'), + url(r'^v2.0/users/?$', 'get_user_by_email'), + url(r'^v2.0/users/(?P.+?)/?$', 'get_user_by_username'), )