Revision 30dc8c1a

b/snf-astakos-app/astakos/im/api.py
45 45
from django.utils import simplejson as json
46 46
from django.core.urlresolvers import reverse
47 47

  
48
from astakos.im.faults import BadRequest, Unauthorized, InternalServerError, Fault
48
from astakos.im.faults import BadRequest, Unauthorized, InternalServerError, \
49
Fault, ItemNotFound, Forbidden
49 50
from astakos.im.models import AstakosUser
50 51
from astakos.im.settings import CLOUD_SERVICES, INVITATIONS_ENABLED, COOKIE_NAME, \
51 52
EMAILCHANGE_ENABLED
52 53
from astakos.im.util import epoch
53 54

  
54 55
logger = logging.getLogger(__name__)
56
format = ('%a, %d %b %Y %H:%M:%S GMT')
55 57

  
56 58
def render_fault(request, fault):
57 59
    if isinstance(fault, InternalServerError) and settings.DEBUG:
......
65 67
    response['Content-Length'] = len(response.content)
66 68
    return response
67 69

  
68
def api_method(http_method=None, token_required=False, perms=[]):
70
def api_method(http_method=None, token_required=False, perms=None):
69 71
    """Decorator function for views that implement an API method."""
72
    if not perms:
73
        perms = []
70 74
    
71 75
    def decorator(func):
72 76
        @wraps(func)
......
81 85
                    try:
82 86
                        user = AstakosUser.objects.get(auth_token=x_auth_token)
83 87
                        if not user.has_perms(perms):
84
                            raise Unauthorized('Unauthorized request')
88
                            raise Forbidden('Unauthorized request')
85 89
                    except AstakosUser.DoesNotExist, e:
86 90
                        raise Unauthorized('Invalid X-Auth-Token')
87 91
                    kwargs['user'] = user
......
221 225

  
222 226
    return HttpResponse(content=data, mimetype=mimetype)
223 227

  
224
@api_method(http_method='GET', token_required=True, perms=['astakos.im.can_find_userid'])
225
def find_userid(request):
226
    # Normal Response Codes: 204
228
@api_method(http_method='GET', token_required=True, perms=['im.can_access_userinfo'])
229
def get_user_by_email(request, user=None):
230
    # Normal Response Codes: 200
227 231
    # Error Response Codes: internalServerError (500)
228 232
    #                       badRequest (400)
229 233
    #                       unauthorised (401)
230
    email = request.GET.get('email')
234
    #                       forbidden (403)
235
    #                       itemNotFound (404)
236
    email = request.GET.get('name')
231 237
    if not email:
232 238
        raise BadRequest('Email missing')
233 239
    try:
234
        user = AstakosUser.objects.get(email = email, is_active=True)
240
        user = AstakosUser.objects.get(email = email)
235 241
    except AstakosUser.DoesNotExist, e:
236
        raise BadRequest('Invalid email')
242
        raise ItemNotFound('Invalid email')
243
    
244
    if not user.is_active:
245
        raise ItemNotFound('Inactive user')
237 246
    else:
238 247
        response = HttpResponse()
239
        response.status=204
240
        user_info = {'userid':user.username}
248
        response.status=200
249
        user_info = {'id':user.id,
250
                     'username':user.username,
251
                     'email':[user.email],
252
                     'enabled':user.is_active,
253
                     'name':user.realname,
254
                     'auth_token_created':user.auth_token_created.strftime(format),
255
                     'auth_token_expires':user.auth_token_expires.strftime(format),
256
                     'has_credits':user.has_credits,
257
                     'groups':[g.name for g in user.groups.all()],
258
                     'user_permissions':[p.codename for p in user.user_permissions.all()],
259
                     'group_permissions': list(user.get_group_permissions())}
241 260
        response.content = json.dumps(user_info)
242 261
        response['Content-Type'] = 'application/json; charset=UTF-8'
243 262
        response['Content-Length'] = len(response.content)
244 263
        return response
245 264

  
246
@api_method(http_method='GET', token_required=True, perms=['astakos.im.can_find_email'])
247
def find_email(request):
248
    # Normal Response Codes: 204
265
@api_method(http_method='GET', token_required=True, perms=['can_access_userinfo'])
266
def get_user_by_username(request, user_id, user=None):
267
    # Normal Response Codes: 200
249 268
    # Error Response Codes: internalServerError (500)
