# BYOEH (Bring-Your-Own-Expectation-Horizon) - Architecture Documentation ## Overview The BYOEH module enables teachers to upload their own Erwartungshorizonte (expectation horizons/grading rubrics) and use them for RAG-assisted grading suggestions. Key design principles: - **Tenant Isolation**: Each teacher/school has an isolated namespace - **No Training Guarantee**: EH content is only used for RAG, never for model training - **Operator Blindness**: Client-side encryption ensures Breakpilot cannot view plaintext - **Rights Confirmation**: Required legal acknowledgment at upload time ## Architecture Diagram ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ klausur-service (Port 8086) │ ├─────────────────────────────────────────────────────────────────────────┤ │ ┌────────────────────┐ ┌─────────────────────────────────────────┐ │ │ │ BYOEH REST API │ │ BYOEH Service Layer │ │ │ │ │ │ │ │ │ │ POST /api/v1/eh │───▶│ - Upload Wizard Logic │ │ │ │ GET /api/v1/eh │ │ - Rights Confirmation │ │ │ │ DELETE /api/v1/eh │ │ - Chunking Pipeline │ │ │ │ POST /rag-query │ │ - Encryption Service │ │ │ └────────────────────┘ └────────────────────┬────────────────────┘ │ └─────────────────────────────────────────────────┼────────────────────────┘ │ ┌───────────────────────────────────────┼───────────────────────┐ │ │ │ ▼ ▼ ▼ ┌──────────────────────┐ ┌──────────────────────────┐ ┌──────────────────────┐ │ PostgreSQL │ │ Qdrant │ │ Encrypted Storage │ │ (Metadata + Audit) │ │ (Vector Search) │ │ /app/eh-uploads/ │ │ │ │ │ │ │ │ In-Memory Storage: │ │ Collection: bp_eh │ │ {tenant}/{eh_id}/ │ │ - erwartungshorizonte│ │ - tenant_id (filter) │ │ encrypted.bin │ │ - eh_chunks │ │ - eh_id │ │ salt.txt │ │ - eh_key_shares │ │ - embedding[1536] │ │ │ │ - eh_klausur_links │ │ - encrypted_content │ └──────────────────────┘ │ - eh_audit_log │ │ │ └──────────────────────┘ └──────────────────────────┘ ``` ## Data Flow ### 1. Upload Flow ``` Browser Backend Storage │ │ │ │ 1. User selects PDF │ │ │ 2. User enters passphrase │ │ │ 3. PBKDF2 key derivation │ │ │ 4. AES-256-GCM encryption │ │ │ 5. SHA-256 key hash │ │ │ │ │ │──────────────────────────────▶│ │ │ POST /api/v1/eh/upload │ │ │ (encrypted blob + key_hash) │ │ │ │──────────────────────────────▶│ │ │ Store encrypted.bin + salt │ │ │◀──────────────────────────────│ │ │ │ │ │ Save metadata to DB │ │◀──────────────────────────────│ │ │ Return EH record │ │ ``` ### 2. Indexing Flow (RAG Preparation) ``` Browser Backend Qdrant │ │ │ │──────────────────────────────▶│ │ │ POST /api/v1/eh/{id}/index │ │ │ (passphrase for decryption) │ │ │ │ │ │ │ 1. Verify key hash │ │ │ 2. Decrypt content │ │ │ 3. Extract text (PDF) │ │ │ 4. Chunk text │ │ │ 5. Generate embeddings │ │ │ 6. Re-encrypt each chunk │ │ │──────────────────────────────▶│ │ │ Index vectors + encrypted │ │ │ chunks with tenant filter │ │◀──────────────────────────────│ │ │ Return chunk count │ │ ``` ### 3. RAG Query Flow ``` Browser Backend Qdrant │ │ │ │──────────────────────────────▶│ │ │ POST /api/v1/eh/rag-query │ │ │ (query + passphrase) │ │ │ │ │ │ │ 1. Generate query embedding │ │ │──────────────────────────────▶│ │ │ 2. Semantic search │ │ │ (tenant-filtered) │ │ │◀──────────────────────────────│ │ │ 3. Decrypt matched chunks │ │◀──────────────────────────────│ │ │ Return decrypted context │ │ ``` ## Security Architecture ### Client-Side Encryption ``` ┌─────────────────────────────────────────────────────────────────┐ │ Browser (Client-Side) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 1. User enters passphrase (NEVER sent to server) │ │ │ │ │ ▼ │ │ 2. Key Derivation: PBKDF2-SHA256(passphrase, salt, 100k iter) │ │ │ │ │ ▼ │ │ 3. Encryption: AES-256-GCM(key, iv, file_content) │ │ │ │ │ ▼ │ │ 4. Key-Hash: SHA-256(derived_key) → server verification only │ │ │ │ │ ▼ │ │ 5. Upload: encrypted_blob + key_hash + salt (NOT key!) │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ### Security Guarantees | Guarantee | Implementation | |-----------|----------------| | **No Training** | `training_allowed: false` on all Qdrant points | | **Operator Blindness** | Passphrase never leaves browser; server only sees key hash | | **Tenant Isolation** | Every query filtered by `tenant_id` | | **Audit Trail** | All actions logged with timestamps | ## Key Sharing System The key sharing system enables first examiners to grant access to their EH to second examiners and supervisors. ### Share Flow ``` First Examiner Backend Second Examiner │ │ │ │ 1. Encrypt passphrase for │ │ │ recipient (client-side) │ │ │ │ │ │─────────────────────────────▶ │ │ POST /eh/{id}/share │ │ │ (encrypted_passphrase, role)│ │ │ │ │ │ │ Store EHKeyShare │ │◀───────────────────────────── │ │ │ │ │ │ │ │ │◀────────────────────────────│ │ │ GET /eh/shared-with-me │ │ │ │ │ │─────────────────────────────▶ │ │ Return shared EH list │ │ │ │ │ │◀────────────────────────────│ │ │ RAG query with decrypted │ │ │ passphrase │ ``` ### Data Structures ```python @dataclass class EHKeyShare: id: str eh_id: str user_id: str # Recipient encrypted_passphrase: str # Client-encrypted for recipient passphrase_hint: str # Optional hint granted_by: str # Grantor user ID granted_at: datetime role: str # second_examiner, third_examiner, supervisor klausur_id: Optional[str] # Link to specific Klausur active: bool @dataclass class EHKlausurLink: id: str eh_id: str klausur_id: str linked_by: str linked_at: datetime ``` ## API Endpoints ### Core EH Endpoints | Method | Endpoint | Description | |--------|----------|-------------| | POST | `/api/v1/eh/upload` | Upload encrypted EH | | GET | `/api/v1/eh` | List user's EH | | GET | `/api/v1/eh/{id}` | Get single EH | | DELETE | `/api/v1/eh/{id}` | Soft delete EH | | POST | `/api/v1/eh/{id}/index` | Index EH for RAG | | POST | `/api/v1/eh/rag-query` | Query EH content | ### Key Sharing Endpoints | Method | Endpoint | Description | |--------|----------|-------------| | POST | `/api/v1/eh/{id}/share` | Share EH with examiner | | GET | `/api/v1/eh/{id}/shares` | List shares (owner) | | DELETE | `/api/v1/eh/{id}/shares/{shareId}` | Revoke share | | GET | `/api/v1/eh/shared-with-me` | List EH shared with user | ### Klausur Integration Endpoints | Method | Endpoint | Description | |--------|----------|-------------| | POST | `/api/v1/eh/{id}/link-klausur` | Link EH to Klausur | | DELETE | `/api/v1/eh/{id}/link-klausur/{klausurId}` | Unlink EH | | GET | `/api/v1/klausuren/{id}/linked-eh` | Get linked EH for Klausur | ### Audit & Admin Endpoints | Method | Endpoint | Description | |--------|----------|-------------| | GET | `/api/v1/eh/audit-log` | Get audit log | | GET | `/api/v1/eh/rights-text` | Get rights confirmation text | | GET | `/api/v1/eh/qdrant-status` | Get Qdrant status (admin) | ## Frontend Components ### EHUploadWizard 5-step wizard for uploading Erwartungshorizonte: 1. **File Selection** - Choose PDF file 2. **Metadata** - Title, Subject, Niveau, Year 3. **Rights Confirmation** - Legal acknowledgment 4. **Encryption** - Set passphrase (2x confirmation) 5. **Summary** - Review and upload ### Integration Points - **KorrekturPage**: Shows EH prompt after first student upload - **GutachtenGeneration**: Uses RAG context from linked EH - **Sidebar Badge**: Shows linked EH count ## File Structure ``` klausur-service/ ├── backend/ │ ├── main.py # API endpoints + data structures │ ├── qdrant_service.py # Vector database operations │ ├── eh_pipeline.py # Chunking, embedding, encryption │ └── requirements.txt # Python dependencies ├── frontend/ │ └── src/ │ ├── components/ │ │ └── EHUploadWizard.tsx │ ├── services/ │ │ ├── api.ts # API client │ │ └── encryption.ts # Client-side crypto │ ├── pages/ │ │ └── KorrekturPage.tsx # EH integration │ └── styles/ │ └── eh-wizard.css └── docs/ ├── BYOEH-Architecture.md └── BYOEH-Developer-Guide.md ``` ## Configuration ### Environment Variables ```env QDRANT_URL=http://qdrant:6333 OPENAI_API_KEY=sk-... # For embeddings BYOEH_ENCRYPTION_ENABLED=true EH_UPLOAD_DIR=/app/eh-uploads ``` ### Docker Services ```yaml # docker-compose.yml services: qdrant: image: qdrant/qdrant:v1.7.4 ports: - "6333:6333" volumes: - qdrant_data:/qdrant/storage ``` ## Audit Events | Action | Description | |--------|-------------| | `upload` | EH uploaded | | `index` | EH indexed for RAG | | `rag_query` | RAG query executed | | `delete` | EH soft deleted | | `share` | EH shared with examiner | | `revoke_share` | Share revoked | | `link_klausur` | EH linked to Klausur | | `unlink_klausur` | EH unlinked from Klausur | --- ## RBAC Extensions for Zeugnis System The RBAC system has been extended to support the Zeugnis (Certificate) workflow. This enables role-based access control for certificate generation, approval, and management. ### Certificate-Related Roles ```python class Role(str, Enum): # Existing exam roles ERSTPRUEFER = "erstpruefer" # First examiner ZWEITPRUEFER = "zweitpruefer" # Second examiner DRITTPRUEFER = "drittpruefer" # Third examiner FACHVORSITZ = "fachvorsitz" # Subject chair # Certificate workflow roles FACHLEHRER = "fachlehrer" # Subject teacher - enters grades KLASSENLEHRER = "klassenlehrer" # Class teacher - approves grades ZEUGNISBEAUFTRAGTER = "zeugnisbeauftragter" # Certificate coordinator SCHULLEITUNG = "schulleitung" # Principal - final sign-off SEKRETARIAT = "sekretariat" # Secretary - printing ``` ### Certificate Resource Types ```python class ResourceType(str, Enum): # Existing types KLAUSUR = "klausur" ERWARTUNGSHORIZONT = "erwartungshorizont" # Certificate types ZEUGNIS = "zeugnis" # Final certificate ZEUGNIS_VORLAGE = "zeugnis_vorlage" # Certificate template ZEUGNIS_ENTWURF = "zeugnis_entwurf" # Draft certificate FACHNOTE = "fachnote" # Subject grade KOPFNOTE = "kopfnote" # Head grade (Arbeits-/Sozialverhalten) BEMERKUNG = "bemerkung" # Certificate remarks STATISTIK = "statistik" # Class/subject statistics NOTENSPIEGEL = "notenspiegel" # Grade distribution ``` ### VerfahrenType Extension ```python class VerfahrenType(str, Enum): # Exam types ABITUR = "abitur" KLAUSUR = "klausur" NACHSCHREIBKLAUSUR = "nachschreibklausur" # Certificate types (NEW) HALBJAHRESZEUGNIS = "halbjahreszeugnis" # Mid-year certificate JAHRESZEUGNIS = "jahreszeugnis" # End-of-year certificate ABSCHLUSSZEUGNIS = "abschlusszeugnis" # Graduation certificate ABGANGSZEUGNIS = "abgangszeugnis" # Leaving certificate @classmethod def is_certificate_type(cls, verfahren: "VerfahrenType") -> bool: """Check if this is a certificate type (not an exam).""" cert_types = { cls.HALBJAHRESZEUGNIS, cls.JAHRESZEUGNIS, cls.ABSCHLUSSZEUGNIS, cls.ABGANGSZEUGNIS } return verfahren in cert_types ``` ### Certificate Workflow Permissions ``` ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────────┐ │ FACHLEHRER │───▶│ KLASSENLEHRER │───▶│ ZEUGNISBEAUFTRAGTER │ │ │ │ │ │ │ │ FACHNOTE: CRUD │ │ ZEUGNIS: CRU │ │ ZEUGNIS: RU │ │ ZEUGNIS_ENTWURF:R │ │ ZEUGNIS_ENTWURF: │ │ ZEUGNIS_VORLAGE: RUU │ │ │ │ CRUD │ │ │ └───────────────────┘ └───────────────────┘ └───────────────────────┘ │ ▼ ┌───────────────────┐ ┌───────────────────────┐ │ SEKRETARIAT │◀───│ SCHULLEITUNG │ │ │ │ │ │ ZEUGNIS: RD │ │ ZEUGNIS: R/SIGN/LOCK │ │ (Print & Archive) │ │ (Final Approval) │ └───────────────────┘ └───────────────────────┘ ``` ### DEFAULT_PERMISSIONS for Certificate Roles ```python DEFAULT_PERMISSIONS = { # ... existing roles ... Role.KLASSENLEHRER: { ResourceType.KLASSE: {Action.READ, Action.UPDATE}, ResourceType.SCHUELER: {Action.READ, Action.CREATE, Action.UPDATE}, ResourceType.FACHNOTE: {Action.CREATE, Action.READ, Action.UPDATE, Action.DELETE}, ResourceType.ZEUGNIS: {Action.CREATE, Action.READ, Action.UPDATE}, ResourceType.ZEUGNIS_ENTWURF: {Action.CREATE, Action.READ, Action.UPDATE, Action.DELETE}, ResourceType.ZEUGNIS_VORLAGE: {Action.READ}, ResourceType.KOPFNOTE: {Action.CREATE, Action.READ, Action.UPDATE}, ResourceType.BEMERKUNG: {Action.CREATE, Action.READ, Action.UPDATE, Action.DELETE}, ResourceType.STATISTIK: {Action.READ}, ResourceType.NOTENSPIEGEL: {Action.READ}, }, Role.ZEUGNISBEAUFTRAGTER: { ResourceType.KLASSE: {Action.READ}, ResourceType.ZEUGNIS: {Action.READ, Action.UPDATE}, ResourceType.ZEUGNIS_ENTWURF: {Action.READ, Action.UPDATE}, ResourceType.ZEUGNIS_VORLAGE: {Action.READ, Action.UPDATE, Action.UPLOAD}, ResourceType.STATISTIK: {Action.READ}, ResourceType.NOTENSPIEGEL: {Action.READ}, }, Role.SEKRETARIAT: { ResourceType.ZEUGNIS: {Action.READ, Action.DOWNLOAD}, ResourceType.ZEUGNIS_VORLAGE: {Action.READ}, }, Role.SCHULLEITUNG: { ResourceType.ZEUGNIS: {Action.READ, Action.SIGN_OFF, Action.LOCK}, ResourceType.ZEUGNIS_ENTWURF: {Action.READ, Action.UPDATE}, ResourceType.ZEUGNIS_VORLAGE: {Action.READ, Action.UPDATE}, ResourceType.STATISTIK: {Action.READ}, ResourceType.NOTENSPIEGEL: {Action.READ}, }, Role.FACHLEHRER: { ResourceType.FACHNOTE: {Action.CREATE, Action.READ, Action.UPDATE, Action.DELETE}, ResourceType.ZEUGNIS: {Action.READ, Action.UPDATE}, ResourceType.ZEUGNIS_ENTWURF: {Action.READ, Action.UPDATE}, ResourceType.STATISTIK: {Action.READ}, ResourceType.NOTENSPIEGEL: {Action.READ}, }, } ``` ### See Also For complete Zeugnis system documentation including: - Full workflow diagrams - Statistics API endpoints - Frontend components - Seed data generator See: [docs/architecture/zeugnis-system.md](../../docs/architecture/zeugnis-system.md)