Added audit logs

This commit is contained in:
Saurab-Shrestha 2024-02-24 18:57:49 +05:45
parent 59d9413898
commit 493963908d
28 changed files with 596 additions and 329 deletions

2
.env
View File

@ -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

View File

@ -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

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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)

View File

@ -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.",

View File

@ -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)}")

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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",
)

View File

@ -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(

View File

@ -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):

View File

@ -61,3 +61,4 @@ def verify_refresh_token(token: str) -> Optional[Dict[str, Any]]:
return payload
except JWTError:
return None

View File

@ -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

View File

@ -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)

View File

@ -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
from .department import Department
from .audit import Audit

View File

@ -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"<Audit(id={self.id}, timestamp={self.timestamp}, user_id={self.user_id}, model={self.model}, action={self.action}, details={self.details})>"

View File

@ -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())
# )

View File

@ -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")
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)
)

View File

@ -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)
)

View File

@ -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
from .department import Department, DepartmentCreate, DepartmentUpdate, DepartmentAdminCreate, DepartmentDelete
from .audit import AuditBase, AuditCreate, AuditUpdate, Audit

View File

@ -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

View File

@ -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()

View File

@ -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()