Skip to content

Commit 0657830

Browse files
authored
added optional global AppAPIAuth middleware (#205)
For some applications it will be useful to simply set global authentication to all endpoints and not write in each endpoint: New: ```python3 APP = FastAPI(lifespan=lifespan) APP.add_middleware(ex_app.AppAPIAuthMiddleware) ``` Old: ```python3 @APP.post("/verify_initial_value") async def verify_initial_value( _nc: typing.Annotated[NextcloudApp, Depends(nc_app)], # <---- this is no needed when using new Global Middleware input1: Button1Format, ): print("Old value: ", input1.initial_value) return responses.JSONResponse(content={"initial_value": str(random.randint(0, 100))}, status_code=200) ``` Limitations: `Talk bots` endpoints using another Auth type, and they should be added to `disable_for` parameter of `AppAPIAuthMiddleware`. --------- Signed-off-by: Alexander Piskun <bigcat88@icloud.com>
1 parent 1061622 commit 0657830

File tree

9 files changed

+82
-12
lines changed

9 files changed

+82
-12
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [0.8.1 - 2024-0x-xx]
6+
7+
### Added
8+
9+
- NextcloudApp: `AppAPIAuthMiddleware` for easy cover all endpoints. #205
10+
511
## [0.8.0 - 2024-01-12]
612

713
### Added

docs/NextcloudApp.rst

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,4 +223,29 @@ and since this is not directly related to working with NextCloud, we will skip t
223223

224224
**ToGif** example `full source <https://github.com/cloud-py-api/nc_py_api/blob/main/examples/as_app/to_gif/lib/main.py>`_ code.
225225

226+
Using AppAPIAuthMiddleware
227+
--------------------------
228+
229+
If your application does not implement `Talk Bot` functionality and you most often do not need
230+
the ``NextcloudApp`` class returned after standard authentication with `Depends`:
231+
232+
.. code-block:: python
233+
234+
nc: Annotated[NextcloudApp, Depends(nc_app)]
235+
236+
In this case, you can use global authentication. It's quite simple, just add this line of code:
237+
238+
.. code-block:: python
239+
240+
from nc_py_api.ex_app import AppAPIAuthMiddleware
241+
242+
APP = FastAPI(lifespan=lifespan)
243+
APP.add_middleware(AppAPIAuthMiddleware)
244+
245+
and it will be called for all your endpoints and check the validity of the connection itself.
246+
247+
``AppAPIAuthMiddleware`` supports **disable_for** optional argument, where you can list all routes for which authentication should be skipped.
248+
249+
You can still use at the same time the *AppAPIAuthMiddleware* and *Depends(nc_app)*, it is clever enough and they won't interfere with each other.
250+
226251
This chapter ends here, but the next topics are even more intriguing.

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
nitpicky = True
6767
nitpick_ignore_regex = [
6868
(r"py:class", r"starlette\.requests\.Request"),
69+
(r"py:class", r"starlette\.requests\.HTTPConnection"),
6970
(r"py:.*", r"httpx.*"),
7071
]
7172

nc_py_api/_session.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
from json import loads
1212
from os import environ
1313

14-
from fastapi import Request as FastAPIRequest
1514
from httpx import AsyncClient, Client, Headers, Limits, ReadTimeout, Request, Response
15+
from starlette.requests import HTTPConnection
1616

1717
from . import options
1818
from ._exceptions import (
@@ -459,7 +459,7 @@ def __init__(self, **kwargs):
459459
self.cfg = AppConfig(**kwargs)
460460
super().__init__(**kwargs)
461461

462-
def sign_check(self, request: FastAPIRequest) -> None:
462+
def sign_check(self, request: HTTPConnection) -> None:
463463
headers = {
464464
"AA-VERSION": request.headers.get("AA-VERSION", ""),
465465
"EX-APP-ID": request.headers.get("EX-APP-ID", ""),

nc_py_api/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Version of nc_py_api."""
22

3-
__version__ = "0.8.0"
3+
__version__ = "0.8.1.dev0"

nc_py_api/ex_app/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from .defs import ApiScope, LogLvl
44
from .integration_fastapi import (
5+
AppAPIAuthMiddleware,
56
anc_app,
67
atalk_bot_app,
78
nc_app,

nc_py_api/ex_app/integration_fastapi.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,20 @@
1212
Depends,
1313
FastAPI,
1414
HTTPException,
15-
Request,
1615
responses,
1716
staticfiles,
1817
status,
1918
)
19+
from starlette.requests import HTTPConnection, Request
20+
from starlette.types import ASGIApp, Receive, Scope, Send
2021

2122
from .._misc import get_username_secret_from_headers
2223
from ..nextcloud import AsyncNextcloudApp, NextcloudApp
2324
from ..talk_bot import TalkBotMessage, aget_bot_secret, get_bot_secret
2425
from .misc import persistent_storage
2526

2627

27-
def nc_app(request: Request) -> NextcloudApp:
28+
def nc_app(request: HTTPConnection) -> NextcloudApp:
2829
"""Authentication handler for requests from Nextcloud to the application."""
2930
user = get_username_secret_from_headers(
3031
{"AUTHORIZATION-APP-API": request.headers.get("AUTHORIZATION-APP-API", "")}
@@ -36,7 +37,7 @@ def nc_app(request: Request) -> NextcloudApp:
3637
return nextcloud_app
3738

3839

39-
def anc_app(request: Request) -> AsyncNextcloudApp:
40+
def anc_app(request: HTTPConnection) -> AsyncNextcloudApp:
4041
"""Async Authentication handler for requests from Nextcloud to the application."""
4142
user = get_username_secret_from_headers(
4243
{"AUTHORIZATION-APP-API": request.headers.get("AUTHORIZATION-APP-API", "")}
@@ -194,3 +195,40 @@ def display(self, msg=None, pos=None):
194195
cache = models[model].pop("cache_dir", persistent_storage())
195196
snapshot_download(model, tqdm_class=TqdmProgress, **models[model], max_workers=workers, cache_dir=cache)
196197
nc.set_init_status(100)
198+
199+
200+
class AppAPIAuthMiddleware:
201+
"""Pure ASGI AppAPIAuth Middleware."""
202+
203+
_disable_for: list[str]
204+
205+
def __init__(
206+
self,
207+
app: ASGIApp,
208+
disable_for: list[str] | None = None,
209+
) -> None:
210+
self.app = app
211+
disable_for = [] if disable_for is None else [i.lstrip("/") for i in disable_for]
212+
self._disable_for = [i for i in disable_for if i != "heartbeat"] + ["heartbeat"]
213+
214+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
215+
"""Method that will be called by Starlette for each event."""
216+
if scope["type"] != "http":
217+
await self.app(scope, receive, send)
218+
return
219+
220+
conn = HTTPConnection(scope)
221+
url_path = conn.url.path.lstrip("/")
222+
if url_path not in self._disable_for:
223+
try:
224+
anc_app(conn)
225+
except HTTPException as exc:
226+
response = self._on_error(exc.status_code, exc.detail)
227+
await response(scope, receive, send)
228+
return
229+
230+
await self.app(scope, receive, send)
231+
232+
@staticmethod
233+
def _on_error(status_code: int = 400, content: str = "") -> responses.PlainTextResponse:
234+
return responses.PlainTextResponse(content, status_code=status_code)

nc_py_api/nextcloud.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
import typing
44
from abc import ABC
55

6-
from fastapi import Request as FastAPIRequest
76
from httpx import Headers
7+
from starlette.requests import HTTPConnection
88

99
from ._exceptions import NextcloudExceptionNotFound
1010
from ._misc import check_capabilities, require_capabilities
@@ -395,7 +395,7 @@ def unregister_talk_bot(self, callback_url: str) -> bool:
395395
return False
396396
return True
397397

398-
def request_sign_check(self, request: FastAPIRequest) -> bool:
398+
def request_sign_check(self, request: HTTPConnection) -> bool:
399399
"""Verifies the signature and validity of an incoming request from the Nextcloud.
400400
401401
:param request: The `Starlette request <https://www.starlette.io/requests/>`_
@@ -535,7 +535,7 @@ async def unregister_talk_bot(self, callback_url: str) -> bool:
535535
return False
536536
return True
537537

538-
def request_sign_check(self, request: FastAPIRequest) -> bool:
538+
def request_sign_check(self, request: HTTPConnection) -> bool:
539539
"""Verifies the signature and validity of an incoming request from the Nextcloud.
540540
541541
:param request: The `Starlette request <https://www.starlette.io/requests/>`_

tests/_install_async.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from contextlib import asynccontextmanager
2-
from typing import Annotated
32

4-
from fastapi import Depends, FastAPI
3+
from fastapi import FastAPI
54
from fastapi.responses import JSONResponse
65

76
from nc_py_api import AsyncNextcloudApp, ex_app
@@ -14,12 +13,12 @@ async def lifespan(_app: FastAPI):
1413

1514

1615
APP = FastAPI(lifespan=lifespan)
16+
APP.add_middleware(ex_app.AppAPIAuthMiddleware)
1717

1818

1919
@APP.put("/sec_check")
2020
async def sec_check(
2121
value: int,
22-
_nc: Annotated[AsyncNextcloudApp, Depends(ex_app.anc_app)],
2322
):
2423
print(value, flush=True)
2524
return JSONResponse(content={"error": ""}, status_code=200)

0 commit comments

Comments
 (0)