Example #1
0
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}")
Example #2
0
def run():
    print("Testing $loggedId...")

    logger.warning("Testing free access")
    try_normal()

    logger.warning("Testing with role restriction")
    try_with_restriction()
Example #3
0
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()
Example #4
0
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()
Example #5
0
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()
Example #8
0
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.")
Example #9
0
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()
Example #10
0
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
    })
Example #11
0
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)
Example #12
0
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()