Exemple #1
0
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
Exemple #2
0
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
Exemple #3
0
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
Exemple #4
0
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
Exemple #5
0
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)
Exemple #6
0
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
Exemple #7
0
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']
Exemple #8
0
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()}
Exemple #9
0
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
Exemple #10
0
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}
Exemple #11
0
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
Exemple #12
0
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
Exemple #13
0
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
Exemple #14
0
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
Exemple #15
0
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)
Exemple #16
0
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
Exemple #17
0
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)
Exemple #18
0
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])
Exemple #19
0
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])