Beispiel #1
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)
Beispiel #2
0
class UserResolver(Resource):
    """
    Resolves an email or uid into a string formatted user object
    """

    decorators = [oauth2.require_oauth('adsws:internal')]

    def get(self, identifier):
        """
        :param identifier: email address or uid
        :return: json containing user info or 404
        """

        try:
            u = user_manipulator.get(int(identifier))
        except ValueError:
            u = user_manipulator.find(email=identifier).first()

        if u is None:
            abort(404)

        return {
            "id": u.id,
            "email": u.email,
        }
Beispiel #3
0
class OAuthProtectedView(Resource):
    """
    Resource for checking that oauth2.require_oauth is satisfied
    """
    decorators = [oauth2.require_oauth()]

    def get(self):
        return {'app': current_app.name, 'oauth': request.oauth.user.email}
Beispiel #4
0
class ProtectedView(Resource):
    """
    This view is oauth2-authentication protected
    """
    decorators = [oauth2.require_oauth()]

    def get(self):
        return {'app': current_app.name, 'user': request.oauth.user.email}, 200
Beispiel #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

        # Decorate the view with ratelimit
        if hasattr(attr_base, 'rate_limit'):
            d = attr_base.rate_limit[0]
            view = ratelimit(
                limit=lambda default=d, **kwargs: limit_func(default),
                per=attr_base.rate_limit[1],
                scope_func=lambda: scope_func(),
                key_func=lambda: request.endpoint
            )(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)
Beispiel #6
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', '/')
        )

    try:
        r = requests.get(url, timeout=5)
    except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
        app.logger.info('Could not discover {0}'.format(service_uri))
        return

    # validate(r.json()) # TODO validate the incoming json

    # 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 r.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)

        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', [])

            # Decorate the view with ratelimit.
            d = properties['rate_limit'][0]
            view = ratelimit(
                limit=lambda default=d, **kwargs: limit_func(default),
                per=properties['rate_limit'][1],
                scope_func=lambda: scope_func(),
                key_func=lambda: request.endpoint,
            )(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])
Beispiel #7
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)
Beispiel #8
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])
Beispiel #9
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])
Beispiel #10
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)