diff --git a/ProcessMaker/Application.php b/ProcessMaker/Application.php index 5cea34ac9a..f9edc37e11 100644 --- a/ProcessMaker/Application.php +++ b/ProcessMaker/Application.php @@ -15,7 +15,6 @@ use Illuminate\Support\Facades\Config; use ProcessMaker\Console\Kernel; use ProcessMaker\Multitenancy\Tenant; -use ProcessMaker\Multitenancy\TenantBootstrapper; /** * Class Application. @@ -103,15 +102,4 @@ public function registerConfiguredProviders() parent::registerConfiguredProviders(); } - - public function bootstrapWith(array $bootstrappers) - { - // Insert TenantBootstrapper after LoadEnvironmentVariables - if ($bootstrappers[0] !== LoadEnvironmentVariables::class) { - throw new \Exception('LoadEnvironmentVariables is not the first bootstrapper. Did a laravel upgrade change this?'); - } - array_splice($bootstrappers, 1, 0, [TenantBootstrapper::class]); - - return parent::bootstrapWith($bootstrappers); - } } diff --git a/ProcessMaker/Console/Commands/HorizonListen.php b/ProcessMaker/Console/Commands/HorizonListen.php deleted file mode 100644 index a3f3dd47b1..0000000000 --- a/ProcessMaker/Console/Commands/HorizonListen.php +++ /dev/null @@ -1,51 +0,0 @@ -info('Multitenancy is disabled'); + + return; + } + + $errors = []; $currentTenant = null; if (app()->has('currentTenant')) { $currentTenant = app('currentTenant'); } - if (config('app.multitenancy') && !$currentTenant) { + if (!$currentTenant) { $this->error('Multitenancy enabled but no current tenant found.'); return; } - $this->info('Current Tenant ID: ' . ($currentTenant?->id ?? 'NONE')); + \Log::warning('TenantsVerify: Current Tenant ID: ' . ($currentTenant?->id ?? 'NONE')); $paths = [ ['Storage Path', storage_path()], @@ -55,32 +64,44 @@ public function handle() ]; // Display paths in a nice table - $this->table(['Path', 'Value'], $paths); + $this->infoTable(['Path', 'Value'], $paths); $configs = [ - 'app.key', - 'app.url', - 'app.instance', - 'cache.prefix', - 'database.redis.options.prefix', - 'cache.stores.cache_settings.prefix', - 'script-runner-microservice.callback', - 'database.connections.processmaker.database', - 'logging.channels.daily.path', - 'filesystems.disks.public.root', - 'filesystems.disks.local.root', - 'filesystems.disks.lang.root', + 'app.key' => null, + 'app.url' => null, + 'app.instance' => null, + 'cache.prefix' => 'tenant_{tenant_id}:', + 'database.redis.options.prefix' => null, + 'cache.stores.cache_settings.prefix' => 'tenant_{tenant_id}:settings', + 'script-runner-microservice.callback' => null, + 'database.connections.processmaker.database' => null, + 'logging.channels.daily.path' => base_path() . '/storage/tenant_{tenant_id}/logs/processmaker.log', + 'filesystems.disks.public.root' => base_path() . '/storage/tenant_{tenant_id}/app/public', + 'filesystems.disks.local.root' => base_path() . '/storage/tenant_{tenant_id}/app', + 'filesystems.disks.lang.root' => base_path() . '/resources/lang/tenant_{tenant_id}', ]; - $configs = array_map(function ($config) { + $configs = array_map(function ($config) use ($configs, $currentTenant, &$errors) { + $ok = ''; + if ($configs[$config] !== null) { + $expected = str_replace('{tenant_id}', $currentTenant->id, $configs[$config]); + if (config($config) === $expected) { + $ok = '✓'; + } else { + $ok = '✗'; + $errors[] = 'Expected: ' . $expected . ' != Actual: ' . config($config); + } + } + return [ $config, config($config), + $ok, ]; - }, $configs); + }, array_keys($configs)); // Display configs in a nice table - $this->table(['Config', 'Value'], $configs); + $this->infoTable(['Config', 'Value', 'OK'], $configs); $env = EnvironmentVariable::first(); if (!$env) { @@ -102,10 +123,62 @@ public function handle() ['Tenant Config Is Cached', File::exists(app()->getCachedConfigPath()) ? 'Yes' : 'No'], ['First username (database check)', User::first()?->username ?? 'No users found'], ['Decrypted check', substr($decrypted, 0, 50)], - ['Original App URL (landlord)', $currentTenant?->getOriginalValue('APP_URL') ?? config('app.url')], + // ['Original App URL (landlord)', $currentTenant?->getOriginalValue('APP_URL') ?? config('app.url')], + ['config("app.url")', config('app.url')], + ['getenv("APP_URL")', getenv('APP_URL')], + ['env("APP_URL")', env('APP_URL')], + ['$_SERVER["APP_URL"]', $_SERVER['APP_URL'] ?? 'NOT SET'], + ['$_ENV["APP_URL"]', $_ENV['APP_URL'] ?? 'NOT SET'], + ['Current PID', getmypid()], ]; // Display other in a nice table - $this->table(['Other', 'Value'], $other); + $this->infoTable(['Other', 'Value'], $other); + + $checkUrls = [ + 'config("app.url")' => config('app.url'), + 'getenv("APP_URL")' => getenv('APP_URL'), + 'env("APP_URL")' => env('APP_URL'), + '$_SERVER["APP_URL"]' => $_SERVER['APP_URL'] ?? 'NOT SET', + '$_ENV["APP_URL"]' => $_ENV['APP_URL'] ?? 'NOT SET', + ]; + + foreach ($checkUrls as $key => $value) { + if ($value !== $currentTenant?->config['app.url']) { + $errors[] = 'Expected: ' . $key . ' to be ' . $currentTenant?->config['app.url'] . ' but got ' . $value; + } + } + + $this->finish($errors); + } + + private function finish($errors) + { + if (count($errors) > 0) { + $this->error('Errors found'); + } else { + $this->info('No errors found'); + } + + if ($this->option('json')) { + $this->jsonData['Errors'] = $errors; + $this->line(json_encode($this->jsonData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + } + } + + private function infoTable($headers, $rows) + { + if ($this->option('json')) { + $section = []; + foreach ($rows as $row) { + $section[$row[0]] = $row[1]; + } + $this->jsonData[$headers[0]] = $section; + } else { + foreach ($rows as $row) { + \Log::warning($row[0] . ': ' . $row[1]); + } + $this->table($headers, $rows); + } } } diff --git a/ProcessMaker/Multitenancy/MakeQueueTenantAwareAction.php b/ProcessMaker/Multitenancy/MakeQueueTenantAwareAction.php index 25118798fd..a66ef5d9da 100644 --- a/ProcessMaker/Multitenancy/MakeQueueTenantAwareAction.php +++ b/ProcessMaker/Multitenancy/MakeQueueTenantAwareAction.php @@ -7,11 +7,14 @@ class MakeQueueTenantAwareAction extends BaseMakeQueueTenantAwareAction { /** - * We're handling tenant aware queues manually, however, we still need to implement this because for some - * reason the Spatie package calls it in Multitenancy::start(), weather it's a configured action or not. + * Non-multitenant environments shouldn't throw an exception if the tenant is not found. */ public function execute() : void { - // Do nothing + if (!config('app.multitenancy')) { + return; + } + + parent::execute(); } } diff --git a/ProcessMaker/Multitenancy/PrefixCacheTask.php b/ProcessMaker/Multitenancy/PrefixCacheTask.php new file mode 100644 index 0000000000..30aa0c127a --- /dev/null +++ b/ProcessMaker/Multitenancy/PrefixCacheTask.php @@ -0,0 +1,32 @@ +getKey() . ':'; + $this->setCachePrefix($cachePrefix); + + $this->originalSettingsPrefix = config('cache.stores.cache_settings.prefix'); + $tenantSettingsPrefix = 'tenant_' . $tenant->getKey() . ':' . $this->originalSettingsPrefix; + config()->set('cache.stores.cache_settings.prefix', $tenantSettingsPrefix); + $this->storeName = 'cache_settings'; + $this->setCachePrefix($cachePrefix); + } + + public function forgetCurrent(): void + { + $this->setCachePrefix($this->originalPrefix); + + config()->set('cache.stores.cache_settings.prefix', $this->originalSettingsPrefix); + $this->storeName = 'cache_settings'; + $this->setCachePrefix($this->originalPrefix); + } +} diff --git a/ProcessMaker/Multitenancy/SwitchTenant.php b/ProcessMaker/Multitenancy/SwitchTenant.php index eb5e319881..d6ea262135 100644 --- a/ProcessMaker/Multitenancy/SwitchTenant.php +++ b/ProcessMaker/Multitenancy/SwitchTenant.php @@ -3,6 +3,9 @@ namespace ProcessMaker\Multitenancy; use Illuminate\Broadcasting\BroadcastManager; +use Illuminate\Support\Arr; +use Illuminate\Support\Env; +use Monolog\Handler\RotatingFileHandler; use ProcessMaker\Application; use ProcessMaker\Multitenancy\Broadcasting\TenantAwareBroadcastManager; use Spatie\Multitenancy\Concerns\UsesMultitenancyConfig; @@ -13,6 +16,8 @@ class SwitchTenant implements SwitchTenantTask { use UsesMultitenancyConfig; + public static $landlordValues = null; + /** * Make the given tenant current. * @@ -25,6 +30,11 @@ public function makeCurrent(IsTenant $tenant): void \Log::debug('SwitchTenant: ' . $tenant->id, ['domain' => request()->getHost()]); + // Save the landlord values for later use + if (!self::$landlordValues) { + self::$landlordValues = $app->make('config')->all(); + } + // Set the tenant's domain in the request headers. Used for things like the global url() helper. request()->headers->set('host', $tenant->domain); @@ -43,29 +53,86 @@ public function makeCurrent(IsTenant $tenant): void */ public function forgetCurrent(): void { + $app = app(); + $app->useStoragePath(base_path('storage')); + + $this->setConfig('logging.channels.daily.path', storage_path('logs/processmaker.log')); + $app->make('log')->reset(); + $app->forgetInstance('log'); + + $app->useLangPath(resource_path('lang')); + + // app key / encrypter + $this->setConfig('app.key', $this->landlordConfig('app.key')); + $app->forgetInstance('encrypter'); + } + + private function landlordConfig($key) + { + return Arr::get(self::$landlordValues, $key); + } + + private function setConfig($key, $value) + { + app()->make('config')->set($key, $value); + } + + private function setEnvironmentVariable($key, $value) + { + putenv("$key=$value"); + $_SERVER[$key] = $value; + $_ENV[$key] = $value; } private function overrideConfigs(Application $app, IsTenant $tenant) { - if ($app->configurationIsCached()) { - return; - } + $this->setEnvironmentVariable('APP_URL', $tenant->config['app.url']); - $newConfig = [ - 'app.instance' => config('app.instance') . '_' . $tenant->id, - ]; + $this->setConfig('app.instance', $this->landlordConfig('app.instance') . '_' . $tenant->id); + $this->setConfig('app.url', $tenant->config['app.url']); + // Microservice callback url if (!isset($tenant->config['script-runner-microservice.callback'])) { - $newConfig['script-runner-microservice.callback'] = str_replace( - $tenant->getOriginalValue('APP_URL'), - config('app.url'), - $tenant->getOriginalValue('SCRIPT_MICROSERVICE_CALLBACK') - ); + $this->setConfig('script-runner-microservice.callback', str_replace( + $this->landlordConfig('app.url'), + $tenant->config['app.url'], + $this->landlordConfig('script-runner-microservice.callback') + )); + } + + // Filesystem roots + $landlordStoragePath = base_path('storage'); + $newStoragePath = base_path('storage/tenant_' . $tenant->id); + foreach ($this->landlordConfig('filesystems.disks') as $disk => $config) { + if (isset($config['root'])) { + $this->setConfig('filesystems.disks.' . $disk . '.root', str_replace( + $landlordStoragePath, + $newStoragePath, + $config['root'] + )); + } } + $app->useStoragePath($newStoragePath); + + // Lang path + $app->useLangPath(resource_path('lang/tenant_' . $tenant->id)); + $this->setConfig('filesystems.disks.lang.root', lang_path()); + + // app key / encrypter + $landlordEncrypter = $app->make('encrypter'); + $this->setConfig('app.key', $landlordEncrypter->decryptString($tenant->config['app.key'])); + $app->forgetInstance('encrypter'); + + // Logging + $this->setConfig('logging.channels.daily.path', storage_path('logs/processmaker.log')); + $app->make('log')->reset(); + $app->forgetInstance('log'); + + // NOTE: Cache prefix and cache settings prefix are handled in PrefixCacheTask if (!isset($tenant->config['app.docker_host_url'])) { // There is no specific override in the tenant's config so set it to the app url - $newConfig['app.docker_host_url'] = config('app.url'); + $this->setConfig('app.docker_host_url', $tenant->config['app.url']); } // Set config from the entry in the tenants table @@ -74,9 +141,7 @@ private function overrideConfigs(Application $app, IsTenant $tenant) if ($key === 'app.key' || $key === 'app.url') { continue; } - $newConfig[$key] = $value; + $this->setConfig($key, $value); } - - config($newConfig); } } diff --git a/ProcessMaker/Multitenancy/TenantBootstrapper.php b/ProcessMaker/Multitenancy/TenantBootstrapper.php deleted file mode 100644 index 7ab897f0e8..0000000000 --- a/ProcessMaker/Multitenancy/TenantBootstrapper.php +++ /dev/null @@ -1,189 +0,0 @@ -bootstrapRun($app); - } catch (\Exception $e) { - file_put_contents(storage_path('logs/tenant_bootstrapper_error.log'), date('Y-m-d H:i:s') . ' ' . get_class($e) . ' in ' . $e->getFile() . ':' . $e->getLine() . ' ' . $e->getMessage() . PHP_EOL, FILE_APPEND); - throw $e; - } - } - - public function bootstrapRun(Application $app) - { - if (!$this->env('MULTITENANCY')) { - return; - } - $this->app = $app; - - $tenantData = null; - - // Try to find tenant by ID first if TENANT env var is set - $envTenant = $this->env('TENANT'); - if ($envTenant) { - $tenantData = $this->executeQuery('SELECT * FROM tenants WHERE id = ?', [$envTenant]); - } else { - $request = Request::capture(); - $host = $request->getHost(); - $tenantData = $this->executeQuery('SELECT * FROM tenants WHERE domain = ? LIMIT 1', [$host]); - } - - if (!$tenantData) { - Tenant::setBootstrappedTenant($app, null); - - return; - } - - // Set storage path - $app->useStoragePath($app->basePath('storage/tenant_' . $tenantData['id'])); - - $this->setTenantEnvironmentVariables($tenantData); - - // Use tenant's translation files. Doing this here so it's available in cached filesystems.php - $app->useLangPath(resource_path('lang/tenant_' . $tenantData['id'])); - - $tenantData['original_values'] = self::$landlordValues; - Tenant::setBootstrappedTenant($app, $tenantData); - } - - private function setTenantEnvironmentVariables($tenantData) - { - // Additional configs are set in SwitchTenant.php - - $config = json_decode($tenantData['config'], true); - - $this->set('APP_CONFIG_CACHE', $this->app->storagePath('config.php')); - $this->set('APP_URL', $config['app.url']); - $this->set('APP_KEY', $this->decrypt($config['app.key'])); - $this->set('DB_DATABASE', $tenantData['database']); - $this->set('DB_USERNAME', $tenantData['username'] ?? $this->getOriginalValue('DB_USERNAME')); - - // Do not set REDIS_PREFIX because it is used by the queue (not tenant specific) - $this->set('CACHE_PREFIX', 'tenant_' . $tenantData['id'] . ':' . $this->getOriginalValue('CACHE_PREFIX')); - $this->set('CACHE_SETTING_PREFIX', 'tenant_' . $tenantData['id'] . ':' . $this->getOriginalValue('CACHE_SETTING_PREFIX')); - - $encryptedPassword = $tenantData['password']; - $password = null; - if ($encryptedPassword) { - $password = $this->decrypt($encryptedPassword); - } else { - $password = $this->getOriginalValue('DB_PASSWORD'); - } - - $this->set('DB_PASSWORD', $password); - $this->set('LOG_PATH', $this->app->storagePath('logs/processmaker.log')); - } - - // Used for debugging - public static function log($message) - { - $message = '[TenantBootstrapper] ' . $message; - $today = date('Y-m-d'); - file_put_contents(base_path('storage/logs/processmaker-' . $today . '.log'), date('Y-m-d H:i:s') . ' ' . $message . PHP_EOL, FILE_APPEND); - } - - private function getOriginalValue($key, $default = '') - { - if (self::$landlordValues === []) { - self::$landlordValues = Dotenv::parse(file_get_contents(base_path('.env'))); - } - - if (!isset(self::$landlordValues[$key])) { - return $default; - } - - return self::$landlordValues[$key]; - } - - private function env($key, $default = null) - { - $value = $_SERVER[$key] ?? $default; - if ($value === 'true') { - $value = true; - } elseif ($value === 'false') { - $value = false; - } - - return $value; - } - - private function set($key, $value) - { - // Env::getRepository() is immutable but will use values from $_SERVER and $_ENV - $_SERVER[$key] = $value; - $_ENV[$key] = $value; - putenv("$key=$value"); - } - - private function decrypt($value) - { - if (!$this->encrypter) { - $key = $this->getOriginalValue('APP_KEY'); - $landlordKey = base64_decode(substr($key, 7)); - $this->encrypter = new Encrypter($landlordKey, 'AES-256-CBC'); - } - - return $this->encrypter->decryptString($value); - } - - private function getLandlordDbConfig(): array - { - return [ - 'host' => $this->getOriginalValue('DB_HOSTNAME', 'localhost'), - 'port' => $this->getOriginalValue('DB_PORT', '3306'), - 'database' => $this->getOriginalValue('LANDLORD_DB_DATABASE', 'landlord'), - 'username' => $this->getOriginalValue('DB_USERNAME'), - 'password' => $this->getOriginalValue('DB_PASSWORD'), - 'charset' => 'utf8mb4', - ]; - } - - private function getPdo(): PDO - { - if (!$this->pdo) { - $landlordConfig = $this->getLandlordDbConfig(); - $dsn = "mysql:host={$landlordConfig['host']};port={$landlordConfig['port']};dbname={$landlordConfig['database']};charset={$landlordConfig['charset']}"; - $this->pdo = new PDO($dsn, $landlordConfig['username'], $landlordConfig['password'], [ - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - ]); - } - - return $this->pdo; - } - - private function executeQuery($query, $params = []) - { - $stmt = $this->getPdo()->prepare($query); - $stmt->execute($params); - - return $stmt->fetch(); - } -} diff --git a/ProcessMaker/Providers/ProcessMakerServiceProvider.php b/ProcessMaker/Providers/ProcessMakerServiceProvider.php index 92d4569127..1ae630b702 100644 --- a/ProcessMaker/Providers/ProcessMakerServiceProvider.php +++ b/ProcessMaker/Providers/ProcessMakerServiceProvider.php @@ -46,7 +46,6 @@ use ProcessMaker\Managers\ScreenCompiledManager; use ProcessMaker\Models; use ProcessMaker\Multitenancy\Tenant; -use ProcessMaker\Multitenancy\TenantBootstrapper; use ProcessMaker\Observers; use ProcessMaker\PolicyExtension; use ProcessMaker\Providers\PermissionServiceProvider; @@ -250,40 +249,6 @@ public function register(): void * This service is used to evaluate the conditional redirect property of a process request token. */ $this->app->bind(ConditionalRedirectServiceInterface::class, ConditionalRedirectService::class); - - $this->app->when(HorizonListen::class)->needs(Listener::class)->give(function ($app) { - return new Listener(base_path()); - }); - - if (config('app.multitenancy')) { - WorkerCommandString::$command = 'exec @php artisan horizon:listen'; - SystemProcessCounter::$command = 'horizon:listen'; - } - } - - /** - * In multitenancy, we need to bootstrap a new app with the tenant id set. - * This is because queue workers are long-running processes that are not - * tenant aware. - */ - private static function bootstrapTenantApp(JobProcessing|JobRetryRequested $event): void - { - Context::hydrate($event->job->payload()['illuminate:log:context'] ?? null); - $tenantId = Context::get(config('multitenancy.current_tenant_context_key')); - if ($tenantId) { - if (!method_exists($event->job, 'getRedisQueue')) { - // Not a redis job - return; - } - - // Create a new tenant app instance - $_SERVER['TENANT'] = $tenantId; - - $app = require base_path('bootstrap/app.php'); - $app->make(\ProcessMaker\Console\Kernel::class)->bootstrap(); - \Illuminate\Container\Container::setInstance($app); - $event->job->getRedisQueue()->setContainer($app); - } } /** @@ -291,14 +256,6 @@ private static function bootstrapTenantApp(JobProcessing|JobRetryRequested $even */ protected static function registerEvents(): void { - Facades\Event::listen(JobProcessing::class, function (JobProcessing $event) { - self::bootstrapTenantApp($event); - }); - - Facades\Event::listen(JobRetryRequested::class, function (JobRetryRequested $event) { - self::bootstrapTenantApp($event); - }); - // Listen to the events for our core screen // types and add our javascript Facades\Event::listen(ScreenBuilderStarting::class, function ($event) { diff --git a/config/multitenancy.php b/config/multitenancy.php index 7c56e74d8f..8f69499381 100644 --- a/config/multitenancy.php +++ b/config/multitenancy.php @@ -1,10 +1,6 @@ [ SwitchTenant::class, + ProcessMaker\Multitenancy\PrefixCacheTask::class, + Spatie\Multitenancy\Tasks\SwitchTenantDatabaseTask::class, ], /* @@ -86,7 +84,7 @@ 'actions' => [ 'make_tenant_current_action' => MakeTenantCurrentAction::class, 'forget_current_tenant_action' => ForgetCurrentTenantAction::class, - 'make_queue_tenant_aware_action' => ProcessMaker\Multitenancy\MakeQueueTenantAwareAction::class, + 'make_queue_tenant_aware_action' => MakeQueueTenantAwareAction::class, 'migrate_tenant' => MigrateTenantAction::class, ], diff --git a/phpunit.xml b/phpunit.xml index 457f6737a1..81690f715b 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -78,5 +78,7 @@ + +