예제 #1
0
    def test_no_cookie(self):
        """
        Make sure it works when there is no cookie with the correct name.
        """
        app = AuthenticationMiddleware(
            EchoEndpoint,
            SessionsAuthBackend(allow_unauthenticated=True),
        )
        client = TestClient(app)

        # Test it with no cookie set
        response = client.get("/")
        self.assertEqual(response.status_code, 200)
        self.assertEqual(
            response.json(),
            {"is_unauthenticated_user": True, "is_authenticated": False},
        )
예제 #2
0
    def test_wrong_cookie_value(self):
        """
        Make sure it works when there is a cookie with the correct name, but
        an incorrect value.
        """
        app = AuthenticationMiddleware(
            EchoEndpoint,
            SessionsAuthBackend(allow_unauthenticated=True),
        )
        client = TestClient(app)

        # Test it with a cookie set, but containing an incorrect token.
        response = client.get("/", cookies={"id": "abc123"})
        self.assertEqual(response.status_code, 200)
        self.assertEqual(
            response.json(),
            {"is_unauthenticated_user": True, "is_authenticated": False},
        )
예제 #3
0
    def __init__(
        self,
        *tables: t.Type[Table],
        auth_table: t.Type[BaseUser] = BaseUser,
        session_table: t.Type[SessionsBase] = SessionsBase,
        session_expiry: timedelta = timedelta(hours=1),
        max_session_expiry: timedelta = timedelta(days=7),
        increase_expiry: t.Optional[timedelta] = timedelta(minutes=20),
        page_size: int = 15,
        read_only: bool = False,
        rate_limit_provider: t.Optional[RateLimitProvider] = None,
        production: bool = False,
        site_name: str = "Piccolo Admin",
    ) -> None:
        super().__init__(title=site_name,
                         description="Piccolo API documentation")

        self.auth_table = auth_table
        self.site_name = site_name
        self.tables = tables

        with open(os.path.join(ASSET_PATH, "index.html")) as f:
            self.template = f.read()

        #######################################################################

        api_app = FastAPI()

        for table in tables:
            FastAPIWrapper(
                root_url=f"/tables/{table._meta.tablename}/",
                fastapi_app=api_app,
                piccolo_crud=PiccoloCRUD(table=table,
                                         read_only=read_only,
                                         page_size=page_size),
                fastapi_kwargs=FastAPIKwargs(all_routes={
                    "tags": [f"{table._meta.tablename.capitalize()}"]
                }, ),
            )

        api_app.add_api_route(
            path="/tables/",
            endpoint=self.get_table_list,
            methods=["GET"],
            response_model=t.List[str],
            tags=["Tables"],
        )

        api_app.add_api_route(
            path="/meta/",
            endpoint=self.get_meta,
            methods=["GET"],
            tags=["Meta"],
            response_model=MetaResponseModel,
        )

        api_app.add_api_route(
            path="/user/",
            endpoint=self.get_user,
            methods=["GET"],
            tags=["User"],
            response_model=UserResponseModel,
        )

        #######################################################################

        auth_app = FastAPI()

        if not rate_limit_provider:
            rate_limit_provider = InMemoryLimitProvider(limit=1000,
                                                        timespan=300)

        auth_app.mount(
            path="/login/",
            app=RateLimitingMiddleware(
                app=session_login(
                    auth_table=self.auth_table,
                    session_table=session_table,
                    session_expiry=session_expiry,
                    max_session_expiry=max_session_expiry,
                    redirect_to=None,
                    production=production,
                ),
                provider=rate_limit_provider,
            ),
        )

        auth_app.add_route(
            path="/logout/",
            route=session_logout(session_table=session_table),
            methods=["POST"],
        )

        #######################################################################

        self.router.add_route(path="/",
                              endpoint=self.get_root,
                              methods=["GET"])

        self.mount(
            path="/css",
            app=StaticFiles(directory=os.path.join(ASSET_PATH, "css")),
        )

        self.mount(
            path="/js",
            app=StaticFiles(directory=os.path.join(ASSET_PATH, "js")),
        )

        auth_middleware = partial(
            AuthenticationMiddleware,
            backend=SessionsAuthBackend(
                auth_table=auth_table,
                session_table=session_table,
                admin_only=True,
                increase_expiry=increase_expiry,
            ),
            on_error=handle_auth_exception,
        )

        self.mount(path="/api", app=auth_middleware(api_app))
        self.mount(path="/auth", app=auth_app)
