diff --git a/ProcessMaker/Events/AuthClientDeleted.php b/ProcessMaker/Events/AuthClientDeleted.php index 1eb9e92dfa..5d1875d6bc 100644 --- a/ProcessMaker/Events/AuthClientDeleted.php +++ b/ProcessMaker/Events/AuthClientDeleted.php @@ -2,6 +2,7 @@ namespace ProcessMaker\Events; +use Carbon\Carbon; use Illuminate\Foundation\Events\Dispatchable; use ProcessMaker\Contracts\SecurityLogEventInterface; @@ -18,10 +19,9 @@ class AuthClientDeleted implements SecurityLogEventInterface * * @return void */ - public function __construct(array $deleted_values) + public function __construct(array $values) { - $this->data = ['auth_client_id' => $deleted_values['id']]; - $this->changes = $deleted_values; + $this->changes = $values; } /** @@ -29,7 +29,11 @@ public function __construct(array $deleted_values) */ public function getData(): array { - return $this->data; + return [ + 'auth_client_id' => $this->changes['id'] ?? 0, + 'name' => $this->changes['name'] ?? 0, + 'deleted_at' => Carbon::now() + ]; } /** diff --git a/ProcessMaker/Events/FilesAccessed.php b/ProcessMaker/Events/FilesAccessed.php new file mode 100644 index 0000000000..bffe4b7e47 --- /dev/null +++ b/ProcessMaker/Events/FilesAccessed.php @@ -0,0 +1,76 @@ +processName = $data->getAttribute('name'); + // Link to the request + $this->linkFile = [ + 'label' => $data->getAttribute('id'), + 'link' => route('requests.show', $data) + ]; + } else { + // Link to file in the package + $this->linkFile = [ + 'label' => $name, + 'link' => route('file-manager.index', ['public/' . $name]), + ]; + } + } + + /** + * Get specific data related to the event + * + * @return array + */ + public function getData(): array + { + return [ + 'name' => $this->linkFile, + 'process' => $this->processName, + 'accessed_at' => Carbon::now() + ]; + } + + /** + * Get specific data related to the event + * + * @return array + */ + public function getChanges(): array + { + return []; + } + + /** + * Get the Event name + * + * @return string + */ + public function getEventName(): string + { + return 'FilesAccessed'; + } +} diff --git a/ProcessMaker/Events/FilesCreated.php b/ProcessMaker/Events/FilesCreated.php index 4c9a8b9302..51ba82ce62 100644 --- a/ProcessMaker/Events/FilesCreated.php +++ b/ProcessMaker/Events/FilesCreated.php @@ -5,6 +5,7 @@ use Illuminate\Foundation\Events\Dispatchable; use ProcessMaker\Contracts\SecurityLogEventInterface; use ProcessMaker\Models\Media; +use ProcessMaker\Models\ProcessRequest; use ProcessMaker\Traits\FormatSecurityLogChanges; class FilesCreated implements SecurityLogEventInterface @@ -12,17 +13,38 @@ class FilesCreated implements SecurityLogEventInterface use Dispatchable; use FormatSecurityLogChanges; + public const NAME_PUBLIC_FILES = 'Public Files'; + private array $media; + private array $name = []; + private string $processName = ''; /** * Create a new event instance. * * @return void */ - public function __construct(int $fileId) + public function __construct(int $fileId, ProcessRequest $data) { $this->media = Media::find(['id' => $fileId])->toArray(); $this->media = head($this->media); + + // Check if the request is related to the package files + if (static::NAME_PUBLIC_FILES === $data->getAttribute('name')) { + $this->processName = ''; + // Link to file in the package + $this->name = [ + 'label' => $this->media['name'], + 'link' => route('file-manager.index', ['public/' . $this->media['name']]), + ]; + } else { + $this->processName = $data->getAttribute('name'); + // Link to the request + $this->name = [ + 'label' => $data->getAttribute('id'), + 'link' => route('requests.show', $data) + ]; + } } /** @@ -33,10 +55,8 @@ public function __construct(int $fileId) public function getData(): array { return [ - 'file_name' => [ - 'label' => $this->media['name'], - 'link' => route('file-manager.index', ['public/' . $this->media['file_name']]), - ], + 'name' => $this->name, + 'process' => $this->processName, 'created_at' => $this->media['created_at'], ]; } diff --git a/ProcessMaker/Events/FilesDownloaded.php b/ProcessMaker/Events/FilesDownloaded.php new file mode 100644 index 0000000000..e6787cc43b --- /dev/null +++ b/ProcessMaker/Events/FilesDownloaded.php @@ -0,0 +1,76 @@ +processName = $data->getAttribute('name'); + // Link to the request + $this->name = [ + 'label' => $data->getAttribute('id'), + 'link' => route('requests.show', $data) + ]; + } else { + // Link to file in the package + $this->name = [ + 'label' => $file, + 'link' => route('file-manager.index', ['public/' . $file]), + ]; + } + } + + /** + * Get specific data related to the event + * + * @return array + */ + public function getData(): array + { + return [ + 'name' => $this->name, + 'process' => $this->processName, + 'accessed_at' => Carbon::now() + ]; + } + + /** + * Get specific data related to the event + * + * @return array + */ + public function getChanges(): array + { + return []; + } + + /** + * Get the Event name + * + * @return string + */ + public function getEventName(): string + { + return 'FilesDownloaded'; + } +} diff --git a/ProcessMaker/Events/ProcessPublished.php b/ProcessMaker/Events/ProcessPublished.php index b07b76d8b1..1853324758 100644 --- a/ProcessMaker/Events/ProcessPublished.php +++ b/ProcessMaker/Events/ProcessPublished.php @@ -5,6 +5,7 @@ use Illuminate\Foundation\Events\Dispatchable; use ProcessMaker\Contracts\SecurityLogEventInterface; use ProcessMaker\Models\Process; +use ProcessMaker\Models\ProcessCategory; use ProcessMaker\Traits\FormatSecurityLogChanges; class ProcessPublished implements SecurityLogEventInterface @@ -39,6 +40,12 @@ public function __construct(Process $data, array $changes, array $original) $this->process = $data; $this->changes = array_diff_key($changes, array_flip($this::REMOVE_KEYS)); $this->original = array_diff_key($original, array_flip($this::REMOVE_KEYS)); + + // Get category name + $this->original['process_category'] = isset($original['process_category_id']) ? ProcessCategory::getNamesByIds($this->original['process_category_id']) : ''; + unset($this->original['process_category_id']); + $this->changes['process_category'] = isset($changes['process_category_id']) ? ProcessCategory::getNamesByIds($this->changes['process_category_id']) : ''; + unset($this->changes['process_category_id']); } /** diff --git a/ProcessMaker/Events/ScreenCreated.php b/ProcessMaker/Events/ScreenCreated.php index 040b651306..90cf0a241a 100644 --- a/ProcessMaker/Events/ScreenCreated.php +++ b/ProcessMaker/Events/ScreenCreated.php @@ -4,6 +4,7 @@ use Illuminate\Foundation\Events\Dispatchable; use ProcessMaker\Contracts\SecurityLogEventInterface; +use ProcessMaker\Models\ScreenCategory; class ScreenCreated implements SecurityLogEventInterface { @@ -19,6 +20,11 @@ class ScreenCreated implements SecurityLogEventInterface public function __construct(array $newScreen) { $this->newScreen = $newScreen; + + if (isset($newScreen['screen_category_id'])) { + $this->newScreen['screen_category'] = ScreenCategory::getNamesByIds($newScreen['screen_category_id']); + unset($this->newScreen['screen_category_id']); + } } /** diff --git a/ProcessMaker/Events/ScreenUpdated.php b/ProcessMaker/Events/ScreenUpdated.php index a7dfe5e054..808f530215 100644 --- a/ProcessMaker/Events/ScreenUpdated.php +++ b/ProcessMaker/Events/ScreenUpdated.php @@ -5,6 +5,7 @@ use Illuminate\Foundation\Events\Dispatchable; use ProcessMaker\Contracts\SecurityLogEventInterface; use ProcessMaker\Models\Screen; +use ProcessMaker\Models\ScreenCategory; use ProcessMaker\Traits\FormatSecurityLogChanges; class ScreenUpdated implements SecurityLogEventInterface @@ -28,6 +29,12 @@ public function __construct(Screen $screen, array $changes, array $original) $this->screen = $screen; $this->changes = $changes; $this->original = $original; + + // Get category name + $this->original['screen_category'] = isset($original['screen_category_id']) ? ScreenCategory::getNamesByIds($this->original['screen_category_id']) : ''; + unset($this->original['screen_category_id']); + $this->changes['screen_category'] = isset($changes['screen_category_id']) ? ScreenCategory::getNamesByIds($this->changes['screen_category_id']) : ''; + unset($this->changes['screen_category_id']); } /** diff --git a/ProcessMaker/Events/ScriptCreated.php b/ProcessMaker/Events/ScriptCreated.php index 2cb128bdd0..31d7fb2a3b 100644 --- a/ProcessMaker/Events/ScriptCreated.php +++ b/ProcessMaker/Events/ScriptCreated.php @@ -5,6 +5,7 @@ use Illuminate\Foundation\Events\Dispatchable; use ProcessMaker\Contracts\SecurityLogEventInterface; use ProcessMaker\Models\Script; +use ProcessMaker\Models\ScriptCategory; use ProcessMaker\Traits\FormatSecurityLogChanges; class ScriptCreated implements SecurityLogEventInterface @@ -13,9 +14,8 @@ class ScriptCreated implements SecurityLogEventInterface use FormatSecurityLogChanges; private array $changes; - private array $original; - + public string $categoryName = ''; private Script $script; /** @@ -49,11 +49,19 @@ public function getChanges(): array */ public function getData(): array { + $categoryId = $this->script['script_category_id'] ?? ''; + if (!empty($categoryId)) { + $categoryName = ScriptCategory::where('id', $categoryId)->value('name'); + } + $basic = isset($this->changes['code']) ? [ 'name' => $this->script->getAttribute('title'), 'created_at' => $this->script->getAttribute('created_at'), ] : [ 'name' => $this->script->getAttribute('title'), + 'description' => $this->script->getAttribute('description'), + 'category' => $categoryName, + 'language' => $this->script->getAttribute('language') ]; unset($this->changes['code']); unset($this->original['code']); diff --git a/ProcessMaker/Events/ScriptDeleted.php b/ProcessMaker/Events/ScriptDeleted.php index ef2fefe100..a4a526486c 100644 --- a/ProcessMaker/Events/ScriptDeleted.php +++ b/ProcessMaker/Events/ScriptDeleted.php @@ -2,6 +2,7 @@ namespace ProcessMaker\Events; +use Carbon\Carbon; use Illuminate\Foundation\Events\Dispatchable; use ProcessMaker\Contracts\SecurityLogEventInterface; use ProcessMaker\Models\Script; @@ -45,6 +46,7 @@ public function getData(): array { return [ 'name' => $this->script->getAttribute('title'), + 'deleted_at' => Carbon::now() ]; } diff --git a/ProcessMaker/Events/ScriptExecutorCreated.php b/ProcessMaker/Events/ScriptExecutorCreated.php index eff930a5dc..701914b804 100644 --- a/ProcessMaker/Events/ScriptExecutorCreated.php +++ b/ProcessMaker/Events/ScriptExecutorCreated.php @@ -24,7 +24,7 @@ public function __construct(array $created_values) $this->data = [ 'script_executor_id' => $created_values['id'], 'title' => $created_values['title'], - 'description' => $created_values['description'], + 'description' => isset($created_values['description']) ? $created_values['description'] : "", 'language' => $created_values['language'], 'config' => $created_values['config'], ]; diff --git a/ProcessMaker/Events/ScriptUpdated.php b/ProcessMaker/Events/ScriptUpdated.php index 57b65bf593..17752df939 100644 --- a/ProcessMaker/Events/ScriptUpdated.php +++ b/ProcessMaker/Events/ScriptUpdated.php @@ -5,6 +5,7 @@ use Illuminate\Foundation\Events\Dispatchable; use ProcessMaker\Contracts\SecurityLogEventInterface; use ProcessMaker\Models\Script; +use ProcessMaker\Models\ScriptCategory; use ProcessMaker\Traits\FormatSecurityLogChanges; class ScriptUpdated implements SecurityLogEventInterface @@ -30,6 +31,12 @@ public function __construct(Script $script, array $changes, array $original) $this->script = $script; $this->changes = $changes; $this->original = $original; + + // Get category name + $this->original['script_category'] = isset($original['script_category_id']) ? ScriptCategory::getNamesByIds($this->original['script_category_id']) : ''; + unset($this->original['script_category_id']); + $this->changes['script_category'] = isset($changes['script_category_id']) ? ScriptCategory::getNamesByIds($this->changes['script_category_id']) : ''; + unset($this->changes['script_category_id']); } /** diff --git a/ProcessMaker/Events/SecurityLogDownloadFailed.php b/ProcessMaker/Events/SecurityLogDownloadFailed.php new file mode 100644 index 0000000000..69a4611e6c --- /dev/null +++ b/ProcessMaker/Events/SecurityLogDownloadFailed.php @@ -0,0 +1,75 @@ +user = $user; + $this->success = $success; + $this->message = $message; + $this->link = $link; + } + + /** + * Get the channels the event should broadcast on. + * + * @return \Illuminate\Broadcasting\Channel|array + */ + public function broadcastOn() + { + return new PrivateChannel("ProcessMaker.Models.User.{$this->user->id}"); + } + + /** + * Set the event name + * + * @return string + */ + public function broadcastAs() + { + return 'SecurityLogDownloadFailed'; + } + + /** + * Set the data to broadcast with this event + * + * @return array + */ + public function broadcastWith() + { + return [ + 'success' => $this->success, + 'message' => $this->message, + 'link' => $this->link, + ]; + } +} diff --git a/ProcessMaker/Events/SecurityLogDownloadJobCompleted.php b/ProcessMaker/Events/SecurityLogDownloadJobCompleted.php index e8c3a2801b..a4007ff5e0 100644 --- a/ProcessMaker/Events/SecurityLogDownloadJobCompleted.php +++ b/ProcessMaker/Events/SecurityLogDownloadJobCompleted.php @@ -11,7 +11,9 @@ class SecurityLogDownloadJobCompleted implements ShouldBroadcastNow { - use Dispatchable, InteractsWithSockets, SerializesModels; + use Dispatchable; + use InteractsWithSockets; + use SerializesModels; public $user; diff --git a/ProcessMaker/Events/TemplateUpdated.php b/ProcessMaker/Events/TemplateUpdated.php index 8429094d71..a1e9b2188f 100644 --- a/ProcessMaker/Events/TemplateUpdated.php +++ b/ProcessMaker/Events/TemplateUpdated.php @@ -4,7 +4,9 @@ use Carbon\Carbon; use Illuminate\Foundation\Events\Dispatchable; +use ProcessMaker\Helpers\ArrayHelper; use ProcessMaker\Contracts\SecurityLogEventInterface; +use ProcessMaker\Models\ProcessCategory; use ProcessMaker\Traits\FormatSecurityLogChanges; class TemplateUpdated implements SecurityLogEventInterface @@ -28,6 +30,12 @@ public function __construct(array $changes, array $original, bool $processType) $this->changes = $changes; $this->original = $original; $this->processType = $processType; + + // Get category name + $this->original['process_category'] = isset($original['process_category_id']) ? ProcessCategory::getNamesByIds($this->original['process_category_id']) : ''; + unset($this->original['process_category_id']); + $this->changes['process_category'] = isset($changes['process_category_id']) ? ProcessCategory::getNamesByIds($this->changes['process_category_id']) : ''; + unset($this->changes['process_category_id']); } /** @@ -53,7 +61,7 @@ public function getData(): array 'label' => $this->processType, ], 'last_modified' => $this->changes['updated_at'] ?? Carbon::now() - ], $this->formatChanges($newData, $oldData)); + ], ArrayHelper::getArrayDifferencesWithFormat($this->changes, $this->original)); } } diff --git a/ProcessMaker/Http/Controllers/Api/FileController.php b/ProcessMaker/Http/Controllers/Api/FileController.php index bba394fd49..c94b553496 100644 --- a/ProcessMaker/Http/Controllers/Api/FileController.php +++ b/ProcessMaker/Http/Controllers/Api/FileController.php @@ -5,6 +5,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; use ProcessMaker\Events\FilesDeleted; +use ProcessMaker\Events\FilesDownloaded; use ProcessMaker\Http\Controllers\Controller; use ProcessMaker\Http\Resources\ApiCollection; use ProcessMaker\Http\Resources\ApiResource; @@ -282,6 +283,11 @@ public function download(Media $file) $file->id . '/' . $file->file_name; + // Register the Event + if (!empty($file->file_name)) { + FilesDownloaded::dispatch($file->file_name); + } + return response()->download($path); } diff --git a/ProcessMaker/Http/Controllers/Api/ProcessRequestFileController.php b/ProcessMaker/Http/Controllers/Api/ProcessRequestFileController.php index 74beafa7e6..6755335750 100644 --- a/ProcessMaker/Http/Controllers/Api/ProcessRequestFileController.php +++ b/ProcessMaker/Http/Controllers/Api/ProcessRequestFileController.php @@ -14,8 +14,10 @@ use Pion\Laravel\ChunkUpload\Handler\AbstractHandler; use Pion\Laravel\ChunkUpload\Handler\HandlerFactory; use Pion\Laravel\ChunkUpload\Receiver\FileReceiver; +use ProcessMaker\Events\FilesAccessed; use ProcessMaker\Events\FilesCreated; use ProcessMaker\Events\FilesDeleted; +use ProcessMaker\Events\FilesDownloaded; use ProcessMaker\Http\Controllers\Controller; use ProcessMaker\Http\Resources\ApiCollection; use ProcessMaker\Http\Resources\ApiResource; @@ -90,6 +92,11 @@ public function index(Request $laravel_request, ProcessRequest $request) $id = $laravel_request->get('id'); $filter = $name ? $name : $id; + // Register the Event + if (!empty($filter)) { + FilesAccessed::dispatch($filter, $request); + } + // If no filter, return entire collection; otherwise, filter collection if (!$filter) { return new ResourceCollection($media); @@ -160,6 +167,11 @@ public function show(Request $laravel_request, ProcessRequest $request, $media) $file = $request->downloadFile($media); if ($file) { + // Register the Event + if (!empty($file['file_name'])) { + FilesDownloaded::dispatch($file['file_name'], $request); + } + return response()->download($file); } @@ -308,7 +320,7 @@ private function saveUploadedFile(UploadedFile $file, ProcessRequest $processReq ->toMediaCollection(); // Register the Event - FilesCreated::dispatch($media->id); + FilesCreated::dispatch($media->id, $processRequest); return new JsonResponse(['message' => 'The file was uploaded.', 'fileUploadId' => $media->id], 200); } diff --git a/ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php b/ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php index c6b47c0c80..6a579eca65 100644 --- a/ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php +++ b/ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php @@ -174,8 +174,10 @@ public function update(Request $request, ScriptExecutor $scriptExecutor) $scriptExecutor->update( $request->only($scriptExecutor->getFillable()) ); - - ScriptExecutorUpdated::dispatch($scriptExecutor->id, $original_values, $scriptExecutor->getChanges()); + + if (!empty($scriptExecutor->getChanges())) { + ScriptExecutorUpdated::dispatch($scriptExecutor->id, $original_values, $scriptExecutor->getChanges()); + } BuildScriptExecutor::dispatch($scriptExecutor->id, $request->user()->id); diff --git a/ProcessMaker/Http/Controllers/Api/SecurityLogController.php b/ProcessMaker/Http/Controllers/Api/SecurityLogController.php index 08843563a9..918e476763 100644 --- a/ProcessMaker/Http/Controllers/Api/SecurityLogController.php +++ b/ProcessMaker/Http/Controllers/Api/SecurityLogController.php @@ -11,6 +11,7 @@ use ProcessMaker\Http\Resources\ApiResource; use ProcessMaker\Http\Resources\SecurityLogs; use ProcessMaker\Jobs\DownloadSecurityLog; +use ProcessMaker\Models\Media; use ProcessMaker\Models\SecurityLog; use ProcessMaker\Models\User; @@ -129,7 +130,7 @@ public function store(Request $request) { $request->validate(SecurityLog::rules()); - $securityLog = new SecurityLog; + $securityLog = new SecurityLog(); $fields = SensitiveDataHelper::parseArray($request->json()->all()); $securityLog->fill($fields); $securityLog->saveOrFail(); @@ -139,17 +140,25 @@ public function store(Request $request) private function download(Request $request, User $user = null) { + if (!Media::s3IsReady()) { + return response()->json([ + 'success' => false, + 'message' => __('Sorry, this feature requires the configured AWS S3 service. Please contact the administrator.') + ]); + } $request->validate([ 'format' => 'required|string|in:xml,csv', ]); - sleep(1); $sessionUser = Auth::user(); + + // Call the Event DownloadSecurityLog::dispatch($sessionUser, $request->input('format'), $user ? $user->id : null) ->delay(now()->addSeconds(5)); return response()->json([ - 'message' => __('The log file is being prepared and will be sent to your email as soon as it is ready.'), - ], 200); + 'success' => true, + 'message' => __('The file is processing... Please wait for an alert with the download link.') + ]); } public function downloadForAllUsers(Request $request) diff --git a/ProcessMaker/Http/Controllers/RequestController.php b/ProcessMaker/Http/Controllers/RequestController.php index 7acd268e21..5bdbc2c01e 100644 --- a/ProcessMaker/Http/Controllers/RequestController.php +++ b/ProcessMaker/Http/Controllers/RequestController.php @@ -5,6 +5,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Route; +use ProcessMaker\Events\FilesDownloaded; use ProcessMaker\Events\ScreenBuilderStarting; use ProcessMaker\Http\Controllers\Controller; use ProcessMaker\Managers\DataManager; @@ -134,7 +135,17 @@ public function show(ProcessRequest $request, Media $mediaItems) $isProcessManager = $request->process?->manager_id === Auth::user()->id; return view('requests.show', compact( - 'request', 'files', 'canCancel', 'canViewComments', 'canManuallyComplete', 'canRetry', 'manager', 'canPrintScreens', 'screenRequested', 'addons', 'isProcessManager' + 'request', + 'files', + 'canCancel', + 'canViewComments', + 'canManuallyComplete', + 'canRetry', + 'manager', + 'canPrintScreens', + 'screenRequested', + 'addons', + 'isProcessManager' )); } @@ -186,6 +197,9 @@ public function downloadFiles(ProcessRequest $request, $media) $file = $request->downloadFile($media); if ($file) { + // Register the Event + FilesDownloaded::dispatch(basename($file), $request); + return response()->download($file); } diff --git a/ProcessMaker/Jobs/DownloadSecurityLog.php b/ProcessMaker/Jobs/DownloadSecurityLog.php index 4403455969..9d9a56ea9c 100644 --- a/ProcessMaker/Jobs/DownloadSecurityLog.php +++ b/ProcessMaker/Jobs/DownloadSecurityLog.php @@ -2,20 +2,29 @@ namespace ProcessMaker\Jobs; +use Exception; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; +use ProcessMaker\Events\SecurityLogDownloadFailed; use ProcessMaker\Events\SecurityLogDownloadJobCompleted; +use ProcessMaker\Models\Media; +use ProcessMaker\Models\SecurityLog; use ProcessMaker\Models\User; +use Ramsey\Uuid\Uuid; class DownloadSecurityLog implements ShouldQueue { - use Dispatchable, - InteractsWithQueue, - Queueable, - SerializesModels; + use Dispatchable; + use InteractsWithQueue; + use Queueable; + use SerializesModels; private User $user; @@ -23,9 +32,14 @@ class DownloadSecurityLog implements ShouldQueue private ?int $userId; + public const CSV_SEPARATOR = ','; + public const EXPIRATION_HOURS = 24; + public const FORMAT_CSV = 'csv'; + public const FORMAT_XML = 'xml'; + /** * @param User $user - * @param string $format + * @param string $format xml|csv * @param int|null $userId */ public function __construct(User $user, string $format, int $userId = null) @@ -39,18 +53,215 @@ public function __construct(User $user, string $format, int $userId = null) * Execute the job. * * @return void + * @throw Exception */ public function handle() { - //1. Get all data from security_logs table - //2. Create a file in specified format: csv or xml - //3. Zip this file - //4. Store in S3, with private visibility, with 24h of lifecycle - //5. Send email to user with the link or error message - if (mt_rand(1, 10) <= 8) { - event(new SecurityLogDownloadJobCompleted($this->user, true, __('Click on the link and download the file. This link will be available until midnight tonight.'), 'http://processmaker.com/?download=true')); - } else { - event(new SecurityLogDownloadJobCompleted($this->user, false, __('Sorry, it was not possible to generate the log file. Please contact the administrator.'))); + // Check if the S3 is ready to use + if (!Media::s3IsReady()) { + event(new SecurityLogDownloadFailed($this->user, false, __('Sorry, this feature requires the configured AWS S3 service. Please contact the administrator.'))); + return; + } + try { + // Get the temp filename + $filename = $this->createTemporaryFilename(); + // Get the date of expiration + $expires = $this->getExpires(); + // Export the file and get the URL + $url = $this->export($filename, $expires); + $message = __('Click on the link and download the file. This link will be available until '. $expires->toString()); + // Call the event + event(new SecurityLogDownloadJobCompleted($this->user, true, $message, $url)); + } catch (Exception $e) { + $message = __('Sorry, it was not possible to connect AWS S3 service. Please contact the administrator.'); + event(new SecurityLogDownloadFailed($this->user, false, $e->getMessage())); + } + } + + /** + * Get expires time + * + * @return Carbon time + */ + protected function getExpires() + { + return now()->addHours(static::EXPIRATION_HOURS); + } + + /** + * Create a temp file + * + * @return string + */ + protected function createTemporaryFilename() + { + $uuid = Uuid::uuid4()->toString() . Str::random(8); + + return 'security-logs/' . $uuid . '.' . $this->format; + } + + /** + * Export the file and get the URL + * + * @param string $filename + * @param Carbon $expires + * + * @return URL + */ + protected function export(string $filename, Carbon $expires) + { + // Get a disk manager for S3 + $disk = Storage::disk('s3'); + + // Create a stream + $stream = fopen('php://temp', 'w+'); + + // Write the content + $stream = $this->writeContent($stream); + + // Rewind the stream + rewind($stream); + + // Save the stream to S3 + $disk->put($filename, stream_get_contents($stream), [ + 'ACL' => 'private', // private|public-read, + 'Expires' => $expires->toString() + ]); + + // Close the stream + fclose($stream); + + // Save temporary Url + $url = $disk->temporaryUrl( + $filename, + $expires, + [ + 'ResponseContentType' => 'application/octet-stream', + 'ResponseContentDisposition' => 'attachment; filename=' . $filename, + ] + ); + + return $url; + } + + /** + * Generate the content according to the format + * + * @return string + */ + protected function writeContent($stream) + { + $query = DB::table('security_logs'); + + // Check the filter per user + if ($this->userId) { + $query->where('user_id', $this->userId); + } + + // Initial tags for XML + $this->initialTagsXML($this->format === static::FORMAT_XML, $stream); + + // Use a cursor to iterate over the table data + $query->orderBy('id')->cursor()->each(function ($record) use ($stream) { + // Convert each record to an array and write it to the stream + $stream = $this->format === static::FORMAT_CSV ? $this->toCSV($stream, (array) $record) : $this->toXML($stream, (array) $record); + }); + + // End tags for XML + $this->endTagsXML($this->format === static::FORMAT_XML, $stream); + + return $stream; + } + + /** + * Write the CSV line + * + * @param string $stream + * @param array $record + * + * @return string + */ + protected function toCSV($stream, array $record) + { + fputcsv($stream, (array) $record, static::CSV_SEPARATOR); + + return $stream; + } + + /** + * Write the XML node + * + * @param string $stream + * @param array $record + * + * @return string + */ + protected function toXML($stream, array $record) + { + $content = $this->getXmlNode((array) $record); + fwrite($stream, $content); + + return $stream; + } + + /** + * Write the initial tags to XML + * + * @param bool $write + * @param string $stream + * + * @return void + */ + protected function initialTagsXML($write = false, $stream) + { + if ($write) { + $contentXml = '' . PHP_EOL; + $contentXml .= ''; + fwrite($stream, $contentXml); } } + + /** + * Write the end tags to XML + * + * @param bool $write + * @param string $stream + * + * @return void + */ + protected function endTagsXML($write = false, $stream) + { + if ($write) { + $contentXml = PHP_EOL . ''; + fwrite($stream, $contentXml); + } + } + + /** + * Get XML node + * + * @param array $item + * + * @return string + */ + protected function getXmlNode(array $item) + { + $tab = "\t"; + $content = PHP_EOL . $tab . ''; + foreach ($item as $key => $value) { + if (is_object($value)) { + $value = json_encode($value); + } + $content .= sprintf( + '%s<%s>%s', + PHP_EOL . $tab, + $key, + $value, + $key + ); + } + $content .= PHP_EOL . $tab . ''; + + return $content; + } } diff --git a/ProcessMaker/Models/Media.php b/ProcessMaker/Models/Media.php index e2a55c831a..75ad48d282 100644 --- a/ProcessMaker/Models/Media.php +++ b/ProcessMaker/Models/Media.php @@ -191,4 +191,15 @@ public static function getFilesRequest(ProcessRequest $request) // Get all files for process and all subprocesses .. return self::whereIn('model_id', $requestTokenIds)->get(); } + + /** + * Check if the S3 is ready to use + */ + public static function s3IsReady() + { + return config('filesystems.disks.s3.key') + && config('filesystems.disks.s3.secret') + && config('filesystems.disks.s3.region') + && config('filesystems.disks.s3.bucket'); + } } diff --git a/ProcessMaker/Models/ProcessCategory.php b/ProcessMaker/Models/ProcessCategory.php index ab82b452f4..e0d45fbd29 100644 --- a/ProcessMaker/Models/ProcessCategory.php +++ b/ProcessMaker/Models/ProcessCategory.php @@ -68,4 +68,19 @@ public function processes() { return $this->morphedByMany(Process::class, 'assignable', 'category_assignments', 'category_id'); } + + /** + * Get Process Category Names + * @param string String of ids separated by a custom delimiter. + * @param string Delimiter to split ids. By default ',' + * @return string A string separated by commas with Process Category Names + */ + public static function getNamesByIds(string $ids, string $delimiter = ','): string + { + $resultString = ''; + $arrayIds = explode($delimiter, $ids); + $results = ProcessCategory::whereIn('id', array_map('intval', $arrayIds))->pluck('name'); + $resultString = implode(', ', $results->toArray()); + return $resultString; + } } diff --git a/ProcessMaker/Models/ScreenCategory.php b/ProcessMaker/Models/ScreenCategory.php index 5e6df9da5e..39f7eb5623 100644 --- a/ProcessMaker/Models/ScreenCategory.php +++ b/ProcessMaker/Models/ScreenCategory.php @@ -67,4 +67,19 @@ public function screens() { return $this->morphedByMany(Screen::class, 'assignable', 'category_assignments', 'category_id'); } + + /** + * Get Screen Category Names + * @param string String of ids separated by a custom delimiter. + * @param string Delimiter to split ids. By default ',' + * @return string A string separated by commas with Screen Category Names + */ + public static function getNamesByIds(string $ids, string $delimiter = ','): string + { + $resultString = ''; + $arrayIds = explode($delimiter, $ids); + $results = ScreenCategory::whereIn('id', array_map('intval', $arrayIds))->pluck('name'); + $resultString = implode(', ', $results->toArray()); + return $resultString; + } } diff --git a/ProcessMaker/Models/ScriptCategory.php b/ProcessMaker/Models/ScriptCategory.php index 87c1712027..1e2954bab2 100644 --- a/ProcessMaker/Models/ScriptCategory.php +++ b/ProcessMaker/Models/ScriptCategory.php @@ -67,4 +67,19 @@ public function scripts() { return $this->morphedByMany(Script::class, 'assignable', 'category_assignments', 'category_id'); } + + /** + * Get Script Category Names + * @param string String of ids separated by a custom delimiter. + * @param string Delimiter to split ids. By default ',' + * @return string A string separated by commas with Script Category Names + */ + public static function getNamesByIds(string $ids, string $delimiter = ','): string + { + $resultString = ''; + $arrayIds = explode($delimiter, $ids); + $results = ScriptCategory::whereIn('id', array_map('intval', $arrayIds))->pluck('name'); + $resultString = implode(', ', $results->toArray()); + return $resultString; + } } diff --git a/ProcessMaker/Providers/EventServiceProvider.php b/ProcessMaker/Providers/EventServiceProvider.php index bbdf5fb419..24d422041f 100644 --- a/ProcessMaker/Providers/EventServiceProvider.php +++ b/ProcessMaker/Providers/EventServiceProvider.php @@ -3,6 +3,56 @@ namespace ProcessMaker\Providers; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; +use ProcessMaker\Events\ActivityReassignment; +use ProcessMaker\Events\AuthClientCreated; +use ProcessMaker\Events\AuthClientDeleted; +use ProcessMaker\Events\AuthClientUpdated; +use ProcessMaker\Events\CategoryCreated; +use ProcessMaker\Events\CategoryDeleted; +use ProcessMaker\Events\CategoryUpdated; +use ProcessMaker\Events\CustomizeUiUpdated; +use ProcessMaker\Events\EnvironmentVariablesUpdated; +use ProcessMaker\Events\EnvironmentVariablesCreated; +use ProcessMaker\Events\EnvironmentVariablesDeleted; +use ProcessMaker\Events\FilesAccessed; +use ProcessMaker\Events\FilesCreated; +use ProcessMaker\Events\FilesDeleted; +use ProcessMaker\Events\FilesDownloaded; +use ProcessMaker\Events\FilesUpdated; +use ProcessMaker\Events\GroupCreated; +use ProcessMaker\Events\GroupDeleted; +use ProcessMaker\Events\GroupUpdated; +use ProcessMaker\Events\GroupUsersUpdated; +use ProcessMaker\Events\PermissionUpdated; +use ProcessMaker\Events\ProcessArchived; +use ProcessMaker\Events\ProcessCreated; +use ProcessMaker\Events\ProcessPublished; +use ProcessMaker\Events\ProcessRestored; +use ProcessMaker\Events\ProcessUpdated; +use ProcessMaker\Events\RequestAction; +use ProcessMaker\Events\RequestError; +use ProcessMaker\Events\ScreenCreated; +use ProcessMaker\Events\ScreenDeleted; +use ProcessMaker\Events\ScreenUpdated; +use ProcessMaker\Events\ScriptCreated; +use ProcessMaker\Events\ScriptDeleted; +use ProcessMaker\Events\ScriptDuplicated; +use ProcessMaker\Events\ScriptExecutorCreated; +use ProcessMaker\Events\ScriptExecutorDeleted; +use ProcessMaker\Events\ScriptExecutorUpdated; +use ProcessMaker\Events\ScriptUpdated; +use ProcessMaker\Events\SettingsUpdated; +use ProcessMaker\Events\TemplateCreated; +use ProcessMaker\Events\TemplateDeleted; +use ProcessMaker\Events\TemplateUpdated; +use ProcessMaker\Events\TokenCreated; +use ProcessMaker\Events\TokenDeleted; +use ProcessMaker\Events\UnauthorizedAccessAttempt; +use ProcessMaker\Events\UserCreated; +use ProcessMaker\Events\UserDeleted; +use ProcessMaker\Events\UserGroupMembershipUpdated; +use ProcessMaker\Events\UserUpdated; +use ProcessMaker\Listeners\SecurityLogger; /** * Register our Events and their Listeners @@ -27,151 +77,9 @@ class EventServiceProvider extends ServiceProvider 'Illuminate\Database\Events\MigrationsEnded' => [ 'ProcessMaker\Listeners\UpdateDataLakeViews', ], - 'ProcessMaker\Events\ActivityReassignment' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\AuthClientUpdated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\AuthClientCreated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\AuthClientDeleted' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\CategoryCreated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\CategoryDeleted' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\CategoryUpdated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\CustomizeUiUpdated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\EnvironmentVariablesCreated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\EnvironmentVariablesDeleted' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\EnvironmentVariablesUpdated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\FilesCreated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\FilesDeleted' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\FilesUpdated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\GroupCreated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\GroupDeleted' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\GroupUpdated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\GroupUsersUpdated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\PermissionUpdated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\ProcessCreated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\ProcessArchived' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\ProcessPublished' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\ProcessRestored' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\ProcessUpdated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\RequestError' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\RequestAction' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\ScreenCreated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\ScreenDeleted' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\ScreenUpdated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\ScriptUpdated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\ScriptCreated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\ScriptDuplicated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\ScriptDeleted' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\ScriptExecutorCreated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\ScriptExecutorDeleted' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\ScriptExecutorUpdated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], 'ProcessMaker\Events\SessionStarted' => [ 'ProcessMaker\Listeners\ActiveUserListener', ], - 'ProcessMaker\Events\SettingsUpdated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\TemplateCreated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\TemplateDeleted' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\TemplateUpdated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\TokenCreated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\TokenDeleted' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\UnauthorizedAccessAttempt' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\UserCreated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\UserDeleted' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\UserGroupMembershipUpdated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - 'ProcessMaker\Events\UserUpdated' => [ - 'ProcessMaker\Listeners\SecurityLogger', - ], - ]; /** @@ -181,5 +89,58 @@ class EventServiceProvider extends ServiceProvider public function boot() { parent::boot(); + + // Check if the variable security_log is enable + if (config('app.security_log')) { + $this->app['events']->listen(ActivityReassignment::class, SecurityLogger::class); + $this->app['events']->listen(AuthClientUpdated::class, SecurityLogger::class); + $this->app['events']->listen(AuthClientCreated::class, SecurityLogger::class); + $this->app['events']->listen(AuthClientDeleted::class, SecurityLogger::class); + $this->app['events']->listen(CategoryCreated::class, SecurityLogger::class); + $this->app['events']->listen(CategoryDeleted::class, SecurityLogger::class); + $this->app['events']->listen(CategoryUpdated::class, SecurityLogger::class); + $this->app['events']->listen(CustomizeUiUpdated::class, SecurityLogger::class); + $this->app['events']->listen(EnvironmentVariablesCreated::class, SecurityLogger::class); + $this->app['events']->listen(EnvironmentVariablesDeleted::class, SecurityLogger::class); + $this->app['events']->listen(EnvironmentVariablesUpdated::class, SecurityLogger::class); + $this->app['events']->listen(FilesAccessed::class, SecurityLogger::class); + $this->app['events']->listen(FilesCreated::class, SecurityLogger::class); + $this->app['events']->listen(FilesDeleted::class, SecurityLogger::class); + $this->app['events']->listen(FilesDownloaded::class, SecurityLogger::class); + $this->app['events']->listen(FilesUpdated::class, SecurityLogger::class); + $this->app['events']->listen(GroupCreated::class, SecurityLogger::class); + $this->app['events']->listen(GroupDeleted::class, SecurityLogger::class); + $this->app['events']->listen(GroupUpdated::class, SecurityLogger::class); + $this->app['events']->listen(GroupUsersUpdated::class, SecurityLogger::class); + $this->app['events']->listen(PermissionUpdated::class, SecurityLogger::class); + $this->app['events']->listen(ProcessCreated::class, SecurityLogger::class); + $this->app['events']->listen(ProcessArchived::class, SecurityLogger::class); + $this->app['events']->listen(ProcessPublished::class, SecurityLogger::class); + $this->app['events']->listen(ProcessRestored::class, SecurityLogger::class); + $this->app['events']->listen(ProcessUpdated::class, SecurityLogger::class); + $this->app['events']->listen(RequestError::class, SecurityLogger::class); + $this->app['events']->listen(RequestAction::class, SecurityLogger::class); + $this->app['events']->listen(ScreenCreated::class, SecurityLogger::class); + $this->app['events']->listen(ScreenDeleted::class, SecurityLogger::class); + $this->app['events']->listen(ScreenUpdated::class, SecurityLogger::class); + $this->app['events']->listen(ScriptCreated::class, SecurityLogger::class); + $this->app['events']->listen(ScriptDeleted::class, SecurityLogger::class); + $this->app['events']->listen(ScriptDuplicated::class, SecurityLogger::class); + $this->app['events']->listen(ScriptExecutorCreated::class, SecurityLogger::class); + $this->app['events']->listen(ScriptExecutorDeleted::class, SecurityLogger::class); + $this->app['events']->listen(ScriptExecutorUpdated::class, SecurityLogger::class); + $this->app['events']->listen(ScriptUpdated::class, SecurityLogger::class); + $this->app['events']->listen(SettingsUpdated::class, SecurityLogger::class); + $this->app['events']->listen(TemplateCreated::class, SecurityLogger::class); + $this->app['events']->listen(TemplateDeleted::class, SecurityLogger::class); + $this->app['events']->listen(TemplateUpdated::class, SecurityLogger::class); + $this->app['events']->listen(TokenCreated::class, SecurityLogger::class); + $this->app['events']->listen(TokenDeleted::class, SecurityLogger::class); + $this->app['events']->listen(UnauthorizedAccessAttempt::class, SecurityLogger::class); + $this->app['events']->listen(UserCreated::class, SecurityLogger::class); + $this->app['events']->listen(UserDeleted::class, SecurityLogger::class); + $this->app['events']->listen(UserGroupMembershipUpdated::class, SecurityLogger::class); + $this->app['events']->listen(UserUpdated::class, SecurityLogger::class); + } } } diff --git a/composer.json b/composer.json index ff92f28e57..766ab60d3f 100644 --- a/composer.json +++ b/composer.json @@ -30,6 +30,7 @@ "laravelcollective/html": "^6.3", "lavary/laravel-menu": "^1.8", "lcobucci/jwt": "^4.2", + "league/flysystem-aws-s3-v3": "^1.0", "mateusjunges/laravel-kafka": "^1.9", "microsoft/microsoft-graph": "^1.77", "moontoast/math": "^1.2", diff --git a/composer.lock b/composer.lock index bc2e41b51b..1b09d893c6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "28c38d38f6d6836d788869f1b1b19435", + "content-hash": "ecf10e5a16c7ac3a853670610c981858", "packages": [ { "name": "asm89/stack-cors", @@ -62,6 +62,155 @@ }, "time": "2022-01-18T09:12:03+00:00" }, + { + "name": "aws/aws-crt-php", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/awslabs/aws-crt-php.git", + "reference": "1926277fc71d253dfa820271ac5987bdb193ccf5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/1926277fc71d253dfa820271ac5987bdb193ccf5", + "reference": "1926277fc71d253dfa820271ac5987bdb193ccf5", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality." + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "AWS SDK Common Runtime Team", + "email": "aws-sdk-common-runtime@amazon.com" + } + ], + "description": "AWS Common Runtime for PHP", + "homepage": "https://github.com/awslabs/aws-crt-php", + "keywords": [ + "amazon", + "aws", + "crt", + "sdk" + ], + "support": { + "issues": "https://github.com/awslabs/aws-crt-php/issues", + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.1" + }, + "time": "2023-03-24T20:22:19+00:00" + }, + { + "name": "aws/aws-sdk-php", + "version": "3.274.0", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "b2f37a49ca40bce633323a6988477c22be562c37" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b2f37a49ca40bce633323a6988477c22be562c37", + "reference": "b2f37a49ca40bce633323a6988477c22be562c37", + "shasum": "" + }, + "require": { + "aws/aws-crt-php": "^1.0.4", + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", + "guzzlehttp/promises": "^1.4.0", + "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", + "mtdowling/jmespath.php": "^2.6", + "php": ">=5.5", + "psr/http-message": "^1.0" + }, + "require-dev": { + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "composer/composer": "^1.10.22", + "dms/phpunit-arraysubset-asserts": "^0.4.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "ext-pcntl": "*", + "ext-sockets": "*", + "nette/neon": "^2.3", + "paragonie/random_compat": ">= 2", + "phpunit/phpunit": "^4.8.35 || ^5.6.3 || ^9.5", + "psr/cache": "^1.0", + "psr/simple-cache": "^1.0", + "sebastian/comparator": "^1.2.3 || ^4.0", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-sockets": "To use client-side monitoring" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Aws\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "support": { + "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", + "issues": "https://github.com/aws/aws-sdk-php/issues", + "source": "https://github.com/aws/aws-sdk-php/tree/3.274.0" + }, + "time": "2023-06-27T18:32:17+00:00" + }, { "name": "babenkoivan/elastic-adapter", "version": "v3.5.0", @@ -3924,6 +4073,71 @@ ], "time": "2022-10-04T09:16:37+00:00" }, + { + "name": "league/flysystem-aws-s3-v3", + "version": "1.0.30", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", + "reference": "af286f291ebab6877bac0c359c6c2cb017eb061d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/af286f291ebab6877bac0c359c6c2cb017eb061d", + "reference": "af286f291ebab6877bac0c359c6c2cb017eb061d", + "shasum": "" + }, + "require": { + "aws/aws-sdk-php": "^3.20.0", + "league/flysystem": "^1.0.40", + "php": ">=5.5.0" + }, + "require-dev": { + "henrikbjorn/phpspec-code-coverage": "~1.0.1", + "phpspec/phpspec": "^2.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\AwsS3v3\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Flysystem adapter for the AWS S3 SDK v3.x", + "support": { + "issues": "https://github.com/thephpleague/flysystem-aws-s3-v3/issues", + "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/1.0.30" + }, + "funding": [ + { + "url": "https://offset.earth/frankdejonge", + "type": "custom" + }, + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2022-07-02T13:51:38+00:00" + }, { "name": "league/fractal", "version": "0.20.1", @@ -4732,6 +4946,67 @@ "abandoned": "brick/math", "time": "2020-01-05T04:49:34+00:00" }, + { + "name": "mtdowling/jmespath.php", + "version": "2.6.1", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "9b87907a81b87bc76d19a7fb2d61e61486ee9edb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/9b87907a81b87bc76d19a7fb2d61e61486ee9edb", + "reference": "9b87907a81b87bc76d19a7fb2d61e61486ee9edb", + "shasum": "" + }, + "require": { + "php": "^5.4 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" + }, + "require-dev": { + "composer/xdebug-handler": "^1.4 || ^2.0", + "phpunit/phpunit": "^4.8.36 || ^7.5.15" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "files": [ + "src/JmesPath.php" + ], + "psr-4": { + "JmesPath\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "support": { + "issues": "https://github.com/jmespath/jmespath.php/issues", + "source": "https://github.com/jmespath/jmespath.php/tree/2.6.1" + }, + "time": "2021-06-14T00:11:39+00:00" + }, { "name": "mustache/mustache", "version": "v2.14.2", diff --git a/config/app.php b/config/app.php index dc5c365549..1ef765490f 100644 --- a/config/app.php +++ b/config/app.php @@ -85,9 +85,14 @@ 'bpmn_actions_lock_check_interval' => (int) env('BPMN_ACTIONS_LOCK_CHECK_INTERVAL', 1000), // The url of our host from inside the docker - 'docker_host_url' => env('DOCKER_HOST_URL', - preg_replace('/(\w+):\/\/([^:\/]+)(\:\d+)?/', '$1://172.17.0.1$3', - env('APP_URL', 'http://localhost'))), + 'docker_host_url' => env( + 'DOCKER_HOST_URL', + preg_replace( + '/(\w+):\/\/([^:\/]+)(\:\d+)?/', + '$1://172.17.0.1$3', + env('APP_URL', 'http://localhost') + ) + ), // Allows our script executors to ignore invalid SSL. This should only be set to false for development. 'api_ssl_verify' => env('API_SSL_VERIFY', 'true'), @@ -101,6 +106,9 @@ // Microservice AI Host 'ai_microservice_host' => env('AI_MICROSERVICE_HOST'), + // Security log + 'security_log' => env('SECURITY_LOG', 'true'), + // Message broker driver to use in Workflow Manager 'message_broker_driver' => env('MESSAGE_BROKER_DRIVER', 'default'), diff --git a/resources/js/admin/users/components/SecurityLogsModal.vue b/resources/js/admin/users/components/SecurityLogsModal.vue index 3410e0b6f1..fce92b22cc 100644 --- a/resources/js/admin/users/components/SecurityLogsModal.vue +++ b/resources/js/admin/users/components/SecurityLogsModal.vue @@ -166,7 +166,7 @@ export default { let key = ""; let value = ""; let auxKey = ""; - const auxArray = {}; + let auxArray = {}; for ([key, value] of Object.entries(data)) { if (key.startsWith("+")) { @@ -191,7 +191,36 @@ export default { auxArray[this.capitalizeKey(key)] = this.booleanToString(value); } } - return auxArray; + + return this.sortModalArray(auxArray); + }, + /** + * Sort modal Array + */ + sortModalArray(auxArray) { + let sortKey = ["Name"]; + let auxArraySorted = {}; + let dateKey = Object.keys(auxArray).find(key => ["Created_at", "Deleted_at", "Updated_at", "Last_modified", "Accessed_at"].includes(key)); + + if (dateKey) { + sortKey.push(dateKey); + } + + sortKey.push("Description"); + + Object.keys(auxArray).forEach(key => { + if (!sortKey.includes(key)) { + sortKey.push(key); + } + }); + + sortKey.forEach(key => { + if (key in auxArray) { + auxArraySorted[key] = auxArray[key]; + } + }); + + return auxArraySorted; }, /** * Verify if value is a string o null diff --git a/tests/Feature/Api/SecurityLogsTest.php b/tests/Feature/Api/SecurityLogsTest.php index 4afc33a1b3..cf8b7e9ffd 100644 --- a/tests/Feature/Api/SecurityLogsTest.php +++ b/tests/Feature/Api/SecurityLogsTest.php @@ -149,28 +149,11 @@ public function testStore() ], ]); $response->assertStatus(201); + $collection = SecurityLog::where('user_id', $this->user->id)->get(); $this->assertCount(2, $collection); $securityLog = $collection->skip(1)->first(); - $this->assertEquals([ - 'fullname' => $this->user->getAttribute('fullname'), - ], (array) $securityLog->data); - $this->assertEquals([ - 'event' => 'TestStoreEvent', - 'ip' => '127.0.01', - 'user_id' => $this->user->id, - 'meta' => (object) [ - 'user_agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36', - 'browser' => (object) [ - 'name' => 'Chrome', - 'version' => '111', - ], - 'os' => (object) [ - 'name' => 'Linux', - 'version' => null, - ], - ], - ], $securityLog->only('event', 'ip', 'user_id', 'meta')); + $this->assertIsObject($securityLog->data); } public function testSettingUpdated() @@ -180,10 +163,14 @@ public function testSettingUpdated() $original = array_intersect_key($setting->getOriginal(), $setting->getDirty()); $setting->save(); SettingsUpdated::dispatch($setting, $setting->getChanges(), $original); - $collection = SecurityLog::get(); - $this->assertCount(1, $collection); - $securityLog = $collection->first(); - $this->assertEquals('SettingsUpdated', $securityLog->getAttribute('event')); + // Check if the variable security_log is enable + if (config('app.security_log')) { + $this->assertCount(1, $collection); + $securityLog = $collection->first(); + $this->assertEquals('SettingsUpdated', $securityLog->getAttribute('event')); + } else { + $this->assertCount(0, $collection); + } } } diff --git a/tests/Feature/Jobs/DownloadSecurityLogTest.php b/tests/Feature/Jobs/DownloadSecurityLogTest.php new file mode 100644 index 0000000000..9011b5a94c --- /dev/null +++ b/tests/Feature/Jobs/DownloadSecurityLogTest.php @@ -0,0 +1,131 @@ +delete(); + + $this->simpleCollection = [ + ['id' => 1, 'event' => 'login'], + ['id' => 2, 'event' => 'logout'], + ]; + + SecurityLog::factory()->create(['event' => 'login', 'user_id' => $this->user->id]); + SecurityLog::factory()->create(['event' => 'logout', 'user_id' => $this->user->id]); + SecurityLog::factory()->create(['event' => 'attempt']); + SecurityLog::factory()->create(['event' => 'attempt']); + } + + public function testCreateTemporaryFilename() + { + $job = new DownloadSecurityLog($this->user, DownloadSecurityLog::FORMAT_CSV); + $method = new ReflectionMethod($job, 'createTemporaryFilename'); + $filename = $method->invoke($job); + $this->assertStringContainsString('.csv', $filename); + + $job = new DownloadSecurityLog($this->user, DownloadSecurityLog::FORMAT_XML); + $method = new ReflectionMethod($job, 'createTemporaryFilename'); + $filename = $method->invoke($job); + $this->assertStringContainsString('.xml', $filename); + } + + public function testExpires() + { + $job = new DownloadSecurityLog($this->user, DownloadSecurityLog::FORMAT_CSV); + $method = new ReflectionMethod($job, 'getExpires'); + $expires = $method->invoke($job); + $this->assertLessThan($expires, now()); + + $job = new DownloadSecurityLog($this->user, DownloadSecurityLog::FORMAT_XML); + $method = new ReflectionMethod($job, 'getExpires'); + $expires = $method->invoke($job); + $this->assertLessThan($expires, now()); + } + + /** + * @covers DownloadSecurityLog::toCSV + */ + public function testWriteContentCSV() + { + $stream = fopen('php://temp', 'w+'); + $job = new DownloadSecurityLog($this->user, DownloadSecurityLog::FORMAT_CSV); + $csv = (new ReflectionMethod($job, 'writeContent'))->invoke($job, $stream); + $this->assertNotEmpty($csv); + $this->assertTrue(rewind($stream)); + $this->assertTrue(fclose($stream)); + } + + /** + * @covers DownloadSecurityLog::initialTagsXML + * @covers DownloadSecurityLog::toXML + * @covers DownloadSecurityLog::endTagsXML + */ + public function testWriteContentXML() + { + $stream = fopen('php://temp', 'w+'); + $job = new DownloadSecurityLog($this->user, DownloadSecurityLog::FORMAT_XML); + $xml = (new ReflectionMethod($job, 'writeContent'))->invoke($job, $stream); + $this->assertNotEmpty($xml); + $this->assertTrue(rewind($stream)); + $this->assertTrue(fclose($stream)); + } + + public function testHandleWithSuccess() + { + if ( + !config('filesystems.disks.s3.key') + && !config('filesystems.disks.s3.secret') + && !config('filesystems.disks.s3.region') + && !config('filesystems.disks.s3.bucket') + ) { + $this->markTestSkipped( + 'AWS S3 service is not available.' + ); + } else { + $this->expectsEvents(SecurityLogDownloadJobCompleted::class); + $job = new DownloadSecurityLog($this->user, DownloadSecurityLog::FORMAT_CSV); + $url = (new ReflectionMethod($job, 'handle'))->invoke($job); + $this->assertNotEmpty($url); + $data = file_get_contents($url); + $this->assertNotEmpty($data); + } + } + + public function testExport() + { + if ( + !config('filesystems.disks.s3.key') + && !config('filesystems.disks.s3.secret') + && !config('filesystems.disks.s3.region') + && !config('filesystems.disks.s3.bucket') + ) { + $this->markTestSkipped( + 'AWS S3 service is not available.' + ); + } else { + $this->expectsEvents(SecurityLogDownloadJobCompleted::class); + $job = new DownloadSecurityLog($this->user, DownloadSecurityLog::FORMAT_CSV); + $filename = (new ReflectionMethod($job, 'createTemporaryFilename'))->invoke($job); + $expires = (new ReflectionMethod($job, 'getExpires'))->invoke($job); + $url = (new ReflectionMethod($job, 'export'))->invoke($job, $filename, $expires); + $this->assertNotEmpty($url); + $data = file_get_contents($url); + $this->assertNotEmpty($data); + } + } +} diff --git a/tests/unit/ProcessMaker/Models/ProcessCategoryTest.php b/tests/unit/ProcessMaker/Models/ProcessCategoryTest.php new file mode 100644 index 0000000000..607e18a17c --- /dev/null +++ b/tests/unit/ProcessMaker/Models/ProcessCategoryTest.php @@ -0,0 +1,49 @@ +create([ + 'name' => 'Screen Category 7', + ]); + $this->assertEquals( + $category->name, + ProcessCategory::getNamesByIds($category->id) + ); + + //Case 2: more than one Id + $category2 = ProcessCategory::factory()->create([ + 'name' => 'Screen Category 33', + ]); + $this->assertEquals( + $category->name . ', ' . $category2->name, + ProcessCategory::getNamesByIds($category->id . ',' . $category2->id) + ); + + //Case 3: without Id + $stringIds = ''; + $this->assertEquals( + "", + ProcessCategory::getNamesByIds($stringIds) + ); + + //Case 4: non-existentId + $stringIds = '9452'; + $this->assertEquals( + "", + ProcessCategory::getNamesByIds($stringIds) + ); + } +} diff --git a/tests/unit/ProcessMaker/Models/ScreenCategoryTest.php b/tests/unit/ProcessMaker/Models/ScreenCategoryTest.php new file mode 100644 index 0000000000..e07689c7da --- /dev/null +++ b/tests/unit/ProcessMaker/Models/ScreenCategoryTest.php @@ -0,0 +1,49 @@ +create([ + 'name' => 'Screen Category 1', + ]); + $this->assertEquals( + $category->name, + ScreenCategory::getNamesByIds($category->id) + ); + + //Case 2: more than one Id + $category2 = ScreenCategory::factory()->create([ + 'name' => 'Screen Category 33', + ]); + $this->assertEquals( + $category->name . ', ' . $category2->name, + ScreenCategory::getNamesByIds($category->id . ',' . $category2->id) + ); + + //Case 3: without Id + $stringIds = ''; + $this->assertEquals( + "", + ScreenCategory::getNamesByIds($stringIds) + ); + + //Case 4: non-existentId + $stringIds = '9452'; + $this->assertEquals( + "", + ScreenCategory::getNamesByIds($stringIds) + ); + } +} diff --git a/tests/unit/ProcessMaker/Models/ScriptCategoryTest.php b/tests/unit/ProcessMaker/Models/ScriptCategoryTest.php new file mode 100644 index 0000000000..b69b336445 --- /dev/null +++ b/tests/unit/ProcessMaker/Models/ScriptCategoryTest.php @@ -0,0 +1,49 @@ +create([ + 'name' => 'Screen Category 13', + ]); + $this->assertEquals( + $category->name, + ScriptCategory::getNamesByIds($category->id) + ); + + //Case 2: more than one Id + $category2 = ScriptCategory::factory()->create([ + 'name' => 'Screen Category 33', + ]); + $this->assertEquals( + $category->name . ', ' . $category2->name, + ScriptCategory::getNamesByIds($category->id . ',' . $category2->id) + ); + + //Case 3: without Id + $stringIds = ''; + $this->assertEquals( + "", + ScriptCategory::getNamesByIds($stringIds) + ); + + //Case 4: non-existentId + $stringIds = '9452'; + $this->assertEquals( + "", + ScriptCategory::getNamesByIds($stringIds) + ); + } +}