Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,15 @@ Hardcoded credentials pose a critical risk because they provide an easy entry po
**Prevention:**
1. Never commit secrets, API keys, or passwords into the source code repository. Always read sensitive configuration using environment variables (e.g., `os.getenv`).
2. Implement secure comparisons utilizing functions designed to prevent timing attacks, like `secrets.compare_digest()`, and properly encode inputs to prevent TypeErrors on non-ASCII characters.

## 2024-05-20 - Missing Authentication on Core API Endpoints

**Vulnerability:**
Many core API endpoints (such as `/labours/`, `/attendance/`, `/materials/`, `/sites/`, and `/payments/`) and management HTML pages were exposed without any authentication checks. This allowed unauthenticated users to access, modify, and delete sensitive data.

**Learning:**
Relying only on frontend UI navigation hiding is insecure. Backend API endpoints must independently verify the user's authentication and authorization state using dependency injection (e.g., `Depends(get_current_user)`) on every protected route.

**Prevention:**
1. Apply a default-deny approach to API routing; endpoints should require authentication by default unless explicitly public (like login).
2. Utilize FastAPI's `dependencies=[Depends(...)]` feature within route decorators to enforce authentication checks consistently across all related endpoints.
58 changes: 29 additions & 29 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ async def labor_management(current_user: str = Depends(get_current_user)):
## Labour details

