diff --git a/src/xmpp_api/api/routers/v1/bot.py b/src/xmpp_api/api/routers/v1/bot.py index fb6d6e3..bb5ba06 100644 --- a/src/xmpp_api/api/routers/v1/bot.py +++ b/src/xmpp_api/api/routers/v1/bot.py @@ -1,8 +1,8 @@ import uuid -from fastapi import APIRouter, HTTPException, Request, Response +from fastapi import APIRouter, HTTPException, Response import sqlalchemy -from sqlmodel import select +from sqlmodel import select, Session from slixmpp.jid import JID from xmpp_api.api.types.v1.bot import ( @@ -15,15 +15,15 @@ from xmpp_api.api.types.v1.bot import ( SendMessageRequest, BotConstraint, BotDomainConstraint, + SendMessageGrafanaWebhook, ) -from xmpp_api.db import BotDep, SessionDep, UserDep +from xmpp_api.db import BotDep, SessionDep, UserDep, AuthorizationBotDep from xmpp_api.db.bot import AllowedJid, Bot, JIDType import xmpp_api.db.bot as db_bot from xmpp_api.util.token import generate_token from xmpp_api.db import get_bot_by_id, get_jids_by_bot_id, get_jid_by_jid_token from xmpp_api.util.constraints import bot_constraint_to_db, bot_constraint_from_db -from xmpp_api.xmpp.component import XmppApiComponentDep - +from xmpp_api.xmpp.component import XmppApiComponentDep, XmppApiComponent router = APIRouter(prefix="/bot") @@ -157,11 +157,41 @@ def delete_bot_jid_token( def post_bot_message( jid_token: str, message: SendMessageRequest, - request: Request, bot: BotDep, session: SessionDep, component: XmppApiComponentDep, ): + _post_message(jid_token, message.body, bot, session, component) + + +@router.post("/message/{jid_token}/grafana") +def post_bot_message_grafana_webhook( + jid_token: str, + message: SendMessageGrafanaWebhook, + bot: AuthorizationBotDep, + session: SessionDep, + component: XmppApiComponentDep, +): + _post_message(jid_token, message.body, bot, session, component) + + +def _post_message( + jid_token: str, + body: str, + bot: Bot, + session: Session, + component: XmppApiComponent, +): + """ + Performs the message sending. + + Args + :jid_token The recipient's JID token. + :body The body of the message. + :bot The bot to send the message as. + :session The database session. + :component The XMPP component instance. + """ # Is the bot allowed to access this JID? jid = session.exec( select(AllowedJid).where( @@ -184,7 +214,7 @@ def post_bot_message( match jid.type: case JIDType.DIRECT: component.send_direct_message( - body=message.body, + body=body, localpart=bot.localpart, nick=bot.name, recipient=jid.jid, diff --git a/src/xmpp_api/api/types/v1/bot.py b/src/xmpp_api/api/types/v1/bot.py index c4b19e8..035fea9 100644 --- a/src/xmpp_api/api/types/v1/bot.py +++ b/src/xmpp_api/api/types/v1/bot.py @@ -109,6 +109,18 @@ class SendMessageRequest(BaseModel): body: str +class SendMessageGrafanaWebhook(BaseModel): + """ + Model for the request to send a grafana webhook to a JID. + """ + + # The alert title + title: str + + # The alert message + body: str + + class GetBotInformation(BotInformation): """ Response for an information request about a given bot. diff --git a/src/xmpp_api/db/__init__.py b/src/xmpp_api/db/__init__.py index ee7c954..f8fd2e2 100644 --- a/src/xmpp_api/db/__init__.py +++ b/src/xmpp_api/db/__init__.py @@ -1,3 +1,4 @@ +from enum import auto, Enum from typing import Annotated, Generator, Sequence, cast from fastapi import Depends, Request, HTTPException @@ -27,10 +28,35 @@ def get_session(engine: EngineDep) -> Generator[Session]: SessionDep = Annotated[Session, Depends(get_session)] +class TokenType(Enum): + """ + Types of authentication tokens. + """ + + # Token is put into the X-Token header + X_TOKEN = auto() + + # Token is put into the "Authorization" header with type "Bearer". + BEARER = auto() + + def get_by_token( - cls: type[SQLModel], request: Request, session: SessionDep + cls: type[SQLModel], + request: Request, + session: SessionDep, + token_type: TokenType = TokenType.X_TOKEN, ) -> SQLModel: - token = request.headers.get("X-Token") + token: str | None = None + match token_type: + case TokenType.X_TOKEN: + token = request.headers.get("X-Token") + case TokenType.BEARER: + token = request.headers.get("Authorization") + if token is not None: + parts = token.split(" ") + if len(parts) == 2 and parts[0] == "Bearer": + token = parts[1] + if token is None: raise HTTPException( detail="No token provided", @@ -47,19 +73,28 @@ def get_by_token( def get_user(request: Request, session: SessionDep) -> User: - return cast(User, get_by_token(User, request, session)) + return cast( + User, get_by_token(User, request, session, token_type=TokenType.X_TOKEN) + ) UserDep = Annotated[User, Depends(get_user)] def get_bot(request: Request, session: SessionDep) -> Bot: - return cast(Bot, get_by_token(Bot, request, session)) + return cast(Bot, get_by_token(Bot, request, session, token_type=TokenType.X_TOKEN)) BotDep = Annotated[Bot, Depends(get_bot)] +def get_authorization_bot(request: Request, session: SessionDep) -> Bot: + return cast(Bot, get_by_token(Bot, request, session, token_type=TokenType.BEARER)) + + +AuthorizationBotDep = Annotated[Bot, Depends(get_authorization_bot)] + + def get_bot_by_id(bot_id: str, user_id: str, session: SessionDep) -> Bot | None: """ Fetches the specified bot from the database. diff --git a/src/xmpp_api/main.py b/src/xmpp_api/main.py index 302cc52..14a4aae 100644 --- a/src/xmpp_api/main.py +++ b/src/xmpp_api/main.py @@ -33,6 +33,7 @@ def startup(): if config.sentry is not None: logging.getLogger("root").info("Setting up Sentry") import sentry_sdk + sentry_sdk.init( config.sentry.dsn, send_default_pii=True,