DirectAccessReportGenerator now, when CaseFilesCore is installed (soft-detected,
no hard dependency), sets the generated Document's folderId to the case
"מסמכים" folder via CaseFolderService so it shows in the CaseDocs panel. The
existing Case link + best-effort NetworkStorage upload are unchanged.
Verified on dev (P7_OK): php -l clean; Case.documents relate is visible via
Document.cases (shared junction); folder placement works.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 10:53:23 +00:00
5 changed files with 141 additions and 10 deletions
"description":"Report file uses subject-only canonical naming and is uploaded to the case folder correctly.",
"details":"DirectAccessReportGenerator: em-dash -> tight hyphen; sanitize via LocalFilesystemClient::sanitizeFileName; save Document with skipNetworkUpload, relate to Case, explicit uploadDocument + DB realign to canonical stored name.",
"description":"הסרת הקריאות ל-dropTables ו-cleanCaseColumns מ-AfterUninstall.php כדי למנוע אובדן נתונים בלתי הפיך בעת הסרה/התקנה מחדש של ההרחבה.",
"details":"## רקע\n\nהממצא F-002 בביקורת האבטחה (SECURITY_AUDIT_2026-04-25.md:137-168) מזהה שהסקריפט `scripts/AfterUninstall.php` מבצע פעולות הרסניות:\n\n1. **מחיקת טבלאות** (שורות 54-73): `DROP TABLE legal_aid` ו-`DROP TABLE direct_access_report`\n2. **מחיקת עמודות** (שורות 75-101): `ALTER TABLE case DROP COLUMN` על 5 עמודות (`c_legal_aid_type`, `c_aid_type`, `c_urgency_level`, `c_appointment_date`, `c_filing_deadline`)\n\nזה מפר את **כלל N2** מ-EXTENSION_DEVELOPMENT_RULES.md:\n> כאשר הרחבה מוסיפה שדות לישויות ליבה (Case, Account, Contact וכו'), הסקריפט AfterUninstall חייב להסיר רק את ההפניות מ-layouts אבל **להשאיר את עמודות הדאטאבייס**, כך שנתוני הלקוח ישרדו מחזור הסרה/התקנה מחדש.\n\n## שינויים נדרשים\n\n### 1. הסרת הפונקציה `dropTables()` (שורות 54-73)\nמחק את כל הפונקציה והקריאה אליה בשורה 31.\n\n### 2. הסרת הפונקציה `cleanCaseColumns()` (שורות 75-101)\nמחק את כל הפונקציה והקריאה אליה בשורה 34.\n\n### 3. הוספת פונקציית ניקוי layouts (אופציונלי)\nאם יש layouts ב-`custom/Espo/Custom/Resources/layouts/Case/*.json` שמפנים לשדות שנוספו, יש להסיר את ההפניות:\n\n```php\nprivate function removeLayoutReferences(Log $log): void\n{\n $layoutDir = 'custom/Espo/Custom/Resources/layouts/Case';\n if (!is_dir($layoutDir)) {\n return;\n }\n\n $fieldsToRemove = [\n 'cLegalAidType',\n 'cAidType',\n 'cUrgencyLevel',\n 'cAppointmentDate',\n 'cFilingDeadline',\n ];\n\n foreach (glob($layoutDir . '/*.json') as $file) {\n $content = file_get_contents($file);\n $layout = json_decode($content, true);\n if (!$layout) continue;\n\n $modified = $this->removeFieldsFromLayout($layout, $fieldsToRemove);\n if ($modified) {\n file_put_contents($file, json_encode($layout, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));\n $log->info(\"LegalAssistance: Removed field references from {$file}\");\n }\n }\n}\n\nprivate function removeFieldsFromLayout(array &$layout, array $fields): bool\n{\n $modified = false;\n foreach ($layout as $key => &$value) {\n if (is_array($value)) {\n if (isset($value['name']) && in_array($value['name'], $fields)) {\n unset($layout[$key]);\n $modified = true;\n } else {\n $modified = $this->removeFieldsFromLayout($value, $fields) || $modified;\n }\n }\n }\n $layout = array_values($layout); // Re-index\n return $modified;\n}\n```\n\n### 4. עדכון תיעוד הקוד בראש הקובץ\nשנה את התיאור ב-PHPDoc להסביר שהסקריפט מסיר רק:\n- רשומות DocumentTemplate\n- קבצי i18n\n- הפניות ב-layouts (אם רלוונטי)\n- וניקוי cache\n\n**לא** מוחק טבלאות או עמודות.\n\n## מבנה הקובץ לאחר התיקון\n\n```php\n<?php\n/************************************************************************\n * LegalAssistance Extension — AfterUninstall\n *\n * 1. Remove DocumentTemplate records created by this extension\n * 2. Remove custom i18n files\n * 3. (Optional) Remove layout references to extension fields\n * 4. Clear cache\n *\n * Note: Database tables and columns are intentionally preserved to\n * protect customer data during uninstall/reinstall cycles (rule N2).\n ************************************************************************/\n\nclass AfterUninstall\n{\n public function run(Container $container): void\n {\n // ... (שמירה על removeDocumentTemplates, removeCustomI18n, clearCache)\n // הסרה מלאה של dropTables ו-cleanCaseColumns\n }\n}\n```",
"testStrategy":"## אסטרטגיית בדיקה\n\n### 1. בדיקת יחידה - סקריפט האנאינסטול\n- וודא שהקובץ `AfterUninstall.php` לא מכיל את המילים `DROP TABLE` או `DROP COLUMN`\n- וודא שאין קריאות לפונקציות `dropTables` או `cleanCaseColumns`\n\n### 2. בדיקת אינטגרציה בסביבת פיתוח\n1. **הכנה**: התקן את ההרחבה על instance ריק של EspoCRM\n2. **יצירת נתונים**: צור Case עם ערכים בשדות הרלוונטיים (אם קיימים)\n3. **הסרה**: הסר את ההרחבה דרך Administration → Extensions\n4. **אימות**: בדוק שהטבלאות `legal_aid` ו-`direct_access_report` עדיין קיימות (אם היו מלכתחילה)\n5. **אימות**: בדוק שהעמודות ב-Case עדיין קיימות (אם היו מלכתחילה)\n6. **התקנה מחדש**: התקן את ההרחבה שוב\n7. **אימות**: וודא שהנתונים שנשמרו בשלב 2 עדיין קיימים\n\n### 3. בדיקת לוגים\n- לאחר הסרה, בדוק את `data/logs/espo-YYYY-MM-DD.log`\n- וודא שאין שורות `Dropping table` או `Dropped column`\n- וודא שיש שורות `Removed DocumentTemplate` ו-`Cache cleared`\n\n### 4. בדיקת קוד סטטית\n```bash\n# בדוק שאין DROP statements\ngrep -n \"DROP TABLE\\|DROP COLUMN\" scripts/AfterUninstall.php\n# צפוי: אין תוצאות\n\n# בדוק שאין קריאות לפונקציות שהוסרו\ngrep -n \"dropTables\\|cleanCaseColumns\" scripts/AfterUninstall.php\n# צפוי: אין תוצאות\n```\n\n### 5. בדיקת רגרסיה\n- וודא שה-DocumentTemplate עדיין נמחק כראוי\n- וודא שקבצי i18n עדיין נמחקים כראוי\n- וודא ש-cache clearing עדיין עובד",
"status":"pending",
"dependencies":[
1
],
"priority":"high",
"subtasks":[]
},
{
"id":3,
"title":"תיקון אבטחה: הוספת בדיקת ACL ברמת רשומה ב-DirectAccessReportGenerator",
"description":"הוספת בדיקת הרשאות ברמת רשומה (record-level ACL) לכלי generate_direct_access_report למניעת IDOR — כרגע הכלי סומך רק על בדיקת scope-level ומאפשר למשתמש עם הרשאת קריאה לתיקים לצרף מסמכים לכל תיק במערכת.",
"details":"## רקע\n\nהממצא F-001 בביקורת האבטחה (SECURITY_AUDIT_2026-04-25.md:61-134) מזהה פגיעות IDOR חמורה:\n\nהכלי `DirectAccessReportGenerator.php` מקבל `caseId` שרירותי ומייצר מסמך DOCX שמצורף לתיק ללא בדיקת הרשאות ברמת הרשומה. הבדיקה היחידה שמתבצעת היא ב-upstream (SmartAssistant ActionExecutor) ברמת scope בלבד (`acl->checkScope('Case', 'read')`).\n\n**וקטור התקיפה:** כל משתמש EspoCRM מאומת עם הרשאת קריאה לתיקים יכול:\n1. לקרוא ל-`POST /api/v1/SmartAssistant/action/executeTool` עם `caseId` של תיק שאינו שלו\n2. הכלי קורא ל-`getEntityById('Case', $caseId)` שעוקף ACL כברירת מחדל\n3. מסמך DOCX מזויף נוצר ומצורף לתיק הקורבן\n4. המסמך מכיל תוכן שנשלט על ידי התוקף אך נושא את שם עורך הדין המוקצה לתיק\n\n## שינויים נדרשים\n\n### 1. הזרקת AclManager לקונסטרקטור\n\n```php\n// files/custom/Espo/Modules/LegalAssistance/SmartAssistant/Tools/DirectAccessReportGenerator.php\n\nuse Espo\\Core\\Acl;\nuse Espo\\Core\\Exceptions\\Forbidden;\n\n// ...\n\npublic function __construct(\n private EntityManager $entityManager,\n private InjectableFactory $injectableFactory,\n private Log $log,\n private User $user,\n private Acl $acl // הוסף\n) {}\n```\n\n### 2. בדיקת הרשאות ברמת רשומה בתחילת execute()\n\nבתחילת המתודה `execute()`, מיד אחרי שליפת התיק (שורה 41-44):\n\n```php\npublic function execute(array $params, ?string $caseId, string $userId): array\n{\n if (!$caseId) {\n throw new Error('generate_direct_access_report requires a case context.');\n }\n\n $this->log->info(\"LegalAssistance: Generating direct access report for case {$caseId}\");\n\n $case = $this->entityManager->getEntityById('Case', $caseId);\n if (!$case) {\n throw new Error(\"Case {$caseId} not found.\");\n }\n\n // === תיקון אבטחה F-001: בדיקת ACL ברמת רשומה ===\n // נדרשת הרשאת עריכה כי אנחנו מצרפים מסמך לתיק\n if (!$this->acl->checkEntityEdit($case)) {\n $this->log->warning(\n \"LegalAssistance: ACL denied — user {$this->user->getId()} \" .\n \"attempted to attach document to case {$caseId}\"\n );\n throw new Forbidden(\"אין לך הרשאה לצרף מסמכים לתיק זה.\");\n }\n\n // ... המשך הקוד הקיים\n}\n```\n\n### 3. שיקולי מימוש\n\n- **למה `checkEntityEdit` ולא `checkEntityRead`?** — יצירת Document וקישורו לתיק היא פעולת כתיבה, לא קריאה. משתמש עם read-only על התיק לא צריך לוכל לשנות את רשימת המסמכים שלו.\n\n- **למה `$this->acl` ולא `AclManager`?** — ה-`Acl` service כבר מוזרק עם ה-User הנכון (המשתמש המבצע את הבקשה). שימוש ב-`AclManager->checkEntity($user, $entity, 'edit')` היה דורש לשלוף את ה-User לפי `$userId` — מיותר כש-DI כבר מספק לנו את זה.\n\n- **לוגים:** חשוב לתעד ניסיונות גישה שנדחו לצורך אבחון וניטור אבטחה.\n\n- **שגיאה בעברית:** ההודעה מוחזרת למשתמש דרך shira-hermes, אז עברית מתאימה לחוויית משתמש.\n\n### 4. קובץ יחיד לעריכה\n\n`files/custom/Espo/Modules/LegalAssistance/SmartAssistant/Tools/DirectAccessReportGenerator.php`\n\n**שורות לעריכה:**\n- שורות 12-19: הוספת `use` statements\n- שורות 26-31: הוספת `Acl` לקונסטרקטור\n- שורות 40-44: הוספת בדיקת ACL אחרי שליפת התיק",
"testStrategy":"## אסטרטגיית בדיקה\n\n### 1. בדיקה ידנית — מניעת IDOR\n\n**הכנה:**\n1. צור שני משתמשים ב-EspoCRM:\n - `lawyer-a` — עם תיק משויך (נקרא לו `case-a`)\n - `lawyer-b` — ללא גישה ל-`case-a`\n\n2. וודא ש-`lawyer-b` יכול לקרוא תיקים (scope-level read) אבל לא את `case-a` (record-level)\n\n**בדיקה:**\n1. התחבר כ-`lawyer-b`\n2. קרא ל-API:\n ```bash\n curl -X POST https://espocrm.dev.marcus-law.co.il/api/v1/SmartAssistant/action/executeTool \\\n -H \"Content-Type: application/json\" \\\n -H \"X-Api-Key: <lawyer-b-api-key>\" \\\n -d '{\n \"tool\": \"generate_direct_access_report\",\n \"caseId\": \"<case-a-id>\",\n \"params\": {\"aidType\": \"יעוץ והדרכה\"}\n }'\n ```\n3. **תוצאה צפויה:** שגיאת 403 Forbidden עם הודעה \"אין לך הרשאה לצרף מסמכים לתיק זה.\"\n4. וודא שלא נוצר Document חדש ב-`case-a`\n\n### 2. בדיקת regression — פעולה תקינה\n\n1. התחבר כ-`lawyer-a` (בעל התיק)\n2. קרא לאותו API עם `caseId` של התיק שלו\n3. **תוצאה צפויה:** הדוח נוצר בהצלחה ומצורף לתיק\n\n### 3. בדיקת לוגים\n\n1. לאחר ניסיון IDOR שנדחה, בדוק בלוג:\n ```bash\n docker exec <espocrm-container> tail -20 data/logs/espo-$(date +%Y-%m-%d).log | grep \"ACL denied\"\n ```\n2. **תוצאה צפויה:** שורת לוג עם WARNING המציינת את user ID וה-case ID\n\n### 4. בדיקת אינטגרציה עם shira-hermes\n\n1. דרך ממשק Shira (צ'אט), בקש מעוזר המשפט ליצור דוח לתיק שאין לך גישה אליו\n2. **תוצאה צפויה:** הודעת שגיאה ידידותית בעברית מוחזרת למשתמש\n\n### 5. בדיקת unit (אופציונלי)\n\nאם קיימת תשתית PHPUnit בפרויקט:\n\n```php\npublic function testExecuteThrowsForbiddenWhenUserCannotEditCase(): void\n{\n // Mock Acl to return false for checkEntityEdit\n $acl = $this->createMock(Acl::class);\n $acl->method('checkEntityEdit')->willReturn(false);\n \n $generator = new DirectAccessReportGenerator(\n $this->entityManager,\n $this->injectableFactory,\n $this->log,\n $this->user,\n $acl\n );\n \n $this->expectException(Forbidden::class);\n $generator->execute(['aidType' => 'test'], 'some-case-id', 'user-id');\n}\n```",
"status":"pending",
"dependencies":[
1
],
"priority":"high",
"subtasks":[]
},
{
"id":4,
"title":"feat: P7 — שיוך דוח גישה ישירה לתיקיית CaseFiles",
"description":"הוספת ענף CaseFiles ליצירת המסמכים: אם CaseFilesCore מותקן (soft-detect), המסמך שנוצר משויך לתיקיית 'מסמכים' של התיק (folderId) כדי שיופיע בפאנל CaseDocs. ההתנהגות הקיימת (קישור לתיק + העלאה ל-NetworkStorage אם פעיל) נשמרת — ללא רגרסיה.",
"details":"אומת על dev 21/06/2026 (P7_OK): php -l נקי; קישור דרך Case.documents נראה דרך Document.cases (אותה טבלת קישור), folderId נקבע לתיקיית 'מסמכים'. ענף זהה לנתב P5/P6 המאומת.",
"testStrategy":"dev: יצירת מסמך עם CaseFilesCore מותקן → folderId=מסמכים, נראה בפאנל.",
Generates the Israeli legal-aid "**דוח גישה ישירה**" (direct-access report) as a DOCX, straight from JSON params (no intermediate entity), and uploads it to the case folder. It is reached as a **SmartAssistant agent tool** — Shira runs an 8-step Hebrew interview to collect the data, then calls the tool. Since Phase 3, the legal tools live in **shira-hermes**, not ai-gateway; this extension is now minimal.
## File-by-file
| File | Purpose |
|---|---|
| `files/.../SmartAssistant/Tools/DirectAccessReportGenerator.php` | **Core:** JSON params → DOCX via PHPWord `TemplateProcessor` (checkbox mapping) → upload to case folder via NetworkStorageIntegration (local fallback). **IDOR via caseId — S1; param→DOCX injection — S3; temp file — S4.** |
| `files/.../Resources/module.json` | Module order 25. |
| `files/.../Resources/metadata/app/smartAssistant.json` | Registers the `generate_direct_access_report` tool handler. |
| `files/.../Resources/metadata/entityDefs/Case.json` | **Empty `{}`** (no Case extension in v2.0.10) — D5. |
"description":"סיוע משפטי — יצירת דוח גישה ישירה (JSON ישירות ל-DOCX), אינטגרציה עם שירה",
"scripts":{
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.