Files
timemaster/backend/app/schemas/company.py
T
patrick 4dc69137dd security: H-1 settings-Whitelist + H-5 UUID-Guard + H-6 DNS-Pinning + H-7 Heartbeat-Timing
H-1: company.settings als typisiertes Sub-Schema
- schemas/company.py: CompanySettingsUpdate mit extra=forbid
- Nur bekannte Keys (carryover_expires_month/day) erlaubt
- Unbekannte Keys → HTTP 422

H-5: SQL-Injection defensiv absichern
- dependencies.py: UUID-Round-Trip str(_uuid.UUID(...)) + Sicherheitskommentar

H-6: CalDAV DNS-Rebinding-Schutz
- caldav_service.py: PinnedIPTransport — IP einmal auflösen, beim Request fixieren
- _validate_caldav_url gibt aufgelöste IP zurück
- Alle HTTP-Methoden nutzen PinnedIPTransport

H-7: Heartbeat-Timestamp nach Route-Logik
- kiosk_security.py: last_heartbeat_at-Update aus Dependency entfernt
- kiosk_service.py: Update erst in process_heartbeat() nach erfolgreicher Auth

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:35:18 +02:00

87 lines
2.9 KiB
Python

import uuid
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field
PersonnelNumberModeT = Literal["manual", "auto"]
class CompanySettingsUpdate(BaseModel):
"""Validiertes Sub-Schema für das company.settings JSONB-Feld.
Nur bekannte Top-Level-Keys sind erlaubt (extra="forbid").
Derzeit genutzte Keys: carryover_expires_month, carryover_expires_day
(gelesen in absences.py für Resturlaub-Verfall-Berechnung).
"""
model_config = ConfigDict(extra="forbid")
carryover_expires_month: int | None = Field(None, ge=1, le=12)
carryover_expires_day: int | None = Field(None, ge=1, le=31)
class CompanyOut(BaseModel):
model_config = {"from_attributes": True}
id: uuid.UUID
name: str
slug: str
plan: str
logo_url: str | None
country: str
state: str | None
settings: dict
personnel_number_required: bool = False
personnel_number_mode: PersonnelNumberModeT = "manual"
personnel_number_next: int = 1
mobile_stamping_enabled: bool = True
overtime_overdraft_allowed: bool = True
overtime_warning_threshold_hours: int = 0
overtime_cap_hours: int | None = None
overtime_expiry_enabled: bool = False
overtime_expiry_month: int = 3
overtime_expiry_day: int = 31
overtime_max_carryover_hours: int | None = None
kiosk_require_approval: bool = True
kiosk_track_current_user: bool = True
kiosk_heartbeat_interval_sec: int = 30
class CompanyUpdate(BaseModel):
name: str | None = Field(None, min_length=2, max_length=255)
state: str | None = Field(None, max_length=10)
settings: CompanySettingsUpdate | None = None
personnel_number_required: bool | None = None
personnel_number_mode: PersonnelNumberModeT | None = None
personnel_number_next: int | None = Field(None, ge=1)
mobile_stamping_enabled: bool | None = None
overtime_overdraft_allowed: bool | None = None
overtime_warning_threshold_hours: int | None = Field(None, ge=0)
overtime_cap_hours: int | None = Field(None, ge=1, le=9999)
overtime_expiry_enabled: bool | None = None
overtime_expiry_month: int | None = Field(None, ge=1, le=12)
overtime_expiry_day: int | None = Field(None, ge=1, le=31)
overtime_max_carryover_hours: int | None = Field(None, ge=0, le=9999)
kiosk_require_approval: bool | None = None
kiosk_track_current_user: bool | None = None
kiosk_heartbeat_interval_sec: int | None = Field(None, ge=10, le=120)
class DepartmentOut(BaseModel):
model_config = {"from_attributes": True}
id: uuid.UUID
company_id: uuid.UUID
name: str
manager_id: uuid.UUID | None
class DepartmentCreate(BaseModel):
name: str = Field(min_length=1, max_length=255)
manager_id: uuid.UUID | None = None
class DepartmentUpdate(BaseModel):
name: str | None = Field(None, min_length=1, max_length=255)
manager_id: uuid.UUID | None = None