Example #1
0
    def configure(
        self,
        template: str = "github",
        *,
        oidc_clients: Optional[List[OIDCClient]] = None,
        **settings: str,
    ) -> None:
        """Change the test application configuration.

        This cannot be used to change the database URL because the internal
        session is not recreated.

        Parameters
        ----------
        template : `str`
            Settings template to use.
        oidc_clients : List[`gafaelfawr.config.OIDCClient`] or `None`
            Configuration information for clients of the OpenID Connect server.
        **settings : str
            Any additional settings to add to the settings file.
        """
        settings_path = build_settings(
            self.tmp_path,
            template,
            oidc_clients,
            **settings,
        )
        config_dependency.set_settings_path(str(settings_path))
        self.config = config_dependency()
Example #2
0
def initialize(tmp_path: Path) -> Config:
    """Do basic initialization and return a configuration.

    This shared logic can be used either with `SetupTest`, which assumes an
    ASGI application and an async test, or with non-async tests such as the
    tests of the command-line interface.

    Parameters
    ----------
    tmp_path : `pathlib.Path`
        The path for temporary files.

    Returns
    -------
    config : `gafaelfawr.config.Config`
        The generated config, using the same defaults as `SetupTest`.
    """
    settings_path = build_settings(tmp_path, "github")
    config_dependency.set_settings_path(str(settings_path))
    config = config_dependency()
    if not os.environ.get("REDIS_6379_TCP_PORT"):
        redis_dependency.is_mocked = True

    # Initialize the database.  Non-SQLite databases need to be reset between
    # tests.
    should_reset = not urlparse(config.database_url).scheme == "sqlite"
    initialize_database(config, reset=should_reset)

    return config
Example #3
0
def test_database_password(tmp_path: Path) -> None:
    settings_path = build_settings(
        tmp_path,
        "github",
        database_url="postgresql://gafaelfawr@localhost/gafaelfawr",
    )

    os.environ["GAFAELFAWR_DATABASE_PASSWORD"] = "******"
    config_dependency.set_settings_path(str(settings_path))
    config = config_dependency()
    del os.environ["GAFAELFAWR_DATABASE_PASSWORD"]

    expected = "postgresql://*****:*****@localhost/gafaelfawr"
    assert config.database_url == expected
Example #4
0
async def startup_event() -> None:
    config = config_dependency()
    engine_args = {}
    if urlparse(config.database_url).scheme == "sqlite":
        engine_args["connect_args"] = {"check_same_thread": False}
    app.add_middleware(
        DBSessionMiddleware,
        db_url=config.database_url,
        engine_args=engine_args,
    )
    app.add_middleware(XForwardedMiddleware, proxies=config.proxies)
    app.add_middleware(StateMiddleware,
                       cookie_name=COOKIE_NAME,
                       state_class=State)
Example #5
0
async def test_redis_password(tmp_path: Path) -> None:
    redis_password_file = store_secret(tmp_path, "redis", b"some-password")
    settings_path = build_settings(
        tmp_path, "github", redis_password_file=str(redis_password_file))
    config_dependency.set_settings_path(str(settings_path))

    function = "gafaelfawr.dependencies.redis.create_redis_pool"
    with patch(function) as mock_create:
        redis_dependency.is_mocked = False
        await redis_dependency(config_dependency())
        assert mock_create.call_args_list == [
            call("redis://localhost:6379/0", password="******")
        ]
        redis_dependency.redis = None
Example #6
0
async def update_service_tokens() -> None:
    """Update service tokens stored in Kubernetes secrets."""
    config = config_dependency()
    logger = structlog.get_logger(config.safir.logger_name)
    if not config.kubernetes:
        logger.info("No Kubernetes secrets configured")
        sys.exit(0)
    async with ComponentFactory.standalone() as factory:
        kubernetes_service = factory.create_kubernetes_service()
        try:
            await kubernetes_service.update_service_secrets()
        except KubernetesError as e:
            msg = "Failed to update service token secrets"
            logger.error(msg, error=str(e))
            sys.exit(1)
Example #7
0
def get_logger(request: Request) -> BoundLogger:
    """Return a logger bound to a request.

    This is a convenience function that can be used where a dependency isn't
    available, such as in middleware.

    Parameters
    ----------
    request : `fastapi.Request`
        The request to which to bind the logger.

    Returns
    -------
    logger : `structlog.BoundLogger`
         The bound logger.
    """
    config = config_dependency()
    return logger_dependency(request, config)