예제 #4
0
    docs_url=None,
    redoc_url=None,
)

# The private app with SessionAuth protected endpoints
private_app = FastAPI(
    routes=[
        Route("/logout/", session_logout(redirect_to="/")),
        # We use a custom Swagger docs endpoint instead of the default FastAPI
        # one, because this one supports CSRF middleware:
        Mount("/docs/", swagger_ui(schema_url="/private/openapi.json")),
    ],
    middleware=[
        Middleware(
            AuthenticationMiddleware,
            backend=SessionsAuthBackend(increase_expiry=datetime.timedelta(
                minutes=30)),
        ),
        # CSRF middleware provides additional protection for older browsers, as
        # we're using cookies.
        Middleware(CSRFMiddleware, allow_form_param=True),
    ],
    # We disable the default Swagger docs, as we have a custom one (see above).
    docs_url=None,
    redoc_url=None,
)

# Mount our private app within the main app.
app.mount("/private/", private_app)

###############################################################################
# Example FastAPI endpoints and Pydantic models.
예제 #5
0
    def __init__(
        self,
        *tables: t.Type[Table],
        auth_table: t.Type[BaseUser] = BaseUser,
        session_table: t.Type[SessionsBase] = SessionsBase,
        session_expiry: timedelta = timedelta(hours=1),
        max_session_expiry: timedelta = timedelta(days=7),
        increase_expiry: t.Optional[timedelta] = timedelta(minutes=20),
        page_size: int = 15,
        read_only: bool = False,
        rate_limit_provider: t.Optional[RateLimitProvider] = None,
        production: bool = False,
    ) -> None:
        self.auth_table = auth_table

        with open(os.path.join(ASSET_PATH, "index.html")) as f:
            self.template = f.read()

        auth_middleware = partial(
            AuthenticationMiddleware,
            backend=SessionsAuthBackend(
                auth_table=auth_table,
                session_table=session_table,
                admin_only=True,
                increase_expiry=increase_expiry,
            ),
            on_error=handle_auth_exception,
        )

        if not rate_limit_provider:
            rate_limit_provider = InMemoryLimitProvider(limit=1000,
                                                        timespan=300)

        table_routes: t.List[BaseRoute] = [
            Mount(
                path=f"/{table._meta.tablename}/",
                app=PiccoloCRUD(table,
                                read_only=read_only,
                                page_size=page_size),
            ) for table in tables
        ]
        table_routes += [
            Route(
                path="/",
                endpoint=self.get_table_list,
                methods=["GET"],
            )
        ]

        routes: t.List[BaseRoute] = [
            Route(path="/", endpoint=self.get_root, methods=["GET"]),
            Mount(
                path="/css",
                app=StaticFiles(directory=os.path.join(ASSET_PATH, "css")),
            ),
            Mount(
                path="/js",
                app=StaticFiles(directory=os.path.join(ASSET_PATH, "js")),
            ),
            Mount(
                path="/api",
                app=Router([
                    Mount(
                        path="/tables/",
                        app=auth_middleware(Router(table_routes)),
                    ),
                    Route(
                        path="/login/",
                        endpoint=RateLimitingMiddleware(
                            app=session_login(
                                auth_table=self.auth_table,
                                session_table=session_table,
                                session_expiry=session_expiry,
                                max_session_expiry=max_session_expiry,
                                redirect_to=None,
                                production=production,
                            ),
                            provider=rate_limit_provider,
                        ),
                        methods=["POST"],
                    ),
                    Route(
                        path="/logout/",
                        endpoint=session_logout(session_table=session_table),
                        methods=["POST"],
                    ),
                    Mount(
                        path="/user/",
                        app=auth_middleware(
                            Router([Route(path="/", endpoint=self.get_user)])),
                    ),
                    Mount(
                        path="/meta/",
                        app=auth_middleware(
                            Router([Route(path="/", endpoint=self.get_meta)])),
                    ),
                ]),
            ),
        ]

        self.tables = tables
        super().__init__(routes)