250 269
    #                       badRequest (400)
251 270
    #                       unauthorised (401)
252
    userid = request.GET.get('userid')
253
    if not userid:
254
        raise BadRequest('Userid missing')
271
    #                       forbidden (403)
272
    #                       itemNotFound (404)
255 273
    try:
256
        user = AstakosUser.objects.get(username = userid)
274
        user = AstakosUser.objects.get(username = user_id)
257 275
    except AstakosUser.DoesNotExist, e:
258
        raise BadRequest('Invalid userid')
276
        raise ItemNotFound('Invalid userid')
259 277
    else:
260 278
        response = HttpResponse()
261
        response.status=204
262
        user_info = {'userid':user.email}
279
        response.status=200
280
        user_info = {'id':user.id,
281
                     'username':user.username,
282
                     'email':[user.email],
283
                     'name':user.realname,
284
                     'auth_token_created':user.auth_token_created.strftime(format),
285
                     'auth_token_expires':user.auth_token_expires.strftime(format),
286
                     'has_credits':user.has_credits,
287
                     'enabled':user.is_active,
288
                     'groups':[g.name for g in user.groups.all()]}
263 289
        response.content = json.dumps(user_info)
264 290
        response['Content-Type'] = 'application/json; charset=UTF-8'
265 291
        response['Content-Length'] = len(response.content)
266
        return response
292
        return response
b/snf-astakos-app/astakos/im/faults.py
49 49

  
50 50
class InternalServerError(Fault):
51 51
    code = 500
52

  
53
class Forbidden(Fault):
54
    code = 403
55

  
56
class ItemNotFound(Fault):
57
    code = 404
b/snf-astakos-app/astakos/im/fixtures/groups.json
19 19
        "fields": {
20 20
            "name": "shibboleth"
21 21
        }
22
    },
23
    {
24
        "model": "auth.group",
25
        "pk": 4,
26
        "fields": {
27
            "name": "helpdesk"
28
        }
22 29
    }
23 30
]
b/snf-astakos-app/astakos/im/management/commands/_common.py
34 34
from datetime import datetime
35 35

  
36 36
from django.utils.timesince import timesince, timeuntil
37
from django.contrib.auth.models import Permission
38
from django.contrib.contenttypes.models import ContentType
37 39

  
38 40
from astakos.im.models import AstakosUser
39 41

  
42
content_type = None
40 43

  
41 44
def get_user(email_or_id, **kwargs):
42 45
    try:
......
59 62
        return timesince(d) + ' ago'
60 63
    else:
61 64
        return 'in ' + timeuntil(d)
65

  
66
def get_astakosuser_content_type():
67
    if content_type:
68
        return content_type
69
    
70
    try:
71
        return ContentType.objects.get(app_label='im',
72
                                       model='astakosuser')
73
    except:
74
        return content_type
75
    
76
def add_user_permission(user, pname):
77
    content_type = get_astakosuser_content_type()
78
    if user.has_perm(pname):
79
        return 0, None
80
    p, created = Permission.objects.get_or_create(codename=pname,
81
                                                  name=pname.capitalize(),
82
                                                  content_type=content_type)
83
    user.user_permissions.add(p)
84
    return 1, created
85

  
86
def add_group_permission(group, pname):
87
    content_type = get_astakosuser_content_type()
88
    if pname in [p.codename for p in group.permissions.all()]:
89
        return 0, None
90
    content_type = ContentType.objects.get(app_label='im',
91
                                           model='astakosuser')
92
    p, created = Permission.objects.get_or_create(codename=pname,
93
                                                  name=pname.capitalize(),
94
                                                  content_type=content_type)
95
    group.permissions.add(p)
96
    return 1, created
97

  
98
def remove_user_permission(user, pname):
99
    content_type = get_astakosuser_content_type()
100
    if user.has_perm(pname):
101
        return 0
102
    try:
103
        p = Permission.objects.get(codename=pname,
104
                                    content_type=content_type)
105
        user.user_permissions.remove(p)
106
        return 1
107
    except Permission.DoesNotExist, e:
108
        return -1
109

  
110
def remove_group_permission(group, pname):
111
    content_type = get_astakosuser_content_type()
112
    if pname not in [p.codename for p in group.permissions.all()]:
113
        return 0
114
    try:
115
        p = Permission.objects.get(codename=pname,
116
                                    content_type=content_type)
117
        group.permissions.remove(p)