Example #8
0
def run_app(tmp_path: Path, settings_path: Path) -> Iterator[SeleniumConfig]:
    """Run the application as a separate process for Selenium access.

    Parameters
    ----------
    tmp_path : `pathlib.Path`
        The temporary directory for testing.
    settings_path : `pathlib.Path`
        The path to the settings file.
    """
    config_dependency.set_settings_path(str(settings_path))
    config = config_dependency()
    initialize_database(config)

    token_path = tmp_path / "token"
    app_source = APP_TEMPLATE.format(
        settings_path=str(settings_path),
        token_path=str(token_path),
    )
    app_path = tmp_path / "testing.py"
    with app_path.open("w") as f:
        f.write(app_source)

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(("127.0.0.1", 0))
    port = s.getsockname()[1]

    cmd = ["uvicorn", "--fd", "0", "testing:app"]
    logging.info("Starting server with command %s", " ".join(cmd))
    p = subprocess.Popen(cmd, cwd=str(tmp_path), stdin=s.fileno())
    s.close()

    logging.info("Waiting for server to start")
    _wait_for_server(port)

    try:
        selenium_config = SeleniumConfig(
            token=Token.from_str(token_path.read_text()),
            url=f"http://localhost:{port}",
        )
        yield selenium_config
    finally:
        p.terminate()
Example #9
0
    def as_cookie(self) -> str:
        """Build an encrypted cookie representation of the state.

        Returns
        -------
        cookie : `str`
            The encrypted cookie value.
        """
        data = {}
        if self.csrf:
            data["csrf"] = self.csrf
        if self.token:
            data["token"] = str(self.token)
        if self.return_url:
            data["return_url"] = self.return_url
        if self.state:
            data["state"] = self.state

        key = config_dependency().session_secret.encode()
        fernet = Fernet(key)
        return fernet.encrypt(json.dumps(data).encode()).decode()
Example #10
0
    def from_cookie(cls, cookie: str, request: Optional[Request]) -> State:
        """Reconstruct state from an encrypted cookie.

        Parameters
        ----------
        cookie : `str`
            The encrypted cookie value.
        key : `bytes`
            The `~cryptography.fernet.Fernet` key used to decrypt it.
        request : `fastapi.Request` or `None`
            The request, used for logging.  If not provided (primarily for the
            test suite), invalid state cookies will not be logged.

        Returns
        -------
        state : `State`
            The state represented by the cookie.
        """
        key = config_dependency().session_secret.encode()
        fernet = Fernet(key)
        try:
            data = json.loads(fernet.decrypt(cookie.encode()).decode())
            token = None
            if "token" in data:
                token = Token.from_str(data["token"])
        except Exception as e:
            if request:
                logger = get_logger(request)
                logger.warning("Discarding invalid state cookie", error=str(e))
            return cls()

        return cls(
            csrf=data.get("csrf"),
            token=token,
            return_url=data.get("return_url"),
            state=data.get("state"),
        )
Example #11
0
    async def standalone(cls) -> AsyncIterator[ComponentFactory]:
        """Build Gafaelfawr components outside of a request.

        Intended for background jobs.  Uses the non-request default values for
        the dependencies of `ComponentFactory`.  Do not use this factory
        inside the web application or anywhere that may use the default
        `ComponentFactory`, since they will interfere with each other's
        Redis pools.

        Notes
        -----
        This creates a database session directly because fastapi_sqlalchemy
        does not work unless an ASGI application has initialized it.

        Yields
        ------
        factory : `ComponentFactory`
            The factory.  Must be used as a context manager.
        """
        config = config_dependency()
        redis = await redis_dependency(config)
        logger = structlog.get_logger(config.safir.logger_name)
        assert logger
        session = create_session(config, logger)
        try:
            async with AsyncClient() as client:
                yield cls(
                    config=config,
                    redis=redis,
                    session=session,
                    http_client=client,
                    logger=logger,
                )
        finally:
            await redis_dependency.close()
            session.close()
Example #12
0
def init(settings: str) -> None:
    """Initialize the database storage."""
    config_dependency.set_settings_path(settings)
    config = config_dependency()
    initialize_database(config)