예제 #6
0
        return PlainTextResponse("top secret")


ROUTER = Router(routes=[
    Route("/", HomeEndpoint, name="home"),
    Route(
        "/login/",
        session_login(),
        name="login",
    ),
    Route("/logout/", session_logout(), name="login"),
    Mount(
        "/secret/",
        AuthenticationMiddleware(
            ProtectedEndpoint,
            SessionsAuthBackend(
                admin_only=True, superuser_only=True, active_only=True),
        ),
    ),
])
APP = ExceptionMiddleware(ROUTER)

###############################################################################


class SessionTestCase(TestCase):
    credentials = {"username": "******", "password": "******"}
    wrong_credentials = {"username": "******", "password": "******"}

    def setUp(self):
        SessionsBase.create_table().run_sync()
        BaseUser.create_table().run_sync()
예제 #7
0
    def __init__(
        self,
        *tables: t.Union[t.Type[Table], TableConfig],
        forms: t.List[FormConfig] = [],
        auth_table: t.Type[BaseUser] = BaseUser,
        session_table: t.Type[SessionsBase] = SessionsBase,
        session_expiry: timedelta = timedelta(hours=1),
        max_session_expiry: timedelta = timedelta(days=7),
        increase_expiry: t.Optional[timedelta] = timedelta(minutes=20),
        page_size: int = 15,
        read_only: bool = False,
        rate_limit_provider: t.Optional[RateLimitProvider] = None,
        production: bool = False,
        site_name: str = "Piccolo Admin",
    ) -> None:
        super().__init__(
            title=site_name, description="Piccolo API documentation"
        )

        #######################################################################
        # Convert any table arguments which are plain ``Table`` classes into
        # ``TableConfig`` instances.

        table_configs: t.List[TableConfig] = []

        for table in tables:
            if isinstance(table, TableConfig):
                table_configs.append(table)
            else:
                table_configs.append(TableConfig(table_class=table))

        self.table_configs = table_configs

        for table_config in table_configs:
            table_class = table_config.table_class
            for column in table_class._meta.columns:
                if column._meta.secret and column._meta.required:
                    message = (
                        f"{table_class._meta.tablename}."
                        f"{column._meta._name} is using `secret` and "
                        f"`required` column args which are incompatible. "
                        f"You may encounter unexpected behavior when using "
                        f"this table within Piccolo Admin."
                    )
                    colored_warning(message, level=Level.high)

        #######################################################################

        self.auth_table = auth_table
        self.site_name = site_name
        self.forms = forms
        self.form_config_map = {form.slug: form for form in self.forms}

        with open(os.path.join(ASSET_PATH, "index.html")) as f:
            self.template = f.read()

        #######################################################################

        api_app = FastAPI(docs_url=None)
        api_app.mount("/docs/", swagger_ui(schema_url="../openapi.json"))

        for table_config in table_configs:
            table_class = table_config.table_class
            visible_column_names = table_config.get_visible_column_names()
            visible_filter_names = table_config.get_visible_filter_names()
            rich_text_columns_names = (
                table_config.get_rich_text_columns_names()
            )
            FastAPIWrapper(
                root_url=f"/tables/{table_class._meta.tablename}/",
                fastapi_app=api_app,
                piccolo_crud=PiccoloCRUD(
                    table=table_class,
                    read_only=read_only,
                    page_size=page_size,
                    schema_extra={
                        "visible_column_names": visible_column_names,
                        "visible_filter_names": visible_filter_names,
                        "rich_text_columns": rich_text_columns_names,
                    },
                ),
                fastapi_kwargs=FastAPIKwargs(
                    all_routes={
                        "tags": [f"{table_class._meta.tablename.capitalize()}"]
                    },
                ),
            )

        api_app.add_api_route(
            path="/tables/",
            endpoint=self.get_table_list,  # type: ignore
            methods=["GET"],
            response_model=t.List[str],
            tags=["Tables"],
        )

        api_app.add_api_route(
            path="/meta/",
            endpoint=self.get_meta,  # type: ignore
            methods=["GET"],
            tags=["Meta"],
            response_model=MetaResponseModel,
        )

        api_app.add_api_route(
            path="/forms/",
            endpoint=self.get_forms,  # type: ignore
            methods=["GET"],
            tags=["Forms"],
            response_model=t.List[FormConfigResponseModel],
        )

        api_app.add_api_route(
            path="/forms/{form_slug:str}/",
            endpoint=self.get_single_form,  # type: ignore
            methods=["GET"],
            tags=["Forms"],
        )

        api_app.add_api_route(
            path="/forms/{form_slug:str}/schema/",
            endpoint=self.get_single_form_schema,  # type: ignore
            methods=["GET"],
            tags=["Forms"],
        )

        api_app.add_api_route(
            path="/forms/{form_slug:str}/",
            endpoint=self.post_single_form,  # type: ignore
            methods=["POST"],
            tags=["Forms"],
        )

        api_app.add_api_route(
            path="/user/",
            endpoint=self.get_user,  # type: ignore
            methods=["GET"],
            tags=["User"],
            response_model=UserResponseModel,
        )

        #######################################################################

        auth_app = FastAPI()

        if not rate_limit_provider:
            rate_limit_provider = InMemoryLimitProvider(
                limit=1000, timespan=300
            )

        auth_app.mount(
            path="/login/",
            app=RateLimitingMiddleware(
                app=session_login(
                    auth_table=self.auth_table,
                    session_table=session_table,
                    session_expiry=session_expiry,
                    max_session_expiry=max_session_expiry,
                    redirect_to=None,
                    production=production,
                ),
                provider=rate_limit_provider,
            ),
        )

        auth_app.add_route(
            path="/logout/",
            route=session_logout(session_table=session_table),
            methods=["POST"],
        )

        #######################################################################

        self.router.add_route(
            path="/", endpoint=self.get_root, methods=["GET"]
        )

        self.mount(
            path="/css",
            app=StaticFiles(directory=os.path.join(ASSET_PATH, "css")),
        )

        self.mount(
            path="/js",
            app=StaticFiles(directory=os.path.join(ASSET_PATH, "js")),
        )

        auth_middleware = partial(
            AuthenticationMiddleware,
            backend=SessionsAuthBackend(
                auth_table=auth_table,
                session_table=session_table,
                admin_only=True,
                increase_expiry=increase_expiry,
            ),
            on_error=handle_auth_exception,
        )

        self.mount(path="/api", app=auth_middleware(api_app))
        self.mount(path="/auth", app=auth_app)

        # We make the meta endpoint available without auth, because it contains
        # the site name.
        self.add_api_route("/meta/", endpoint=self.get_meta)  # type: ignore
예제 #8
0
def on_auth_error(request: Request, exc: Exception):
    return RedirectResponse("/login/")


private_app = Starlette(
    routes=[
        Route(
            "/change-password/",
            change_password(),
        ),
    ],
    middleware=[
        Middleware(
            AuthenticationMiddleware,
            on_error=on_auth_error,
            backend=SessionsAuthBackend(admin_only=False),
        ),
    ],
)

app = Starlette(
    routes=[
        Route("/", HomeEndpoint),
        Route("/login/", session_login()),
        Route(
            "/register/",
            register(redirect_to="/login/", user_defaults={"active": True}),
        ),
        Mount("/private/", private_app),
    ],
    middleware=[