118
        return 1
119
    except Permission.DoesNotExist, e:
120
        return -1
b/snf-astakos-app/astakos/im/management/commands/addgroup.py
39 39
from os.path import abspath
40 40

  
41 41
from django.core.management.base import BaseCommand, CommandError
42

  
43 42
from django.contrib.auth.models import Group
44 43

  
44
from ._common import add_group_permission
45

  
45 46
class Command(BaseCommand):
46
    args = "<name>"
47
    args = "<groupname> [<permission> ...]"
47 48
    help = "Insert group"
48 49
    
49 50
    def handle(self, *args, **options):
50
        if len(args) != 1:
51
        if len(args) < 1:
51 52
            raise CommandError("Invalid number of arguments")
52 53
        
53 54
        name = args[0].decode('utf8')
......
58 59
        except Group.DoesNotExist, e:
59 60
            group = Group(name=name)
60 61
            group.save()
61
        
62
        msg = "Created group id %d" % (group.id,)
63
        self.stdout.write(msg + '\n')
62
            msg = "Created group id %d" % (group.id,)
63
            self.stdout.write(msg + '\n')
64
            try:
65
                for pname in args[1:]:
66
                    r, created = add_group_permission(group, pname)
67
                    if created:
68
                        self.stdout.write('Permission: %s created successfully\n' % pname)
69
                    if r == 0:
70
                        self.stdout.write('Group has already permission: %s\n' % pname)
71
                    else:
72
                        self.stdout.write('Permission: %s added successfully\n' % pname)
73
            except Exception, e:
74
                raise CommandError(e)
b/snf-astakos-app/astakos/im/management/commands/addgrouppermissions.py
1
# Copyright 2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

  
34
from optparse import make_option
35

  
36
from django.core.management.base import BaseCommand, CommandError
37
from django.contrib.auth.models import Group, Permission
38
from django.contrib.contenttypes.models import ContentType
39
from django.core.exceptions import ValidationError
40

  
41
from astakos.im.models import AstakosUser
42
from ._common import add_group_permission
43

  
44
class Command(BaseCommand):
45
    args = "<groupname> <permission> [<permissions> ...]"
46
    help = "Add group permissions"
47
    
48
    def handle(self, *args, **options):
49
        if len(args) < 2:
50
            raise CommandError("Please provide a group name and at least one permission")
51
        
52
        group = None
53
        try:
54
            if args[0].isdigit():
55
                group = Group.objects.get(id=args[0])
56
            else:
57
                group = Group.objects.get(name=args[0])
58
        except Group.DoesNotExist, e:
59
            raise CommandError("Invalid group")
60
        
61
        try:
62
            content_type = ContentType.objects.get(app_label='im',
63
                                                       model='astakosuser')
64
            for pname in args[1:]:
65
                r, created = add_group_permission(group, pname)
66
                if created:
67
                    self.stdout.write('Permission: %s created successfully\n' % pname)
68
                if r == 0:
69
                    self.stdout.write('Group has already permission: %s\n' % pname)
70
                else:
71
                    self.stdout.write('Permission: %s added successfully\n' % pname)
72
        except Exception, e:
73
            raise CommandError(e)
b/snf-astakos-app/astakos/im/management/commands/createuser.py
41 41
from django.core.management.base import BaseCommand, CommandError
42 42
from django.core.validators import validate_email
43 43
from django.core.exceptions import ValidationError
44
from django.contrib.auth.models import Group, Permission
45
from django.contrib.contenttypes.models import ContentType
44 46

  
45 47
from astakos.im.models import AstakosUser
46 48
from astakos.im.util import reserved_email
47 49

  
50
from ._common import add_user_permission
51

  
48 52
class Command(BaseCommand):
49 53
    args = "<email> <first name> <last name> <affiliation>"
50 54
    help = "Create a user"
......
63 67
        make_option('--password',
64 68
            dest='password',
65 69
            metavar='PASSWORD',
66
            help="Set user's password")
70
            help="Set user's password"),
71
        make_option('--add-group',
72
            dest='add-group',
73
            help="Add user group"),
74
        make_option('--add-permission',
75
            dest='add-permission',
76
            help="Add user permission")
67 77
        )
68 78
    
69 79
    def handle(self, *args, **options):
......
108 118
            if options['password'] is None:
109 119
                msg += " with password '%s'" % (password,)
110 120
            self.stdout.write(msg + '\n')
