From 493963908da207895fbce29a9bcaaffb40ac70c7 Mon Sep 17 00:00:00 2001 From: Saurab-Shrestha Date: Sat, 24 Feb 2024 18:57:49 +0545 Subject: [PATCH] Added audit logs --- .env | 2 +- alembic/env.py | 1 + .../versions/0aeaf9df35a6_create_models.py | 116 ----------- .../62c1a29320fc_updated_audit_log.py | 52 +++++ alembic/versions/8c4bd1aaf45a_create_model.py | 118 ----------- .../versions/b526c0676007_audit_log_update.py | 50 +++++ ...ent_model.py => eb0a7f182c75_audit_log.py} | 27 ++- private_gpt/__main__.py | 6 + private_gpt/server/ingest/ingest_router.py | 30 ++- private_gpt/users/api/deps.py | 11 +- private_gpt/users/api/v1/api.py | 3 +- private_gpt/users/api/v1/routers/audits.py | 30 +++ private_gpt/users/api/v1/routers/auth.py | 14 +- .../users/api/v1/routers/departments.py | 196 +++++++++++++----- private_gpt/users/api/v1/routers/users.py | 81 ++++++++ private_gpt/users/core/config.py | 2 +- private_gpt/users/core/security.py | 1 + private_gpt/users/crud/__init__.py | 1 + private_gpt/users/crud/audit_crud.py | 14 ++ private_gpt/users/models/__init__.py | 3 +- private_gpt/users/models/audit.py | 18 ++ private_gpt/users/models/department.py | 40 ++-- private_gpt/users/models/document.py | 30 ++- private_gpt/users/models/user.py | 21 +- private_gpt/users/schemas/__init__.py | 3 +- private_gpt/users/schemas/audit.py | 30 +++ private_gpt/users/utils/ad_auth.py | 1 - private_gpt/users/utils/audit.py | 24 +++ 28 files changed, 596 insertions(+), 329 deletions(-) delete mode 100644 alembic/versions/0aeaf9df35a6_create_models.py create mode 100644 alembic/versions/62c1a29320fc_updated_audit_log.py delete mode 100644 alembic/versions/8c4bd1aaf45a_create_model.py create mode 100644 alembic/versions/b526c0676007_audit_log_update.py rename alembic/versions/{36beb9b73c64_update_department_model.py => eb0a7f182c75_audit_log.py} (50%) create mode 100644 private_gpt/users/api/v1/routers/audits.py create mode 100644 private_gpt/users/crud/audit_crud.py create mode 100644 private_gpt/users/models/audit.py create mode 100644 private_gpt/users/schemas/audit.py create mode 100644 private_gpt/users/utils/audit.py diff --git a/.env b/.env index be7405d3..8f71f45d 100644 --- a/.env +++ b/.env @@ -4,7 +4,7 @@ ENVIRONMENT=dev DB_HOST=localhost DB_USER=postgres DB_PORT=5432 -DB_PASSWORD=admin +DB_PASSWORD=quick DB_NAME=QuickGpt SUPER_ADMIN_EMAIL=superadmin@email.com diff --git a/alembic/env.py b/alembic/env.py index 6f369228..4a86145e 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -16,6 +16,7 @@ from private_gpt.users.models.subscription import Subscription from private_gpt.users.models.company import Company from private_gpt.users.models.document import Document from private_gpt.users.models.department import Department +from private_gpt.users.models.audit import Audit # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config diff --git a/alembic/versions/0aeaf9df35a6_create_models.py b/alembic/versions/0aeaf9df35a6_create_models.py deleted file mode 100644 index 709be046..00000000 --- a/alembic/versions/0aeaf9df35a6_create_models.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Create models - -Revision ID: 0aeaf9df35a6 -Revises: -Create Date: 2024-02-20 19:16:15.608391 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '0aeaf9df35a6' -down_revision: Union[str, None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('companies', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_companies_id'), 'companies', ['id'], unique=False) - op.create_index(op.f('ix_companies_name'), 'companies', ['name'], unique=True) - op.create_table('roles', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(length=100), nullable=True), - sa.Column('description', sa.Text(), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_roles_id'), 'roles', ['id'], unique=False) - op.create_index(op.f('ix_roles_name'), 'roles', ['name'], unique=False) - op.create_table('departments', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(), nullable=True), - sa.Column('company_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['company_id'], ['companies.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_departments_id'), 'departments', ['id'], unique=False) - op.create_index(op.f('ix_departments_name'), 'departments', ['name'], unique=True) - op.create_table('subscriptions', - sa.Column('sub_id', sa.Integer(), nullable=False), - sa.Column('company_id', sa.Integer(), nullable=True), - sa.Column('start_date', sa.DateTime(), nullable=True), - sa.Column('end_date', sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint(['company_id'], ['companies.id'], ), - sa.PrimaryKeyConstraint('sub_id') - ) - op.create_index(op.f('ix_subscriptions_sub_id'), 'subscriptions', ['sub_id'], unique=False) - op.create_table('users', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('email', sa.String(length=225), nullable=False), - sa.Column('hashed_password', sa.String(), nullable=False), - sa.Column('fullname', sa.String(length=225), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('last_login', sa.DateTime(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=True), - sa.Column('updated_at', sa.DateTime(), nullable=True), - sa.Column('company_id', sa.Integer(), nullable=True), - sa.Column('department_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['company_id'], ['companies.id'], ), - sa.ForeignKeyConstraint(['department_id'], ['departments.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email'), - sa.UniqueConstraint('fullname'), - sa.UniqueConstraint('fullname', name='unique_username_no_spacing') - ) - op.create_table('document', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('filename', sa.String(length=225), nullable=False), - sa.Column('uploaded_by', sa.Integer(), nullable=False), - sa.Column('uploaded_at', sa.DateTime(), nullable=False), - sa.Column('department_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['department_id'], ['departments.id'], ), - sa.ForeignKeyConstraint(['uploaded_by'], ['users.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('filename') - ) - op.create_index(op.f('ix_document_id'), 'document', ['id'], unique=False) - op.create_table('user_roles', - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('role_id', sa.Integer(), nullable=False), - sa.Column('company_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['company_id'], ['companies.id'], ), - sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), - sa.PrimaryKeyConstraint('user_id', 'role_id', 'company_id'), - sa.UniqueConstraint('user_id', 'role_id', 'company_id', name='unique_user_role') - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('user_roles') - op.drop_index(op.f('ix_document_id'), table_name='document') - op.drop_table('document') - op.drop_table('users') - op.drop_index(op.f('ix_subscriptions_sub_id'), table_name='subscriptions') - op.drop_table('subscriptions') - op.drop_index(op.f('ix_departments_name'), table_name='departments') - op.drop_index(op.f('ix_departments_id'), table_name='departments') - op.drop_table('departments') - op.drop_index(op.f('ix_roles_name'), table_name='roles') - op.drop_index(op.f('ix_roles_id'), table_name='roles') - op.drop_table('roles') - op.drop_index(op.f('ix_companies_name'), table_name='companies') - op.drop_index(op.f('ix_companies_id'), table_name='companies') - op.drop_table('companies') - # ### end Alembic commands ### diff --git a/alembic/versions/62c1a29320fc_updated_audit_log.py b/alembic/versions/62c1a29320fc_updated_audit_log.py new file mode 100644 index 00000000..8bfaf661 --- /dev/null +++ b/alembic/versions/62c1a29320fc_updated_audit_log.py @@ -0,0 +1,52 @@ +"""Updated audit log + +Revision ID: 62c1a29320fc +Revises: b526c0676007 +Create Date: 2024-02-24 17:27:33.407895 + +""" +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 = '62c1a29320fc' +down_revision: Union[str, None] = 'b526c0676007' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('audit', 'timestamp', + existing_type=postgresql.TIMESTAMP(), + nullable=False) + op.alter_column('audit', 'model', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('audit', 'action', + existing_type=sa.VARCHAR(), + nullable=False) + op.drop_column('audit', 'url') + op.drop_column('audit', 'model_id') + # op.create_unique_constraint('unique_user_role', 'user_roles', ['user_id', 'role_id', 'company_id']) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + # op.drop_constraint('unique_user_role', 'user_roles', type_='unique') + op.add_column('audit', sa.Column('model_id', sa.INTEGER(), autoincrement=False, nullable=True)) + op.add_column('audit', sa.Column('url', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.alter_column('audit', 'action', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('audit', 'model', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('audit', 'timestamp', + existing_type=postgresql.TIMESTAMP(), + nullable=True) + # ### end Alembic commands ### diff --git a/alembic/versions/8c4bd1aaf45a_create_model.py b/alembic/versions/8c4bd1aaf45a_create_model.py deleted file mode 100644 index d467b1f9..00000000 --- a/alembic/versions/8c4bd1aaf45a_create_model.py +++ /dev/null @@ -1,118 +0,0 @@ -"""Create model - -Revision ID: 8c4bd1aaf45a -Revises: -Create Date: 2024-02-22 13:19:29.947241 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '8c4bd1aaf45a' -down_revision: Union[str, None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('companies', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_companies_id'), 'companies', ['id'], unique=False) - op.create_index(op.f('ix_companies_name'), 'companies', ['name'], unique=True) - op.create_table('roles', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(length=100), nullable=True), - sa.Column('description', sa.Text(), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_roles_id'), 'roles', ['id'], unique=False) - op.create_index(op.f('ix_roles_name'), 'roles', ['name'], unique=False) - op.create_table('departments', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(), nullable=True), - sa.Column('company_id', sa.Integer(), nullable=True), - sa.Column('total_users', sa.Integer(), nullable=True), - sa.Column('total_documents', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['company_id'], ['companies.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_departments_id'), 'departments', ['id'], unique=False) - op.create_index(op.f('ix_departments_name'), 'departments', ['name'], unique=True) - op.create_table('subscriptions', - sa.Column('sub_id', sa.Integer(), nullable=False), - sa.Column('company_id', sa.Integer(), nullable=True), - sa.Column('start_date', sa.DateTime(), nullable=True), - sa.Column('end_date', sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint(['company_id'], ['companies.id'], ), - sa.PrimaryKeyConstraint('sub_id') - ) - op.create_index(op.f('ix_subscriptions_sub_id'), 'subscriptions', ['sub_id'], unique=False) - op.create_table('users', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('email', sa.String(length=225), nullable=False), - sa.Column('hashed_password', sa.String(), nullable=False), - sa.Column('fullname', sa.String(length=225), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('last_login', sa.DateTime(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=True), - sa.Column('updated_at', sa.DateTime(), nullable=True), - sa.Column('company_id', sa.Integer(), nullable=True), - sa.Column('department_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['company_id'], ['companies.id'], ), - sa.ForeignKeyConstraint(['department_id'], ['departments.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email'), - sa.UniqueConstraint('fullname'), - sa.UniqueConstraint('fullname', name='unique_username_no_spacing') - ) - op.create_table('document', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('filename', sa.String(length=225), nullable=False), - sa.Column('uploaded_by', sa.Integer(), nullable=False), - sa.Column('uploaded_at', sa.DateTime(), nullable=False), - sa.Column('department_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['department_id'], ['departments.id'], ), - sa.ForeignKeyConstraint(['uploaded_by'], ['users.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('filename') - ) - op.create_index(op.f('ix_document_id'), 'document', ['id'], unique=False) - op.create_table('user_roles', - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('role_id', sa.Integer(), nullable=False), - sa.Column('company_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['company_id'], ['companies.id'], ), - sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), - sa.PrimaryKeyConstraint('user_id', 'role_id', 'company_id'), - sa.UniqueConstraint('user_id', 'role_id', 'company_id', name='unique_user_role') - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('user_roles') - op.drop_index(op.f('ix_document_id'), table_name='document') - op.drop_table('document') - op.drop_table('users') - op.drop_index(op.f('ix_subscriptions_sub_id'), table_name='subscriptions') - op.drop_table('subscriptions') - op.drop_index(op.f('ix_departments_name'), table_name='departments') - op.drop_index(op.f('ix_departments_id'), table_name='departments') - op.drop_table('departments') - op.drop_index(op.f('ix_roles_name'), table_name='roles') - op.drop_index(op.f('ix_roles_id'), table_name='roles') - op.drop_table('roles') - op.drop_index(op.f('ix_companies_name'), table_name='companies') - op.drop_index(op.f('ix_companies_id'), table_name='companies') - op.drop_table('companies') - # ### end Alembic commands ### diff --git a/alembic/versions/b526c0676007_audit_log_update.py b/alembic/versions/b526c0676007_audit_log_update.py new file mode 100644 index 00000000..8f5163d3 --- /dev/null +++ b/alembic/versions/b526c0676007_audit_log_update.py @@ -0,0 +1,50 @@ +"""Audit log update + +Revision ID: b526c0676007 +Revises: eb0a7f182c75 +Create Date: 2024-02-24 12:00:11.632187 + +""" +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 = 'b526c0676007' +down_revision: Union[str, None] = 'eb0a7f182c75' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('audit', sa.Column('timestamp', sa.DateTime(), nullable=True)) + op.add_column('audit', sa.Column('user_id', sa.Integer(), nullable=True)) + op.add_column('audit', sa.Column('model', sa.String(), nullable=True)) + op.add_column('audit', sa.Column('model_id', sa.Integer(), nullable=True)) + op.add_column('audit', sa.Column('action', sa.String(), nullable=True)) + op.add_column('audit', sa.Column('details', postgresql.JSONB(astext_type=sa.Text()), nullable=True)) + op.create_foreign_key(None, 'audit', 'users', ['user_id'], ['id']) + op.drop_column('audit', 'headers') + op.drop_column('audit', 'response') + op.drop_column('audit', 'method') + # op.create_unique_constraint('unique_user_role', 'user_roles', ['user_id', 'role_id', 'company_id']) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + # op.drop_constraint('unique_user_role', 'user_roles', type_='unique') + op.add_column('audit', sa.Column('method', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column('audit', sa.Column('response', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column('audit', sa.Column('headers', postgresql.ARRAY(sa.VARCHAR()), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'audit', type_='foreignkey') + op.drop_column('audit', 'details') + op.drop_column('audit', 'action') + op.drop_column('audit', 'model_id') + op.drop_column('audit', 'model') + op.drop_column('audit', 'user_id') + op.drop_column('audit', 'timestamp') + # ### end Alembic commands ### diff --git a/alembic/versions/36beb9b73c64_update_department_model.py b/alembic/versions/eb0a7f182c75_audit_log.py similarity index 50% rename from alembic/versions/36beb9b73c64_update_department_model.py rename to alembic/versions/eb0a7f182c75_audit_log.py index 9b511dfd..c43a7de7 100644 --- a/alembic/versions/36beb9b73c64_update_department_model.py +++ b/alembic/versions/eb0a7f182c75_audit_log.py @@ -1,8 +1,8 @@ -"""update department model +"""Audit log -Revision ID: 36beb9b73c64 -Revises: 0aeaf9df35a6 -Create Date: 2024-02-21 15:12:07.840057 +Revision ID: eb0a7f182c75 +Revises: +Create Date: 2024-02-24 11:37:43.713632 """ from typing import Sequence, Union @@ -12,16 +12,23 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision: str = '36beb9b73c64' -down_revision: Union[str, None] = '0aeaf9df35a6' +revision: str = 'eb0a7f182c75' +down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.add_column('departments', sa.Column('total_users', sa.Integer(), nullable=True)) - op.add_column('departments', sa.Column('total_documents', sa.Integer(), nullable=True)) + op.create_table('audit', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('url', sa.String(), nullable=True), + sa.Column('headers', sa.ARRAY(sa.String()), nullable=True), + sa.Column('method', sa.String(), nullable=True), + sa.Column('response', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_audit_id'), 'audit', ['id'], unique=False) # op.create_unique_constraint('unique_user_role', 'user_roles', ['user_id', 'role_id', 'company_id']) # ### end Alembic commands ### @@ -29,6 +36,6 @@ def upgrade() -> None: def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### # op.drop_constraint('unique_user_role', 'user_roles', type_='unique') - op.drop_column('departments', 'total_documents') - op.drop_column('departments', 'total_users') + op.drop_index(op.f('ix_audit_id'), table_name='audit') + op.drop_table('audit') # ### end Alembic commands ### diff --git a/private_gpt/__main__.py b/private_gpt/__main__.py index fad84442..cf752232 100644 --- a/private_gpt/__main__.py +++ b/private_gpt/__main__.py @@ -1,5 +1,10 @@ # start a fastapi server with uvicorn +from datetime import datetime +from fastapi.middleware import Middleware +from private_gpt.users.db.session import SessionLocal +from private_gpt.users.models import Audit, User, Department, Document +from private_gpt.users.api.deps import get_audit_logger, get_db import uvicorn from private_gpt.main import app @@ -10,6 +15,7 @@ from private_gpt.constants import UPLOAD_DIR # Set log_config=None to do not use the uvicorn logging configuration, and # use ours instead. For reference, see below: # https://github.com/tiangolo/fastapi/discussions/7457#discussioncomment-5141108 + app.mount("/static", StaticFiles(directory=UPLOAD_DIR), name="static") uvicorn.run(app, host="0.0.0.0", port=settings().server.port, log_config=None) diff --git a/private_gpt/server/ingest/ingest_router.py b/private_gpt/server/ingest/ingest_router.py index bbd673af..00b4fc2f 100644 --- a/private_gpt/server/ingest/ingest_router.py +++ b/private_gpt/server/ingest/ingest_router.py @@ -131,6 +131,7 @@ def delete_ingested(request: Request, doc_id: str) -> None: def delete_file( request: Request, delete_input: DeleteFilename, + log_audit: models.Audit = Depends(deps.get_audit_logger), db: Session = Depends(deps.get_db), current_user: models.User = Security( deps.get_current_user, @@ -159,9 +160,12 @@ def delete_file( print("Unable to delete file from the static directory") document = crud.documents.get_by_filename(db,file_name=filename) if document: + log_audit(model='Document', action='delete', + details={"status": "SUCCESS", "message": f"{filename}' successfully deleted."}, user_id=current_user.id) crud.documents.remove(db=db, id=document.id) return {"status": "SUCCESS", "message": f"{filename}' successfully deleted."} except Exception as e: + print(traceback.print_exc()) logger.error( f"Unexpected error deleting documents with filename '{filename}': {str(e)}") raise HTTPException( @@ -171,6 +175,8 @@ def delete_file( @ingest_router.post("/ingest/file", response_model=IngestResponse, tags=["Ingestion"]) def ingest_file( request: Request, + log_audit: models.Audit = Depends(deps.get_audit_logger), + db: Session = Depends(deps.get_db), file: UploadFile = File(...), current_user: models.User = Security( @@ -210,12 +216,23 @@ def ingest_file( with open(upload_path, "rb") as f: ingested_documents = service.ingest_bin_data(file.filename, f) logger.info(f"{file.filename} is uploaded by the {current_user.fullname}.") - - return IngestResponse(object="list", model="private-gpt", data=ingested_documents) + response = IngestResponse( + object="list", model="private-gpt", data=ingested_documents) + log_audit(model='Document', action='create', + details={ + 'status': '200', + 'filename': file.filename, + 'user': current_user.fullname, + }, user_id=current_user.id) + return response except HTTPException: + print(traceback.print_exc()) raise except Exception as e: + print(traceback.print_exc()) + log_audit(model='Document', action='create', + details={"status": 500, "detail": "Internal Server Error: Unable to ingest file.", }, user_id=current_user.id) logger.error(f"There was an error uploading the file(s): {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -225,11 +242,13 @@ def ingest_file( async def common_ingest_logic( request: Request, + db: Session, ocr_file, current_user, ): service = request.state.injector.get(IngestService) + log_audit: models.Audit = Depends(deps.get_audit_logger) try: with open(ocr_file, 'rb') as file: file_name = Path(ocr_file).name @@ -257,6 +276,8 @@ async def common_ingest_logic( f.write(file.read()) file.seek(0) # Move the file pointer back to the beginning ingested_documents = service.ingest_bin_data(file_name, file) + log_audit(model='Document', action='create', + details={'status': 200, 'message': "file uploaded successfully."}, user_id=current_user.id) logger.info( f"{file_name} is uploaded by the {current_user.fullname}.") @@ -264,10 +285,13 @@ async def common_ingest_logic( return ingested_documents except HTTPException: + print(traceback.print_exc()) raise except Exception as e: - logger.error(f"There was an error uploading the file(s): {str(e)}") + print(traceback.print_exc()) + log_audit(model='Document', action='create', + details={"status": 500, "detail": "Internal Server Error: Unable to ingest file.", }, user_id=current_user.id) raise HTTPException( status_code=500, detail="Internal Server Error: Unable to ingest file.", diff --git a/private_gpt/users/api/deps.py b/private_gpt/users/api/deps.py index 83abe934..274b51a0 100644 --- a/private_gpt/users/api/deps.py +++ b/private_gpt/users/api/deps.py @@ -1,3 +1,4 @@ +from fastapi import Request, Depends, HTTPException import logging from private_gpt.users.core.config import settings from private_gpt.users.constants.role import Role @@ -5,7 +6,7 @@ from typing import Union, Any, Generator from datetime import datetime from private_gpt.users import crud, models, schemas from private_gpt.users.db.session import SessionLocal -from fastapi import Depends, HTTPException, status +from fastapi import Depends, HTTPException, Request, status from fastapi.security import OAuth2PasswordBearer, SecurityScopes from private_gpt.users.core.security import ( ALGORITHM, @@ -13,6 +14,7 @@ from private_gpt.users.core.security import ( ) from fastapi import Depends, HTTPException, Security, status from jose import jwt +from private_gpt.users.utils.audit import log_audit_entry from pydantic import ValidationError from private_gpt.users.constants.role import Role from private_gpt.users.schemas.token import TokenPayload @@ -130,3 +132,10 @@ def get_active_subscription( status_code=status.HTTP_403_FORBIDDEN, detail="Access Forbidden - No Active Subscription", ) + + +def get_audit_logger(request: Request, db: Session = Depends(get_db)): + try: + return lambda model, action, details, user_id=None: log_audit_entry(db, model, action, details, user_id) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error in get_audit_logger: {str(e)}") diff --git a/private_gpt/users/api/v1/api.py b/private_gpt/users/api/v1/api.py index 0c6f0779..53cb59c5 100644 --- a/private_gpt/users/api/v1/api.py +++ b/private_gpt/users/api/v1/api.py @@ -1,4 +1,4 @@ -from private_gpt.users.api.v1.routers import auth, roles, user_roles, users, subscriptions, companies, departments, documents +from private_gpt.users.api.v1.routers import auth, roles, user_roles, users, subscriptions, companies, departments, documents, audits from fastapi import APIRouter api_router = APIRouter(prefix="/v1") @@ -11,4 +11,5 @@ api_router.include_router(companies.router) api_router.include_router(subscriptions.router) api_router.include_router(departments.router) api_router.include_router(documents.router) +api_router.include_router(audits.router) diff --git a/private_gpt/users/api/v1/routers/audits.py b/private_gpt/users/api/v1/routers/audits.py new file mode 100644 index 00000000..86373858 --- /dev/null +++ b/private_gpt/users/api/v1/routers/audits.py @@ -0,0 +1,30 @@ +from typing import Any, List + +from sqlalchemy.orm import Session +from fastapi.responses import JSONResponse +from fastapi.encoders import jsonable_encoder +from fastapi import APIRouter, Depends, HTTPException, status, Security + +from private_gpt.users.api import deps +from private_gpt.users.constants.role import Role +from private_gpt.users import crud, models, schemas + + +router = APIRouter(prefix="/audit", tags=["Companies"]) + + +@router.get("", response_model=List[schemas.Audit]) +def list_companies( + db: Session = Depends(deps.get_db), + skip: int = 0, + limit: int = 100, + current_user: models.User = Security( + deps.get_current_user, + scopes=[Role.SUPER_ADMIN["name"]], + ), +) -> List[schemas.Audit]: + """ + Retrieve a list of companies with pagination support. + """ + logs = crud.audit.get_multi(db, skip=skip, limit=limit) + return logs diff --git a/private_gpt/users/api/v1/routers/auth.py b/private_gpt/users/api/v1/routers/auth.py index 326ffa99..ba3e4ffe 100644 --- a/private_gpt/users/api/v1/routers/auth.py +++ b/private_gpt/users/api/v1/routers/auth.py @@ -19,7 +19,7 @@ logger = logging.getLogger(__name__) LDAP_SERVER = settings.LDAP_SERVER # LDAP_ENABLE = settings.LDAP_ENABLE -LDAP_ENABLE = True +LDAP_ENABLE = False router = APIRouter(prefix="/auth", tags=["auth"]) @@ -108,6 +108,7 @@ def ad_user_register( @router.post("/login", response_model=schemas.TokenSchema) def login_access_token( + log_audit: models.Audit = Depends(deps.get_audit_logger), db: Session = Depends(deps.get_db), form_data: OAuth2PasswordRequestForm = Depends(), ) -> Any: @@ -168,6 +169,8 @@ def login_access_token( "user": token_payload, "token_type": "bearer", } + log_audit(model='User', action='update', + details=token_payload, user_id=user.id) return JSONResponse(content=response_dict) @@ -197,6 +200,8 @@ def refresh_access_token( @router.post("/register", response_model=schemas.TokenSchema) def register( *, + log_audit: models.Audit = Depends(deps.get_audit_logger), + db: Session = Depends(deps.get_db), email: str = Body(...), fullname: str = Body(...), @@ -218,6 +223,8 @@ def register( existing_user = crud.user.get_by_email(db, email=email) if existing_user: + log_audit(model='User', action='creation', + details={"status": '409', 'detail': "The user with this email already exists!", }, user_id=current_user.id) raise HTTPException( status_code=409, detail="The user with this email already exists!", @@ -245,6 +252,8 @@ def register( ) user_role_name = role_name or Role.GUEST["name"] user_role = create_user_role(db, user, user_role_name, company) + log_audit(model='user_roles', action='creation', + details={"status": '201', 'detail': "User role created successfully.", }, user_id=current_user.id) except Exception as e: print(traceback.format_exc()) raise HTTPException( @@ -258,4 +267,7 @@ def register( "token_type": "bearer", "password": random_password, } + log_audit(model='User', action='creation', + details={"status": '201', 'detail': "User created successfully.", }, user_id=current_user.id) + return JSONResponse(content=response_dict, status_code=status.HTTP_201_CREATED) diff --git a/private_gpt/users/api/v1/routers/departments.py b/private_gpt/users/api/v1/routers/departments.py index 61ff4c4b..c8ec0fa4 100644 --- a/private_gpt/users/api/v1/routers/departments.py +++ b/private_gpt/users/api/v1/routers/departments.py @@ -1,4 +1,5 @@ -from typing import Any, List +import logging +import traceback from sqlalchemy.orm import Session from fastapi.responses import JSONResponse @@ -9,28 +10,47 @@ from private_gpt.users.api import deps from private_gpt.users.constants.role import Role from private_gpt.users import crud, models, schemas - +logger = logging.getLogger(__name__) router = APIRouter(prefix="/departments", tags=["Departments"]) -@router.get("", response_model=List[schemas.Department]) -def list_deparments( +def log_audit_department( + db: Session, + current_user: models.User, + action: str, + details: dict +): + try: + audit_entry = models.Audit( + user_id=current_user.id, + model='Department', + action=action, + details=details, + ) + db.add(audit_entry) + db.commit() + except Exception as e: + print(traceback.format_exc()) + + +@router.get("", response_model=list[schemas.Department]) +def list_departments( db: Session = Depends(deps.get_db), skip: int = 0, limit: int = 100, current_user: models.User = Security( deps.get_current_user, ), -) -> List[schemas.Department]: +) -> list[schemas.Department]: """ - Retrieve a list of department with pagination support. + Retrieve a list of departments with pagination support. """ - deparments = crud.department.get_multi(db, skip=skip, limit=limit) - return deparments + departments = crud.department.get_multi(db, skip=skip, limit=limit) + return departments @router.post("/create", response_model=schemas.Department) -def create_deparment( +def create_department( department_in: schemas.DepartmentCreate, db: Session = Depends(deps.get_db), current_user: models.User = Security( @@ -41,18 +61,35 @@ def create_deparment( """ Create a new department """ - company_id = current_user.company_id - department_create_in = schemas.DepartmentAdminCreate(name=department_in.name, company_id=company_id) - department = crud.department.create(db=db, obj_in=department_create_in) - department = jsonable_encoder(department) + try: + company_id = current_user.company_id + department_create_in = schemas.DepartmentAdminCreate( + name=department_in.name, company_id=company_id) + department = crud.department.create(db=db, obj_in=department_create_in) + department1 = jsonable_encoder(department) - return JSONResponse( - status_code=status.HTTP_201_CREATED, - content={ - "message": "Department created successfully", - "department": department - }, - ) + details = { + 'user_id': current_user.id, + 'department_id': department.id, + 'department_name': department.name + } + + log_audit_department(db, current_user, 'create', details) + + return JSONResponse( + status_code=status.HTTP_201_CREATED, + content={ + "message": "Department created successfully", + "department": department1 + }, + ) + except Exception as e: + print(traceback.format_exc()) + logger.error(f"Error creating department: {str(e)}") + raise HTTPException( + status_code=500, + detail="Internal Server Error", + ) @router.post("/read", response_model=schemas.Department) @@ -67,10 +104,28 @@ def read_department( """ Read a Department by ID """ - department = crud.department.get_by_id(db, id=department_id) - if department is None: - raise HTTPException(status_code=404, detail="department not found") - return department + try: + department = crud.department.get_by_id(db, id=department_id) + if department is None: + raise HTTPException(status_code=404, detail="Department not found") + + details = { + 'status': 200, + 'user_id': current_user.id, + 'department_id': department.id, + 'department_name': department.name + } + + log_audit_department(db, current_user, 'read', details) + + return department + except Exception as e: + print(traceback.format_exc()) + logger.error(f"Error reading department: {str(e)}") + raise HTTPException( + status_code=500, + detail="Internal Server Error", + ) @router.post("/update", response_model=schemas.Department) @@ -85,20 +140,40 @@ def update_department( """ Update a Department by ID """ - department = crud.department.get_by_id(db, id=department_in.id) - if department is None: - raise HTTPException(status_code=404, detail="department not found") + try: + department = crud.department.get_by_id(db, id=department_in.id) + old_name = department.name + if department is None: + raise HTTPException(status_code=404, detail="Department not found") - updated_department = crud.department.update( - db=db, db_obj=department, obj_in=department_in) - updated_department = jsonable_encoder(updated_department) - return JSONResponse( - status_code=status.HTTP_200_OK, - content={ - "message": f"{department_in} department updated successfully", - "department": updated_department - }, - ) + updated_department = crud.department.update( + db=db, db_obj=department, obj_in=department_in) + updated_department = jsonable_encoder(updated_department) + + details = { + 'status': '200', + 'user_id': current_user.id, + 'department_id': department.id, + 'old_department_name': old_name, + 'new_department_name': department.name, + } + + log_audit_department(db, current_user, 'update', details) + + return JSONResponse( + status_code=status.HTTP_200_OK, + content={ + "message": f"Department updated successfully", + "department": updated_department + }, + ) + except Exception as e: + print(traceback.format_exc()) + logger.error(f"Error updating department: {str(e)}") + raise HTTPException( + status_code=500, + detail="Internal Server Error", + ) @router.post("/delete", response_model=schemas.Department) @@ -113,19 +188,36 @@ def delete_department( """ Delete a Department by ID """ - department_id = department_in.id - department = crud.department.get(db, id=department_id) - if department is None: - raise HTTPException(status_code=404, detail="User not found") + try: + department_id = department_in.id + department = crud.department.get(db, id=department_id) + if department is None: + raise HTTPException(status_code=404, detail="Department not found") - department = crud.department.remove(db=db, id=department_id) - if department is None: - raise HTTPException(status_code=404, detail="department not found") - department = jsonable_encoder(department) - return JSONResponse( - status_code=status.HTTP_200_OK, - content={ - "message": "Department deleted successfully", - "deparment": department, - }, - ) + details = { + 'status': 200, + 'user_id': current_user.id, + 'department_id': department.id, + 'department_name': department.name + } + + log_audit_department(db, current_user, 'delete', details) + + deleted_department = crud.department.remove(db=db, id=department_id) + deleted_department = jsonable_encoder(deleted_department) + + + return JSONResponse( + status_code=status.HTTP_200_OK, + content={ + "message": "Department deleted successfully", + "department": deleted_department, + }, + ) + except Exception as e: + print(traceback.format_exc()) + logger.error(f"Error deleting department: {str(e)}") + raise HTTPException( + status_code=500, + detail="Internal Server Error", + ) diff --git a/private_gpt/users/api/v1/routers/users.py b/private_gpt/users/api/v1/routers/users.py index 5249fe0b..62a76b54 100644 --- a/private_gpt/users/api/v1/routers/users.py +++ b/private_gpt/users/api/v1/routers/users.py @@ -1,3 +1,4 @@ +import traceback from typing import Any, List, Optional from sqlalchemy.orm import Session @@ -14,6 +15,26 @@ from private_gpt.users.core.security import verify_password, get_password_hash router = APIRouter(prefix="/users", tags=["users"]) + +def log_audit_user( + db: Session, + current_user: models.User, + action: str, + details: dict +): + try: + audit_entry = models.Audit( + user_id=current_user.id, + model='User', + action=action, + details=details, + ) + db.add(audit_entry) + db.commit() + except Exception as e: + print(traceback.format_exc()) + + @router.get("", response_model=List[schemas.User]) def read_users( skip: int = 0, @@ -78,6 +99,17 @@ def create_user( detail="The user with this email already exists in the system.", ) user = crud.user.create(db, obj_in=user_in) + + details = { + 'admin_id': current_user.id, + 'user_id': user.id, + 'email': user.email, + 'fullname': user.fullname, + 'company_id': user.company_id, + 'department_id': user.department_id, + } + log_audit_user(db, current_user, 'create', details) + return JSONResponse( status_code=status.HTTP_201_CREATED, content={"message": "User created successfully", "user": jsonable_encoder(user)}, @@ -102,6 +134,14 @@ def update_username( company_id=user.company_id, department_id=user.department_id, ) + details = { + 'user_id': user.id, + 'email': user.email, + 'fullname': user.fullname, + 'company_id': user.company_id, + 'department_id': user.department_id, + } + log_audit_user(db, current_user, 'update_username', details) return JSONResponse( status_code=status.HTTP_200_OK, content={"message": "Username updated successfully", @@ -158,6 +198,15 @@ def change_password( company_id= current_user.company_id, department_id=current_user.department_id, ) + + details = { + 'user_id': current_user.id, + 'email': current_user.email, + 'fullname': current_user.fullname, + 'company_id': current_user.company_id, + 'department_id': current_user.department_id, + } + log_audit_user(db, current_user, 'change_password', details) return JSONResponse( status_code=status.HTTP_200_OK, @@ -201,6 +250,7 @@ def update_user( Update a user. """ user = crud.user.get(db, id=user_id) + if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -214,6 +264,14 @@ def update_user( company_id=user.company_id, department_id=user.department_id, ) + details = { + 'user_id': user.id, + 'email': user.email, + 'fullname': user.fullname, + 'company_id': user.company_id, + 'department_id': user.department_id, + } + log_audit_user(db, current_user, 'update user', details) return JSONResponse( status_code=status.HTTP_200_OK, content={"message": "User updated successfully", "user": jsonable_encoder(user_data)}, @@ -286,6 +344,18 @@ def delete_user( """ user_id = delete_user.id user = crud.user.get(db, id=user_id) + + details = { + 'admin_id': current_user.id, + 'deleted_user_id': user.id, + 'email': user.email, + 'fullname': user.fullname, + 'company_id': user.company_id, + 'department_id': user.department_id, + } + + log_audit_user(db, current_user, 'delete', details) + if user is None: raise HTTPException(status_code=404, detail="User not found") crud.user.remove(db, id=user_id) @@ -340,6 +410,17 @@ def admin_update_user( role = crud.user_role.update(db, db_obj=user_role, obj_in=role_in) user_in = schemas.UserAdmin(fullname=user_update.fullname, department_id=user_update.department_id) + + details = { + 'admin_id': current_user.id, + 'user_id': existing_user.id, + 'email': existing_user.email, + 'fullname': existing_user.fullname, + 'company_id': existing_user.company_id, + 'department_id': existing_user.department_id, + } + log_audit_user(db, current_user, 'admin_update', details) + crud.user.update(db, db_obj=existing_user, obj_in=user_in) return JSONResponse( diff --git a/private_gpt/users/core/config.py b/private_gpt/users/core/config.py index 4cc02b24..34d668cc 100644 --- a/private_gpt/users/core/config.py +++ b/private_gpt/users/core/config.py @@ -8,7 +8,7 @@ SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{username}:{password}@{host}:{p port='5432', db_name='QuickGpt', username='postgres', - password="admin", + password="quick", ) class Settings(BaseSettings): diff --git a/private_gpt/users/core/security.py b/private_gpt/users/core/security.py index 334eeca1..b053d75f 100644 --- a/private_gpt/users/core/security.py +++ b/private_gpt/users/core/security.py @@ -61,3 +61,4 @@ def verify_refresh_token(token: str) -> Optional[Dict[str, Any]]: return payload except JWTError: return None + diff --git a/private_gpt/users/crud/__init__.py b/private_gpt/users/crud/__init__.py index ba64120a..fd7663a9 100644 --- a/private_gpt/users/crud/__init__.py +++ b/private_gpt/users/crud/__init__.py @@ -5,3 +5,4 @@ from .company_crud import company from .subscription_crud import subscription from .document_crud import documents from .department_crud import department +from .audit_crud import audit \ No newline at end of file diff --git a/private_gpt/users/crud/audit_crud.py b/private_gpt/users/crud/audit_crud.py new file mode 100644 index 00000000..d6ec3ccd --- /dev/null +++ b/private_gpt/users/crud/audit_crud.py @@ -0,0 +1,14 @@ +from typing import Optional + +from private_gpt.users.crud.base import CRUDBase +from private_gpt.users.models.audit import Audit +from private_gpt.users.schemas.audit import AuditCreate, AuditUpdate +from sqlalchemy.orm import Session + + +class CRUDAudit(CRUDBase[Audit, AuditCreate, AuditUpdate]): + def get_by_id(self, db: Session, *, id: str) -> Optional[Audit]: + return db.query(self.model).filter(Audit.id == id).first() + + +audit = CRUDAudit(Audit) diff --git a/private_gpt/users/models/__init__.py b/private_gpt/users/models/__init__.py index 0a0227f0..9063335e 100644 --- a/private_gpt/users/models/__init__.py +++ b/private_gpt/users/models/__init__.py @@ -4,4 +4,5 @@ from .user_role import UserRole from .role import Role from .document import Document from .subscription import Subscription -from .department import Department \ No newline at end of file +from .department import Department +from .audit import Audit \ No newline at end of file diff --git a/private_gpt/users/models/audit.py b/private_gpt/users/models/audit.py new file mode 100644 index 00000000..0855473c --- /dev/null +++ b/private_gpt/users/models/audit.py @@ -0,0 +1,18 @@ +from datetime import datetime +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String +from private_gpt.users.db.base_class import Base +from sqlalchemy.dialects.postgresql import JSONB + + +class Audit(Base): + __tablename__ = "audit" + + id = Column(Integer, primary_key=True, index=True) + timestamp = Column(DateTime, nullable=False, default=datetime.utcnow) + user_id = Column(Integer, ForeignKey("users.id"), nullable=True) + model = Column(String, nullable=False) + action = Column(String, nullable=False) + details = Column(JSONB, nullable=True) + + def __repr__(self): + return f"" diff --git a/private_gpt/users/models/department.py b/private_gpt/users/models/department.py index fb54fbe9..ba23e52c 100644 --- a/private_gpt/users/models/department.py +++ b/private_gpt/users/models/department.py @@ -3,8 +3,8 @@ from sqlalchemy.orm import relationship, Session from sqlalchemy import Column, Integer, String from private_gpt.users.db.base_class import Base -from private_gpt.users.models.document import Document -from private_gpt.users.models.user import User +# from private_gpt.users.models.document import Document +# from private_gpt.users.models.user import User class Department(Base): @@ -25,21 +25,25 @@ class Department(Base): total_documents = Column(Integer, default=0) - -def update_total_users(mapper, connection, target): - session = Session(bind=connection) - target.total_users = session.query(User).filter_by( - department_id=target.id).count() +# @event.listens_for(Department, 'after_insert') +# @event.listens_for(Department, 'after_update') +# def update_total_users(mapper, connection, target): +# print("--------------------------------------------------------------Calling Event User------------------------------------------------------------------------") +# connection.execute( +# Department.__table__.update(). +# where(Department.id == target.id). +# values(total_users=Session.object_session(target).query( +# User).filter_by(department_id=target.id).count()) +# ) -def update_total_documents(mapper, connection, target): - session = Session(bind=connection) - target.total_documents = session.query( - Document).filter_by(department_id=target.id).count() - - -# Attach event listeners to Department model -event.listen(Department, 'after_insert', update_total_users) -event.listen(Department, 'after_update', update_total_users) -event.listen(Department, 'after_insert', update_total_documents) -event.listen(Department, 'after_update', update_total_documents) +# @event.listens_for(Department, 'after_insert') +# @event.listens_for(Department, 'after_update') +# def update_total_documents(mapper, connection, target): +# print("--------------------------------------------------------------Calling Event Department------------------------------------------------------------------------") +# connection.execute( +# Department.__table__.update(). +# where(Department.id == target.id). +# values(total_documents=Session.object_session(target).query( +# Document).filter_by(department_id=target.id).count()) +# ) diff --git a/private_gpt/users/models/document.py b/private_gpt/users/models/document.py index fbed63c1..66143303 100644 --- a/private_gpt/users/models/document.py +++ b/private_gpt/users/models/document.py @@ -1,7 +1,10 @@ from private_gpt.users.db.base_class import Base + from datetime import datetime +from sqlalchemy import event, select, func, update from sqlalchemy.orm import relationship from sqlalchemy import Column, Integer, String, ForeignKey, DateTime +from private_gpt.users.models.department import Department class Document(Base): @@ -25,6 +28,27 @@ class Document(Base): department_id = Column(Integer, ForeignKey( "departments.id"), nullable=False) - uploaded_by_user = relationship( - "User", back_populates="uploaded_documents") - department = relationship("Department", back_populates="documents") \ No newline at end of file + + department = relationship("Department", back_populates="documents") + + +@event.listens_for(Document, 'after_insert') +@event.listens_for(Document, 'after_delete') +def update_total_documents(mapper, connection, target): + department_id = target.department_id + print(f"Department ID is: {department_id}") + + # Use SQLAlchemy's ORM constructs for better readability and maintainability: + total_documents = connection.execute( + select([func.count()]).select_from(Document).where( + Document.department_id == department_id) + ).scalar() + + print(f"Total documents is: {total_documents}") + print("Updating total documents") + + # Use the correct update construct for SQLAlchemy: + connection.execute( + update(Department).values(total_documents=total_documents).where( + Department.id == department_id) + ) diff --git a/private_gpt/users/models/user.py b/private_gpt/users/models/user.py index 6ea22df5..08ccdb0f 100644 --- a/private_gpt/users/models/user.py +++ b/private_gpt/users/models/user.py @@ -9,9 +9,10 @@ from sqlalchemy import ( DateTime, ForeignKey ) +from sqlalchemy import event, func, select, update from sqlalchemy.orm import relationship from private_gpt.users.db.base_class import Base - +from private_gpt.users.models.department import Department class User(Base): """Models a user table""" __tablename__ = "users" @@ -53,3 +54,21 @@ class User(Base): __table_args__ = ( UniqueConstraint('fullname', name='unique_username_no_spacing'), ) + + +@event.listens_for(User, 'after_insert') +@event.listens_for(User, 'after_delete') +def update_total_users(mapper, connection, target): + department_id = target.department_id + print(f"Department ID is: {department_id}") + total_users = connection.execute( + select([func.count()]).select_from(User).where( + User.department_id == department_id) + ).scalar() + print(f"Total users is: {total_users}") + connection.execute( + update(Department).values(total_users=total_users).where( + Department.id == department_id) + ) + + diff --git a/private_gpt/users/schemas/__init__.py b/private_gpt/users/schemas/__init__.py index 4493df33..a92b0625 100644 --- a/private_gpt/users/schemas/__init__.py +++ b/private_gpt/users/schemas/__init__.py @@ -5,4 +5,5 @@ from .user_role import UserRole, UserRoleCreate, UserRoleInDB, UserRoleUpdate from .subscription import Subscription, SubscriptionBase, SubscriptionCreate, SubscriptionUpdate from .company import Company, CompanyBase, CompanyCreate, CompanyUpdate from .documents import Document, DocumentCreate, DocumentsBase, DocumentUpdate, DocumentList -from .department import Department, DepartmentCreate, DepartmentUpdate, DepartmentAdminCreate, DepartmentDelete \ No newline at end of file +from .department import Department, DepartmentCreate, DepartmentUpdate, DepartmentAdminCreate, DepartmentDelete +from .audit import AuditBase, AuditCreate, AuditUpdate, Audit \ No newline at end of file diff --git a/private_gpt/users/schemas/audit.py b/private_gpt/users/schemas/audit.py new file mode 100644 index 00000000..c2991fcb --- /dev/null +++ b/private_gpt/users/schemas/audit.py @@ -0,0 +1,30 @@ +from datetime import datetime +from typing import Optional +from pydantic import BaseModel + + +class AuditBase(BaseModel): + id: int + model: str + user_id: int + action: str + details: dict + timestamp: Optional[datetime] + + +class AuditCreate(AuditBase): + pass + + +class AuditUpdate(AuditBase): + id: int + + +class AuditInDB(AuditBase): + id: int + + class Config: + orm_mode = True + +class Audit(AuditBase): + pass \ No newline at end of file diff --git a/private_gpt/users/utils/ad_auth.py b/private_gpt/users/utils/ad_auth.py index 1c7717a6..c71f73de 100644 --- a/private_gpt/users/utils/ad_auth.py +++ b/private_gpt/users/utils/ad_auth.py @@ -7,7 +7,6 @@ class Ldap: self.server = ldap3.Server(server_uri, get_info=ldap3.ALL) print(f"Connected to ldap server: {self.server}") self.conn = ldap3.Connection(self.server, user=ldap_user, password=ldap_pass, auto_bind=True) - print(self.conn) def who_am_i(self): return self.conn.extend.standard.who_am_i() diff --git a/private_gpt/users/utils/audit.py b/private_gpt/users/utils/audit.py new file mode 100644 index 00000000..5c8ff640 --- /dev/null +++ b/private_gpt/users/utils/audit.py @@ -0,0 +1,24 @@ +from datetime import datetime +from sqlalchemy.orm import Session +from private_gpt.users.models.audit import Audit + +def log_audit_entry( + session: Session, + model: str, + action: str, + details: dict, + user_id: int = None, +): + audit_entry = Audit( + timestamp=datetime.utcnow(), + user_id=user_id, + model=model, + action=action, + details=details, + ) + + session.add(audit_entry) + session.commit() + + +