Allow to automatically fix missing grants & elevalte database users
Co-authored-by: Johannes Meyer
This commit is contained in:
parent
12bc95099e
commit
2657f032dc
|
@ -4,8 +4,11 @@
|
||||||
|
|
||||||
namespace Icinga\Controllers;
|
namespace Icinga\Controllers;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
use Icinga\Application\Hook\MigrationHook;
|
use Icinga\Application\Hook\MigrationHook;
|
||||||
|
use Icinga\Application\Icinga;
|
||||||
use Icinga\Application\MigrationManager;
|
use Icinga\Application\MigrationManager;
|
||||||
|
use Icinga\Common\Database;
|
||||||
use Icinga\Exception\MissingParameterException;
|
use Icinga\Exception\MissingParameterException;
|
||||||
use Icinga\Forms\MigrationForm;
|
use Icinga\Forms\MigrationForm;
|
||||||
use Icinga\Web\Notification;
|
use Icinga\Web\Notification;
|
||||||
|
@ -19,6 +22,13 @@ use ipl\Web\Widget\ActionLink;
|
||||||
|
|
||||||
class MigrationsController extends CompatController
|
class MigrationsController extends CompatController
|
||||||
{
|
{
|
||||||
|
use Database;
|
||||||
|
|
||||||
|
public function init()
|
||||||
|
{
|
||||||
|
Icinga::app()->getModuleManager()->loadModule('setup');
|
||||||
|
}
|
||||||
|
|
||||||
public function indexAction(): void
|
public function indexAction(): void
|
||||||
{
|
{
|
||||||
$mm = MigrationManager::instance();
|
$mm = MigrationManager::instance();
|
||||||
|
@ -44,6 +54,8 @@ class MigrationsController extends CompatController
|
||||||
}
|
}
|
||||||
|
|
||||||
$migrateListForm = new MigrationForm();
|
$migrateListForm = new MigrationForm();
|
||||||
|
$migrateListForm->setRenderDatabaseUserChange(! $mm->validateDatabasePrivileges());
|
||||||
|
|
||||||
$migrateGlobalForm = new MigrationForm();
|
$migrateGlobalForm = new MigrationForm();
|
||||||
$migrateGlobalForm->getAttributes()->set('name', sprintf('migrate-%s', MigrationHook::ALL_MIGRATIONS));
|
$migrateGlobalForm->getAttributes()->set('name', sprintf('migrate-%s', MigrationHook::ALL_MIGRATIONS));
|
||||||
|
|
||||||
|
@ -193,12 +205,18 @@ class MigrationsController extends CompatController
|
||||||
$form->on(MigrationForm::ON_SUCCESS, function (MigrationForm $form) {
|
$form->on(MigrationForm::ON_SUCCESS, function (MigrationForm $form) {
|
||||||
$mm = MigrationManager::instance();
|
$mm = MigrationManager::instance();
|
||||||
|
|
||||||
|
/** @var array<string, string> $elevatedPrivileges */
|
||||||
|
$elevatedPrivileges = $form->getValue('database_setup');
|
||||||
|
if ($elevatedPrivileges !== null && $elevatedPrivileges['grant_privileges'] === 'y') {
|
||||||
|
$mm->fixIcingaWebMysqlGrants($this->getDb(), $elevatedPrivileges);
|
||||||
|
}
|
||||||
|
|
||||||
$pressedButton = $form->getPressedSubmitElement();
|
$pressedButton = $form->getPressedSubmitElement();
|
||||||
if ($pressedButton) {
|
if ($pressedButton) {
|
||||||
$name = substr($pressedButton->getName(), 8);
|
$name = substr($pressedButton->getName(), 8);
|
||||||
switch ($name) {
|
switch ($name) {
|
||||||
case MigrationHook::ALL_MIGRATIONS:
|
case MigrationHook::ALL_MIGRATIONS:
|
||||||
if ($mm->applyAll()) {
|
if ($mm->applyAll($elevatedPrivileges)) {
|
||||||
Notification::success($this->translate('Applied all migrations successfully'));
|
Notification::success($this->translate('Applied all migrations successfully'));
|
||||||
} else {
|
} else {
|
||||||
Notification::error(
|
Notification::error(
|
||||||
|
@ -211,7 +229,7 @@ class MigrationsController extends CompatController
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$migration = $mm->getMigration($name);
|
$migration = $mm->getMigration($name);
|
||||||
if ($mm->apply($migration)) {
|
if ($mm->apply($migration, $elevatedPrivileges)) {
|
||||||
Notification::success($this->translate('Applied pending migrations successfully'));
|
Notification::success($this->translate('Applied pending migrations successfully'));
|
||||||
} else {
|
} else {
|
||||||
Notification::error(
|
Notification::error(
|
||||||
|
|
|
@ -4,20 +4,34 @@
|
||||||
|
|
||||||
namespace Icinga\Forms;
|
namespace Icinga\Forms;
|
||||||
|
|
||||||
|
use Icinga\Application\MigrationManager;
|
||||||
|
use ipl\Html\Attributes;
|
||||||
use ipl\Html\Form;
|
use ipl\Html\Form;
|
||||||
|
use ipl\Html\FormElement\CheckboxElement;
|
||||||
|
use ipl\Html\FormElement\FieldsetElement;
|
||||||
|
use ipl\Html\HtmlElement;
|
||||||
|
use ipl\Html\Text;
|
||||||
|
use ipl\I18n\Translation;
|
||||||
|
use ipl\Validator\CallbackValidator;
|
||||||
use ipl\Web\Common\CsrfCounterMeasure;
|
use ipl\Web\Common\CsrfCounterMeasure;
|
||||||
use ipl\Web\Common\FormUid;
|
use ipl\Web\Common\FormUid;
|
||||||
|
use ipl\Web\FormDecorator\IcingaFormDecorator;
|
||||||
|
use PDOException;
|
||||||
|
|
||||||
class MigrationForm extends Form
|
class MigrationForm extends Form
|
||||||
{
|
{
|
||||||
use CsrfCounterMeasure;
|
use CsrfCounterMeasure;
|
||||||
use FormUid;
|
use FormUid;
|
||||||
|
use Translation;
|
||||||
|
|
||||||
protected $defaultAttributes = [
|
protected $defaultAttributes = [
|
||||||
'class' => ['icinga-form', 'migration-form', 'icinga-controls'],
|
'class' => ['icinga-form', 'migration-form', 'icinga-controls'],
|
||||||
'name' => 'migration-form'
|
'name' => 'migration-form'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** @var bool Whether to allow changing the current database user and password */
|
||||||
|
protected $renderDatabaseUserChange = false;
|
||||||
|
|
||||||
public function hasBeenSubmitted(): bool
|
public function hasBeenSubmitted(): bool
|
||||||
{
|
{
|
||||||
if (! $this->hasBeenSent()) {
|
if (! $this->hasBeenSent()) {
|
||||||
|
@ -25,11 +39,99 @@ class MigrationForm extends Form
|
||||||
}
|
}
|
||||||
|
|
||||||
$pressedButton = $this->getPressedSubmitElement();
|
$pressedButton = $this->getPressedSubmitElement();
|
||||||
|
|
||||||
return $pressedButton && strpos($pressedButton->getName(), 'migrate-') !== false;
|
return $pressedButton && strpos($pressedButton->getName(), 'migrate-') !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setRenderDatabaseUserChange(bool $value = true): self
|
||||||
|
{
|
||||||
|
$this->renderDatabaseUserChange = $value;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
protected function assemble(): void
|
protected function assemble(): void
|
||||||
{
|
{
|
||||||
$this->addHtml($this->createUidElement());
|
$this->addHtml($this->createUidElement());
|
||||||
|
|
||||||
|
if ($this->renderDatabaseUserChange) {
|
||||||
|
$mm = MigrationManager::instance();
|
||||||
|
$newDbSetup = new FieldsetElement('database_setup', ['required' => true]);
|
||||||
|
$newDbSetup
|
||||||
|
->setDefaultElementDecorator(new IcingaFormDecorator())
|
||||||
|
->addElement('text', 'username', [
|
||||||
|
'required' => true,
|
||||||
|
'label' => $this->translate('Username'),
|
||||||
|
'description' => $this->translate(
|
||||||
|
'A user which is able to create and/or alter the database schema.'
|
||||||
|
)
|
||||||
|
])
|
||||||
|
->addElement('password', 'password', [
|
||||||
|
'required' => true,
|
||||||
|
'autocomplete' => 'new-password',
|
||||||
|
'label' => $this->translate('Password'),
|
||||||
|
'description' => $this->translate('The password for the database user defined above.'),
|
||||||
|
'validators' => [
|
||||||
|
new CallbackValidator(function ($_, CallbackValidator $validator) use ($mm, $newDbSetup): bool {
|
||||||
|
/** @var array<string, string> $values */
|
||||||
|
$values = $this->getValue('database_setup');
|
||||||
|
/** @var CheckboxElement $checkBox */
|
||||||
|
$checkBox = $newDbSetup->getElement('grant_privileges');
|
||||||
|
$canIssueGrants = $checkBox->isChecked();
|
||||||
|
$elevationConfig = [
|
||||||
|
'username' => $values['username'],
|
||||||
|
'password' => $values['password']
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (! $mm->validateDatabasePrivileges($elevationConfig, $canIssueGrants)) {
|
||||||
|
$validator->addMessage(sprintf(
|
||||||
|
$this->translate(
|
||||||
|
'The provided credentials cannot be used to execute "%s" SQL commands'
|
||||||
|
. ' and/or grant the missing privileges to other users.'
|
||||||
|
),
|
||||||
|
implode(' ,', $mm->getRequiredDatabasePrivileges())
|
||||||
|
));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$validator->addMessage($e->getMessage());
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
]
|
||||||
|
])
|
||||||
|
->addElement('checkbox', 'grant_privileges', [
|
||||||
|
'required' => false,
|
||||||
|
'label' => $this->translate('Grant Missing Privileges'),
|
||||||
|
'description' => $this->translate(
|
||||||
|
'Allows to automatically grant the required privileges to the database user specified'
|
||||||
|
. ' in the respective resource config. If you do not want to provide additional credentials'
|
||||||
|
. ' each time, you can enable this and Icinga Web will grant the active database user the'
|
||||||
|
. ' missing privileges.'
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->addHtml(
|
||||||
|
new HtmlElement(
|
||||||
|
'div',
|
||||||
|
Attributes::create(['class' => 'change-database-user-description']),
|
||||||
|
new HtmlElement('span', null, Text::create(sprintf(
|
||||||
|
$this->translate(
|
||||||
|
'It seems that the currently used database user does not have the required privileges to'
|
||||||
|
. ' execute the %s SQL commands. Please provide an alternative user'
|
||||||
|
. ' that has the appropriate credentials to resolve this issue.'
|
||||||
|
),
|
||||||
|
implode(', ', $mm->getRequiredDatabasePrivileges())
|
||||||
|
)))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->addElement($newDbSetup);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,8 +27,7 @@ class DbMigration
|
||||||
public function __construct(string $version, string $scriptPath)
|
public function __construct(string $version, string $scriptPath)
|
||||||
{
|
{
|
||||||
$this->scriptPath = $scriptPath;
|
$this->scriptPath = $scriptPath;
|
||||||
|
$this->version = $version;
|
||||||
$this->setVersion($version);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -41,20 +40,6 @@ class DbMigration
|
||||||
return $this->version;
|
return $this->version;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the sql script version the queries are loaded from
|
|
||||||
*
|
|
||||||
* @param string $version
|
|
||||||
*
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setVersion(string $version): self
|
|
||||||
{
|
|
||||||
$this->version = $version;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get upgrade script relative path name
|
* Get upgrade script relative path name
|
||||||
*
|
*
|
||||||
|
|
|
@ -14,13 +14,16 @@ use Icinga\Application\Icinga;
|
||||||
use Icinga\Application\Logger;
|
use Icinga\Application\Logger;
|
||||||
use Icinga\Application\Modules\Module;
|
use Icinga\Application\Modules\Module;
|
||||||
use Icinga\Model\Schema;
|
use Icinga\Model\Schema;
|
||||||
|
use Icinga\Module\Setup\Utils\DbTool;
|
||||||
use Icinga\Web\Session;
|
use Icinga\Web\Session;
|
||||||
use ipl\I18n\Translation;
|
use ipl\I18n\Translation;
|
||||||
use ipl\Orm\Query;
|
use ipl\Orm\Query;
|
||||||
use ipl\Sql\Adapter\Pgsql;
|
use ipl\Sql\Adapter\Pgsql;
|
||||||
use ipl\Sql\Connection;
|
use ipl\Sql\Connection;
|
||||||
use ipl\Stdlib\Filter;
|
use ipl\Stdlib\Filter;
|
||||||
|
use ipl\Stdlib\Str;
|
||||||
use PDO;
|
use PDO;
|
||||||
|
use PDOException;
|
||||||
use SplFileInfo;
|
use SplFileInfo;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
|
|
||||||
|
@ -128,6 +131,13 @@ abstract class MigrationHook implements Countable
|
||||||
*/
|
*/
|
||||||
abstract public function getVersion(): string;
|
abstract public function getVersion(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a database connection
|
||||||
|
*
|
||||||
|
* @return Connection
|
||||||
|
*/
|
||||||
|
abstract public function getDb(): Connection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all the pending migrations of this hook
|
* Get all the pending migrations of this hook
|
||||||
*
|
*
|
||||||
|
@ -166,9 +176,12 @@ abstract class MigrationHook implements Countable
|
||||||
*
|
*
|
||||||
* @return bool Whether the migration(s) have been successfully applied
|
* @return bool Whether the migration(s) have been successfully applied
|
||||||
*/
|
*/
|
||||||
public function run(): bool
|
final public function run(Connection $conn = null): bool
|
||||||
{
|
{
|
||||||
|
if (! $conn) {
|
||||||
$conn = $this->getDb();
|
$conn = $this->getDb();
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($this->getMigrations() as $migration) {
|
foreach ($this->getMigrations() as $migration) {
|
||||||
try {
|
try {
|
||||||
$migration->apply($conn);
|
$migration->apply($conn);
|
||||||
|
@ -176,7 +189,7 @@ abstract class MigrationHook implements Countable
|
||||||
$this->version = $migration->getVersion();
|
$this->version = $migration->getVersion();
|
||||||
unset($this->migrations[$migration->getVersion()]);
|
unset($this->migrations[$migration->getVersion()]);
|
||||||
|
|
||||||
Logger::error(
|
Logger::info(
|
||||||
"Applied %s pending migration version %s successfully",
|
"Applied %s pending migration version %s successfully",
|
||||||
$this->getName(),
|
$this->getName(),
|
||||||
$migration->getVersion()
|
$migration->getVersion()
|
||||||
|
@ -243,13 +256,6 @@ abstract class MigrationHook implements Countable
|
||||||
return count($this->getMigrations());
|
return count($this->getMigrations());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a database connection
|
|
||||||
*
|
|
||||||
* @return Connection
|
|
||||||
*/
|
|
||||||
abstract protected function getDb(): Connection;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a schema version query
|
* Get a schema version query
|
||||||
*
|
*
|
||||||
|
|
|
@ -8,11 +8,14 @@ use Countable;
|
||||||
use Generator;
|
use Generator;
|
||||||
use Icinga\Application\Hook\MigrationHook;
|
use Icinga\Application\Hook\MigrationHook;
|
||||||
use Icinga\Exception\NotFoundError;
|
use Icinga\Exception\NotFoundError;
|
||||||
|
use Icinga\Module\Setup\Utils\DbTool;
|
||||||
|
use Icinga\Module\Setup\WebWizard;
|
||||||
use ipl\I18n\Translation;
|
use ipl\I18n\Translation;
|
||||||
|
use ipl\Sql;
|
||||||
|
use ReflectionClass;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migration manager encapsulates PHP code and DB migrations and manages all pending migrations in a
|
* Migration manager allows you to manage all pending migrations in a structured way.
|
||||||
* structured way.
|
|
||||||
*/
|
*/
|
||||||
final class MigrationManager implements Countable
|
final class MigrationManager implements Countable
|
||||||
{
|
{
|
||||||
|
@ -82,7 +85,7 @@ final class MigrationManager implements Countable
|
||||||
*
|
*
|
||||||
* @return MigrationHook
|
* @return MigrationHook
|
||||||
*
|
*
|
||||||
* @throws NotFoundError When there are no pending PHP code migrations matching the given module name
|
* @throws NotFoundError When there are no pending migrations matching the given module name
|
||||||
*/
|
*/
|
||||||
public function getMigration(string $module): MigrationHook
|
public function getMigration(string $module): MigrationHook
|
||||||
{
|
{
|
||||||
|
@ -124,10 +127,11 @@ final class MigrationManager implements Countable
|
||||||
* Apply the given migration hook
|
* Apply the given migration hook
|
||||||
*
|
*
|
||||||
* @param MigrationHook $hook
|
* @param MigrationHook $hook
|
||||||
|
* @param ?array<string, string> $elevateConfig
|
||||||
*
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function apply(MigrationHook $hook): bool
|
public function apply(MigrationHook $hook, array $elevateConfig = null): bool
|
||||||
{
|
{
|
||||||
if ($hook->isModule() && $this->hasMigrations(MigrationHook::DEFAULT_MODULE)) {
|
if ($hook->isModule() && $this->hasMigrations(MigrationHook::DEFAULT_MODULE)) {
|
||||||
Logger::error(
|
Logger::error(
|
||||||
|
@ -137,7 +141,12 @@ final class MigrationManager implements Countable
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($hook->run()) {
|
$conn = $hook->getDb();
|
||||||
|
if ($elevateConfig && ! $this->checkRequiredPrivileges($conn)) {
|
||||||
|
$conn = $this->elevateDatabaseConnection($conn, $elevateConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hook->run($conn)) {
|
||||||
unset($this->pendingMigrations[$hook->getModuleName()]);
|
unset($this->pendingMigrations[$hook->getModuleName()]);
|
||||||
|
|
||||||
Logger::info('Applied pending %s migrations successfully', $hook->getName());
|
Logger::info('Applied pending %s migrations successfully', $hook->getName());
|
||||||
|
@ -151,21 +160,23 @@ final class MigrationManager implements Countable
|
||||||
/**
|
/**
|
||||||
* Apply all pending modules/framework migrations
|
* Apply all pending modules/framework migrations
|
||||||
*
|
*
|
||||||
|
* @param ?array<string, string> $elevateConfig
|
||||||
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function applyAll(): bool
|
public function applyAll(array $elevateConfig = null): bool
|
||||||
{
|
{
|
||||||
$default = MigrationHook::DEFAULT_MODULE;
|
$default = MigrationHook::DEFAULT_MODULE;
|
||||||
if ($this->hasMigrations($default)) {
|
if ($this->hasMigrations($default)) {
|
||||||
$migration = $this->getMigration($default);
|
$migration = $this->getMigration($default);
|
||||||
if (! $this->apply($migration)) {
|
if (! $this->apply($migration, $elevateConfig)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$succeeded = true;
|
$succeeded = true;
|
||||||
foreach ($this->getPendingMigrations() as $migration) {
|
foreach ($this->getPendingMigrations() as $migration) {
|
||||||
if (! $this->apply($migration) && $succeeded) {
|
if (! $this->apply($migration, $elevateConfig) && $succeeded) {
|
||||||
$succeeded = false;
|
$succeeded = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -189,6 +200,99 @@ final class MigrationManager implements Countable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the required database privileges for database migrations
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getRequiredDatabasePrivileges(): array
|
||||||
|
{
|
||||||
|
return ['CREATE','SELECT','INSERT','UPDATE','DELETE','DROP','ALTER','CREATE VIEW','INDEX','EXECUTE'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify whether all database users of all pending migrations do have the required SQL privileges
|
||||||
|
*
|
||||||
|
* @param ?array<string, string> $elevateConfig
|
||||||
|
* @param bool $canIssueGrant
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function validateDatabasePrivileges(array $elevateConfig = null, bool $canIssueGrant = false): bool
|
||||||
|
{
|
||||||
|
if (! $this->hasPendingMigrations()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->getPendingMigrations() as $migration) {
|
||||||
|
if (! $this->checkRequiredPrivileges($migration->getDb(), $elevateConfig, $canIssueGrant)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there are missing grants for the Icinga Web database and fix them
|
||||||
|
*
|
||||||
|
* This fixes the following problems on existing installations:
|
||||||
|
* - Setups made by the wizard have no access to `icingaweb_schema`
|
||||||
|
* - Setups made by the wizard have no DDL grants
|
||||||
|
* - Setups done manually using the advanced documentation chapter have no DDL grants
|
||||||
|
*
|
||||||
|
* @param Sql\Connection $db
|
||||||
|
* @param array<string, string> $elevateConfig
|
||||||
|
*/
|
||||||
|
public function fixIcingaWebMysqlGrants(Sql\Connection $db, array $elevateConfig): void
|
||||||
|
{
|
||||||
|
$wizardProperties = (new ReflectionClass(WebWizard::class))
|
||||||
|
->getDefaultProperties();
|
||||||
|
/** @var array<int, string> $privileges */
|
||||||
|
$privileges = $wizardProperties['databaseUsagePrivileges'];
|
||||||
|
/** @var array<int, string> $tables */
|
||||||
|
$tables = $wizardProperties['databaseTables'];
|
||||||
|
|
||||||
|
$actualUsername = $db->getConfig()->username;
|
||||||
|
$db = $this->elevateDatabaseConnection($db, $elevateConfig);
|
||||||
|
$tool = $this->createDbTool($db);
|
||||||
|
$tool->connectToDb();
|
||||||
|
|
||||||
|
if ($tool->checkPrivileges(['SELECT'], [], $actualUsername)) {
|
||||||
|
// Checks only database level grants. If this succeeds, the grants were issued manually.
|
||||||
|
if (! $tool->checkPrivileges($privileges, [], $actualUsername) && $tool->isGrantable($privileges)) {
|
||||||
|
// Any missing grant is now granted on database level as well, not to mix things up
|
||||||
|
$tool->grantPrivileges($privileges, [], $actualUsername);
|
||||||
|
}
|
||||||
|
} elseif (! $tool->checkPrivileges($privileges, $tables, $actualUsername) && $tool->isGrantable($privileges)) {
|
||||||
|
// The above ensures that if this fails, we can safely apply table level grants, as it's
|
||||||
|
// very likely that the existing grants were issued by the setup wizard
|
||||||
|
$tool->grantPrivileges($privileges, $tables, $actualUsername);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and return a DbTool instance
|
||||||
|
*
|
||||||
|
* @param Sql\Connection $db
|
||||||
|
*
|
||||||
|
* @return DbTool
|
||||||
|
*/
|
||||||
|
private function createDbTool(Sql\Connection $db): DbTool
|
||||||
|
{
|
||||||
|
$config = $db->getConfig();
|
||||||
|
|
||||||
|
return new DbTool(array_merge([
|
||||||
|
'db' => $config->db,
|
||||||
|
'host' => $config->host,
|
||||||
|
'port' => $config->port,
|
||||||
|
'dbname' => $config->dbname,
|
||||||
|
'username' => $config->username,
|
||||||
|
'password' => $config->password,
|
||||||
|
'charset' => $config->charset
|
||||||
|
], $db->getAdapter()->getOptions($config)));
|
||||||
|
}
|
||||||
|
|
||||||
protected function load(): void
|
protected function load(): void
|
||||||
{
|
{
|
||||||
$this->pendingMigrations = [];
|
$this->pendingMigrations = [];
|
||||||
|
@ -205,6 +309,55 @@ final class MigrationManager implements Countable
|
||||||
ksort($this->pendingMigrations);
|
ksort($this->pendingMigrations);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the required SQL privileges of the given connection
|
||||||
|
*
|
||||||
|
* @param Sql\Connection $conn
|
||||||
|
* @param ?array<string, string> $elevateConfig
|
||||||
|
* @param bool $canIssueGrants
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function checkRequiredPrivileges(
|
||||||
|
Sql\Connection $conn,
|
||||||
|
array $elevateConfig = null,
|
||||||
|
bool $canIssueGrants = false
|
||||||
|
): bool {
|
||||||
|
if ($elevateConfig) {
|
||||||
|
$conn = $this->elevateDatabaseConnection($conn, $elevateConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dbTool = $this->createDbTool($conn);
|
||||||
|
$dbTool->connectToDb();
|
||||||
|
if (! $dbTool->checkPrivileges($this->getRequiredDatabasePrivileges())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($canIssueGrants && ! $dbTool->isGrantable($this->getRequiredDatabasePrivileges())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override the database config of the given connection by the specified new config
|
||||||
|
*
|
||||||
|
* Overrides only the username and password of existing database connection.
|
||||||
|
*
|
||||||
|
* @param Sql\Connection $conn
|
||||||
|
* @param array<string, string> $elevateConfig
|
||||||
|
* @return Sql\Connection
|
||||||
|
*/
|
||||||
|
protected function elevateDatabaseConnection(Sql\Connection $conn, array $elevateConfig): Sql\Connection
|
||||||
|
{
|
||||||
|
$config = clone $conn->getConfig();
|
||||||
|
$config->username = $elevateConfig['username'];
|
||||||
|
$config->password = $elevateConfig['password'];
|
||||||
|
|
||||||
|
return new Sql\Connection($config);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all pending migrations as an array
|
* Get all pending migrations as an array
|
||||||
*
|
*
|
||||||
|
|
|
@ -8,10 +8,18 @@ use Icinga\Application\Hook\MigrationHook;
|
||||||
use Icinga\Common\Database;
|
use Icinga\Common\Database;
|
||||||
use Icinga\Model\Schema;
|
use Icinga\Model\Schema;
|
||||||
use ipl\Orm\Query;
|
use ipl\Orm\Query;
|
||||||
|
use ipl\Sql\Connection;
|
||||||
|
|
||||||
class DbMigration extends MigrationHook
|
class DbMigration extends MigrationHook
|
||||||
{
|
{
|
||||||
use Database;
|
use Database {
|
||||||
|
getDb as public getPublicDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDb(): Connection
|
||||||
|
{
|
||||||
|
return $this->getPublicDb();
|
||||||
|
}
|
||||||
|
|
||||||
public function getName(): string
|
public function getName(): string
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,13 +2,11 @@
|
||||||
|
|
||||||
@visual-width: 1.5em;
|
@visual-width: 1.5em;
|
||||||
|
|
||||||
.migration-state-banner {
|
.migration-state-banner, .change-database-user-description {
|
||||||
.rounded-corners();
|
.rounded-corners();
|
||||||
|
|
||||||
border: 1px solid @gray-lighter;
|
border: 1px solid @gray-light;
|
||||||
color: @text-color-light;
|
color: @text-color;
|
||||||
padding: 1em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.migrations {
|
.migrations {
|
||||||
|
@ -45,6 +43,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layout
|
// Layout
|
||||||
|
|
||||||
|
.migration-state-banner, .change-database-user-description {
|
||||||
|
padding: 1em;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&.change-database-user-description {
|
||||||
|
max-width: 50em;
|
||||||
|
padding: .5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.pending-migrations-hint {
|
.pending-migrations-hint {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
@ -63,6 +72,10 @@
|
||||||
.icinga-form { // Reset Icinga Form layout styles
|
.icinga-form { // Reset Icinga Form layout styles
|
||||||
width: unset;
|
width: unset;
|
||||||
max-width: unset;
|
max-width: unset;
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
max-width: 50em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.migration-list-control {
|
.migration-list-control {
|
||||||
|
|
Loading…
Reference in New Issue