This repository has been archived by the owner on Nov 15, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
/
app.py
628 lines (532 loc) · 21.1 KB
/
app.py
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
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
import os
import requests
from bouncer import Bouncer
from flask import Flask, url_for, redirect, \
render_template, session, request, Response, \
flash, get_flashed_messages, jsonify
from flask_login import LoginManager, login_required, login_user, \
logout_user, current_user, UserMixin
from oauthlib.oauth2 import OAuth2Error
from elasticsearch_dsl import Search
from requests_oauthlib import OAuth2Session
from requests.exceptions import HTTPError
from oauth2client.client import verify_id_token
from oauth2client.crypt import AppIdentityError
from urllib import urlencode
import urllib2
from decode_cookie import decodeFlaskCookie
from utils import redact_email, decrypt, encrypt, new_iv
import logging
basedir = os.path.abspath(os.path.dirname(__file__))
"""App Configuration"""
class Auth:
"""Google Project Credentials"""
CLIENT_ID = os.environ['GOOGLE_CLIENT_ID']
CLIENT_SECRET = os.environ['GOOGLE_CLIENT_SECRET']
DCC_DASHBOARD_HOST = 'localhost'
DCC_DASHBOARD_PORT = '5000'
DCC_DASHBOARD_PROTOCOL = 'https'
if 'DCC_DASHBOARD_HOST' in os.environ.keys():
DCC_DASHBOARD_HOST = os.environ['DCC_DASHBOARD_HOST']
if 'DCC_DASHBOARD_PORT' in os.environ.keys():
DCC_DASHBOARD_PORT = os.environ['DCC_DASHBOARD_PORT']
if 'DCC_DASHBOARD_PROTOCOL' in os.environ.keys():
DCC_DASHBOARD_PROTOCOL = os.environ['DCC_DASHBOARD_PROTOCOL']
REDIRECT_URI = DCC_DASHBOARD_PROTOCOL+'://'+DCC_DASHBOARD_HOST+'/gCallback'
AUTH_URI = 'https://accounts.google.com/o/oauth2/auth'
TOKEN_URI = 'https://accounts.google.com/o/oauth2/token'
USER_INFO = 'https://www.googleapis.com/userinfo/v2/me'
REVOKE_TOKEN = 'https://accounts.google.com/o/oauth2/revoke'
SCOPE = ['https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email']
class Config:
"""Base config"""
APP_NAME = "Test Google Login"
SECRET_KEY = os.environ.get("SECRET_KEY") or "somethingsecret"
GOOGLE_SITE_VERIFICATION_CODE = os.environ.get("GOOGLE_SITE_VERIFICATION_CODE") or ""
# Make cookies secure so that the tokens stored in them are safe and only travel over https
SESSION_COOKIE_SECURE = True
REMEMBER_COOKIE_SECURE = True
class DevConfig(Config):
"""Dev config"""
DEBUG = True
class ProdConfig(Config):
"""Production config"""
DEBUG = False
config = {
"dev": DevConfig,
"prod": ProdConfig,
"default": DevConfig
}
"""APP creation and configuration"""
def set_prod_logging_level(logger, level):
for handler in logger.handlers:
if handler.__class__.__name__ == 'ProductionHandler':
handler.level = level
if not logger.isEnabledFor(level):
logger.setLevel(level)
"""APP creation and configuration"""
app = Flask(__name__)
app.config.from_object(config['prod'])
set_prod_logging_level(app.logger, logging.INFO)
login_manager = LoginManager(app)
login_manager.login_view = "login"
login_manager.session_protection = "strong"
# make a global bouncer instance to avoid needless re-instantiation
if os.getenv('EMAIL_WHITELIST_NAME'):
whitelist_checker = Bouncer(os.getenv('EMAIL_WHITELIST_NAME'))
else:
whitelist_checker = None
class User(UserMixin):
def __init__(self, user=None, name=None, picture=None):
"""
Pulls the user's info from the session. We use @property to keep the
session as the one source of truth, but allow access and setting of
user properties here.
"""
if user is not None:
session['email'] = user
if name is not None:
session['name'] = name
if picture is not None:
session['avatar'] = picture
# self._created_at = session.get('created_at', datetime.datetime.utcnow())
@property
def email(self):
return session.get('email', None)
@email.setter
def email(self, value):
session['email'] = value
@property
def name(self):
return session.get('name', None)
@name.setter
def name(self, value):
session['name'] = value
@property
def picture(self):
return session.get('avatar', None)
@picture.setter
def picture(self, value):
session['avatar'] = value
@property
def is_active(self):
return self.email is not None
@property
def is_authenticated(self):
return self.refresh_token is not None
@property
def is_anonymous(self):
return self.email is None
def get_id(self):
return self.email
@property
def access_token(self):
encrypted_token = session.get('access_token', None)
iv = session['access_iv']
return decrypt(encrypted_token, iv)
@access_token.setter
def access_token(self, value):
iv = new_iv()
session['access_iv'] = iv
session['access_token'] = encrypt(value, iv)
@property
def refresh_token(self):
encrypted_token = session.get('refresh_token', None)
iv = session['refresh_iv']
return decrypt(encrypted_token, iv)
@refresh_token.setter
def refresh_token(self, value):
# store the initialization vector in the session. It doesn't need to be secure
iv = new_iv()
session['refresh_iv'] = iv
session['refresh_token'] = encrypt(value, iv)
def logout(self):
"""Clean up all the stuff we left in the session cookie"""
# as per google's docs "The token can be an access token or a refresh token.
# If the token is an access token and it has a corresponding refresh token,
# the refresh token will also be revoked."
if session.get('access_token'):
res = requests.post(Auth.REVOKE_TOKEN, params={'token': session['access_token']},
headers={'content-type': 'application/x-www-form-urlencoded'})
if res.status_code != 200:
print('Failed to revoke tokens. Expected 200 response, received '
'{} with message: {}'.format(res.status_code, res.text))
for attr in 'email', 'name', 'avatar', 'access_token', 'refresh_token':
try:
del session[attr]
except KeyError:
print('Could not clear {} from session'.format(attr))
pass
@login_manager.user_loader
def load_user(user_id):
return User()
""" OAuth Session creation """
def get_google_auth(state=None, token=None):
if token:
return OAuth2Session(Auth.CLIENT_ID, token=token)
if state:
return OAuth2Session(
Auth.CLIENT_ID,
state=state,
redirect_uri=Auth.REDIRECT_URI)
oauth = OAuth2Session(
Auth.CLIENT_ID,
redirect_uri=Auth.REDIRECT_URI,
scope=Auth.SCOPE)
return oauth
def query_es_rna_seq(es_object, index, query_params, cardinality):
"""Returns the cardinality based from the inputs
GET burn_idx/_search
{
"query": {
"bool": {
"must": [
{
"regexp": {
"experimentalStrategy": "[rR][nN][aA][-][Ss][Ee][Qq]"
}
},{
"regexp":{
"software": "[Ss]pinnaker"
}
}
]
}
},
"aggs": {
"filtered_jobs":{
"cardinality": {
"field": "repoDataBundleId"
}
}
}
}
es_object -- the es object to query against
index -- the name of the index to query on
query_params -- tuple with form (query type, field, value)
cardinality -- field to get the cardinality from
"""
# Create search obejct
s = Search(using=es_object, index=index)
# Add the queries
s = reduce(lambda s, x: s.query(x[0], **{x[1]: x[2]}), query_params, s)
# Add the aggregates
s.aggs.metric("filtered_jobs", 'cardinality', field=cardinality,
precision_threshold="40000")
# Execute the query
response = s.execute()
return response.aggregations.filtered_jobs.value
@app.route('/')
def index():
"""
Render the main page.
"""
return html_rend('index')
def parse_token():
"""
Parses the Authorization token from the request header
:return: the bearer and token string
"""
authorization_header = request.headers.get("Authorization", None)
assert authorization_header is not None, "No Authorization header in the request"
parts = authorization_header.split()
# Return the bearer and token string
return parts[0], parts[1]
def new_google_access_token():
"""
Tries to get new access token.
If refresh fails an OAuth2Error will be raised
"""
refresh_token = current_user.refresh_token
oauth = get_google_auth()
extra = {
'client_id': Auth.CLIENT_ID,
'client_secret': Auth.CLIENT_SECRET,
}
# this call may throw an OAuth2Error
resp = oauth.refresh_token(Auth.TOKEN_URI, refresh_token=refresh_token, **extra)
current_user.access_token = resp['access_token']
return resp['access_token']
def make_request(url, headers):
try:
req = urllib2.Request(url, headers=headers)
handler = urllib2.urlopen(req)
content_type = handler.headers['content-type']
response = Response(handler.read(), mimetype=content_type)
content_encoding = 'content-encoding'
if content_encoding in handler.headers.keys():
response.headers[content_encoding] = handler.headers[
content_encoding]
return response
except urllib2.HTTPError as e:
return e.message, e.code
@app.route('/check_session/<cookie>')
def check_session(cookie):
if not request.headers.get("Authorization", None):
return jsonify({"error": "No Authorization header in the request"})
else:
# Make sure the auth token is the right one
try:
bearer, auth_token = parse_token()
assert bearer == "Bearer", "Authorization must start with Bearer"
assert auth_token == os.getenv("LOG_IN_TOKEN", 'ITS_A_SECRET!')
except AssertionError as e:
response = {
'error': e.message
}
return jsonify(response)
# we have to decode the cookie manually b/c we're not getting it automatically through
# flask, rather it has to be passed to and fro with node and client and dashboard
decoded_cookie = decodeFlaskCookie(os.getenv('SECRET_KEY', 'somethingsecret'), cookie)
email = decoded_cookie['email']
if email is None:
response = {
'error': 'No user is stored in the session. The user is not '
'logged in.'
}
else:
response = {
'email': email,
'name': decoded_cookie['name'],
'avatar': decoded_cookie['avatar']
}
return jsonify(response)
def _get_user_info_from_token(token=None):
"""
Try and get the user's info. By default the access token in the session is used.
returns the response object
"""
google = get_google_auth(token={
'access_token': current_user.access_token if token is None else token})
return google.get(Auth.USER_INFO)
def get_user_info(token=None):
"""
Get user's info, retry with refreshed token if failed, and raise ValueError
or OAuth2Error if failure
If access token is provided, use that first
"""
resp = _get_user_info_from_token(token=token)
if 400 <= resp.status_code < 500:
if token:
raise ValueError('The provided token was not accepted')
# token expired, try once more
try:
new_google_access_token()
except OAuth2Error:
# erase old tokens if they're broken / expired
app.logger.warning('Could not refresh access token')
session.pop('access_token')
session.pop('refresh_token')
raise
resp = _get_user_info_from_token()
# If there is a 5xx error, or some unexpected 4xx we will return the message but
# leave the token's intact b/c they're not necessarily to blame for the error.
if resp.status_code != 200:
raise ValueError(resp.text)
return resp.json()
@app.route('/me')
def me():
"""
returns information about the user making the request.
If there are any problems getting the user's info, refreshing the token, etc
then just return the anonymous user.
"""
# Do we have an access token?
if current_user.is_anonymous:
app.logger.debug('Request %s by user anonymous', request.path)
return jsonify({'name': 'anonymous'})
try:
user_data = get_user_info()
except (ValueError, OAuth2Error):
app.logger.error('Request path %s by unknown user', request.path)
return jsonify({'name': 'anonymous'})
output = dict((k, user_data[k]) for k in ('name', 'email'))
output['avatar'] = user_data['picture']
app.logger.info('Request path %s by user with email %s', request.path, user_data['email'])
return jsonify(output)
@app.route('/authorization')
def authorization():
"""
This endpoint determines if the caller is authorized of not.
If there is a bearer token, we try and use that. Otherwise we use
the access token in the session. If the token fails, then try and
refresh.
If we get a working token, then ping google for user info, get
their email and check it against bouncer.
The user needs to be logged in with Google in order to be
authorized. The method returns the following HTTP status
codes:
204 user is authorized regardless of whether user
is on the whitelist or not
401 user info is not available
403 user is not authorized
"""
try:
# parsing succeeds if there is an auth header
bearer, auth_token = parse_token()
except AssertionError:
auth_token = None
else:
if bearer != "Bearer":
return "Authorization must start with Bearer", 401
if auth_token is None and current_user.is_anonymous:
return "No token provided", 401
# use access token in session
try:
user_data = get_user_info(auth_token)
except ValueError as e:
return e.message, 401
except OAuth2Error as e:
return 'Failed to get user info: ' + e.message, 401
# Now that we have the user data we can verify the email
if whitelist_checker is None:
app.logger.info(
'Request path %s. No whitelist; User with email %s is logged in',
request.path, user_data['email'])
return '', 204
elif whitelist_checker.is_authorized(user_data['email']):
app.logger.info(
'Request path %s. User with email %s is authorized',
request.path, user_data['email'])
return '', 204
else:
app.logger.info(
'Request path %s. User with email %s is not authorized',
request.path, user_data['email'])
return '', 403
@app.route('/<name>.html')
def html_rend(name):
"""
Render templates based on their name.
Handle the templates differently depending
on its name.
"""
data = os.environ['DCC_DASHBOARD_SERVICE']
coreClientVersion = os.getenv('DCC_CORE_CLIENT_VERSION', '1.1.0')
if name == 'index':
whitelist_validation_required = bool(os.getenv('EMAIL_WHITELIST_NAME'))
contact_email = os.getenv('CONTACT_EMAIL', '')
return render_template(name + '.html',
whitelist_validation_required=whitelist_validation_required,
contact_email=contact_email)
if name == 'unauthorized':
return render_template(name + '.html')
return render_template(name + '.html')
@app.route('/file_browser/')
def html_rend_file_browser():
"""
Helper method to redirect URLs ending in <url>/file_browser/
to the file browser page.
"""
return redirect(url_for('html_rend', name='file_browser'))
@app.route('/boardwalk')
def boardwalk():
return redirect(url_for('boardwalk'))
@app.route('/privacy')
def privacy():
return redirect(url_for('privacy'))
@app.route('/unauthorized')
def unauthorized():
account = request.args.get('account')
project = os.getenv('PROJECT_NAME', '')
contact = os.getenv('CONTACT_EMAIL', '')
return render_template('unauthorized.html',
contact=contact, project=project, account=account)
@app.route('/login')
def login():
"""
Endpoint to Login into the page
"""
if current_user.is_authenticated:
app.logger.info('Request path %s. Current user with ID %s is authenticated; redirecting to index URL', request.path, current_user.get_id())
return redirect(url_for('index'))
google = get_google_auth()
auth_url, state = google.authorization_url(
Auth.AUTH_URI, access_type='offline',
prompt='select_account consent')
session['oauth_state'] = state
app.logger.info('Request path %s. Redirecting current user with ID %s to authorization URL', request.path, current_user.get_id())
return redirect(auth_url)
@app.route('/gCallback')
def callback():
"""
Callback method required by Google's OAuth 2.0
"""
if current_user is not None and current_user.is_authenticated:
app.logger.info('Request path %s. Current user with ID %s is authenticated; redirecting to index URL', request.path, current_user.get_id())
return redirect(url_for('index'))
if 'error' in request.args:
if request.args.get('error') == 'access_denied':
if current_user is not None:
app.logger.error('Request path %s. Current user with ID %s access is denied', request.path, current_user.get_id())
else:
app.logger.error('Request path %s. Access is denied for current user None', request.path)
return 'You are denied access.'
return 'Error encountered.'
if 'code' not in request.args and 'state' not in request.args:
if current_user is not None:
app.logger.info('Request path %s. Redirecting current user with ID %s to login URL', request.path, current_user.get_id())
else:
app.logger.info('Request path %s. Redirecting current user None to login URL', request.path)
return redirect(url_for('login'))
else:
google = get_google_auth(state=session['oauth_state'])
try:
token = google.fetch_token(
Auth.TOKEN_URI,
client_secret=Auth.CLIENT_SECRET,
authorization_response=request.url)
except HTTPError:
if current_user is not None:
app.logger.error('Request path %s. Could not fetch token for current user with ID %s', request.path, current_user.get_id())
else:
app.logger.error('Request path %s. Could not fetch token for current user None', request.path)
return 'HTTPError occurred.'
# Testing the token verification step.
try:
# jwt = verify_id_token(token['id_token'], Auth.CLIENT_ID)
verify_id_token(token['id_token'], Auth.CLIENT_ID)
except AppIdentityError:
app.logger.error('Request path %s. Could not verify token for current user with ID %s', request.path, current_user.get_id())
return 'Could not verify token.'
# Check if you have the appropriate domain
# Commenting this section out to let anyone with
# a google account log in.
# if 'hd' not in jwt or jwt['hd'] != 'ucsc.edu':
# flash('You must login with a ucsc.edu account. \
# Please try again.', 'error')
# return redirect(url_for('index'))
google = get_google_auth(token=token)
resp = google.get(Auth.USER_INFO)
if resp.status_code == 200:
user_data = resp.json()
email = user_data['email']
# If so configured, check for whitelist and redirect to
# unauthorized page if not in whitelist, e.g.,
if whitelist_checker is not None and not whitelist_checker.is_authorized(email):
app.logger.info('Request path %s. User with email %s is not authorized', request.path, user_data['email'])
return redirect(url_for('unauthorized', account=redact_email(email)))
user = User()
for attr in 'email', 'name', 'picture':
setattr(user, attr, user_data[attr])
user.refresh_token = token['refresh_token']
user.access_token = token['access_token']
login_user(user)
# Empty flashed messages
get_flashed_messages()
# Set a new success flash message
flash('You are now logged in!', 'success')
app.logger.info('Request path %s. User with email %s was logged in; redirecting to index URL', request.path, user_data['email'])
return redirect(url_for('boardwalk'))
app.logger.error('Could not fetch information for current user')
return 'Could not fetch your information.'
@app.route('/logout')
@login_required
def logout():
app.logger.info('Request path %s. Current user with ID %s will be logged out', request.path, current_user.get_id())
current_user.logout()
logout_user()
return redirect(url_for('index'))
if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)