From 5e413b11e06a5df804ec3aec620afc2b6bef4ab6 Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Wed, 4 Mar 2026 23:36:17 +0100 Subject: [PATCH] add permission check for form productor and product --- backend/src/auth/auth.py | 67 +++++++++++-------- backend/src/forms/forms.py | 23 ++++++- backend/src/forms/service.py | 17 ++++- backend/src/productors/service.py | 16 +++++ backend/src/products/service.py | 29 ++++++++ backend/src/users/service.py | 26 ++++--- backend/src/users/users.py | 37 ++++++---- .../src/components/Productors/Modal/index.tsx | 8 +-- 8 files changed, 164 insertions(+), 59 deletions(-) diff --git a/backend/src/auth/auth.py b/backend/src/auth/auth.py index 02d90e4..3015828 100644 --- a/backend/src/auth/auth.py +++ b/backend/src/auth/auth.py @@ -1,20 +1,21 @@ -from typing import Annotated -from fastapi import APIRouter, Security, HTTPException, Depends, Request, Cookie -from fastapi.responses import RedirectResponse, Response -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials -from sqlmodel import Session, select -import jwt -from jwt import PyJWKClient - -from src.settings import AUTH_URL, TOKEN_URL, JWKS_URL, ISSUER, LOGOUT_URL, settings -import src.users.service as service -from src.database import get_session -from src.models import UserCreate, User, UserPublic - import secrets -import requests +from typing import Annotated from urllib.parse import urlencode + +import jwt +import requests import src.messages as messages +import src.users.service as service +from fastapi import (APIRouter, Cookie, Depends, HTTPException, Request, + Security) +from fastapi.responses import RedirectResponse, Response +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer +from jwt import PyJWKClient +from sqlmodel import Session, select +from src.database import get_session +from src.models import User, UserCreate, UserPublic +from src.settings import (AUTH_URL, ISSUER, JWKS_URL, LOGOUT_URL, TOKEN_URL, + settings) router = APIRouter(prefix='/auth') @@ -98,7 +99,7 @@ def callback(code: str, session: Session = Depends(get_session)): 'client_secret': settings.keycloak_client_secret, 'refresh_token': token_data['refresh_token'], } - res = requests.post(LOGOUT_URL, data=data) + requests.post(LOGOUT_URL, data=data) resp = RedirectResponse(f'{settings.origins}?userNotAllowed=true') return resp roles = resource_access.get(settings.keycloak_client_id) @@ -108,7 +109,7 @@ def callback(code: str, session: Session = Depends(get_session)): 'client_secret': settings.keycloak_client_secret, 'refresh_token': token_data['refresh_token'], } - res = requests.post(LOGOUT_URL, data=data) + requests.post(LOGOUT_URL, data=data) resp = RedirectResponse(f'{settings.origins}?userNotAllowed=true') return resp @@ -160,12 +161,15 @@ def verify_token(token: str): ) return decoded except jwt.ExpiredSignatureError: - raise HTTPException(status_code=401, - detail=messages.Messages.tokenexipired) + raise HTTPException( + status_code=401, + detail=messages.Messages.tokenexipired + ) except jwt.InvalidTokenError: raise HTTPException( status_code=401, - detail=messages.Messages.invalidtoken) + detail=messages.Messages.invalidtoken + ) def get_current_user( @@ -173,21 +177,30 @@ def get_current_user( session: Session = Depends(get_session)): access_token = request.cookies.get('access_token') if not access_token: - raise HTTPException(status_code=401, - detail=messages.Messages.notauthenticated) + raise HTTPException( + status_code=401, + detail=messages.Messages.notauthenticated + ) payload = verify_token(access_token) if not payload: - raise HTTPException(status_code=401, detail='aze') + raise HTTPException( + status_code=401, + detail='aze' + ) email = payload.get('email') if not email: - raise HTTPException(status_code=401, - detail=messages.Messages.notauthenticated) + raise HTTPException( + status_code=401, + detail=messages.Messages.notauthenticated + ) user = session.exec(select(User).where(User.email == email)).first() if not user: - raise HTTPException(status_code=401, - detail=messages.Messages.not_found('user')) + raise HTTPException( + status_code=401, + detail=messages.Messages.not_found('user') + ) return user @@ -249,6 +262,6 @@ def me(user: UserPublic = Depends(get_current_user)): 'name': user.name, 'email': user.email, 'id': user.id, - 'roles': [role.name for role in user.roles] + 'roles': user.roles } } diff --git a/backend/src/forms/forms.py b/backend/src/forms/forms.py index 86fffbd..23253c9 100644 --- a/backend/src/forms/forms.py +++ b/backend/src/forms/forms.py @@ -32,7 +32,10 @@ async def get_forms_filtered( @router.get('/{_id}', response_model=models.FormPublic) -async def get_form(_id: int, session: Session = Depends(get_session)): +async def get_form( + _id: int, + session: Session = Depends(get_session) +): result = service.get_one(session, _id) if result is None: raise HTTPException( @@ -48,6 +51,11 @@ async def create_form( user: models.User = Depends(get_current_user), session: Session = Depends(get_session) ): + if not service.is_allowed(session, user, form=form): + raise HTTPException( + status_code=403, + detail=messages.Messages.not_allowed('forms', 'update') + ) try: form = service.create_one(session, form) except exceptions.ProductorNotFoundError as error: @@ -61,10 +69,16 @@ async def create_form( @router.put('/{_id}', response_model=models.FormPublic) async def update_form( - _id: int, form: models.FormUpdate, + _id: int, + form: models.FormUpdate, user: models.User = Depends(get_current_user), session: Session = Depends(get_session) ): + if not service.is_allowed(session, user, _id=_id): + raise HTTPException( + status_code=403, + detail=messages.Messages.not_allowed('forms', 'update') + ) try: result = service.update_one(session, _id, form) except exceptions.FormNotFoundError as error: @@ -82,6 +96,11 @@ async def delete_form( user: models.User = Depends(get_current_user), session: Session = Depends(get_session) ): + if not service.is_allowed(session, user, _id=_id): + raise HTTPException( + status_code=403, + detail=messages.Messages.not_allowed('forms', 'delete') + ) try: result = service.delete_one(session, _id) except exceptions.FormNotFoundError as error: diff --git a/backend/src/forms/service.py b/backend/src/forms/service.py index 0ae765b..1f46177 100644 --- a/backend/src/forms/service.py +++ b/backend/src/forms/service.py @@ -108,12 +108,25 @@ def delete_one(session: Session, _id: int) -> models.FormPublic: return result -def is_allowed(session: Session, user: models.User, _id: int) -> bool: +def is_allowed( + session: Session, + user: models.User, + _id: int = None, + form: models.FormCreate = None +) -> bool: + if not _id: + statement = ( + select(models.Productor) + .where(models.Productor.id == form.productor_id) + ) + productor = session.exec(statement).first() + return productor.type in [r.name for r in user.roles] statement = ( select(models.Form) .join( models.Productor, - models.Form.productor_id == models.Productor.id) + models.Form.productor_id == models.Productor.id + ) .where(models.Form.id == _id) .where( models.Productor.type.in_( diff --git a/backend/src/productors/service.py b/backend/src/productors/service.py index 73248ca..ff31b41 100644 --- a/backend/src/productors/service.py +++ b/backend/src/productors/service.py @@ -92,3 +92,19 @@ def delete_one(session: Session, id: int) -> models.ProductorPublic: session.delete(productor) session.commit() return result + +def is_allowed( + session: Session, + user: models.User, + _id: int, + productor: models.ProductorCreate +) -> bool: + if not _id: + return productor.type in [r.name for r in user.roles] + statement = ( + select(models.Productor) + .where(models.Productor.id == _id) + .where(models.Productor.type.in_([r.name for r in user.roles])) + .distinct() + ) + return len(session.exec(statement).all()) > 0 diff --git a/backend/src/products/service.py b/backend/src/products/service.py index df35dcc..f35d745 100644 --- a/backend/src/products/service.py +++ b/backend/src/products/service.py @@ -85,3 +85,32 @@ def delete_one(session: Session, id: int) -> models.ProductPublic: session.delete(product) session.commit() return result + +def is_allowed( + session: Session, + user: models.User, + _id: int, + product: models.ProductCreate +) -> bool: + if not _id: + statement = ( + select(models.Product) + .join( + models.Productor, + models.Product.productor_id == models.Productor.id + ) + .where(models.Product.id == product.productor_id) + ) + productor = session.exec(statement).first() + return productor.type in [r.name for r in user.roles] + statement = ( + select(models.Product) + .join( + models.Productor, + models.Product.productor_id == models.Productor.id + ) + .where(models.Product.id == _id) + .where(models.Productor.type.in_([r.name for r in user.roles])) + .distinct() + ) + return len(session.exec(statement).all()) > 0 \ No newline at end of file diff --git a/backend/src/users/service.py b/backend/src/users/service.py index ed8f7c0..991517e 100644 --- a/backend/src/users/service.py +++ b/backend/src/users/service.py @@ -56,7 +56,9 @@ def get_or_create_user(session: Session, user_create: models.UserCreate): def get_roles(session: Session): - statement = select(models.ContractType) + statement = ( + select(models.ContractType) + ) return session.exec(statement.order_by(models.ContractType.name)).all() @@ -64,7 +66,9 @@ def create_one(session: Session, user: models.UserCreate) -> models.UserPublic: if user is None: raise exceptions.UserCreateError( messages.Messages.invalid_input( - 'user', 'input cannot be None')) + 'user', 'input cannot be None' + ) + ) new_user = models.User( name=user.name, email=user.email @@ -81,17 +85,19 @@ def create_one(session: Session, user: models.UserCreate) -> models.UserPublic: def update_one( session: Session, - id: int, + _id: int, user: models.UserCreate) -> models.UserPublic: if user is None: raise exceptions.UserCreateError( - messages.s.invalid_input( - 'user', 'input cannot be None')) - statement = select(models.User).where(models.User.id == id) + messages.Messages.invalid_input( + 'user', 'input cannot be None' + ) + ) + statement = select(models.User).where(models.User.id == _id) result = session.exec(statement) new_user = result.first() if not new_user: - raise exceptions.UserNotFoundError(f'User {id} not found') + raise exceptions.UserNotFoundError(f'User {_id} not found') new_user.email = user.email new_user.name = user.name @@ -103,12 +109,12 @@ def update_one( return new_user -def delete_one(session: Session, id: int) -> models.UserPublic: - statement = select(models.User).where(models.User.id == id) +def delete_one(session: Session, _id: int) -> models.UserPublic: + statement = select(models.User).where(models.User.id == _id) result = session.exec(statement) user = result.first() if not user: - raise exceptions.UserNotFoundError(f'User {id} not found') + raise exceptions.UserNotFoundError(f'User {_id} not found') result = models.UserPublic.model_validate(user) session.delete(user) session.commit() diff --git a/backend/src/users/users.py b/backend/src/users/users.py index 55e26aa..6e8308c 100644 --- a/backend/src/users/users.py +++ b/backend/src/users/users.py @@ -32,16 +32,18 @@ def get_roles( return service.get_roles(session) -@router.get('/{id}', response_model=models.UserPublic) -def get_users( - id: int, +@router.get('/{_id}', response_model=models.UserPublic) +def get_user( + _id: int, user: models.User = Depends(get_current_user), session: Session = Depends(get_session) ): - result = service.get_one(session, id) + result = service.get_one(session, _id) if result is None: - raise HTTPException(status_code=404, - detail=messages.Messages.not_found('user')) + raise HTTPException( + status_code=404, + detail=messages.Messages.not_found('user') + ) return result @@ -54,22 +56,27 @@ def create_user( try: user = service.create_one(session, user) except exceptions.UserCreateError as error: - raise HTTPException(status_code=400, detail=str(error)) + raise HTTPException( + status_code=400, + detail=str(error) + ) from error return user -@router.put('/{id}', response_model=models.UserPublic) +@router.put('/{_id}', response_model=models.UserPublic) def update_user( - id: int, + _id: int, user: models.UserUpdate, logged_user: models.User = Depends(get_current_user), session: Session = Depends(get_session) ): try: - result = service.update_one(session, id, user) + result = service.update_one(session, _id, user) except exceptions.UserNotFoundError as error: - raise HTTPException(status_code=404, - detail=messages.Messages.not_found('user')) + raise HTTPException( + status_code=404, + detail=messages.Messages.not_found('user') + ) from error return result @@ -82,6 +89,8 @@ def delete_user( try: result = service.delete_one(session, id) except exceptions.UserNotFoundError as error: - raise HTTPException(status_code=404, - detail=messages.Messages.not_found('user')) + raise HTTPException( + status_code=404, + detail=messages.Messages.not_found('user') + ) from error return result diff --git a/frontend/src/components/Productors/Modal/index.tsx b/frontend/src/components/Productors/Modal/index.tsx index 183a480..1d5f3b4 100644 --- a/frontend/src/components/Productors/Modal/index.tsx +++ b/frontend/src/components/Productors/Modal/index.tsx @@ -19,7 +19,7 @@ import { type ProductorInputs, } from "@/services/resources/productors"; import { useMemo } from "react"; -import { useGetRoles } from "@/services/api"; +import { useAuth } from "@/services/auth/AuthProvider"; export type ProductorModalProps = ModalBaseProps & { currentProductor?: Productor; @@ -32,7 +32,7 @@ export function ProductorModal({ currentProductor, handleSubmit, }: ProductorModalProps) { - const { data: allRoles } = useGetRoles(); + const { loggedUser } = useAuth(); const form = useForm({ initialValues: { @@ -58,8 +58,8 @@ export function ProductorModal({ }); const roleSelect = useMemo(() => { - return allRoles?.map((role) => ({ value: String(role.name), label: role.name })); - }, [allRoles]); + return loggedUser?.user?.roles?.map((role) => ({ value: String(role.name), label: role.name })); + }, [loggedUser?.user?.roles]); return (