121
            
122
            groupname = options.get('add-group')
123
            if groupname is not None:
124
                try:
125
                    group = Group.objects.get(name=groupname)
126
                    user.groups.add(group)
127
                    self.stdout.write('Group: %s added successfully\n' % groupname)
128
                except Group.DoesNotExist, e:
129
                    self.stdout.write('Group named %s does not exist\n' % groupname)
130
            
131
            pname = options.get('add-permission')
132
            if pname is not None:
133
                try:
134
                    r, created = add_user_permission(user, pname)
135
                    if created:
136
                        self.stdout.write('Permission: %s created successfully\n' % pname)
137
                    if r > 0:
138
                        self.stdout.write('Permission: %s added successfully\n' % pname)
139
                    elif r==0:
140
                        self.stdout.write('User has already permission: %s\n' % pname)
141
                except Exception, e:
142
                    raise CommandError(e)
b/snf-astakos-app/astakos/im/management/commands/listgroups.py
58 58
        
59 59
        groups = Group.objects.all()
60 60
        
61
        labels = ('id', 'name')
62
        columns = (1, 2)
61
        labels = ('id', 'name', 'permissions')
62
        columns = (3, 12, 50)
63 63
        
64 64
        if not options['csv']:
65 65
            line = ' '.join(l.rjust(w) for l, w in zip(labels, columns))
......
68 68
            self.stdout.write(sep + '\n')
69 69
        
70 70
        for group in groups:
71
            fields = (str(group.id), group.name)
71
            fields = (str(group.id), group.name,
72
                      ','.join(p.codename for p in group.permissions.all()))
72 73
            
73 74
            if options['csv']:
74 75
                line = '|'.join(fields)
b/snf-astakos-app/astakos/im/management/commands/listusers.py
64 64
        if options['pending']:
65 65
            users = users.filter(is_active=False)
66 66
        
67
        labels = ('id', 'email', 'real name', 'affiliation', 'active', 'admin', 'provider')
68
        columns = (3, 24, 24, 12, 6, 5, 12)
67
        labels = ('id', 'email', 'real name', 'active', 'admin', 'provider', 'groups')
68
        columns = (3, 24, 24, 6, 5, 12,  24)
69 69
        
70 70
        if not options['csv']:
71 71
            line = ' '.join(l.rjust(w) for l, w in zip(labels, columns))
......
77 77
            id = str(user.id)
78 78
            active = format_bool(user.is_active)
79 79
            admin = format_bool(user.is_superuser)
80
            fields = (id, user.email, user.realname, user.affiliation, active,
81
                      admin, user.provider)
80
            fields = (id, user.email, user.realname, active, admin, user.provider,
81
                      ','.join([g.name for g in user.groups.all()]))
82 82
            
83 83
            if options['csv']:
84 84
                line = '|'.join(fields)
b/snf-astakos-app/astakos/im/management/commands/modifyuser.py
34 34
from optparse import make_option
35 35

  
36 36
from django.core.management.base import BaseCommand, CommandError
37
from django.contrib.auth.models import Group
37
from django.contrib.auth.models import Group, Permission
38
from django.contrib.contenttypes.models import ContentType
38 39
from django.core.exceptions import ValidationError
39 40

  
40 41
from astakos.im.models import AstakosUser
42
from ._common import remove_user_permission, add_user_permission
41 43

  
42 44
class Command(BaseCommand):
43 45
    args = "<user ID>"
......
87 89
        make_option('--delete-group',
88 90
            dest='delete-group',
89 91
            help="Delete user group"),
92
        make_option('--add-permission',
93
            dest='add-permission',
94
            help="Add user permission"),
95
        make_option('--delete-permission',
96
            dest='delete-permission',
97
            help="Delete user permission"),
90 98
        )
91 99
    
92 100
    def handle(self, *args, **options):
......
121 129
                group = Group.objects.get(name=groupname)
122 130
                user.groups.add(group)
123 131
            except Group.DoesNotExist, e:
124
                raise CommandError("Group named %s does not exist." % groupname)
132
                self.stdout.write("Group named %s does not exist\n" % groupname)
125 133
        
126 134
        groupname = options.get('delete-group')
127 135
        if groupname is not None:
......
129 137
                group = Group.objects.get(name=groupname)
130 138
                user.groups.remove(group)
131 139
            except Group.DoesNotExist, e:
