def wrapper(self, *args, **kwargs): try: return func(self, *args, **kwargs) except BaseException: task_id = self.request.id task_name = self.request.task log.error("Celery task {} failed ({})", task_id, task_name) arguments = str(self.request.args) log.error("Failed task arguments: {}", arguments[0:256]) log.error("Task error: {}", traceback.format_exc()) if send_mail_is_active(): log.info("Sending error report by email", task_id, task_name) body = """ Celery task {} failed Name: {} Arguments: {} Error: {} """.format(task_id, task_name, str(self.request.args), traceback.format_exc()) project = get_project_configuration( "project.title", default='Unkown title', ) subject = "{}: task {} failed".format(project, task_name) send_mail(body, subject)
def post(self): v = self.get_input() if len(v) == 0: raise RestApiException('Empty input', status_code=hcodes.HTTP_BAD_REQUEST) if self.neo4j_enabled: self.graph = self.get_service_instance('neo4j') is_admin = self.auth.verify_admin() is_local_admin = self.auth.verify_local_admin() if not is_admin and not is_local_admin: raise RestApiException( "You are not authorized: missing privileges", status_code=hcodes.HTTP_BAD_UNAUTHORIZED, ) schema = self.get_endpoint_custom_definition() if 'get_schema' in v: new_schema = schema[:] if send_mail_is_active(): new_schema.append({ "name": "email_notification", "description": "Notify password by email", "type": "boolean", "default": False, "custom": { "htmltype": "checkbox", "label": "Notify password by email", }, }) if 'autocomplete' in v and not v['autocomplete']: for idx, val in enumerate(new_schema): # FIXME: groups management is only implemented for neo4j if val["name"] == "group": new_schema[idx]["default"] = None if "custom" not in new_schema[idx]: new_schema[idx]["custom"] = {} new_schema[idx]["custom"]["htmltype"] = "select" new_schema[idx]["custom"]["label"] = "Group" new_schema[idx]["enum"] = [] for g in self.graph.Group.nodes.all(): group_name = "{} - {}".format( g.shortname, g.fullname) new_schema[idx]["enum"].append( {g.uuid: group_name}) if new_schema[idx]["default"] is None: new_schema[idx]["default"] = g.uuid # Roles as multi checkbox if val["name"] == "roles": roles = self.auth.get_roles() is_admin = self.auth.verify_admin() allowed_roles = get_project_configuration( "variables.backend.allowed_roles", default=[], ) del new_schema[idx] for r in roles: if is_admin: if r.description == 'automatic': continue else: if r.name not in allowed_roles: continue role = { "type": "checkbox", "name": "roles_{}".format(r.name), "custom": { "label": r.description }, } new_schema.insert(idx, role) if is_admin: return self.force_response(new_schema) current_user = self.get_current_user() for idx, val in enumerate(new_schema): # FIXME: groups management is only implemented for neo4j if val["name"] == "group": new_schema[idx]["default"] = None if "custom" not in new_schema[idx]: new_schema[idx]["custom"] = {} new_schema[idx]["custom"]["htmltype"] = "select" new_schema[idx]["custom"]["label"] = "Group" new_schema[idx]["enum"] = [] default_group = self.graph.Group.nodes.get_or_none( shortname="default") defg = None if default_group is not None: new_schema[idx]["enum"].append( {default_group.uuid: default_group.shortname}) # new_schema[idx]["default"] = default_group.uuid defg = default_group.uuid for g in current_user.coordinator.all(): if g == default_group: continue group_name = "{} - {}".format(g.shortname, g.fullname) new_schema[idx]["enum"].append({g.uuid: group_name}) if defg is None: defg = g.uuid # if new_schema[idx]["default"] is None: # new_schema[idx]["default"] = g.uuid if (len(new_schema[idx]["enum"])) == 1: new_schema[idx]["default"] = defg return self.force_response(new_schema) # INIT # properties = self.read_properties(schema, v) roles = self.parse_roles(v) if not is_admin: allowed_roles = get_project_configuration( "variables.backend.allowed_roles", default=[], ) for r in roles: if r not in allowed_roles: raise RestApiException( "You are not allowed to assign users to this role") if "password" in properties and properties["password"] == "": del properties["password"] if "password" in properties: unhashed_password = properties["password"] else: unhashed_password = None try: user = self.auth.create_user(properties, roles) except AttributeError as e: # Duplicated from decorators prefix = "Can't create user .*:\nNode\([0-9]+\) already exists with label" m = re.search( "{} `(.+)` and property `(.+)` = '(.+)'".format(prefix), str(e)) if m: node = m.group(1) prop = m.group(2) val = m.group(3) error = "A {} already exists with {} = {}".format( node, prop, val) raise RestApiException(error, status_code=hcodes.HTTP_BAD_CONFLICT) else: raise e if self.sql_enabled: try: self.auth.db.session.commit() except IntegrityError: self.auth.db.session.rollback() raise RestApiException("This user already exists") # If created by admins, credentials # must accept privacy at the login if "privacy_accepted" in v: if not v["privacy_accepted"]: if hasattr(user, 'privacy_accepted'): user.privacy_accepted = False self.auth.save_user(user) # FIXME: groups management is only implemented for neo4j group = None if 'group' in v: group = self.parse_group(v) if group is not None: if not is_admin and group.shortname != "default": current_user = self.get_current_user() if not group.coordinator.is_connected(current_user): raise RestApiException( "You are not allowed to assign users to this group") user.belongs_to.connect(group) email_notification = v.get('email_notification', False) if email_notification and unhashed_password is not None: self.send_notification(user, unhashed_password, is_update=False) return self.force_response(user.uuid)
def post(self): if not send_mail_is_active(): raise RestApiException( 'Server misconfiguration, unable to reset password. ' + 'Please report this error to adminstrators', status_code=hcodes.HTTP_BAD_REQUEST) reset_email = self.get_input(single_parameter='reset_email') if reset_email is None: raise RestApiException( 'Invalid reset email', status_code=hcodes.HTTP_BAD_FORBIDDEN) reset_email = reset_email.lower() user = self.auth.get_user_object(username=reset_email) if user is None: raise RestApiException( 'Sorry, %s ' % reset_email + 'is not recognized as a valid username or email address', status_code=hcodes.HTTP_BAD_FORBIDDEN) title = mem.customizer._configurations \ .get('project', {}) \ .get('title', "Unkown title") # invalidate previous reset tokens tokens = self.auth.get_tokens(user=user) for t in tokens: token_type = t.get("token_type") if token_type is None: continue if token_type != self.auth.PWD_RESET: continue tok = t.get("token") if self.auth.invalidate_token(tok): log.info("Previous reset token invalidated: %s", tok) # Generate a new reset token reset_token, jti = self.auth.create_temporary_token( user, duration=86400, token_type=self.auth.PWD_RESET ) domain = os.environ.get("DOMAIN") if PRODUCTION: protocol = "https" else: protocol = "http" u = "%s://%s/public/reset/%s" % (protocol, domain, reset_token) body = "link to reset password: %s" % u replaces = { "url": u } html_body = get_html_template("reset_password.html", replaces) # html_body = "link to reset password: <a href='%s'>click here</a>" % u subject = "%s Password Reset" % title send_mail(html_body, subject, reset_email, plain_body=body) self.auth.save_token( user, reset_token, jti, token_type=self.auth.PWD_RESET) msg = "We are sending an email to your email address where " + \ "you will find the link to enter a new password" return msg
def post(self): v = self.get_input() if len(v) == 0: raise RestApiException( 'Empty input', status_code=hcodes.HTTP_BAD_REQUEST) if not detector.check_availability('neo4j'): log.warning("This endpoint is implemented only for neo4j") return self.force_response('0') self.graph = self.get_service_instance('neo4j') is_admin = self.auth.verify_admin() is_group_admin = self.auth.verify_group_admin() if not is_admin and not is_group_admin: raise RestApiException( "You are not authorized: missing privileges", status_code=hcodes.HTTP_BAD_UNAUTHORIZED) schema = self.get_endpoint_custom_definition() if 'get_schema' in v: new_schema = schema[:] if send_mail_is_active(): new_schema.append( { "name": "email_notification", "description": "Notify password by email", "type": "boolean", "default": False, "custom": { "htmltype": "checkbox", "label": "Notify password by email" } } ) if is_admin: return self.force_response(new_schema) # institutes = self.graph.Institute.nodes # users = self.graph.User.nodes current_user = self.get_current_user() for idx, val in enumerate(new_schema): if val["name"] == "group": new_schema[idx]["default"] = None new_schema[idx]["custom"] = { "htmltype": "select", "label": "Group" } new_schema[idx]["enum"] = [] for g in current_user.coordinator.all(): new_schema[idx]["enum"].append( {g.uuid: g.shortname} ) if new_schema[idx]["default"] is None: new_schema[idx]["default"] = g.uuid return self.force_response(new_schema) # INIT # properties = self.read_properties(schema, v) roles = self.parse_roles(v) if not is_admin: allowed_roles = mem.customizer._configurations \ .get('variables', {}) \ .get('backend', {}) \ .get('allowed_roles', []) for r in roles: if r not in allowed_roles: raise RestApiException( "You are allowed to assign users to this role") if "password" in properties and properties["password"] == "": del properties["password"] if "password" in properties: unhashed_password = properties["password"] else: unhashed_password = None user = self.auth.create_user(properties, roles) group = None if 'group' in v: group = self.parse_group(v) if group is not None: if not is_admin: current_user = self.get_current_user() if not group.coordinator.is_connected(current_user): raise RestApiException( "You are allowed to assign users to this group") user.belongs_to.connect(group) email_notification = v.get('email_notification', False) if email_notification and unhashed_password is not None: self.send_notification(user, unhashed_password, is_update=False) return self.force_response(user.uuid)
def post(self): v = self.get_input() if len(v) == 0: raise RestApiException('Empty input', status_code=hcodes.HTTP_BAD_REQUEST) if not detector.check_availability('neo4j'): log.warning("This endpoint is implemented only for neo4j") return self.force_response('0') self.graph = self.get_service_instance('neo4j') is_admin = self.auth.verify_admin() is_local_admin = self.auth.verify_local_admin() if not is_admin and not is_local_admin: raise RestApiException( "You are not authorized: missing privileges", status_code=hcodes.HTTP_BAD_UNAUTHORIZED) schema = self.get_endpoint_custom_definition() if 'get_schema' in v: new_schema = schema[:] if send_mail_is_active(): new_schema.append({ "name": "email_notification", "description": "Notify password by email", "type": "boolean", "default": False, "custom": { "htmltype": "checkbox", "label": "Notify password by email" } }) if 'autocomplete' in v and not v['autocomplete']: for idx, val in enumerate(new_schema): if val["name"] == "group": new_schema[idx]["default"] = None if "custom" not in new_schema[idx]: new_schema[idx]["custom"] = {} new_schema[idx]["custom"]["htmltype"]: "select" new_schema[idx]["custom"]["label"]: "Group" new_schema[idx]["enum"] = [] for g in self.graph.Group.nodes.all(): new_schema[idx]["enum"].append( {g.uuid: g.fullname}) if new_schema[idx]["default"] is None: new_schema[idx]["default"] = g.uuid # Roles as multi checkbox if val["name"] == "roles": cypher = "MATCH (r:Role)" if not self.auth.verify_admin(): allowed_roles = mem.customizer._configurations \ .get('variables', {}) \ .get('backend', {}) \ .get('allowed_roles', []) cypher += " WHERE r.name in %s" % allowed_roles # Admin only else: cypher += " WHERE r.description <> 'automatic'" cypher += " RETURN r ORDER BY r.name ASC" result = self.graph.cypher(cypher) del new_schema[idx] for row in result: r = self.graph.Role.inflate(row[0]) role = { "type": "checkbox", # "name": "roles[%s]" % r.name, "name": "roles_%s" % r.name, # "name": r.name, "custom": { "label": r.description } } new_schema.insert(idx, role) if is_admin: return self.force_response(new_schema) # institutes = self.graph.Institute.nodes # users = self.graph.User.nodes current_user = self.get_current_user() for idx, val in enumerate(new_schema): if val["name"] == "group": new_schema[idx]["default"] = None new_schema[idx]["custom"] = { "htmltype": "select", "label": "Group" } new_schema[idx]["enum"] = [] default_group = self.graph.Group.nodes.get_or_none( shortname="default") if default_group is not None: new_schema[idx]["enum"].append( {default_group.uuid: default_group.shortname}) new_schema[idx]["default"] = default_group.uuid for g in current_user.coordinator.all(): if g == default_group: continue new_schema[idx]["enum"].append({g.uuid: g.shortname}) if new_schema[idx]["default"] is None: new_schema[idx]["default"] = g.uuid return self.force_response(new_schema) # INIT # properties = self.read_properties(schema, v) roles = self.parse_roles(v) if not is_admin: allowed_roles = mem.customizer._configurations \ .get('variables', {}) \ .get('backend', {}) \ .get('allowed_roles', []) for r in roles: if r not in allowed_roles: raise RestApiException( "You are allowed to assign users to this role") if "password" in properties and properties["password"] == "": del properties["password"] if "password" in properties: unhashed_password = properties["password"] else: unhashed_password = None user = self.auth.create_user(properties, roles) group = None if 'group' in v: group = self.parse_group(v) if group is not None: if not is_admin: current_user = self.get_current_user() if not group.coordinator.is_connected(current_user): raise RestApiException( "You are allowed to assign users to this group") user.belongs_to.connect(group) email_notification = v.get('email_notification', False) if email_notification and unhashed_password is not None: self.send_notification(user, unhashed_password, is_update=False) return self.force_response(user.uuid)
def post(self): if not send_mail_is_active(): raise RestApiException( 'Server misconfiguration, unable to reset password. ' + 'Please report this error to adminstrators', status_code=hcodes.HTTP_BAD_REQUEST, ) reset_email = self.get_input(single_parameter='reset_email') if reset_email is None: raise RestApiException( 'Invalid reset email', status_code=hcodes.HTTP_BAD_FORBIDDEN ) reset_email = reset_email.lower() user = self.auth.get_user_object(username=reset_email) if user is None: raise RestApiException( 'Sorry, {} is not recognized as a valid username'.format(reset_email), status_code=hcodes.HTTP_BAD_FORBIDDEN, ) if user.is_active is not None and not user.is_active: # Beware, frontend leverages on this exact message, # do not modified it without fix also on frontend side raise RestApiException( "Sorry, this account is not active", status_code=hcodes.HTTP_BAD_UNAUTHORIZED, ) title = get_project_configuration( "project.title", default='Unkown title' ) reset_token, jti = self.auth.create_reset_token(user, self.auth.PWD_RESET) domain = os.environ.get("DOMAIN") if PRODUCTION: protocol = "https" else: protocol = "http" rt = reset_token.replace(".", "+") var = "RESET_PASSWORD_URI" uri = detector.get_global_var(key=var, default='/public/reset') complete_uri = "{}://{}{}/{}".format(protocol, domain, uri, rt) ################## # Send email with internal or external SMTP obj = meta.get_customizer_class('apis.profile', 'CustomReset') if obj is None: # normal activation + internal smtp send_internal_password_reset(complete_uri, title, reset_email) else: # external smtp obj.request_reset(user.name, user.email, complete_uri) ################## # Completing the reset task self.auth.save_token(user, reset_token, jti, token_type=self.auth.PWD_RESET) msg = "You will receive an email shortly with a link to a page where you can create a new password, please check your spam/junk folder." return self.force_response(msg)
def post(self): """ Register new user """ if not send_mail_is_active(): raise RestApiException( 'Server misconfiguration, unable to reset password. ' + 'Please report this error to adminstrators', status_code=hcodes.HTTP_BAD_REQUEST, ) v = self.get_input() if len(v) == 0: raise RestApiException('Empty input', status_code=hcodes.HTTP_BAD_REQUEST) # INIT # # schema = self.get_endpoint_custom_definition() # properties = self.read_properties(schema, v) if 'password' not in v: raise RestApiException( "Missing input: password", status_code=hcodes.HTTP_BAD_REQUEST ) if 'email' not in v: raise RestApiException( "Missing input: email", status_code=hcodes.HTTP_BAD_REQUEST ) if 'name' not in v: raise RestApiException( "Missing input: name", status_code=hcodes.HTTP_BAD_REQUEST ) if 'surname' not in v: raise RestApiException( "Missing input: surname", status_code=hcodes.HTTP_BAD_REQUEST ) user = self.auth.get_user_object(username=v['email']) if user is not None: raise RestApiException( "This user already exists: {}".format(v['email']), status_code=hcodes.HTTP_BAD_REQUEST, ) v['is_active'] = False user = self.auth.create_user(v, [self.auth.default_role]) try: self.auth.custom_post_handle_user_input(user, v) send_activation_link(self.auth, user) notify_registration(user) msg = ( "We are sending an email to your email address where " + "you will find the link to activate your account" ) except BaseException as e: log.error("Errors during account registration: {}", str(e)) user.delete() raise RestApiException(str(e)) else: custom_extra_registration(v) return self.force_response(msg)
def create_app( name=__name__, init_mode=False, destroy_mode=False, worker_mode=False, testing_mode=False, skip_endpoint_mapping=False, **kwargs, ): """ Create the server istance for Flask application """ if PRODUCTION and testing_mode: log.exit("Unable to execute tests in production") # Initialize reading of all files mem.customizer = Customizer(testing_mode, init_mode) mem.geo_reader = geolite2.reader() # when to close?? # geolite2.close() # Add template dir for output in HTML kwargs['template_folder'] = os.path.join(ABS_RESTAPI_PATH, 'templates') # Flask app instance microservice = Flask(name, **kwargs) # Add commands to 'flask' binary if init_mode: microservice.config['INIT_MODE'] = init_mode skip_endpoint_mapping = True elif destroy_mode: microservice.config['DESTROY_MODE'] = destroy_mode skip_endpoint_mapping = True elif testing_mode: microservice.config['TESTING'] = testing_mode init_mode = True elif worker_mode: skip_endpoint_mapping = True # Fix proxy wsgi for production calls microservice.wsgi_app = ProxyFix(microservice.wsgi_app) # CORS if not PRODUCTION: cors = CORS( allow_headers=[ 'Content-Type', 'Authorization', 'X-Requested-With', 'x-upload-content-length', 'x-upload-content-type', 'content-range' ], supports_credentials=['true'], methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], ) cors.init_app(microservice) log.verbose("FLASKING! Injected CORS") # Enabling our internal Flask customized response microservice.response_class = InternalResponse # Flask configuration from config file microservice.config.from_object(config) log.debug("Flask app configured") if PRODUCTION: log.info("Production server mode is ON") # Find services and try to connect to the ones available extensions = detector.init_services( app=microservice, worker_mode=worker_mode, project_init=init_mode, project_clean=destroy_mode, ) if worker_mode: microservice.extensions = extensions # Restful plugin if not skip_endpoint_mapping: # Triggering automatic mapping of REST endpoints rest_api = Api(catch_all_404s=True) # Basic configuration (simple): from example class if len(mem.customizer._endpoints) < 1: log.error("No endpoints found!") raise AttributeError("Follow the docs and define your endpoints") for resource in mem.customizer._endpoints: # urls = [uri for _, uri in resource.uris.items()] urls = list(resource.uris.values()) # Create the restful resource with it; # this method is from RESTful plugin rest_api.add_resource(resource.cls, *urls) log.verbose("Map '{}' to {}", resource.cls.__name__, urls) # Enable all schema endpoints to be mapped with this extra step if len(mem.customizer._schema_endpoint.uris) > 0: log.debug("Found one or more schema to expose") urls = [ uri for _, uri in mem.customizer._schema_endpoint.uris.items() ] rest_api.add_resource(mem.customizer._schema_endpoint.cls, *urls) # HERE all endpoints will be registered by using FlaskRestful rest_api.init_app(microservice) microservice.services_instances = {} for m in detector.services_classes: ExtClass = detector.services_classes.get(m) microservice.services_instances[m] = ExtClass(microservice) # FlaskApiSpec experimentation from apispec import APISpec from flask_apispec import FlaskApiSpec from apispec.ext.marshmallow import MarshmallowPlugin # from apispec_webframeworks.flask import FlaskPlugin microservice.config.update({ 'APISPEC_SPEC': APISpec( title=glom(mem.customizer._configurations, 'project.title', default='0.0.1'), version=glom(mem.customizer._configurations, 'project.version', default='Your application name'), openapi_version="2.0", # OpenApi 3 not working with FlaskApiSpec # -> Duplicate parameter with name body and location body # https://github.com/jmcarp/flask-apispec/issues/170 # Find other warning like this by searching: # **FASTAPI** # openapi_version="3.0.2", plugins=[MarshmallowPlugin()], ), 'APISPEC_SWAGGER_URL': '/api/swagger', # 'APISPEC_SWAGGER_UI_URL': '/api/swagger-ui', # Disable Swagger-UI 'APISPEC_SWAGGER_UI_URL': None, }) docs = FlaskApiSpec(microservice) with microservice.app_context(): for resource in mem.customizer._endpoints: urls = list(resource.uris.values()) try: docs.register(resource.cls) except TypeError as e: # log.warning("{} on {}", type(e), resource.cls) # Enable this warning to start conversion to FlaskFastApi # Find other warning like this by searching: # **FASTAPI** log.verbose("{} on {}", type(e), resource.cls) # Clean app routes ignore_verbs = {"HEAD", "OPTIONS"} for rule in microservice.url_map.iter_rules(): rulename = str(rule) # Skip rules that are only exposing schemas if '/schemas/' in rulename: continue endpoint = microservice.view_functions[rule.endpoint] if not hasattr(endpoint, 'view_class'): continue newmethods = ignore_verbs.copy() for verb in rule.methods - ignore_verbs: method = verb.lower() if method in mem.customizer._original_paths[rulename]: # remove from flask mapping # to allow 405 response newmethods.add(verb) else: log.verbose("Removed method {}.{} from mapping", rulename, verb) rule.methods = newmethods # Logging responses @microservice.after_request def log_response(response): response.headers["_RV"] = str(__version__) PROJECT_VERSION = get_project_configuration("project.version", default=None) if PROJECT_VERSION is not None: response.headers["Version"] = str(PROJECT_VERSION) # NOTE: if it is an upload, # I must NOT consume request.data or request.json, # otherwise the content gets lost do_not_log_types = ['application/octet-stream', 'multipart/form-data'] if request.mimetype in do_not_log_types: data = 'STREAM_UPLOAD' else: try: data = handle_log_output(request.data) # Limit the parameters string size, sometimes it's too big for k in data: try: if isinstance(data[k], dict): for kk in data[k]: v = str(data[k][kk]) if len(v) > MAX_CHAR_LEN: v = v[:MAX_CHAR_LEN] + "..." data[k][kk] = v continue if not isinstance(data[k], str): data[k] = str(data[k]) if len(data[k]) > MAX_CHAR_LEN: data[k] = data[k][:MAX_CHAR_LEN] + "..." except IndexError: pass except Exception: data = 'OTHER_UPLOAD' # Obfuscating query parameters url = urllib_parse.urlparse(request.url) try: params = urllib_parse.unquote( urllib_parse.urlencode(handle_log_output(url.query))) url = url._replace(query=params) except TypeError: log.error("Unable to url encode the following parameters:") print(url.query) url = urllib_parse.urlunparse(url) log.info("{} {} {} {}", request.method, url, data, response) return response if send_mail_is_active(): if not test_smtp_client(): log.critical("Bad SMTP configuration, unable to create a client") else: log.info("SMTP configuration verified") # and the flask App is ready now: log.info("Boot completed") if SENTRY_URL is not None: if not PRODUCTION: log.info("Skipping Sentry, only enabled in PRODUCTION mode") else: import sentry_sdk from sentry_sdk.integrations.flask import FlaskIntegration sentry_sdk.init(dsn=SENTRY_URL, integrations=[FlaskIntegration()]) log.info("Enabled Sentry {}", SENTRY_URL) # return our flask app return microservice