diff --git a/backend/requirements.txt b/backend/requirements.txt index 6852be61..59f7ed0c 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -10,6 +10,7 @@ user-agents==2.2.0 boto3==1.28.39 autodynatrace==2.0.0 PyJWT==2.8.0 +cryptography==42.0.8 # ML basegun-ml==1.0.1 # Dev diff --git a/backend/src/config.py b/backend/src/config.py index 41b86c2b..7eb8e0e1 100644 --- a/backend/src/config.py +++ b/backend/src/config.py @@ -3,7 +3,9 @@ from smtplib import SMTP import boto3 +from fastapi.security import OpenIdConnect from gelfformatter import GelfFormatter +from jwt import PyJWKClient CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -124,3 +126,10 @@ def get_base_logs(user_agent, user_id: str) -> dict: # Emails SMTPClient = SMTP(os.environ["EMAIL_HOST"], os.environ["EMAIL_PORT"]) + +# Authentication +KID = os.environ["KID"] +JWKS_CLIENT = PyJWKClient(os.environ["JWKS_URL"]) +PUBLIC_KEY = JWKS_CLIENT.get_signing_key(KID).key +OPENIDCONNECT_URL = os.environ["OPENIDCONNECT_URL"] +OAUTH2_SCHEME = OpenIdConnect(openIdConnectUrl=os.environ["OPENIDCONNECT_URL"]) diff --git a/backend/src/models.py b/backend/src/models.py new file mode 100644 index 00000000..c3dc6cb5 --- /dev/null +++ b/backend/src/models.py @@ -0,0 +1,20 @@ +from pydantic import BaseModel + + +class EmailData(BaseModel): + firstname: str + lastname: str + nigend: str + service: str | None + phone: str + email: str + seizure: str + una_or_procedure_number: str + gun_type: str + gun_length: int | None + gun_barrel_length: int | None + markings_description: str | None + right_picture: str + left_picture: str + markings_pictures: str + magazine_picture: str | None diff --git a/backend/src/router.py b/backend/src/router.py index e87eb7d6..be8f05ee 100644 --- a/backend/src/router.py +++ b/backend/src/router.py @@ -1,7 +1,7 @@ import logging import os import time -from typing import Union +from typing import Annotated, Union from uuid import uuid4 from basegun_ml.classification import get_typology @@ -10,6 +10,7 @@ APIRouter, BackgroundTasks, Cookie, + Depends, File, Form, HTTPException, @@ -21,7 +22,8 @@ from user_agents import parse from .config import APP_VERSION, S3_PREFIX, TYPOLOGIES_MEASURED, get_base_logs -from .utils import upload_image +from .models import EmailData +from .utils import get_current_user, send_mail, upload_image router = APIRouter(prefix="/api") @@ -163,3 +165,33 @@ async def log_identification_dummy( extras_logging["bg_" + key] = res[key] logging.info("Identification dummy", extra=extras_logging) + + +# Currently missing because we don't know if we can send attachements or if target can use S3 link +# Photo face droite : {request.right_picture} +# Photo face gauche : {request.left_picture} +# Photo des marquages : {request.markings_pictures} +# Photo du chargeur : {request.magazine_picture} +@router.post("/expert-contact") +async def expert_contact( + request: EmailData, + current_user: Annotated[dict, Depends(get_current_user)], +): + send_mail( + subject="[Basegun] Demande d'identification", + to="db.dcpc.ircgn@gendarmerie.interieur.gouv.fr", + message=f""" + Nom : {request.lastname} + Prénom : {request.firstname} + NIGEND / matricule : {request.nigend} + Service d'affectation : {request.service} + Téléphone : {request.phone} + Email : {request.email} + Saisie : {request.seizure} + N° de procédure : {request.una_or_procedure_number} + Typologie de l'arme (épaule ou poing) : {request.gun_type} + Longueur de l'arme : {request.gun_length} + Longueur du canon de l'arme : {request.gun_barrel_length} + Précision sur les marquages présents sur l'arme : {request.markings_description} + """, + ) diff --git a/backend/src/utils.py b/backend/src/utils.py index f69f5802..aace7e6a 100644 --- a/backend/src/utils.py +++ b/backend/src/utils.py @@ -2,10 +2,13 @@ import time from datetime import datetime from email.message import EmailMessage +from typing import Annotated +import jwt +from fastapi import Depends, HTTPException, status from src.config import SMTPClient -from .config import S3, S3_BUCKET_NAME +from .config import OAUTH2_SCHEME, PUBLIC_KEY, S3, S3_BUCKET_NAME def upload_image(content: bytes, image_key: str): @@ -33,3 +36,20 @@ def send_mail(subject: str, to: str, message: str): msg["To"] = to msg.set_content(message) SMTPClient.send_message(msg) + + +async def get_current_user(token: Annotated[str, Depends(OAUTH2_SCHEME)]): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + return jwt.decode( + token.split()[1], + PUBLIC_KEY, + algorithms=["RS256"], + audience=["master-realm", "account"], + ) + except jwt.InvalidTokenError: + raise credentials_exception diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 31430a79..14f64198 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -1,7 +1,6 @@ import json import os import time -from io import BytesIO import boto3 import pytest @@ -119,6 +118,7 @@ def test_headers(self): for header_to_add in HEADERS_TO_ADD: assert header_to_add["name"].lower() in CURRENT_HEADERS + class TestUpload: def test_revolver_without_card(self): with open("./tests/revolver.jpg", "rb") as f: @@ -150,4 +150,4 @@ def test_semi_auto_without_card(self): assert response.data["confidence_level"] == "high" assert response.data["gun_length"] is not None assert response.data["gun_barrel_length"] is not None - assert response.data["conf_card"] is not None \ No newline at end of file + assert response.data["conf_card"] is not None diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py new file mode 100644 index 00000000..68f06700 --- /dev/null +++ b/backend/tests/test_auth.py @@ -0,0 +1,22 @@ +import jwt +import pytest +from fastapi.testclient import TestClient +from src.config import PUBLIC_KEY +from src.main import app + +client = TestClient(app) + +token = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItWmVZS3ZiMFEwdmJyZ2tEc2I5Rk5YLTU3QkdEMjNxRWhnUE1kdERHNUY4In0.eyJleHAiOjE3MjE3Mzc5ODksImlhdCI6MTcyMTczNzA4OSwiYXV0aF90aW1lIjoxNzIxNzM2ODk4LCJqdGkiOiI0NTkxMzI5Zi02YjIzLTQxYmMtYjU4Yy03ZmM3NjFhYjIzMzgiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvcmVhbG1zL21hc3RlciIsImF1ZCI6WyJtYXN0ZXItcmVhbG0iLCJhY2NvdW50Il0sInN1YiI6IjQ2YTUzMDM1LWExZGMtNDExOS1hZmYwLTM0NDY5OTJkMzFiOSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImJhc2VndW4iLCJzaWQiOiI5M2RlZjg3My1lZTIxLTRhY2YtOTI4Ny03N2UwNzA3OTcxODEiLCJhY3IiOiIwIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiY3JlYXRlLXJlYWxtIiwiZGVmYXVsdC1yb2xlcy1tYXN0ZXIiLCJvZmZsaW5lX2FjY2VzcyIsImFkbWluIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJtYXN0ZXItcmVhbG0iOnsicm9sZXMiOlsidmlldy1pZGVudGl0eS1wcm92aWRlcnMiLCJ2aWV3LXJlYWxtIiwibWFuYWdlLWlkZW50aXR5LXByb3ZpZGVycyIsImltcGVyc29uYXRpb24iLCJjcmVhdGUtY2xpZW50IiwibWFuYWdlLXVzZXJzIiwicXVlcnktcmVhbG1zIiwidmlldy1hdXRob3JpemF0aW9uIiwicXVlcnktY2xpZW50cyIsInF1ZXJ5LXVzZXJzIiwibWFuYWdlLWV2ZW50cyIsIm1hbmFnZS1yZWFsbSIsInZpZXctZXZlbnRzIiwidmlldy11c2VycyIsInZpZXctY2xpZW50cyIsIm1hbmFnZS1hdXRob3JpemF0aW9uIiwibWFuYWdlLWNsaWVudHMiLCJxdWVyeS1ncm91cHMiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWRtaW4ifQ.hU0hJgMQayorct84UK9jiXDWOoBZ2KgmGa-JK0OOvn5Dfq0i_uZEOwzSjNwsvsYjX5NtHRrVBIxIcYzD-6IubUB2eUk7dgbYTqcVyjkWFrjwuv6ieTYvk-OuUg5iCXSe67ZbFuQwvNCg_8ns3JhEAEgHD4mLmhkPDlln4FkK605vAGZ5bDDYuPbaBI3ao4zpFr837r8zP5BGnzsclzk-T9k03pbTZ2aIv3PqlhfBVl2rKM0KYYKL8n3zLvObFMnZSx22-AuTPKKxkv3IrpCX2Zr_pAv-Bb2dw9LfEy_jxKv1i175Awjy3ayLbltvMcRzDTEzZ7YdBiuKoGoYjdoHng" + + +@pytest.mark.skip(reason="Cannot currently run in CI.") +class TestAuthentication: + def test_jwks(self): + print( + jwt.decode( + token, + PUBLIC_KEY, + algorithms=["RS256"], + audience=["master-realm", "account"], + ) + ) diff --git a/docker-compose.yml b/docker-compose.yml index fa309953..837cee8d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,9 @@ services: - AWS_SECRET_ACCESS_KEY=minioadmin - EMAIL_HOST=mailpit - EMAIL_PORT=1025 + - JWKS_URL=http://keycloak:8080/realms/master/protocol/openid-connect/certs + - OPENIDCONNECT_URL=http://localhost:8080/realms/master/.well-known/openid-configuration + - KID=-ZeYKvb0Q0vbrgkDsb9FNX-57BGD23qEhgPMdtDG5F8 - http_proxy - https_proxy - no_proxy diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 3cfdda89..ebd4ffe7 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -208,7 +208,16 @@ const routes: RouteRecordRaw[] = [ beforeEnter: (to, from) => { mgr.getUser().then((user) => { console.log(user); - if (user === null) mgr.signinRedirect(); + if (user === null) + mgr + .signinRedirect() + .then((data) => console.log(data)) + .catch((err) => { + console.log(err); + return { + name: "PageNotFound", + }; + }); }); }, },