forked from quantifiedcode/quantifiedcode
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdecorators.py
More file actions
executable file
·380 lines (315 loc) · 14.1 KB
/
decorators.py
File metadata and controls
executable file
·380 lines (315 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
# -*- coding: utf-8 -*-
"""
Implements decorators used to retrieve and validate users/projects/teams/organizations/etc.
"""
from __future__ import unicode_literals
from __future__ import print_function
import re
from functools import wraps
from flask import request, jsonify
from collections import Sequence
from quantifiedcode.settings import backend, settings
from .helpers.snapshot import get_snapshot
from .models import AccessToken, IssueClass, Issue, Project, User, Snapshot, Diff, FileRevision
def optional_decorator(func):
""" Specifies a decorator function which can be called with optional arguments, i.e.
@optional_decorator
def my_decorator(f=None, optional=None)
pass
can be used as:
@my_decorator
@my_decorator()
@my_decorator(optional=True)
:param func: decorator function to wrap
:return: decorated function
"""
@wraps(func)
def decorated(f=None, *args, **kwargs):
def partial(f):
return func(f, *args, **kwargs)
return partial(f) if f else partial
return decorated
def requires_request_attribute(attribute_name, status_code=401):
""" Functions wrapped with this decorator require the presence of an attribute with the given name on the
flask request object.
:param attribute_name: name of the attribute to ensure the presence of on the request object
:param status_code: status code of the response that is returned
:return:
"""
def decorator(func):
@wraps(func)
def decorated(*args, **kwargs):
if not hasattr(request, attribute_name):
return {'message': 'Invalid {}'.format(attribute_name)}, status_code
return func(*args, **kwargs)
return decorated
return decorator
@optional_decorator
def valid_project(f=None, id_key='project_id', roles=None, public_ok=False, private_ok=False, only=None, raw=False,
optional=False, include=None):
""" Ensures that the wrapped resource method can only be called by members of the organization specified in
the wrapped function under the `organization_id` argument.
:param f:
:param id_key:
:param roles: A list of roles that needs to be fulfilled by a given
user to access this project (e.g. admin, owner). Each
element of the list can be a role name, or again list
of role names, in which case it will be checked if the
user has any of the given roles (like an OR rule).
:param private_ok: If set to `True`, wiil bypass the role checking for
this project entirely.
:param public_ok: If set to `True`, will bypass the role checking for
this project if the project is public (i.e. if the
project contains a `public = True` entry).
:param only: passed as a parameter to backend when getting the project
:param raw: passed as a parameter to backend when getting the project
:param optional: passed as a parameter to backend when getting the project
:param include: passed as a parameter to backend when getting the project
:return: (return value, status code) tuple
"""
if only is None:
only = {'stats': False}
if roles is None:
roles = ('admin', 'collaborator', 'owner')
@wraps(f)
@requires_request_attribute("user")
def decorated_function(*args, **kwargs):
if id_key not in kwargs or kwargs[id_key] is None:
request.project = None
if not optional:
return {'message': 'no project was specified'}, 404
return f(*args, **kwargs)
try:
project_id = kwargs[id_key]
project = backend.get(Project, {
'$or': [
{'pk': project_id},
{'permalink': project_id}
]},
raw=raw, only=only, include=include)
if project.get('delete', False):
return {'message': 'project marked for deletion'}, 422
# We get all organizations where the user is an owner
if not private_ok and not (public_ok and project.get('public', False)):
if request.user is None or not project.is_authorized(request.user, roles=roles):
return {'message': 'Authorization denied'}, 403
except Project.DoesNotExist:
return {'message': 'Invalid project'}, 404
request.project = project
return f(*args, **kwargs)
return decorated_function
@optional_decorator
def valid_issue_class(f=None, id_key='issue_class_id',
include=('tags', 'categories')):
"""
:param f:
:param id_key:
:param include:
:return:
"""
@wraps(f)
@requires_request_attribute("user")
def decorated_function(*args, **kwargs):
if id_key not in kwargs:
return {'message': 'you must specify an issue class ID'}, 404
issue_class_id = kwargs[id_key]
try:
issue_class = backend.get(
IssueClass,
{'$or': [
{'pk': issue_class_id},
{'code': issue_class_id}
]},
include=include)
except IssueClass.DoesNotExist as e:
return {'message': e.message if e.message else 'invalid issue class'}, 404
request.issue_class = issue_class
return f(*args, **kwargs)
return decorated_function
@optional_decorator
def valid_issue(f=None, id_key='issue_id', include=()):
"""
:param f:
:param id_key:
:param include:
:return:
"""
@wraps(f)
@requires_request_attribute("user")
def decorated_function(*args, **kwargs):
if id_key not in kwargs:
return {'message': 'you must specify an issue ID'}, 404
issue_id = kwargs[id_key]
try:
issue = backend.get(
Issue,
{'pk' : issue_id},
include=include)
except Issue.DoesNotExist as e:
return {'message': e.message if e.message else 'invalid issue'}, 404
#we make sure the issue belongs to the project for which the user is authenticated
if issue.project != request.project:
return {'message': 'access denied'}, 403
request.issue = issue
return f(*args, **kwargs)
return decorated_function
@optional_decorator
def valid_diff(f=None, id_key_a='snapshot_a_id', id_key_b='snapshot_b_id', only=None, include=None, raw=False,
store_as='diff'):
"""
:param f:
:param id_key_a: parameter name in the wrapped method where the id of snapshot a is stored
:param id_key_b: parameter name in the wrapped method where the id of snapshot b is stored
:param only: passed as a parameter to backend when getting the project
:param include: passed as a parameter to backend when getting the project
:param raw: passed as a parameter to backend when getting the project
:param store_as: name of the attribute on the request object where diff will be stored at
:return:
"""
@wraps(f)
@requires_request_attribute("project", status_code=404)
@requires_request_attribute("user")
def decorated_function(*args, **kwargs):
snapshot_a_id = kwargs.get(id_key_a, '')
snapshot_b_id = kwargs.get(id_key_b, '')
try:
snapshot_a = get_snapshot(request.project, snapshot_a_id, only=('pk',))
except Snapshot.DoesNotExist as e:
return {'message': e.message if e.message else 'invalid snapshot A'}, 404
try:
snapshot_b = get_snapshot(request.project, snapshot_b_id, only=('pk',))
except Snapshot.DoesNotExist as e:
return {'message': e.message if e.message else 'invalid snapshot B'}, 404
try:
diff = backend.get(Diff, {'snapshot_a.pk': snapshot_a['pk'],
'snapshot_b.pk': snapshot_b['pk']}, include=include)
except Diff.DoesNotExist as e:
return {'message': e.message if e.message else 'invalid diff'}, 404
setattr(request, store_as, diff)
return f(*args, **kwargs)
return decorated_function
@optional_decorator
def valid_snapshot(f=None, id_key='snapshot_id', only=None, include=None, raw=False, store_as='snapshot'):
"""
:param f:
:param id_key: parameter name in the wrapped method where the id of snapshot is stored
:param only: passed as a parameter to backend when getting the project
:param include: passed as a parameter to backend when getting the project
:param raw: passed as a parameter to backend when getting the project
:param store_as: name of the attribute on the request object where snapshot will be stored at
:return:
"""
if only is None:
only = {'summary': False}
@wraps(f)
@requires_request_attribute("project", status_code=404)
@requires_request_attribute("user")
def decorated_function(*args, **kwargs):
snapshot_id = kwargs.get(id_key, '')
try:
snapshot = get_snapshot(request.project, snapshot_id, raw=raw, only=only, include=include)
except Snapshot.DoesNotExist as e:
return {'message': e.message if e.message else 'invalid snapshot'}, 404
setattr(request, store_as, snapshot)
return f(*args, **kwargs)
return decorated_function
@optional_decorator
def valid_file_revision(f=None, snapshot_id_key='snapshot_id', file_revision_id_key='file_revision_id',
path_key='path', only=None, raw=False):
"""
:param f:
:param snapshot_id_key:
:param file_revision_id_key:
:param path_key:
:param only: passed as a parameter to backend when getting the project
:param raw: passed as a parameter to backend when getting the project
:return:
"""
@wraps(f)
@requires_request_attribute("project", status_code=404)
@requires_request_attribute("user")
def decorated_function(*args, **kwargs):
if (snapshot_id_key in kwargs and
path_key in kwargs and
kwargs[snapshot_id_key] is not None and
kwargs[path_key] is not None):
try:
snapshot = get_snapshot(request.project, kwargs[snapshot_id_key], raw=False)
except Snapshot.DoesNotExist:
return {'message': 'invalid snapshot'}, 404
try:
file_revision = backend.get(FileRevision, {
'snapshots': snapshot,
'path': kwargs[path_key],
})
request.file_revision = file_revision
except (FileRevision.DoesNotExist, FileRevision.MultipleDocumentsReturned):
# TODO is multipledocumentsreturned a 404?
return {'message': 'invalid file revision'}, 404
elif file_revision_id_key in kwargs:
try:
file_revision = backend.get(FileRevision, {
'pk': kwargs[file_revision_id_key],
'project': request.project,
})
request.file_revision = file_revision
except FileRevision.DoesNotExist:
return {'message': 'invalid file revision'}, 404
else:
return {'message': 'you must specify either a snapshot ID and path or a file revision ID'}, 404
return f(*args, **kwargs)
return decorated_function
@optional_decorator
def valid_user(f=None, anon_ok=False, raw=False, only=None, superuser=False, include=None):
""" Used on resources which require request.user to be set.
:param f: function to decorate
:param anon_ok: if True the endpoint will also allow access to users who are not logged in
:param raw: provided as a parameter to backend when getting the user
:param only: provided as a parameter to backend when getting the user
:param superuser: if True to user must be a superuser to get access to the endpoint
:param include: provided as a parameter to backend when getting the user
:return: decorated function
"""
@wraps(f)
def decorated_function(*args, **kwargs):
def process_anonymously():
request.user = None
request.access_token = None
return f(*args, **kwargs)
def invalid_token(message, status_code=401, cookie_token=False):
response = jsonify({'message': message})
if cookie_token:
response.set_cookie('access_token', '', expires=0)
return response, status_code
cookie_token = False
if request.args.get('access_token'):
access_token_key = request.args['access_token']
elif request.cookies.get('access_token'):
access_token_key = request.cookies['access_token']
cookie_token = True
else:
authorization = request.headers.get('Authorization', '')
match = re.match(r"^bearer\s+([\w\d]+)$", authorization, re.I)
if not match:
if anon_ok:
return process_anonymously()
return {'message': 'Authorization header not valid'}, 401
access_token_key = match.group(1)
try:
access_token = backend.get(AccessToken, {'token': access_token_key})
except AccessToken.DoesNotExist:
if anon_ok:
return process_anonymously()
# redirect to login
return invalid_token('Invalid / expired access token: %s' % access_token_key, cookie_token=cookie_token)
request.access_token = access_token
try:
request.user = backend.get(User, {'pk': access_token.user['pk']}, raw=raw, only=only, include=include)
except User.DoesNotExist:
with backend.transaction():
backend.delete(access_token)
return invalid_token('User does not exist', status_code=404, cookie_token=cookie_token)
if superuser and not request.user.is_superuser():
return {'message': 'This endpoint requires super-user privileges. Sorry :/'}, 401
return f(*args, **kwargs)
return decorated_function