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 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, }
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}
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
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)
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])
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])
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)