def load_user_endpoints(): logger.debug("Looking for custom endpoints...") # Load every .py file inside the api/ folder for folder in ("api", "endpoints"): try: pyfiles = [f for f in listdir(folder) if f.endswith(".py")] except FileNotFoundError: continue if folder == "api" and pyfiles: logger.warning( "Please rename the folder that contains your endpoints to 'endpoints/' instead of 'api/'" ) logger.warning( "Support for the 'api/' folder will be dropped in the future.") for pyfile in pyfiles: module_name = folder + "." + splitext(pyfile)[0] logger.debug(f"Found endpoint file: {module_name}") try: importlib.import_module(module_name) except ImportError: raise RuntimeError( f"Could not load the API file {module_name}")
def run(): print("Testing $loggedId...") logger.warning("Testing free access") try_normal() logger.warning("Testing with role restriction") try_with_restriction()
def handle(args): from silence.server import manager as server_manager logger.info(f"Silence v{__version__}") logger.debug("Current settings:\n" + str(settings)) new_ver = check_for_new_version() if new_ver: logger.warning( f"A new Silence version (v{new_ver}) is available. Run 'pip install --upgrade Silence' to upgrade." ) server_manager.setup() server_manager.run()
def run(): print("Testing /register...") logger.warning("testing register no user") try_register_no_user() logger.warning("testing register no password") try_register_no_password() logger.warning("testing register non unique email") try_register_repeated_email() logger.warning("testing register success") try_register_ok() logger.warning("testing register existing") try_register_repeated()
def run(): print("Testing role restrictions...") logger.warning("Testing free access") try_free_access() logger.warning("Testing only logged") try_only_logged() logger.warning("Testing only Manager or CEO") try_only_manager_or_ceo() logger.warning("Testing only CEO") try_only_ceo()
def run(): print("Testing banned capabilities restrictions...") logger.warning("Testing login with allowed user.") try_loggin_allowed_user() logger.warning("Testing login with banned user.") try_loggin_banned_user() logger.warning("Testing login with unbanned user.") try_loggin_unbanned_user()
def run(): print("Testing /employees...") logger.warning("Testing getting top employees from a view") try_get_from_view_topemployees() logger.warning("testing getting all employees sorted.") try_get_all_sorted() logger.warning("testing getting all employees filtered.") try_get_all_filtered() logger.warning("testing getting all employees paginated.") try_get_all_paginated() logger.warning("testing getting one employees.") try_get_one_ok() logger.warning("testing getting one employee nonexistent.") try_get_one_not_exists() logger.warning("testing getting one employee unauthorized.") try_create_unauthorized() logger.warning("testing creating employee with existing unique email.") try_create_repeated_email() logger.warning("testing creating employee without providing password.") try_create_no_password() logger.warning("testing creating employee without providing name.") try_create_no_name() logger.warning("testing creating employee without invalid salary.") try_create_invalid_salary() logger.warning("testing creating employee") try_create_ok() logger.warning("testing editing employee unauthorized") try_edit_unauthorized() logger.warning("testing editing employee") try_edit_ok() logger.warning("testing deleting employee unauthorized") try_delete_unauthorized() logger.warning("testing deleting employee") try_delete_ok()
def download_from_github(project_name, repo_url): # Check that the current directory does not contain a folder with the same name if isdir(project_name): logger.error( f"A folder named '{project_name}' already exists in the current directory." ) sys.exit(1) # Remove the trailing .git or slash if they exist # We could use .removesuffix, but it was added in 3.9... maybe if we bump # the required Python version some time in the future suffixes = (".git", "/") for suffix in suffixes: if repo_url.endswith(suffix): repo_url = repo_url[:-len(suffix)] # Check that the repo URL is acceptable m = RE_REPO_URL.match(repo_url.lower()) if not m: logger.error( "Invalid repo URL, please check your spelling and try again.") sys.exit(1) host, username, repo_name = m.groups() # Check that the host is supported if host not in ("github.com", "github.eii.us.es"): logger.error( "Only repos hosted in github.com or github.eii.us.es are supported." ) sys.exit(1) # Download it (this takes care of querying the relevant API to find out # how the default branch is called, and exiting if the repo does not exist) git_clone(host, username, repo_name, project_name + "/") # Unpack it (everything is inside the <name>-<branch> folder) branch_folder_name = listdir(project_name)[0] # Move everything inside that folder outside branch_folder = join(project_name, branch_folder_name) for elem in listdir(branch_folder): move(join(branch_folder, elem), join(project_name, elem)) # Remove the now empty folder rmtree(branch_folder) # Look for .gitkeep files and remove them (especially useful in the case # of the blank template project) for gitkeep_path in Path(project_name).rglob(".gitkeep"): remove(gitkeep_path) # Read the settings.py file of the downloaded project, removing the existing # SECRET_KEY if it exists and creating a new one. Will also raise a warning # if the project does not contain a settings.py file. settings_path = join(project_name, "settings.py") try: settings_lines = open(settings_path, "r", encoding="utf-8").readlines() settings_lines = list( filter(lambda line: not line.startswith("SECRET_KEY"), settings_lines)) # Generate the random string for the secret key # and add it to the settings.py file secret_key = token_urlsafe(32) settings_lines += [ "\n", "# A random string that is used for security purposes\n", "# (this has been generated automatically upon project creation)\n", f'SECRET_KEY = "{secret_key}"\n' ] open(settings_path, "w", encoding="utf-8").writelines(settings_lines) except FileNotFoundError: logger.warning( "The downloaded project does not have a settings.py file " + "at its root, it may not be a valid Silence project.")
def run(): print("Testing /login...") logger.warning("testing login empty fields") try_login_empty() logger.warning("testing login empty password") try_login_no_password() logger.warning("testing login empty user") try_login_no_id() logger.warning("testing login incorrect user") try_login_incorrect_email() logger.warning("testing login incorrect password") try_login_incorrect_password() logger.warning("testing login success") try_login_ok()
def setup_endpoint(route, method, sql, auth_required=False, allowed_roles=["*"], description=None, request_body_params=[]): logger.debug(f"Setting up endpoint {method} {route}") # if the query is requesting the logged user. logged_user = "******" in sql if logged_user and not auth_required: logger.warning( "You're using $loggedId but are not requesting authorization, in endpoint: " + str(route)) # Construct the API route taking the prefix into account route_prefix = settings.API_PREFIX if route_prefix.endswith("/"): route_prefix = route_prefix[:-1] # Drop the final / full_route = route_prefix + route # Warn if the pair SQL operation - HTTP verb is not the proper one check_method(sql, method, route) # Warn if the values of auth_required and allowed_roles don't make sense together check_auth_roles(auth_required, allowed_roles, method, route) # Extract the list of parameters that the user expects to receive # in the URL and in the SQL string sql_params = extract_params(sql) url_params = extract_params(route) # Get the required SQL operation sql_op = get_sql_op(sql) # If it's a SELECT or a DELETE, make sure that all SQL params can be # obtained from the url if sql_op in (SQL.SELECT, SQL.DELETE): check_params_match(sql_params, url_params, route) # If it's a SELECT or a DELETE, make sure that all SQL params can be # obtained from the url AND the request body if sql_op in (SQL.INSERT, SQL.UPDATE): check_params_match(sql_params, url_params + request_body_params, route) # The handler function that will be passed to flask def route_handler(*args, **kwargs): # If this endpoint requires authentication, check that the # user has provided a session token and that it is valid if auth_required: userId = check_session(allowed_roles) # Collect all url pattern params request_url_params_dict = kwargs # If endpoint requires the logged userId it adds the pair (loggedId, loggedUserId) if logged_user: if not auth_required: userId = check_session(allowed_roles) if userId != None: request_url_params_dict["loggedId"] = userId else: request_url_params_dict["loggedId"] = None # Convert the silence-style placeholders in the SQL query to proper MySQL placeholders query_string = silence_to_mysql(sql) # Default outputs res = None status = 200 # SELECT/GET operations if sql_op == SQL.SELECT: # The URL params have been checked to be enough to fill all SQL params url_pattern_params = tuple(request_url_params_dict[param] for param in sql_params) res = dal.api_safe_query(query_string, url_pattern_params) # Filter these results according to the URL query string, if there is one # Possible TO-DO: do this by directly editing the SQL query for extra efficiency res = filter_query_results(res, request.args) # In our teaching context, it is safe to assume that if the URL ends # with a parameter and we have no results, we should return a 404 code if RE_QUERY_PARAM.match(route) and not res: raise HTTPError(404, "Not found") else: # POST/PUT/DELETE operations #Construct a dict for all params expected in the request body, setting them to None if they have not been provided form = request.json if request.is_json else request.form body_params = { param: form.get(param, None) for param in request_body_params } # We have checked that sql_params is a subset of url_params U body_params, # construct a joint param object and use it to fill the SQL placeholders for param in url_params: body_params[param] = request_url_params_dict[param] if logged_user and auth_required: body_params["loggedId"] = userId param_tuple = tuple(body_params[param] for param in sql_params) param_tuple = tuple(body_params[param] for param in sql_params) # Run the execute query res = dal.api_safe_update(query_string, param_tuple) return jsonify(res), status # flaskify_url() adapts the URL so that all $variables are converted to Flask-style <variables> server_manager.APP.add_url_rule(flaskify_url(full_route), method + route, route_handler, methods=[method]) server_manager.API_SUMMARY.register_endpoint({ "route": full_route, "method": method.upper(), "description": description })
def setup(): # Configures the web server APP.secret_key = settings.SECRET_KEY APP.config["SESSION_TYPE"] = "filesystem" APP.config["SEND_FILE_MAX_AGE_DEFAULT"] = settings.HTTP_CACHE_TIME # Mute Flask's startup messages def noop(*args, **kwargs): pass click.echo = noop click.secho = noop # Add our Flask filter to customize Flask logging messages logging.getLogger("werkzeug").addFilter(FlaskFilter()) # Override the default JSON encoder so that it works with the Decimal type APP.json_encoder = SilenceJSONEncoder # Manually set up the MIME type for .js files # This patches a known issue on Windows, where the MIME type for JS files # is sometimes incorrectly set to text/plain in the registry mimetypes.add_type("application/javascript", ".js", strict=True) # Set up the error handle for our custom exception type @APP.errorhandler(HTTPError) def handle_HTTPError(error): response = jsonify(error.to_dict()) response.status_code = error.status_code return response # Set up the generic Exception handler for server errors @APP.errorhandler(Exception) def handle_generic_error(exc): # Pass through our own HTTP error exception if isinstance(exc, HTTPError): return exc # Create a similar JSON response for Werkzeug's exceptions if isinstance(exc, HTTPException): code = exc.code res = jsonify({"message": exc.description, "code": code}) return res, code # We're facing an uncontrolled server exception logger.exception(exc) exc_type = type(exc).__name__ msg = str(exc) err = HTTPError(500, msg, exc_type) return handle_HTTPError(err) # Check if clear text passwords can be used for login, and show a warning # if that is the case if settings.ALLOW_CLEAR_PASSWORDS: logger.warning( "This project allows clear text passwords in the DB to be used for login\n" + "(ALLOW_CLEAR_PASSWORDS is set to True)\n" + "This is NOT RECOMMENDED outside testing purposes.") # Load the user-provided API endpoints and the default ones if settings.RUN_API: load_default_endpoints() load_user_endpoints() if settings.SHOW_ENDPOINT_LIST: API_SUMMARY.print_endpoints() # Load the web static files if settings.RUN_WEB: logger.debug("Setting up web server") @APP.route("/") def root(): return APP.send_static_file("index.html") @APP.route("/<path:path>") def other_path(path): return APP.send_static_file(path)
def run(): print("Testing /...") logger.warning("testing the summary endpoint") try_get_endpoints()
def run(): print("Testing /departments...") logger.warning("testing getting all departments sorted.") try_get_all_sorted() logger.warning("testing getting all departments filtered.") try_get_all_filtered() logger.warning("testing getting all departments paginated.") try_get_all_paginated() logger.warning("testing getting one department.") try_get_one_ok() logger.warning("testing getting nonexistent department.") try_get_one_not_exists() logger.warning("testing getting one department unauthorized.") try_create_unauthorized() logger.warning("testing creating one department.") try_create_ok() logger.warning("testing creating one department that already exists") try_create_repeated() logger.warning("testing editing one department aunauthorized") try_edit_unauthorized() logger.warning("testing editing one department") try_edit_ok() logger.warning("testing deleting one department aunauthorized") try_delete_unauthorized() logger.warning("testing deleting one department") try_delete_ok()