diff --git a/flowsint-api/alembic/versions/1d0f26dbbef5_add_passive_delete_v2.py b/flowsint-api/alembic/versions/1d0f26dbbef5_add_passive_delete_v2.py new file mode 100644 index 0000000..14176df --- /dev/null +++ b/flowsint-api/alembic/versions/1d0f26dbbef5_add_passive_delete_v2.py @@ -0,0 +1,32 @@ +"""add passive_delete_v2 + +Revision ID: 1d0f26dbbef5 +Revises: afdaf9aa539c +Create Date: 2025-09-17 22:48:31.379106 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '1d0f26dbbef5' +down_revision: Union[str, None] = 'afdaf9aa539c' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/flowsint-api/alembic/versions/6be831edfda7_add_investigation_roles_permissions.py b/flowsint-api/alembic/versions/6be831edfda7_add_investigation_roles_permissions.py new file mode 100644 index 0000000..dcaef47 --- /dev/null +++ b/flowsint-api/alembic/versions/6be831edfda7_add_investigation_roles_permissions.py @@ -0,0 +1,35 @@ +"""add investigation roles permissions + + +Revision ID: 6be831edfda7 +Revises: c82bf6af92e5 +Create Date: 2025-09-17 22:02:46.159090 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '6be831edfda7' +down_revision: Union[str, None] = 'c82bf6af92e5' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('investigation_user_roles', sa.Column('roles', postgresql.ARRAY(sa.Enum('OWNER', 'EDITOR', 'VIEWER', name='role_enum', create_constraint=True)), server_default='{}', nullable=False)) + op.drop_column('investigation_user_roles', 'role') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('investigation_user_roles', sa.Column('role', postgresql.ENUM('OWNER', 'EDITOR', 'VIEWER', name='role_enum'), autoincrement=False, nullable=False)) + op.drop_column('investigation_user_roles', 'roles') + # ### end Alembic commands ### diff --git a/flowsint-api/alembic/versions/6e49acfb3816_add_investigation_roles_permissions.py b/flowsint-api/alembic/versions/6e49acfb3816_add_investigation_roles_permissions.py new file mode 100644 index 0000000..a88456d --- /dev/null +++ b/flowsint-api/alembic/versions/6e49acfb3816_add_investigation_roles_permissions.py @@ -0,0 +1,63 @@ +"""add investigation roles permissions + + +Revision ID: 6e49acfb3816 +Revises: 1098b7a5eabc +Create Date: 2025-09-17 21:46:14.314402 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '6e49acfb3816' +down_revision: Union[str, None] = '1098b7a5eabc' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('investigation_user_roles', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('user_id', sa.UUID(), nullable=False), + sa.Column('investigation_id', sa.UUID(), nullable=False), + sa.Column('role', sa.Enum('OWNER', 'EDITOR', 'VIEWER', name='role_enum', create_constraint=True), nullable=False), + sa.ForeignKeyConstraint(['investigation_id'], ['investigations.id'], onupdate='CASCADE', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['profiles.id'], onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user_id', 'investigation_id', name='uq_user_investigation') + ) + op.create_index('idx_investigation_roles_investigation_id', 'investigation_user_roles', ['investigation_id'], unique=False) + op.create_index('idx_investigation_roles_user_id', 'investigation_user_roles', ['user_id'], unique=False) + op.drop_index('idx_investigations_profiles_investigation_id', table_name='investigations_profiles') + op.drop_index('idx_investigations_profiles_profile_id', table_name='investigations_profiles') + op.drop_index('projects_profiles_unique_profile_project', table_name='investigations_profiles') + op.drop_table('investigations_profiles') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('investigations_profiles', + sa.Column('id', sa.UUID(), autoincrement=False, nullable=False), + sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), + sa.Column('investigation_id', sa.UUID(), autoincrement=False, nullable=True), + sa.Column('profile_id', sa.UUID(), autoincrement=False, nullable=True), + sa.Column('role', sa.VARCHAR(), server_default=sa.text("'member'::character varying"), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['investigation_id'], ['investigations.id'], name='investigations_profiles_investigation_id_fkey', onupdate='CASCADE', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['profile_id'], ['profiles.id'], name='investigations_profiles_profile_id_fkey', onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name='investigations_profiles_pkey') + ) + op.create_index('projects_profiles_unique_profile_project', 'investigations_profiles', ['profile_id', 'investigation_id'], unique=False) + op.create_index('idx_investigations_profiles_profile_id', 'investigations_profiles', ['profile_id'], unique=False) + op.create_index('idx_investigations_profiles_investigation_id', 'investigations_profiles', ['investigation_id'], unique=False) + op.drop_index('idx_investigation_roles_user_id', table_name='investigation_user_roles') + op.drop_index('idx_investigation_roles_investigation_id', table_name='investigation_user_roles') + op.drop_table('investigation_user_roles') + # ### end Alembic commands ### diff --git a/flowsint-api/alembic/versions/8d0e12b68d1e_fix_backpopulate_issue.py b/flowsint-api/alembic/versions/8d0e12b68d1e_fix_backpopulate_issue.py new file mode 100644 index 0000000..abe7868 --- /dev/null +++ b/flowsint-api/alembic/versions/8d0e12b68d1e_fix_backpopulate_issue.py @@ -0,0 +1,32 @@ +"""fix_backpopulate issue + +Revision ID: 8d0e12b68d1e +Revises: 1d0f26dbbef5 +Create Date: 2025-09-17 22:55:20.721587 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '8d0e12b68d1e' +down_revision: Union[str, None] = '1d0f26dbbef5' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/flowsint-api/alembic/versions/afdaf9aa539c_add_passive_delete.py b/flowsint-api/alembic/versions/afdaf9aa539c_add_passive_delete.py new file mode 100644 index 0000000..62b1a75 --- /dev/null +++ b/flowsint-api/alembic/versions/afdaf9aa539c_add_passive_delete.py @@ -0,0 +1,32 @@ +"""add passive_delete + +Revision ID: afdaf9aa539c +Revises: 6be831edfda7 +Create Date: 2025-09-17 22:46:21.139127 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'afdaf9aa539c' +down_revision: Union[str, None] = '6be831edfda7' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/flowsint-api/alembic/versions/c82bf6af92e5_add_investigation_roles_permissions.py b/flowsint-api/alembic/versions/c82bf6af92e5_add_investigation_roles_permissions.py new file mode 100644 index 0000000..d581385 --- /dev/null +++ b/flowsint-api/alembic/versions/c82bf6af92e5_add_investigation_roles_permissions.py @@ -0,0 +1,33 @@ +"""add investigation roles permissions + + +Revision ID: c82bf6af92e5 +Revises: d39941278a91 +Create Date: 2025-09-17 21:55:56.756716 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'c82bf6af92e5' +down_revision: Union[str, None] = 'd39941278a91' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/flowsint-api/alembic/versions/d39941278a91_init.py b/flowsint-api/alembic/versions/d39941278a91_init.py new file mode 100644 index 0000000..79f3f26 --- /dev/null +++ b/flowsint-api/alembic/versions/d39941278a91_init.py @@ -0,0 +1,32 @@ +"""init + +Revision ID: d39941278a91 +Revises: 6e49acfb3816 +Create Date: 2025-09-17 21:52:57.142634 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'd39941278a91' +down_revision: Union[str, None] = '6e49acfb3816' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/flowsint-api/app/api/routes/investigations.py b/flowsint-api/app/api/routes/investigations.py index 816a6c2..527f0c5 100644 --- a/flowsint-api/app/api/routes/investigations.py +++ b/flowsint-api/app/api/routes/investigations.py @@ -1,10 +1,19 @@ from uuid import UUID, uuid4 +from app.security.permissions import check_investigation_permission from fastapi import APIRouter, HTTPException, Depends, status from typing import List from datetime import datetime +from flowsint_core.core.types import Role +from sqlalchemy import or_ from sqlalchemy.orm import Session, selectinload from flowsint_core.core.postgre_db import get_db -from flowsint_core.core.models import Analysis, Investigation, Profile, Sketch +from flowsint_core.core.models import ( + Analysis, + Investigation, + InvestigationUserRole, + Profile, + Sketch, +) from app.api.deps import get_current_user from app.api.schemas.investigation import ( InvestigationRead, @@ -17,17 +26,52 @@ from flowsint_core.core.graph_db import neo4j_connection router = APIRouter() +def get_user_accessible_investigations( + user_id: str, db: Session, allowed_roles: list[Role] = None +) -> list[Investigation]: + """ + Returns all investigations accessible to user depending on its roles + """ + query = db.query(Investigation).join( + InvestigationUserRole, + InvestigationUserRole.investigation_id == Investigation.id, + ) + + query = query.filter(InvestigationUserRole.user_id == user_id) + + if allowed_roles: + # ARRAY(Role) contains any of allowed_roles + conditions = [InvestigationUserRole.roles.any(role) for role in allowed_roles] + # Inclut également le propriétaire de l’investigation + query = query.filter(or_(*conditions, Investigation.owner_id == user_id)) + + return ( + query.options( + selectinload(Investigation.sketches), + selectinload(Investigation.analyses), + selectinload(Investigation.owner), + ) + .distinct() + .all() + ) + + # Get the list of all investigations @router.get("", response_model=List[InvestigationRead]) def get_investigations( - db: Session = Depends(get_db), current_user: Profile = Depends(get_current_user) + db: Session = Depends(get_db), + current_user: Profile = Depends(get_current_user), ): - investigations = ( - db.query(Investigation) - .options(selectinload(Investigation.sketches), selectinload(Investigation.analyses), selectinload(Investigation.owner)) - .filter(Investigation.owner_id == current_user.id) - .all() + """ + Récupère toutes les investigations accessibles à l'utilisateur + selon ses rôles (OWNER, EDITOR, VIEWER). + """ + allowed_roles_for_read = [Role.OWNER, Role.EDITOR, Role.VIEWER] + + investigations = get_user_accessible_investigations( + user_id=current_user.id, db=db, allowed_roles=allowed_roles_for_read ) + return investigations @@ -46,12 +90,21 @@ def create_investigation( description=payload.description or payload.name, owner_id=current_user.id, status="active", - created_at=datetime.utcnow(), - last_updated_at=datetime.utcnow(), ) db.add(new_investigation) + + new_roles = InvestigationUserRole( + id=uuid4(), + user_id=current_user.id, + investigation_id=new_investigation.id, + roles=[Role.OWNER], + ) + db.add(new_roles) + db.commit() db.refresh(new_investigation) + db.refresh(new_roles) + return new_investigation @@ -62,9 +115,14 @@ def get_investigation_by_id( db: Session = Depends(get_db), current_user: Profile = Depends(get_current_user), ): + check_investigation_permission(current_user.id, investigation_id, actions=["read"], db=db) investigation = ( db.query(Investigation) - .options(selectinload(Investigation.sketches), selectinload(Investigation.analyses), selectinload(Investigation.owner)) + .options( + selectinload(Investigation.sketches), + selectinload(Investigation.analyses), + selectinload(Investigation.owner), + ) .filter(Investigation.id == investigation_id) .filter(Investigation.owner_id == current_user.id) .first() diff --git a/flowsint-api/app/api/routes/sketches.py b/flowsint-api/app/api/routes/sketches.py index 7273ce3..dce6dba 100644 --- a/flowsint-api/app/api/routes/sketches.py +++ b/flowsint-api/app/api/routes/sketches.py @@ -1,10 +1,10 @@ +from app.security.permissions import check_investigation_permission from fastapi import APIRouter, HTTPException, Depends, status from pydantic import BaseModel, Field -from typing import Dict, Any, Literal +from typing import Literal, List from fastapi import HTTPException from pydantic import BaseModel, Field from flowsint_core.utils import flatten -from typing import Dict, Any, List from sqlalchemy.orm import Session from app.api.schemas.sketch import SketchCreate, SketchRead, SketchUpdate from flowsint_core.core.models import Sketch, Profile @@ -63,6 +63,9 @@ def create_sketch( current_user: Profile = Depends(get_current_user), ): sketch_data = data.dict() + check_investigation_permission( + current_user.id, sketch_data.get("investigation_id"), actions=["create"], db=db + ) sketch_data["owner_id"] = current_user.id sketch = Sketch(**sketch_data) db.add(sketch) diff --git a/flowsint-api/app/security/permissions.py b/flowsint-api/app/security/permissions.py new file mode 100644 index 0000000..46b0a08 --- /dev/null +++ b/flowsint-api/app/security/permissions.py @@ -0,0 +1,33 @@ +from fastapi import HTTPException +from flowsint_core.core.models import InvestigationUserRole +from flowsint_core.core.types import Role + + +def can_user(roles: list[Role], actions: list[str]) -> bool: + """ + Vérifie si au moins un rôle de la liste autorise au moins une action de la liste. + """ + for role in roles: + for action in actions: + if role == Role.OWNER: + return True + if role == Role.EDITOR and action in ["read", "create", "update"]: + return True + if role == Role.VIEWER and action == "read": + return True + return False + + +from fastapi import HTTPException + + +def check_investigation_permission(user_id: str, investigation_id: str, actions: list[str], db): + role_entry = ( + db.query(InvestigationUserRole) + .filter_by(user_id=user_id, investigation_id=investigation_id) + .first() + ) + + if not role_entry or not can_user(role_entry.roles, actions): + raise HTTPException(status_code=403, detail="Forbidden") + return True diff --git a/flowsint-api/poetry.lock b/flowsint-api/poetry.lock index d844963..69b4650 100644 --- a/flowsint-api/poetry.lock +++ b/flowsint-api/poetry.lock @@ -1019,7 +1019,7 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} +markers = {dev = "platform_system == \"Windows\""} [[package]] name = "confection" @@ -1039,49 +1039,49 @@ srsly = ">=2.4.0,<3.0.0" [[package]] name = "cryptography" -version = "45.0.6" +version = "45.0.7" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.7" groups = ["main"] files = [ - {file = "cryptography-45.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:048e7ad9e08cf4c0ab07ff7f36cc3115924e22e2266e034450a890d9e312dd74"}, - {file = "cryptography-45.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44647c5d796f5fc042bbc6d61307d04bf29bccb74d188f18051b635f20a9c75f"}, - {file = "cryptography-45.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e40b80ecf35ec265c452eea0ba94c9587ca763e739b8e559c128d23bff7ebbbf"}, - {file = "cryptography-45.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:00e8724bdad672d75e6f069b27970883179bd472cd24a63f6e620ca7e41cc0c5"}, - {file = "cryptography-45.0.6-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a3085d1b319d35296176af31c90338eeb2ddac8104661df79f80e1d9787b8b2"}, - {file = "cryptography-45.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1b7fa6a1c1188c7ee32e47590d16a5a0646270921f8020efc9a511648e1b2e08"}, - {file = "cryptography-45.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:275ba5cc0d9e320cd70f8e7b96d9e59903c815ca579ab96c1e37278d231fc402"}, - {file = "cryptography-45.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4028f29a9f38a2025abedb2e409973709c660d44319c61762202206ed577c42"}, - {file = "cryptography-45.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ee411a1b977f40bd075392c80c10b58025ee5c6b47a822a33c1198598a7a5f05"}, - {file = "cryptography-45.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2a21a8eda2d86bb604934b6b37691585bd095c1f788530c1fcefc53a82b3453"}, - {file = "cryptography-45.0.6-cp311-abi3-win32.whl", hash = "sha256:d063341378d7ee9c91f9d23b431a3502fc8bfacd54ef0a27baa72a0843b29159"}, - {file = "cryptography-45.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:833dc32dfc1e39b7376a87b9a6a4288a10aae234631268486558920029b086ec"}, - {file = "cryptography-45.0.6-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:3436128a60a5e5490603ab2adbabc8763613f638513ffa7d311c900a8349a2a0"}, - {file = "cryptography-45.0.6-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d9ef57b6768d9fa58e92f4947cea96ade1233c0e236db22ba44748ffedca394"}, - {file = "cryptography-45.0.6-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea3c42f2016a5bbf71825537c2ad753f2870191134933196bee408aac397b3d9"}, - {file = "cryptography-45.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:20ae4906a13716139d6d762ceb3e0e7e110f7955f3bc3876e3a07f5daadec5f3"}, - {file = "cryptography-45.0.6-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dac5ec199038b8e131365e2324c03d20e97fe214af051d20c49db129844e8b3"}, - {file = "cryptography-45.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:18f878a34b90d688982e43f4b700408b478102dd58b3e39de21b5ebf6509c301"}, - {file = "cryptography-45.0.6-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5bd6020c80c5b2b2242d6c48487d7b85700f5e0038e67b29d706f98440d66eb5"}, - {file = "cryptography-45.0.6-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:eccddbd986e43014263eda489abbddfbc287af5cddfd690477993dbb31e31016"}, - {file = "cryptography-45.0.6-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:550ae02148206beb722cfe4ef0933f9352bab26b087af00e48fdfb9ade35c5b3"}, - {file = "cryptography-45.0.6-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5b64e668fc3528e77efa51ca70fadcd6610e8ab231e3e06ae2bab3b31c2b8ed9"}, - {file = "cryptography-45.0.6-cp37-abi3-win32.whl", hash = "sha256:780c40fb751c7d2b0c6786ceee6b6f871e86e8718a8ff4bc35073ac353c7cd02"}, - {file = "cryptography-45.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b"}, - {file = "cryptography-45.0.6-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:705bb7c7ecc3d79a50f236adda12ca331c8e7ecfbea51edd931ce5a7a7c4f012"}, - {file = "cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:826b46dae41a1155a0c0e66fafba43d0ede1dc16570b95e40c4d83bfcf0a451d"}, - {file = "cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:cc4d66f5dc4dc37b89cfef1bd5044387f7a1f6f0abb490815628501909332d5d"}, - {file = "cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f68f833a9d445cc49f01097d95c83a850795921b3f7cc6488731e69bde3288da"}, - {file = "cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3b5bf5267e98661b9b888a9250d05b063220dfa917a8203744454573c7eb79db"}, - {file = "cryptography-45.0.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2384f2ab18d9be88a6e4f8972923405e2dbb8d3e16c6b43f15ca491d7831bd18"}, - {file = "cryptography-45.0.6-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fc022c1fa5acff6def2fc6d7819bbbd31ccddfe67d075331a65d9cfb28a20983"}, - {file = "cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3de77e4df42ac8d4e4d6cdb342d989803ad37707cf8f3fbf7b088c9cbdd46427"}, - {file = "cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:599c8d7df950aa68baa7e98f7b73f4f414c9f02d0e8104a30c0182a07732638b"}, - {file = "cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:31a2b9a10530a1cb04ffd6aa1cd4d3be9ed49f7d77a4dafe198f3b382f41545c"}, - {file = "cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:e5b3dda1b00fb41da3af4c5ef3f922a200e33ee5ba0f0bc9ecf0b0c173958385"}, - {file = "cryptography-45.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:629127cfdcdc6806dfe234734d7cb8ac54edaf572148274fa377a7d3405b0043"}, - {file = "cryptography-45.0.6.tar.gz", hash = "sha256:5c966c732cf6e4a276ce83b6e4c729edda2df6929083a952cc7da973c539c719"}, + {file = "cryptography-45.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee"}, + {file = "cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6"}, + {file = "cryptography-45.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339"}, + {file = "cryptography-45.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8"}, + {file = "cryptography-45.0.7-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf"}, + {file = "cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513"}, + {file = "cryptography-45.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3"}, + {file = "cryptography-45.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3"}, + {file = "cryptography-45.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6"}, + {file = "cryptography-45.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd"}, + {file = "cryptography-45.0.7-cp311-abi3-win32.whl", hash = "sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8"}, + {file = "cryptography-45.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443"}, + {file = "cryptography-45.0.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2"}, + {file = "cryptography-45.0.7-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691"}, + {file = "cryptography-45.0.7-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59"}, + {file = "cryptography-45.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4"}, + {file = "cryptography-45.0.7-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3"}, + {file = "cryptography-45.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1"}, + {file = "cryptography-45.0.7-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27"}, + {file = "cryptography-45.0.7-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17"}, + {file = "cryptography-45.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b"}, + {file = "cryptography-45.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c"}, + {file = "cryptography-45.0.7-cp37-abi3-win32.whl", hash = "sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5"}, + {file = "cryptography-45.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90"}, + {file = "cryptography-45.0.7-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:de58755d723e86175756f463f2f0bddd45cc36fbd62601228a3f8761c9f58252"}, + {file = "cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a20e442e917889d1a6b3c570c9e3fa2fdc398c20868abcea268ea33c024c4083"}, + {file = "cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:258e0dff86d1d891169b5af222d362468a9570e2532923088658aa866eb11130"}, + {file = "cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d97cf502abe2ab9eff8bd5e4aca274da8d06dd3ef08b759a8d6143f4ad65d4b4"}, + {file = "cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:c987dad82e8c65ebc985f5dae5e74a3beda9d0a2a4daf8a1115f3772b59e5141"}, + {file = "cryptography-45.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c13b1e3afd29a5b3b2656257f14669ca8fa8d7956d509926f0b130b600b50ab7"}, + {file = "cryptography-45.0.7-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a862753b36620af6fc54209264f92c716367f2f0ff4624952276a6bbd18cbde"}, + {file = "cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:06ce84dc14df0bf6ea84666f958e6080cdb6fe1231be2a51f3fc1267d9f3fb34"}, + {file = "cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d0c5c6bac22b177bf8da7435d9d27a6834ee130309749d162b26c3105c0795a9"}, + {file = "cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:2f641b64acc00811da98df63df7d59fd4706c0df449da71cb7ac39a0732b40ae"}, + {file = "cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:f5414a788ecc6ee6bc58560e85ca624258a55ca434884445440a810796ea0e0b"}, + {file = "cryptography-45.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f3d56f73595376f4244646dd5c5870c14c196949807be39e79e7bd9bac3da63"}, + {file = "cryptography-45.0.7.tar.gz", hash = "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971"}, ] [package.dependencies] @@ -1094,7 +1094,7 @@ nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_full_version >= \"3.8 pep8test = ["check-sdist ; python_full_version >= \"3.8.0\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==45.0.6)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test = ["certifi (>=2024)", "cryptography-vectors (==45.0.7)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] @@ -1198,25 +1198,26 @@ wmi = ["wmi (>=1.5.1)"] [[package]] name = "docker" -version = "6.1.3" +version = "7.1.0" description = "A Python library for the Docker Engine API." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["main"] files = [ - {file = "docker-6.1.3-py3-none-any.whl", hash = "sha256:aecd2277b8bf8e506e484f6ab7aec39abe0038e29fa4a6d3ba86c3fe01844ed9"}, - {file = "docker-6.1.3.tar.gz", hash = "sha256:aa6d17830045ba5ef0168d5eaa34d37beeb113948c413affe1d5991fc11f9a20"}, + {file = "docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0"}, + {file = "docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c"}, ] [package.dependencies] -packaging = ">=14.0" pywin32 = {version = ">=304", markers = "sys_platform == \"win32\""} requests = ">=2.26.0" urllib3 = ">=1.26.0" -websocket-client = ">=0.32.0" [package.extras] +dev = ["coverage (==7.2.7)", "pytest (==7.4.2)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.1.0)", "ruff (==0.1.8)"] +docs = ["myst-parser (==0.18.0)", "sphinx (==5.1.1)"] ssh = ["paramiko (>=2.4.3)"] +websockets = ["websocket-client (>=1.3.0)"] [[package]] name = "ecdsa" @@ -1372,7 +1373,8 @@ develop = true alembic = "1.13.0" asyncpg = "^0.29" celery = "^5.3" -docker = "^6.1" +cryptography = "^45.0.7" +docker = "^7.1.0" flowsint-transforms = {path = "../flowsint-transforms", develop = true} neo4j = "^5.0" networkx = "^2.6.3" @@ -1380,6 +1382,7 @@ passlib = {version = "^1.7", extras = ["bcrypt"]} phonenumbers = "^9.0.8" psycopg2-binary = "^2.9" pydantic = {version = "^2.11.7", extras = ["email"]} +pytest = "^8.4.2" python-dotenv = "^1.0" python-jose = {version = "^3.3", extras = ["cryptography"]} python-multipart = "^0.0.20" @@ -1404,7 +1407,6 @@ develop = true [package.dependencies] dnspython = "^2.4" -docker = "^6.1" flowsint-core = {path = "../flowsint-core", develop = true} flowsint-types = {path = "../flowsint-types", develop = true} hibpwned = "^1.3.9" @@ -1830,7 +1832,7 @@ version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, @@ -3266,7 +3268,7 @@ version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, @@ -3997,43 +3999,25 @@ files = [ [[package]] name = "pytest" -version = "7.4.4" +version = "8.4.2" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, - {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" - -[package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-asyncio" -version = "0.21.2" -description = "Pytest support for asyncio" -optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b"}, - {file = "pytest_asyncio-0.21.2.tar.gz", hash = "sha256:d67738fc232b94b326b9d060750beb16e0074210b98dd8b58a5239fa2a154f45"}, + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, ] [package.dependencies] -pytest = ">=7.0.0" +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" [package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] -testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] name = "python-bidi" @@ -5488,23 +5472,6 @@ files = [ {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] -[[package]] -name = "websocket-client" -version = "1.8.0" -description = "WebSocket client for Python with low level API options" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, - {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, -] - -[package.extras] -docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] -optional = ["python-socks", "wsaccel"] -test = ["websockets"] - [[package]] name = "werkzeug" version = "3.1.3" @@ -5778,4 +5745,4 @@ propcache = ">=0.2.1" [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.13" -content-hash = "713413e0c4f9fe453cc2fbb96fe82650caa2776f40af2fae82601326e4981774" +content-hash = "4a980251fbdec9cd0fde83177e6a8b96e21ce2af0b852b628095da088ce3af41" diff --git a/flowsint-api/pyproject.toml b/flowsint-api/pyproject.toml index a65f18d..dbfef7b 100644 --- a/flowsint-api/pyproject.toml +++ b/flowsint-api/pyproject.toml @@ -26,13 +26,11 @@ alembic = "1.13.0" passlib = {extras = ["bcrypt"], version = "^1.7"} sse-starlette = "^1.8" networkx = "^2.6.3" -pytest-asyncio = "^0.21" email-validator = "^2.2.0" mistralai = "^1.9.3" python-multipart = "^0.0.20" [tool.poetry.group.dev.dependencies] -pytest = "^7.4" black = "^23.0" isort = "^5.12" flake8 = "^6.0" diff --git a/flowsint-api/pytest.ini b/flowsint-api/pytest.ini deleted file mode 100644 index 9e0dbca..0000000 --- a/flowsint-api/pytest.ini +++ /dev/null @@ -1,6 +0,0 @@ -[tool:pytest] -asyncio_mode = auto -testpaths = tests -python_files = test_*.py -python_functions = test_* -addopts = -v -s \ No newline at end of file diff --git a/flowsint-app/src/renderer/src/components/analyses/analyses-list.tsx b/flowsint-app/src/renderer/src/components/analyses/analyses-list.tsx index dc73bd5..056ca6f 100644 --- a/flowsint-app/src/renderer/src/components/analyses/analyses-list.tsx +++ b/flowsint-app/src/renderer/src/components/analyses/analyses-list.tsx @@ -12,6 +12,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query" import { toast } from "sonner" import { formatDistanceToNow } from "date-fns" import { queryKeys } from "@/api/query-keys" +import ErrorState from "../shared/error-state" const AnalysisItem = ({ analysis, active }: { analysis: Analysis, active: boolean }) => { return ( @@ -45,7 +46,7 @@ const AnalysisList = () => { const navigate = useNavigate() // Fetch all analyses for this investigation - const { data: analyses, isLoading, error } = useQuery({ + const { data: analyses, isLoading, error, refetch } = useQuery({ queryKey: queryKeys.analyses.byInvestigation(investigationId || ""), queryFn: () => analysisService.getByInvestigationId(investigationId || ""), enabled: !!investigationId, @@ -82,7 +83,14 @@ const AnalysisList = () => { } }) - if (error) return
- Create your first investigation to start organizing your data. -
-