# Create Labour
@app.post("/labours/", response_model=schemas.Laborer)
@app.post("/labours/", response_model=schemas.Laborer, dependencies=[Depends(get_current_user)])
async def create_labour(labour: schemas.LaborerCreate, db: AsyncSession = Depends(get_db)):
try:
return await crud.create_labour(db, labour)
Expand All @@ -140,7 +140,7 @@ async def create_labour(labour: schemas.LaborerCreate, db: AsyncSession = Depend
raise HTTPException(status_code=500, detail="An unexpected error occurred")

# Get All Labours
@app.get("/labours/", response_model=list[schemas.Laborer])
@app.get("/labours/", response_model=list[schemas.Laborer], dependencies=[Depends(get_current_user)])
async def read_labours(skip: int = 0, limit: int = 10, db: AsyncSession = Depends(get_db)):
try:
labours = await crud.get_labours(db=db, skip=skip, limit=limit)
Expand All @@ -149,15 +149,15 @@ async def read_labours(skip: int = 0, limit: int = 10, db: AsyncSession = Depend
raise HTTPException(status_code=500, detail="Database error occurred") # Generic error message

# Search Labours by Name
@app.get("/labours/search/", response_model=list[schemas.Laborer])
@app.get("/labours/search/", response_model=list[schemas.Laborer], dependencies=[Depends(get_current_user)])
async def search_labours(name: str, db: AsyncSession = Depends(get_db)):
try:
return await crud.search_labours(db, name)
except SQLAlchemyError:
raise HTTPException(status_code=500, detail="Database error occurred") # Generic error message

# Get a single Labour by ID
@app.get("/labours/{labour_id}", response_model=schemas.Laborer)
@app.get("/labours/{labour_id}", response_model=schemas.Laborer, dependencies=[Depends(get_current_user)])
async def get_labour(labour_id: int, db: AsyncSession = Depends(get_db)):
try:
labour = await crud.get_labour(db, labour_id)
Expand All @@ -168,7 +168,7 @@ async def get_labour(labour_id: int, db: AsyncSession = Depends(get_db)):
raise HTTPException(status_code=500, detail="Database error occurred") # Generic error message

# Update Labour
@app.put("/labours/{labour_id}", response_model=schemas.LaborerUpdate)
@app.put("/labours/{labour_id}", response_model=schemas.LaborerUpdate, dependencies=[Depends(get_current_user)])
async def update_labour(labour_id: int, updated_data: schemas.LaborerCreate, db: AsyncSession = Depends(get_db)):
try:
labour = await crud.update_labour(db, labour_id, updated_data)
Expand All @@ -179,7 +179,7 @@ async def update_labour(labour_id: int, updated_data: schemas.LaborerCreate, db:
raise HTTPException(status_code=500, detail="Database error occurred") # Generic error message

# Delete Labour
@app.delete("/labours/{labour_id}")
@app.delete("/labours/{labour_id}", dependencies=[Depends(get_current_user)])
async def delete_labour(labour_id: int, db: AsyncSession = Depends(get_db)):
try:
result = await crud.delete_labour(db, labour_id)
Expand All @@ -190,7 +190,7 @@ async def delete_labour(labour_id: int, db: AsyncSession = Depends(get_db)):
raise HTTPException(status_code=500, detail="Database error occurred") # Generic error message

# Record Attendance
@app.post("/labours/{labour_id}/attendance/", response_model=schemas.Attendance)
@app.post("/labours/{labour_id}/attendance/", response_model=schemas.Attendance, dependencies=[Depends(get_current_user)])
async def record_attendance(labour_id: int, attendance_data: schemas.AttendanceCreate, db: AsyncSession = Depends(get_db)):
logging.info(f"Received labour attendance: {attendance_data}")
try:
Expand Down Expand Up @@ -219,15 +219,15 @@ async def record_attendance(labour_id: int, attendance_data: schemas.AttendanceC


# Get Attendance History
@app.get("/labours/{labour_id}/attendance/", response_model=list[schemas.Attendance])
@app.get("/labours/{labour_id}/attendance/", response_model=list[schemas.Attendance], dependencies=[Depends(get_current_user)])
async def get_attendance_history(labour_id: int, db: AsyncSession = Depends(get_db)):
try:
return await crud.get_attendance_history(db, labour_id)
except SQLAlchemyError:
raise HTTPException(status_code=500, detail="Database error occurred") # Generic error message

# Get All Attendance Records with Pagination
@app.get("/attendance/", response_model=schemas.AttendanceResponse) # Adjust to your schema
@app.get("/attendance/", response_model=schemas.AttendanceResponse, dependencies=[Depends(get_current_user)]) # Adjust to your schema
async def get_all_attendance(
db: AsyncSession = Depends(get_db),
skip: int = Query(0, ge=0), # Starting point (offset)
Expand All @@ -247,7 +247,7 @@ async def get_all_attendance(
raise HTTPException(status_code=500, detail="Database error occurred")

# Get Attendance by ID
@app.get("/attendance/{attendance_id}", response_model=schemas.Attendance)
@app.get("/attendance/{attendance_id}", response_model=schemas.Attendance, dependencies=[Depends(get_current_user)])
async def get_attendance(attendance_id: int, db: AsyncSession = Depends(get_db)):
try:
attendance = await crud.get_attendance(db, attendance_id)
Expand All @@ -259,7 +259,7 @@ async def get_attendance(attendance_id: int, db: AsyncSession = Depends(get_db))


# Update Attendance Record
@app.put("/attendance/{attendance_id}", response_model=schemas.Attendance)
@app.put("/attendance/{attendance_id}", response_model=schemas.Attendance, dependencies=[Depends(get_current_user)])
async def update_attendance(attendance_id: int, attendance_data: schemas.AttendanceUpdate, db: AsyncSession = Depends(get_db)):
try:
updated_attendance = await crud.update_attendance(db, attendance_id, attendance_data)
Expand All @@ -273,7 +273,7 @@ async def update_attendance(attendance_id: int, attendance_data: schemas.Attenda


# Delete Attendance Record
@app.delete("/attendance/{attendance_id}")
@app.delete("/attendance/{attendance_id}", dependencies=[Depends(get_current_user)])
async def delete_attendance(attendance_id: int, db: AsyncSession = Depends(get_db)):
try:
result = await crud.delete_attendance(db, attendance_id)
Expand All @@ -287,51 +287,51 @@ async def delete_attendance(attendance_id: int, db: AsyncSession = Depends(get_d
## Material Management Codes

# Serve the materials management page
@app.get("/materials-management/", response_class=HTMLResponse)
@app.get("/materials-management/", response_class=HTMLResponse, dependencies=[Depends(get_current_user)])
async def materials_management_page():
with open("frontend/inventory-management.html") as file:
return file.read()

# CRUD Operations for Materials

# Create Material
@app.post("/materials/", response_model=schemas.Material)
@app.post("/materials/", response_model=schemas.Material, dependencies=[Depends(get_current_user)])
async def create_material(material: schemas.MaterialCreate, db: AsyncSession = Depends(get_db)):
return await crud.create_material(db, material)

@app.get("/materials/", response_model=List[schemas.Material])
@app.get("/materials/", response_model=List[schemas.Material], dependencies=[Depends(get_current_user)])
async def get_materials(skip: int = 0, limit: int = 10, db: AsyncSession = Depends(get_db)):
return await crud.get_materials(db, skip, limit)

@app.get("/materials/{material_id}", response_model=schemas.Material)
@app.get("/materials/{material_id}", response_model=schemas.Material, dependencies=[Depends(get_current_user)])
async def get_material(material_id: int, db: AsyncSession = Depends(get_db)):
material = await crud.get_material(db, material_id)
if not material:
raise HTTPException(status_code=404, detail="Material not found")
return material

@app.put("/materials/{material_id}", response_model=schemas.Material)
@app.put("/materials/{material_id}", response_model=schemas.Material, dependencies=[Depends(get_current_user)])
async def update_material_endpoint(material_id: int, material: schemas.MaterialUpdate, db: AsyncSession = Depends(get_db)):
updated_material = await crud.update_material(db, material_id, material)
if not updated_material:
raise HTTPException(status_code=404, detail="Material not found")
return updated_material

# Delete Material
@app.delete("/materials/{material_id}", response_model=schemas.Material)
@app.delete("/materials/{material_id}", response_model=schemas.Material, dependencies=[Depends(get_current_user)])
async def delete_material(material_id: int, db: AsyncSession = Depends(get_db)):
deleted_material = await crud.delete_material(db, material_id)
if not deleted_material:
raise HTTPException(status_code=404, detail="Material not found")
return deleted_material

# Create Site
@app.post("/sites/", response_model=schemas.Site)
@app.post("/sites/", response_model=schemas.Site, dependencies=[Depends(get_current_user)])
async def create_site(site: schemas.SiteCreate, db: AsyncSession = Depends(get_db)):
return await crud.create_site(db, site)

# Get All Sites
@app.get("/sites/", response_model=list[schemas.Site])
@app.get("/sites/", response_model=list[schemas.Site], dependencies=[Depends(get_current_user)])
async def get_sites(skip: int = 0, limit: int = 10, db: AsyncSession = Depends(get_db)):
try:
sites = await crud.get_sites(db, skip=skip, limit=limit)
Expand All @@ -341,12 +341,12 @@ async def get_sites(skip: int = 0, limit: int = 10, db: AsyncSession = Depends(g
raise HTTPException(status_code=500, detail="Database error occurred")

# Get Site by ID
@app.get("/sites/{site_id}", response_model=schemas.Site)
@app.get("/sites/{site_id}", response_model=schemas.Site, dependencies=[Depends(get_current_user)])
async def get_site(site_id: int, db: AsyncSession = Depends(get_db)):
return await crud.get_site(db, site_id)

# Update Site
@app.put("/sites/{site_id}", response_model=schemas.Site)
@app.put("/sites/{site_id}", response_model=schemas.Site, dependencies=[Depends(get_current_user)])
async def update_site(site_id: int, site: schemas.SiteUpdate, db: AsyncSession = Depends(get_db)):
# Check if the site exists
existing_site = await db.get(models.Site, site_id)
Expand All @@ -371,20 +371,20 @@ async def update_site(site_id: int, site: schemas.SiteUpdate, db: AsyncSession =
raise HTTPException(status_code=500, detail="Database error occurred")

# Delete Site
@app.delete("/sites/{site_id}")
@app.delete("/sites/{site_id}", dependencies=[Depends(get_current_user)])
async def delete_site(site_id: int, db: AsyncSession = Depends(get_db)):
result = await crud.delete_site(db, site_id)
return {"message": "Site deleted successfully"} if result else {"message": "Site not found"}

## Payments API Module
# Serve the payment management page
@app.get("/payment-management/", response_class=HTMLResponse)
@app.get("/payment-management/", response_class=HTMLResponse, dependencies=[Depends(get_current_user)])
async def read_payments_management():
# Load and return the payment-management.html file
with open(os.path.join("frontend/payment-management.html")) as file:
return file.read()

@app.post("/payments/", response_model=schemas.Payment)
@app.post("/payments/", response_model=schemas.Payment, dependencies=[Depends(get_current_user)])
async def create_payment(payment: schemas.PaymentCreate, db: AsyncSession = Depends(get_db)):
db_payment = models.Payment(
amount=payment.amount,
Expand All @@ -400,19 +400,19 @@ async def create_payment(payment: schemas.PaymentCreate, db: AsyncSession = Depe
return db_payment


@app.get("/payments/", response_model=List[schemas.Payment])
@app.get("/payments/", response_model=List[schemas.Payment], dependencies=[Depends(get_current_user)])
async def get_payments_endpoint(skip: int = 0, limit: int = 10, db: AsyncSession = Depends(get_db)):
return await crud.get_payments(db, skip, limit)

@app.get("/payments/{payment_id}", response_model=schemas.Payment)
@app.get("/payments/{payment_id}", response_model=schemas.Payment, dependencies=[Depends(get_current_user)])
async def get_payment_endpoint(payment_id: int, db: AsyncSession = Depends(get_db)):
return await crud.get_payment(db, payment_id)

@app.put("/payments/{payment_id}", response_model=schemas.Payment)
@app.put("/payments/{payment_id}", response_model=schemas.Payment, dependencies=[Depends(get_current_user)])
async def update_payment_endpoint(payment_id: int, payment: schemas.PaymentUpdate, db: AsyncSession = Depends(get_db)):
return await crud.update_payment(db, payment_id, payment)

@app.delete("/payments/{payment_id}", response_model=schemas.Payment)
@app.delete("/payments/{payment_id}", response_model=schemas.Payment, dependencies=[Depends(get_current_user)])
async def delete_payment_endpoint(payment_id: int, db: AsyncSession = Depends(get_db)):
return await crud.delete_payment(db, payment_id)

Expand Down