class UserAuthView(Resource): """ Implements login and logout functionality """ decorators = [ratelimit.shared_limit_and_check("30/120 second", scope=scope_func)] def post(self): """ Authenticate the user, logout the current user, login the new user :return: dict containing success message """ try: data = get_post_data(request) email = data['username'] password = data['password'] except (AttributeError, KeyError): return {'error': 'malformed request'}, 400 u = user_manipulator.first(email=email) if u is None or not u.validate_password(password): abort(401) if u.confirmed_at is None: return {"error": "account has not been verified"}, 403 # Logout of previous user (may have been bumblebee) if current_user.is_authenticated(): logout_user() login_user(u) # Login to real user user_manipulator.update( u, last_login_at=datetime.datetime.now(), login_count=u.login_count+1 if u.login_count else 1, ) return {"message": "success"}, 200
class BenchmarkDoubleRedirectView(Resource): """ View that contacts a second service that will contact the API and return a response. """ decorators = [ratelimit.shared_limit_and_check("1000/1 second", scope=scope_func)] # Flask Limiter def post(self): post_data = get_post_data(request) post_data['last_sent'] = 'benchmark/double_redirect' post_data['sent_from'].append('benchmark/double_redirect') if 'sleep' not in post_data: post_data['sleep'] = 0 #sql = text("SELECT datid, datname, pid, usename, client_addr, state, query FROM pg_stat_activity, pg_sleep({}) where datname = 'adsws';".format(post_data['sleep'])) #result = db.session.execute(sql) # Post to the next point r = requests.post( 'http://localhost/benchmark/redirect', data=json.dumps(post_data) ) # Get the response content if there was no failure try: _json = r.json() except: _json = {'msg': r.text} return _json, r.status_code
class BenchmarkRedirectView(Resource): """ View that contacts a second service which will return a response. """ decorators = [ratelimit.shared_limit_and_check("1000/1 second", scope=scope_func)] # Flask Limiter def post(self): post_data = get_post_data(request) post_data['last_sent'] = 'benchmark/redirect' post_data['sent_from'].append('benchmark/redirect') if 'sleep' not in post_data: post_data['sleep'] = 0 # Post to the end point r = requests.post( 'http://localhost/benchmark/end', data=json.dumps(post_data) ) # Get the response content if there was no failure try: _json = r.json() except: _json = {'msg': r.text} return _json, r.status_code
class BenchmarkEndView(Resource): """ View that returns a response. """ decorators = [ratelimit.shared_limit_and_check("1000/1 second", scope=scope_func)] # Flask Limiter def post(self): """ POST response """ start_time = time.gmtime().tm_sec post_data = get_post_data(request) post_data['last_sent'] = 'benchmark/end' post_data['sent_from'].append('benchmark/end') post_data['service'] = { 'received_time': start_time, } if 'sleep' not in post_data: post_data['sleep'] = 0 sql = text("SELECT datid, datname, pid, usename, client_addr, state, query FROM pg_stat_activity, pg_sleep({}) where datname = 'adsws';".format(post_data['sleep'])) result = db.session.execute(sql) return post_data, 200
def bootstrap_local_module(service_uri, deploy_path, app): """ Incorporates the routes of an existing app into this one :param service_uri: the path to the target application :param deploy_path: the path on which to make the target app discoverable :param app: flask.Flask application instance :return: None """ app.logger.debug( 'Attempting bootstrap_local_module [{0}]'.format(service_uri)) module = import_module(service_uri) local_app = module.create_app() # Add the target app's config to the parent app's config. # Do not overwrite any config already present in the parent app for k, v in local_app.config.iteritems(): if k not in app.config: app.config[k] = v for rule in local_app.url_map.iter_rules(): view = local_app.view_functions[rule.endpoint] route = os.path.join(deploy_path, rule.rule[1:]) # view_class is attached to a function view in the case of # class-based views, and that view.view_class is the element # that has the scopes and docstring attributes if hasattr(view, 'view_class'): attr_base = view.view_class else: attr_base = view # ensure the current_app matches local_app and not API app view = local_app_context(local_app)(view) # Decorate the view with ratelimit if hasattr(attr_base, 'rate_limit'): d = attr_base.rate_limit[0] view = ratelimit.shared_limit_and_check( lambda counts=d, per_second=attr_base.rate_limit[1]: limit_func(counts, per_second), scope=scope_func, key_func=key_func, methods=rule.methods, )(view) # Decorate the view with require_oauth if hasattr(attr_base, 'scopes'): view = oauth2.require_oauth(*attr_base.scopes)(view) # Add cache-control headers if app.config.get('API_PROXYVIEW_HEADERS'): view = headers(app.config['API_PROXYVIEW_HEADERS'])(view) # Let flask handle OPTIONS, which it will not do if we explicitly # add it to the url_map if 'OPTIONS' in rule.methods: rule.methods.remove('OPTIONS') app.add_url_rule(route, route, view, methods=rule.methods)
class ChangeEmailView(Resource): """ Implements change email functionality """ decorators = [ ratelimit.shared_limit_and_check("5/600 second", scope=scope_func), login_required, ] def post(self): """ POST desired email and password to change the current user's email. Checks that the desired new email isn't already registerd This will create the new user. The encoded email payload will have a second argument `id` which is the id of the user making this request. We assume that the view responsible for verifying emails knows what to do with this extra argument. This should be deprecated by using signals in the future. """ try: data = get_post_data(request) email = data['email'] password = data['password'] verify_url = data['verify_url'] except (AttributeError, KeyError): return {'error': 'malformed request'}, 400 u = user_manipulator.first(email=current_user.email) if not u.validate_password(password): abort(401) if user_manipulator.first(email=email) is not None: return { "error": "{0} has already been registered".format(email) }, 403 send_email( email_addr=email, base_url=verify_url, email_template=VerificationEmail, payload=[email, u.id] ) send_email( email_addr=current_user.email, email_template=EmailChangedNotification ) user_manipulator.create( email=email, password=password, active=True, registered_at=datetime.datetime.now(), login_count=0, ) return {"message": "success"}, 200
class GlobalResourcesView(Resource): """ Endpoint that exposes all of the resources that the adsws knows about. This endpoint, while public, is useful mostly for developers/debugging """ decorators = [ ratelimit.shared_limit_and_check("100/86400 second", scope=scope_func) ] def get(self): return current_app.config['resources']
class CSRFView(Resource): """ Returns a csrf token """ decorators = [ratelimit.shared_limit_and_check("50/600 second", scope=scope_func)] def get(self): """ Returns a csrf token """ return {'csrf': generate_csrf()}
class UserRegistrationView(Resource): """ Implements new user registration """ decorators = [ ratelimit.shared_limit_and_check("50/600 second", scope=scope_func) ] def post(self): """ Standard user registration workflow; verifies that the email is available, creates a de-activated accounts, and sends verification email that serves to activate said account """ try: data = get_post_data(request) email = data['email'] password = data['password1'] repeated = data['password2'] verify_url = data['verify_url'] except (AttributeError, KeyError): return {'error': 'malformed request'}, 400 if not verify_recaptcha(request): return {'error': 'captcha was not verified'}, 403 if password != repeated: return {'error': 'passwords do not match'}, 400 try: validate_email(email) validate_password(password) except ValidationError, e: return {'error': 'validation error'}, 400 if user_manipulator.first(email=email) is not None: return { 'error': 'an account is already' ' registered for {0}'.format(email) }, 409 send_email(email_addr=email, base_url=verify_url, email_template=WelcomeVerificationEmail, payload=email) u = user_manipulator.create( email=email, password=password, active=True, registered_at=datetime.datetime.now(), login_count=0, ) return {"message": "success"}, 200
class VerifyEmailView(Resource): """ Decode a TimerSerializer token into an email, returning an error message to the client if this task fails If the token is decoded, set User.confirm_at to datetime.now() """ decorators = [ ratelimit.shared_limit_and_check("20/600 second", scope=scope_func) ] def get(self, token): try: email = current_app.ts.loads(token, max_age=86400, salt=VerificationEmail.salt) except: current_app.logger.warning( "{0} verification token not validated".format(token)) return {"error": "unknown verification token"}, 404 # This logic is necessary to de-activate accounts via the change-email # workflow. This strong coupling should be deprecated by using signals. previous_uid = None if " " in email: email, previous_uid = email.split() u = user_manipulator.first(email=email) if u is None: return { "error": "no user associated " "with that verification token" }, 404 if u.confirmed_at is not None: return { "error": "this user and email " "has already been validated" }, 400 if previous_uid: # De-activate previous accounts by deleting the account associated # with the new email address, then update the old account with the # new email address. Again, this should be deprecated with signals. user_manipulator.delete(u) u = user_manipulator.first(id=previous_uid) user_manipulator.update(u, email=email, confirmed_at=datetime.datetime.now()) else: user_manipulator.update(u, confirmed_at=datetime.datetime.now()) login_user(u) return {"message": "success", "email": email}
class BenchmarkTimeoutEndView(Resource): """ View that returns a response. """ decorators = [ratelimit.shared_limit_and_check("1000/1 second", scope=scope_func)] # Flask Limiter def get(self): """ GET response """ sleep = request.args.get('sleep', default = 0, type = int) if sleep > 0: one_second = 1 for i in xrange(sleep): current_app.logger.info('Iteration %s - Waiting %s second(s) for a total of %s seconds', i, one_second, sleep) sql = text("SELECT datid, datname, pid, usename, client_addr, state, query FROM pg_stat_activity, pg_sleep({}) where datname = 'adsws';".format(one_second)) result = db.session.execute(sql) return {'msg': "Slept during {} seconds!".format(sleep)}, 200
class BenchmarkTimeoutRedirectView(Resource): """ View that contacts a second service which will return a response. """ decorators = [ratelimit.shared_limit_and_check("1000/1 second", scope=scope_func)] # Flask Limiter def get(self): sleep = request.args.get('sleep', default = 0, type = int) timeout = request.args.get('timeout', default = 60, type = int) current_app.logger.info('Sending request to timeout_end with a sleep order of %s seconds and a timeout of %s seconds', sleep, timeout) # Get the end point r = requests.get( 'http://localhost/benchmark/timeout_end?sleep={}'.format(sleep), timeout=timeout, ) # Get the response content if there was no failure try: _json = r.json() except: _json = {'msg': r.text} return _json, r.status_code
class SlackFeedback(Resource): """ Forwards a user's feedback to slack chat using a web end """ decorators = [ratelimit.shared_limit_and_check("500/600 second", scope=scope_func)] @staticmethod def prettify_post(post_data): """ Converts the given input into a prettified version :param post_data: the post data to prettify, dictionary expected :return: prettified_post data, dictionary """ channel = post_data.get('channel', '#feedback') username = post_data.get('username', 'TownCrier') name = post_data.get('name', 'TownCrier') reply_to = post_data.get('_replyto', '*****@*****.**') try: comments = post_data['comments'] except BadRequestKeyError: raise feedback_email = 'no email sent' if post_data.has_key('_replyto') and post_data.has_key('name'): try: res = send_feedback_email(name, reply_to, comments) feedback_email = 'success' except Exception as e: current_app.logger.info('Sending feedback mail failed: %s' % str(e)) feedback_email = 'failed' icon_emoji = current_app.config['FEEDBACK_SLACK_EMOJI'] text = [ '```Incoming Feedback```', '*Commenter*: {}'.format(name), '*e-mail*: {}'.format(reply_to), '*Feedback*: {}'.format(comments), '*sent to adshelp*: {}'.format(feedback_email) ] used = ['channel', 'username', 'name', '_replyto', 'comments', 'g-recaptcha-response'] for key in post_data: if key in used: continue text.append('*{}*: {}'.format(key, post_data[key])) text = '\n'.join(text) prettified_data = { 'text': text, 'username': username, 'channel': channel, 'icon_emoji': icon_emoji } return prettified_data def post(self): """ HTTP POST request :return: status code from the slack end point """ post_data = get_post_data(request) current_app.logger.info('Received feedback: {0}'.format(post_data)) if not post_data.get('g-recaptcha-response', False) or \ not verify_recaptcha(request): current_app.logger.info('The captcha was not verified!') return err(ERROR_UNVERIFIED_CAPTCHA) else: current_app.logger.info('Skipped captcha!') try: current_app.logger.info('Prettifiying post data: {0}' .format(post_data)) formatted_post_data = json.dumps(self.prettify_post(post_data)) current_app.logger.info('Data prettified: {0}' .format(formatted_post_data)) except BadRequestKeyError as error: current_app.logger.error('Missing keywords: {0}, {1}' .format(error, post_data)) return err(ERROR_MISSING_KEYWORDS) try: slack_response = requests.post( url=current_app.config['FEEDBACK_SLACK_END_POINT'], data=formatted_post_data, timeout=60 ) except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): return b'504 Gateway Timeout', 504 current_app.logger.info('slack response: {0}' .format(slack_response.status_code)) # Slack annoyingly redirects if you have the wrong end point current_app.logger.info('Slack API' in slack_response.text) if 'Slack API' in slack_response.text: return err(ERROR_WRONG_ENDPOINT) elif slack_response.status_code == 200: return {}, 200 else: return {'msg': 'Unknown error'}, slack_response.status_code
class UserFeedback(Resource): """ Forwards a user's feedback to Slack and/or email """ decorators = [ratelimit.shared_limit_and_check("500/600 second", scope=scope_func)] @staticmethod def create_email_body(post_data): """ Takes the data from the feedback and fills out the appropriate template :param post_data: the post data to fill out email template, dictionary expected :return: email body, string """ # We will be manipulating the dictionary with POST data, so make a copy email_data = copy.copy(post_data) # Determine the origin of the feedback. There are some origin-specific actions origin = post_data.get('origin', 'NA') if origin == current_app.config['BBB_FEEDBACK_ORIGIN']: try: comments = email_data['comments'] except BadRequestKeyError: raise email_data['_subject'] = 'Bumblebee Feedback' email_data['comments'] = post_data['comments'].encode('utf-8') used = ['channel', 'username', 'name', '_replyto', 'g-recaptcha-response'] for key in used: email_data.pop(key, None) # Retrieve the appropriate template template = current_app.config['FEEDBACK_TEMPLATES'].get(email_data.get('_subject')) # For abstract corrections, we determine a diff from the original and updated records. # In case this fails we fall back on the POST data "diff" attribute that contains # the updated fields in Github "diff" format, URL encoded. For display purposes, # this needs to be decoded. if post_data.get('_subject') == 'Updated Record': try: email_data['diff'] = make_diff(post_data['original'], post_data['new']) except: email_data['diff'] = unquote(post_data.get('diff','')) # In the case of a new record the mail body will show a summary # In this summary it's easier to show a author list in the form of a string # We also attach the JSON data of the new record as a file if post_data.get('_subject') == 'New Record': try: email_data['new']['author_list'] = ";".join([a for a in post_data['new']['authors']]) except: email_data['new']['author_list'] = "" # Construct the email body body = render_template(template, data=email_data) # If there is a way to insert tabs in the template, it should happen there # (currently, this only happens in the missing_references.txt template) body = body.replace('[tab]','\t') return body def post(self): """ HTTP POST request :return: status code from the slack end point and for sending user feedback emails """ post_data = get_post_data(request) current_app.logger.info('Received feedback of type {0}: {1}'.format(post_data.get('_subject'), post_data)) if not post_data.get('g-recaptcha-response', False) or \ not verify_recaptcha(request): current_app.logger.info('The captcha was not verified!') return err(ERROR_UNVERIFIED_CAPTCHA) else: current_app.logger.info('Skipped captcha!') # We only allow POST data from certain origins allowed_origins = [v for k,v in current_app.config.items() if k.endswith('_ORIGIN')] origin = post_data.get('origin', 'NA') if origin == 'NA' or origin not in allowed_origins: return err(ERROR_UNKNOWN_ORIGIN) # Some variable definitions email_body = '' slack_data = '' attachments=[] # Generate the email body based on the data in the POST payload try: email_body = self.create_email_body(post_data) except BadRequestKeyError as error: current_app.logger.error('Missing keywords: {0}, {1}' .format(error, post_data)) return err(ERROR_MISSING_KEYWORDS) except Exception as error: current_app.logger.error('Fatal error creating email body: {0}'.format(error)) return err(ERROR_EMAILBODY_PROBLEM) # Retrieve the name of the person submitting the feedback name = post_data.get('name', 'TownCrier') # There are some origin-specific actions if origin == current_app.config['FEEDBACK_FORMS_ORIGIN']: # The reply_to for feedback form data reply_to = post_data.get('email') # In the case of new or corrected records, attachments are sent along if post_data.get('_subject') == 'New Record': attachments.append(('new_record.json', post_data['new'])) if post_data.get('_subject') == 'Updated Record': attachments.append(('updated_record.json', post_data['new'])) attachments.append(('original_record.json', post_data['original'])) # Prepare a minimal Slack message channel = post_data.get('channel', '#feedback') username = post_data.get('username', 'TownCrier') icon_emoji = current_app.config['FORM_SLACK_EMOJI'] text = 'Received data from feedback form "{0}" from {1}'.format(post_data.get('_subject'), post_data.get('email')) slack_data = { 'text': text, 'username': username, 'channel': channel, 'icon_emoji': icon_emoji } elif origin == current_app.config['BBB_FEEDBACK_ORIGIN']: # The reply_to for the general feedback data reply_to = post_data.get('_replyto', '*****@*****.**') # Prepare the Slack message with submitted data text = '```Incoming Feedback```\n' + email_body channel = post_data.get('channel', '#feedback') username = post_data.get('username', 'TownCrier') icon_emoji = current_app.config['FEEDBACK_SLACK_EMOJI'] slack_data = { 'text': text, 'username': username, 'channel': channel, 'icon_emoji': icon_emoji } # If we have an email body (should always be the case), send out the email if email_body: email_sent = False try: res = send_feedback_email(name, reply_to, post_data['_subject'], email_body, attachments=attachments) email_sent = True except Exception as e: current_app.logger.error('Fatal error while processing feedback form data: {0}'.format(e)) email_sent = False if not email_sent: # If the email could not be sent, we can still log the data submitted current_app.logger.error('Sending of email failed. Feedback data submitted by {0}: {1}'.format(post_data.get('email'), post_data)) return err(ERROR_EMAIL_NOT_SENT) # If we have Slack data, post the message to Slack if slack_data: slack_data['text'] += '\n*sent to adshelp*: {0}'.format(email_sent) try: slack_response = requests.post( url=current_app.config['FEEDBACK_SLACK_END_POINT'], data=json.dumps(slack_data), timeout=60 ) except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): return b'504 Gateway Timeout', 504 current_app.logger.info('slack response: {0}' .format(slack_response.status_code)) # Slack annoyingly redirects if you have the wrong end point current_app.logger.info('Slack API' in slack_response.text) if 'Slack API' in slack_response.text: return err(ERROR_WRONG_ENDPOINT) elif slack_response.status_code == 200: return {}, 200 else: return {'msg': 'Unknown error'}, slack_response.status_code return {}, 200
def bootstrap_local_module(service_uri, deploy_path, app): """ Incorporates the routes of an existing app into this one :param service_uri: the path to the target application :param deploy_path: the path on which to make the target app discoverable :param app: flask.Flask application instance :return: None """ app.logger.debug( 'Attempting bootstrap_local_module [{0}]'.format(service_uri) ) module = import_module(service_uri) local_app = module.create_app() # Add the target app's config to the parent app's config. # Do not overwrite any config already present in the parent app for k, v in local_app.config.iteritems(): if k not in app.config: app.config[k] = v for rule in local_app.url_map.iter_rules(): view = local_app.view_functions[rule.endpoint] route = os.path.join(deploy_path, rule.rule[1:]) # view_class is attached to a function view in the case of # class-based views, and that view.view_class is the element # that has the scopes and docstring attributes if hasattr(view, 'view_class'): attr_base = view.view_class else: attr_base = view # ensure the current_app matches local_app and not API app view = local_app_context(local_app)(view) if deploy_path in local_app.config.get('AFFINITY_ENHANCED_ENDPOINTS', []): view = affinity_decorator(ratelimit._storage.storage, name=local_app.config['AFFINITY_ENHANCED_ENDPOINTS'].get(deploy_path))(view) # Decorate the view with ratelimit if hasattr(attr_base, 'rate_limit'): d = attr_base.rate_limit[0] view = ratelimit.shared_limit_and_check( lambda counts=d, per_second=attr_base.rate_limit[1]: limit_func(counts, per_second), scope=scope_func, key_func=key_func, methods=rule.methods, )(view) # Decorate the view with require_oauth if hasattr(attr_base, 'scopes'): view = oauth2.require_oauth(*attr_base.scopes)(view) # Add cache-control headers if app.config.get('API_PROXYVIEW_HEADERS'): view = headers(app.config['API_PROXYVIEW_HEADERS'])(view) # Let flask handle OPTIONS, which it will not do if we explicitly # add it to the url_map if 'OPTIONS' in rule.methods: rule.methods.remove('OPTIONS') app.add_url_rule(route, route, view, methods=rule.methods)
class PersonalTokenView(Resource): """ Implements getting/setting a personal API token """ decorators = [ ratelimit.shared_limit_and_check("500/43200 second", scope=scope_func), login_required, ] def get(self): """ This endpoint returns the ADS API client token, which is effectively a personal access token """ client = OAuthClient.query.filter_by( user_id=current_user.get_id(), name=u'ADS API client', ).first() if not client: return {'message': 'no ADS API client found'}, 200 token = OAuthToken.query.filter_by( client_id=client.client_id, user_id=current_user.get_id(), ).first() if not token: current_app.logger.error( 'no ADS API client token ' 'found for {email}. This should not happen!'.format( email=current_user.email)) return {'message': 'no ADS API token found'}, 500 output = print_token(token) output['client_id'] = client.client_id output['user_id'] = current_user.get_id() return output def put(self): """ Generates a new API key :return: dict containing the API key data structure """ client = OAuthClient.query.filter_by( user_id=current_user.get_id(), name=u'ADS API client', ).first() if client is None: # If no client exists, create a new one client = OAuthClient( user_id=current_user.get_id(), name=u'ADS API client', description=u'ADS API client', is_confidential=False, is_internal=True, _default_scopes=' '.join( current_app.config['USER_API_DEFAULT_SCOPES']), ) client.gen_salt() token = OAuthToken( client_id=client.client_id, user_id=current_user.get_id(), access_token=gen_salt(40), refresh_token=gen_salt(40), expires=datetime.datetime(2500, 1, 1), _scopes=' '.join( current_app.config['USER_API_DEFAULT_SCOPES']), is_personal=False, ) db.session.add(client) db.session.add(token) try: db.session.commit() except Exception, e: current_app.logger.error("Unknown DB error: {0}".format(e)) abort(503) current_app.logger.info( "Created ADS API client+token for {0}".format( current_user.email)) else: # Client exists; find its token and change the access_key
class UserInfoView(Resource): """ Implements getting user info from session ID, user id, access token or client id. It should be limited to internal use only. """ decorators = [ ratelimit.shared_limit_and_check("500/43200 second", scope=scope_func), oauth2.require_oauth('adsws:internal') ] def get(self, account_data): """ This endpoint provides the full identifying data associated to a given session, user id, access token or client id. Example: curl -H 'authorization: Bearer:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 'https://dev.adsabs.harvard.edu/v1/accounts/info/yyyy' Where 'yyyy' can be a session, access token, user id or client id. Notice that sessions are not server side, but client stored and server signed to avoid user manipulation. """ ## Input data can be a session, a access token or a user id # 1) Try to treat input data as a session try: session_data = self._decodeFlaskCookie(account_data) if '_id' in session_data: session_id = session_data['_id'] except Exception: # Try next identifier type pass else: if 'oauth_client' in session_data: # Anonymous users always have their oauth_client id in the session token = OAuthToken.query.filter_by( client_id=session_data['oauth_client']).first() if token: return self._translate(token.user_id, token.client_id, token.user.email, source="session:client_id") else: # Token not found in database return {'message': 'Identifier not found [ERR 010]'}, 404 elif 'user_id' in session_data: # There can be more than one token per user (generally one for # BBB and one for API requests), when client id is not stored # in the session (typically for authenticated users) we pick # just the first in the database that corresponds to BBB since # sessions are used by BBB and not API requests client = OAuthClient.query.filter_by( user_id=session_data['user_id'], name=u'BB client').first() if client: token = OAuthToken.query.filter_by( client_id=client.client_id, user_id=session_data['user_id']).first() if token: return self._translate(token.user_id, token.client_id, token.user.email, source="session:user_id") else: # Token not found in database return { 'message': 'Identifier not found [ERR 020]' }, 404 else: # Client ID not found in database return {'message': 'Identifier not found [ERR 030]'}, 404 else: # This should not happen, all ADS created session should contain that parameter return { 'message': 'Missing oauth_client/user_id parameter in session' }, 500 # 2) Try to treat input data as user id try: user_id = int(account_data) except ValueError: # Try next identifier type pass else: token = OAuthToken.query.filter_by(user_id=user_id).first() if token: return self._translate(token.user_id, token.client_id, token.user.email, source="user_id") else: # Token not found in database return {'message': 'Identifier not found [ERR 040]'}, 404 # 3) Try to treat input data as access token token = OAuthToken.query.filter_by(access_token=account_data).first() if token: return self._translate(token.user_id, token.client_id, token.user.email, source="access_token") # 4) Try to treat input data as client id token = OAuthToken.query.filter_by(client_id=account_data).first() if token: return self._translate(token.user_id, token.client_id, token.user.email, source="client_id") # Data not decoded sucessfully/Identifier not found return {'message': 'Identifier not found [ERR 050]'}, 404 def _translate(self, user_id, client_id, user_email, source=None): if user_email == current_app.config['BOOTSTRAP_USER_EMAIL']: anonymous = True elif user_email: anonymous = False else: anonymous = None # 10 rounds of SHA-256 hash digest algorithm for HMAC (pseudorandom function) # with a length of 2x32 # NOTE: 100,000 rounds is recommended but it is too slow and security is not # that important here, thus we just do 10 rounds hashed_user_id = binascii.hexlify( hashlib.pbkdf2_hmac( 'sha256', str(user_id), current_app.secret_key, 10, dklen=32)) if user_id else None hashed_client_id = binascii.hexlify( hashlib.pbkdf2_hmac( 'sha256', str(client_id), current_app.secret_key, 10, dklen=32)) if client_id else None return { 'hashed_user_id': hashed_user_id, # Permanent, but all the anonymous users have the same one (id 1) 'hashed_client_id': hashed_client_id, # A single user has a client ID for the BB token and another for the API, anonymous users have a unique client ID linked to the anonymous user id (id 1) 'anonymous': anonymous, # True, False or None if email could not be retreived/anonymous validation could not be executed 'source': source, # Identifier used to recover information: session:client_id, session:user_id, user_id, access_token, client_id }, 200 def _decodeFlaskCookie(self, cookie_value): sscsi = SecureCookieSessionInterface() signingSerializer = sscsi.get_signing_serializer(current_app) return signingSerializer.loads(cookie_value)
def bootstrap_remote_service(service_uri, deploy_path, app): """ Incorporates the routes of a remote app into this one by registering views that forward traffic to those remote endpoints :param service_uri: the http url of the target application :param deploy_path: the path on which to make the target app discoverable :param app: flask.Flask application instance :return: None """ app.logger.debug( 'Attempting bootstrap_remote_service [{0}]'.format(service_uri)) if service_uri.startswith('consul://'): cs = ConsulService( service_uri, nameservers=[app.config.get("CONSUL_DNS", "172.17.42.1")]) url = urljoin(cs.base_url, app.config.get('WEBSERVICES_PUBLISH_ENDPOINT', '/')) print "base url", cs.base_url else: url = urljoin(service_uri, app.config.get('WEBSERVICES_PUBLISH_ENDPOINT', '/')) cache_key = service_uri.replace('/', '').replace('\\', '').replace('.', '') cache_dir = app.config.get('WEBSERVICES_DISCOVERY_CACHE_DIR', '') cache_path = os.path.join(cache_dir, cache_key) resource_json = {} # discover the ratelimits/urls/permissions from the service itself; # if not available, use a cached values (if any) try: r = requests.get(url, timeout=5) resource_json = r.json() if cache_dir: try: with open(cache_path, 'w') as cf: cf.write(json.dumps(resource_json)) except IOError: app.logger.error( 'Cant write cached resource {0}'.format(cache_path)) except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): if cache_dir and os.path.exists(cache_path): with open(cache_path, 'r') as cf: resource_json = json.loads(cf.read()) else: app.logger.info('Could not discover {0}'.format(service_uri)) return # Start constructing the ProxyViews based on what we got when querying # the /resources route. # If any part of this procedure fails, log that we couldn't produce this # ProxyView, but otherwise continue. for resource, properties in resource_json.iteritems(): properties.setdefault('rate_limit', [1000, 86400]) properties.setdefault('scopes', []) if resource.startswith('/'): resource = resource[1:] route = os.path.join(deploy_path, resource) remote_route = urljoin(service_uri, resource) # Make an instance of the ProxyView. We need to instantiate the class # to save instance attributes, which will be necessary to re-construct # the location to the third party resource (ProxyView.endpoint) with app.app_context(): # app_context to allow config lookup via current_app in __init__ proxyview = ProxyView(remote_route, service_uri, deploy_path, route) _update_symbolic_ratelimits(app, route, properties) for method in properties['methods']: if method not in proxyview.methods: app.logger.warning("Could not create a ProxyView for " "method {meth} for {ep}".format( meth=method, ep=service_uri)) continue view = proxyview.dispatcher # Decorate the view with ratelimit. d = properties['rate_limit'][0] view = ratelimit.shared_limit_and_check( lambda counts=d, per_second=properties['rate_limit'][ 1]: limit_func(counts, per_second), scope=scope_func, key_func=key_func, methods=[method], per_method=False)(view) if deploy_path in app.config.get('AFFINITY_ENHANCED_ENDPOINTS', []): view = affinity_decorator( ratelimit._storage.storage, name=app.config['AFFINITY_ENHANCED_ENDPOINTS'].get( deploy_path))(view) # Decorate with the advertised oauth2 scopes view = oauth2.require_oauth(*properties['scopes'])(view) # Add cache-control headers if app.config.get('API_PROXYVIEW_HEADERS'): view = headers(app.config['API_PROXYVIEW_HEADERS'])(view) # Either make a new route with this view, or append the new method # to an existing route if one exists with the same name try: rule = next(app.url_map.iter_rules(endpoint=route)) if method not in rule.methods: rule.methods.update([method]) except KeyError: app.add_url_rule(route, route, view, methods=[method])
def bootstrap_remote_service(service_uri, deploy_path, app): """ Incorporates the routes of a remote app into this one by registering views that forward traffic to those remote endpoints :param service_uri: the http url of the target application :param deploy_path: the path on which to make the target app discoverable :param app: flask.Flask application instance :return: None """ app.logger.debug( 'Attempting bootstrap_remote_service [{0}]'.format(service_uri) ) if service_uri.startswith('consul://'): cs = ConsulService( service_uri, nameservers=[app.config.get("CONSUL_DNS", "172.17.42.1")] ) url = urljoin( cs.base_url, app.config.get('WEBSERVICES_PUBLISH_ENDPOINT', '/') ) print "base url", cs.base_url else: url = urljoin( service_uri, app.config.get('WEBSERVICES_PUBLISH_ENDPOINT', '/') ) cache_key = service_uri.replace('/', '').replace('\\', '').replace('.', '') cache_dir = app.config.get('WEBSERVICES_DISCOVERY_CACHE_DIR', '') cache_path = os.path.join(cache_dir, cache_key) resource_json = {} # discover the ratelimits/urls/permissions from the service itself; # if not available, use a cached values (if any) try: r = requests.get(url, timeout=5) resource_json = r.json() if cache_dir: try: with open(cache_path, 'w') as cf: cf.write(json.dumps(resource_json)) except IOError: app.logger.error('Cant write cached resource {0}'.format(cache_path)) except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): if cache_dir and os.path.exists(cache_path): with open(cache_path, 'r') as cf: resource_json = json.loads(cf.read()) else: app.logger.info('Could not discover {0}'.format(service_uri)) return # Start constructing the ProxyViews based on what we got when querying # the /resources route. # If any part of this procedure fails, log that we couldn't produce this # ProxyView, but otherwise continue. for resource, properties in resource_json.iteritems(): if resource.startswith('/'): resource = resource[1:] route = os.path.join(deploy_path, resource) remote_route = urljoin(service_uri, resource) # Make an instance of the ProxyView. We need to instantiate the class # to save instance attributes, which will be necessary to re-construct # the location to the third party resource (ProxyView.endpoint) with app.app_context(): # app_context to allow config lookup via current_app in __init__ proxyview = ProxyView(remote_route, service_uri, deploy_path, route) for method in properties['methods']: if method not in proxyview.methods: app.logger.warning("Could not create a ProxyView for " "method {meth} for {ep}" .format(meth=method, ep=service_uri)) continue view = proxyview.dispatcher properties.setdefault('rate_limit', [1000, 86400]) properties.setdefault('scopes', []) if deploy_path in app.config.get('AFFINITY_ENHANCED_ENDPOINTS', []): view = affinity_decorator(ratelimit._storage.storage, name=app.config['AFFINITY_ENHANCED_ENDPOINTS'].get(deploy_path))(view) # Decorate the view with ratelimit. d = properties['rate_limit'][0] view = ratelimit.shared_limit_and_check( lambda counts=d, per_second=properties['rate_limit'][1]: limit_func(counts, per_second), scope=scope_func, key_func=key_func, methods=[method], )(view) # Decorate with the advertised oauth2 scopes view = oauth2.require_oauth(*properties['scopes'])(view) # Add cache-control headers if app.config.get('API_PROXYVIEW_HEADERS'): view = headers(app.config['API_PROXYVIEW_HEADERS'])(view) # Either make a new route with this view, or append the new method # to an existing route if one exists with the same name try: rule = next(app.url_map.iter_rules(endpoint=route)) if method not in rule.methods: rule.methods.update([method]) except KeyError: app.add_url_rule(route, route, view, methods=[method])