def init_rate_limiter(app: FastAPI, settings: pydantic.BaseSettings) -> None: if getattr(settings, "RATE_LIMIT", None): provider = InMemoryLimitProvider( limit=getattr(settings, "RATE_LIMIT"), timespan=getattr(settings, "RATE_LIMIT_TIME_SPAN"), block_duration=getattr(settings, "RATE_LIMIT_BLOCK_DURATION")) app.add_middleware(RateLimitingMiddleware, provider=provider)
def test_limit(self): """ Make sure a request is rejected if the client has exceeded the limit. """ app = RateLimitingMiddleware( Router([Route("/", Endpoint)]), InMemoryLimitProvider(limit=5, timespan=1, block_duration=1), ) client = TestClient(app) successful = 0 for i in range(20): response = client.get("/") if response.status_code == 429: break else: successful += 1 self.assertTrue(successful == 5) # After the 'block_duration' has expired, requests should be allowed # again. sleep(1.1) response = client.get("/") self.assertTrue(response.status_code == 200)
def test_memory_usage(self): """ Make sure the memory used doesn't continue to increase over time (it should reset regularly at intervals of 'timespan' seconds). """ provider = InMemoryLimitProvider(limit=10, timespan=1, block_duration=1) for i in range(100): provider.increment(str(i)) self.assertTrue(len(provider.request_dict.keys()) == 100) sleep(1) # This should cause a reset, as the timespan has elapsed: provider.increment("1234") self.assertTrue(len(provider.request_dict.keys()) == 1)
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)
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)
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