def git_clone(host, username, repo_name, clone_dir): # Get the default branch (we've checked previously that the host is one of # the following) api_url = { "github.com": f"https://api.github.com/repos/{username}/{repo_name}", "github.eii.us.es": f"https://github.eii.us.es/api/v3/repos/{username}/{repo_name}" }[host] api_response = requests.get(api_url) if api_response.status_code == 404: logger.error("Repo not found") sys.exit(1) branch = api_response.json()["default_branch"] git_url = f"https://{host}/{username}/{repo_name}" repo_file = f"{branch}.zip" repo_zip = git_url + f"/archive/{repo_file}" if isfile(repo_file): remove(repo_file) fn = basename(urlparse(repo_zip).path) zip_path = save(repo_zip, join(getcwd(), fn)) with zipfile.ZipFile(zip_path, 'r') as zip_ref: zip_ref.extractall(clone_dir) if isfile(zip_path): remove(zip_path)
def handle(args): query_url = f"https://api.github.com/orgs/{settings.GITHUB_TEMPLATES_OWNER}/repos" try: repo_data = requests.get(query_url).json() except Exception as exc: logger.debug(traceback.format_exc()) logger.error( "An error has occurred when querying GitHub's API to obtain the list of templates." ) if not settings.DEBUG_ENABLED: logger.error("Add --debug to see the full stack trace.") sys.exit(1) templates = [] for repo in repo_data: name = repo["name"].lower() if name.startswith("silence-template"): template_name = name.replace("silence-template-", "") desc = repo["description"] templates.append({"name": template_name, "desc": desc}) templates.sort(key=lambda x: x["name"]) print("Available templates:") for tmpl in templates: name = tmpl["name"] default = " (default)" if name == settings.DEFAULT_TEMPLATE_NAME.lower( ) else "" desc = f': {tmpl["desc"]}' if tmpl["desc"] else "" print(f" · {name}{default}{desc}")
def check_auth_roles(auth_required, allowed_roles, method, route): # Raise an error if allowed_roles is not a list if not isinstance(allowed_roles, list): logger.error( f"The value '{allowed_roles}' for the allowed_roles parameter in " + f"endpoint {method.upper()} {route} is not allowed, it must be a " + "list of allowed roles.") sys.exit(1) # Warn if the user has specified some roles but auth_required is false, # since it will result in all users having access if not auth_required and len(allowed_roles) > 0 and allowed_roles != ["*"]: logger.warn( f"You have specified allowed roles in endpoint {method.upper()} {route}, " + "but auth_required is False. This will result in all users having access " + "regardless of their role.") # Warn if the user has specified an empty list of roles, and auth_required is true, # because it will result in noone having access if auth_required and len(allowed_roles) == 0: logger.warn( f"You have set auth_required to True in endpoint {method.upper()} {route}, " + "but the list of allowed roles is empty. This will result in noone being able " + "to access the endpoint.")
def check_params_match(sql_params, user_params, route): sql_params_set = set(sql_params) user_params_set = set(user_params) diff = sql_params_set.difference(user_params_set) if "loggedId" in diff: diff.remove("loggedId") if diff: params_str = ", ".join(f"${param}" for param in diff) logger.error( f"Error creating endpoint {route}: the parameters " + f"{params_str} are expected by the SQL query but they are not provided in the URL " + "or the request body.") sys.exit(1)
def try_endpoint_with_user(route, email, expected_access): token = get_token_for_user(email) headers = {"Token": token} r = requests.get(f"{BASE_URL}/{route}", headers=headers) expected_code = 200 if expected_access else 401 try: assert int(r.status_code) == int(expected_code) except AssertionError: logger.error( f"The employee {email} got status code {r.status_code} when expecting {expected_code}" ) raise
def try_endpoint_with_user(route, email, expected_access): password = email.split("@")[0] r = requests.post(f"{BASE_URL}/{route}", data={ "email": email, "password": password }) expected_code = 200 if expected_access else 401 try: assert int(r.status_code) == int(expected_code) except AssertionError: logger.error( f"The employee {email} got status code {r.status_code} when expecting {expected_code}" ) raise
def check_method(sql, verb, endpoint): sql_op = get_sql_op(sql) if sql_op in OP_VERBS: correct_verb = OP_VERBS[sql_op] if correct_verb != verb.lower(): # Warn the user about the correct verb to use logger.warn( f"The '{verb.upper()}' HTTP verb is not correct for the SQL {sql_op.upper()} " + f"operation in endpoint {verb.upper()} {endpoint}, the correct verb is {correct_verb.upper()}." ) else: # What has the user put here? logger.error( f"The SQL query '{sql}' in the endpoint {endpoint} is not supported," + " please use only SELECT/INSERT/UPDATE/DELETE.") sys.exit(1)
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.")
import test_employees # Order is important! tests = [ test_summary, test_register, test_login, test_roles, test_banned, test_loggedId, test_departments, test_employees, ] failed_tests = [] for test in tests: try: test.run() except AssertionError: print_exc() logger.error(f"{test.__name__} failed. Proceeding with the next one.") failed_tests.append(test.__name__) if failed_tests: logger.error( f"The following tests failed: {', '.join(failed_tests)}. Exiting with error code." ) exit_code = 0 if not failed_tests else 1 sys.exit(exit_code)