132
                raise CommandError("Group named %s does not exist." % groupname)
140
                self.stdout.write("Group named %s does not exist\n" % groupname)
141
        
142
        pname = options.get('add-permission')
143
        if pname is not None:
144
            try:
145
                r, created = add_user_permission(user, pname)
146
                if created:
147
                    self.stdout.write('Permission: %s created successfully\n' % pname)
148
                if r > 0:
149
                    self.stdout.write('Permission: %s added successfully\n' % pname)
150
                elif r==0:
151
                    self.stdout.write('User has already permission: %s\n' % pname)
152
            except Exception, e:
153
                raise CommandError(e)
154
        
155
        pname  = options.get('delete-permission')
156
        if pname is not None and not user.has_perm(pname):
157
            try:
158
                r = remove_user_permission(user, pname)
159
                if r < 0:
160
                    self.stdout.write('Invalid permission codename: %s\n' % pname)
161
                elif r == 0:
162
                    self.stdout.write('User has not permission: %s\n' % pname)
163
                elif r > 0:
164
                    self.stdout.write('Permission: %s removed successfully\n' % pname)
165
            except Exception, e:
166
                raise CommandError(e)
133 167
        
134 168
        level = options.get('level')
135 169
        if level is not None:
b/snf-astakos-app/astakos/im/management/commands/removegrouppermissions.py
1
# Copyright 2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

  
34
from optparse import make_option
35

  
36
from django.core.management.base import BaseCommand, CommandError
37
from django.contrib.auth.models import Group
38
from django.core.exceptions import ValidationError
39

  
40
from astakos.im.models import AstakosUser
41
from ._common import remove_group_permission
42

  
43
class Command(BaseCommand):
44
    args = "<groupname> <permission> [<permissions> ...]"
45
    help = "Remove group permissions"
46
    
47
    def handle(self, *args, **options):
48
        if len(args) < 2:
49
            raise CommandError("Please provide a group name and at least one permission")
50
        
51
        group = None
52
        try:
53
            if args[0].isdigit():
54
                group = Group.objects.get(id=args[0])
55
            else:
56
                group = Group.objects.get(name=args[0])
57
        except Group.DoesNotExist, e:
58
            raise CommandError("Invalid group")
59
        
60
        try:
61
            for pname in args[1:]:
62
                r = remove_group_permission(group, pname)
63
                if r < 0:
64
                    self.stdout.write('Invalid permission codename: %s\n' % pname)
65
                elif r == 0:
66
                    self.stdout.write('Group has not permission: %s\n' % pname)
67
                elif r > 0:
68
                    self.stdout.write('Permission: %s removed successfully\n' % pname)
69
        except Exception, e:
70
            raise CommandError(e)
b/snf-astakos-app/astakos/im/management/commands/showuser.py
67 67
                'last login': format_date(user.last_login),
68 68
                'date joined': format_date(user.date_joined),
69 69
                'last update': format_date(user.updated),
70
                'token': user.auth_token,
70
                #'token': user.auth_token,
71 71
                'token expiration': format_date(user.auth_token_expires),
72 72
                'invitations': user.invitations,
73 73
                'invitation level': user.level,
......
75 75
                'verified': format_bool(user.is_verified),
76 76
                'has_credits': format_bool(user.has_credits),
77 77
                'groups': [elem.name for elem in user.groups.all()],
78
                'permissions': [elem.codename for elem in user.user_permissions.all()],
79
                'group_permissions': user.get_group_permissions(),
78 80
                'third_party_identifier': user.third_party_identifier,
79
                'email_verified': format_bool(user.email_verified)
81
                'email_verified': format_bool(user.email_verified),
82
                'username': user.username
80 83
            }
81 84
            if get_latest_terms():
82 85
                has_signed_terms = user.signed_terms()
b/snf-astakos-app/astakos/im/urls.py
98 98
    url(r'^authenticate/v2/?$', 'authenticate'),
99 99
    url(r'^get_services/?$', 'get_services'),
100 100
    url(r'^get_menu/?$', 'get_menu'),
101
    url(r'^find_userid/?$', 'find_userid'),
102
    url(r'^find_email/?$', 'find_email'),
101
    url(r'^v2.0/users/?$', 'get_user_by_email'),
102
    url(r'^v2.0/users/(?P<user_id>.+?)/?$', 'get_user_by_username'),
103 103
)

Also available in: Unified diff