From aa74603a998538e9976f45b322ba1ac55aa38afa Mon Sep 17 00:00:00 2001 From: Benn Oshrin Date: Mon, 22 Aug 2022 14:18:26 -0400 Subject: [PATCH] Additional changes to setup script (CFM-28) --- app/config/database.php.dist | 126 ++++++++++++++++++++++++ app/config/schema/schema.json | 8 ++ app/resources/locales/en_US/command.po | 15 +++ app/src/Command/SetupCommand.php | 111 +++++++++++++++------ app/src/Command/TransmogrifyCommand.php | 7 ++ app/src/Lib/Traits/HistoryTrait.php | 7 +- app/src/Model/Entity/Meta.php | 40 ++++++++ app/src/Model/Table/GroupsTable.php | 14 +++ app/src/Model/Table/MetaTable.php | 91 +++++++++++++++++ app/src/Model/Table/TypesTable.php | 22 +++++ 10 files changed, 409 insertions(+), 32 deletions(-) create mode 100644 app/config/database.php.dist create mode 100644 app/src/Model/Entity/Meta.php create mode 100644 app/src/Model/Table/MetaTable.php diff --git a/app/config/database.php.dist b/app/config/database.php.dist new file mode 100644 index 000000000..bf55794e1 --- /dev/null +++ b/app/config/database.php.dist @@ -0,0 +1,126 @@ + [ + 'default' => [ + 'className' => 'Cake\Database\Connection', + // Cake supports "Mysql", "Postgres", "Sqlite", or "Sqlserver", + // but Registry only supports the first two. + 'driver' => 'Cake\Database\Driver\Postgres', + 'persistent' => false, + 'host' => 'localhost', + 'username' => 'comanage', + 'password' => 'somepasswordhere', + 'database' => 'registry', + /** + * CakePHP will use the default DB port based on the driver selected + * MySQL on MAMP uses port 8889, MAMP users will want to uncomment + * the following line and set the port accordingly + */ + //'port' => 'non_standard_port_number', + 'encoding' => 'utf8', + 'timezone' => 'UTC', + 'flags' => [], + 'cacheMetadata' => true, + 'log' => false, + + /** + * Set identifier quoting to true if you are using reserved words or + * special characters in your table or column names. Enabling this + * setting will result in queries built using the Query Builder having + * identifiers quoted when creating SQL. It should be noted that this + * decreases performance because each query needs to be traversed and + * manipulated before being executed. + */ + // Set this to true for MySQL + 'quoteIdentifiers' => false, + + /** + * During development, if using MySQL < 5.6, uncommenting the + * following line could boost the speed at which schema metadata is + * fetched from the database. It can also be set directly with the + * mysql configuration directive 'innodb_stats_on_metadata = 0' + * which is the recommended value in production environments + */ + //'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'], + + 'url' => env('DATABASE_URL', null), + ], + + /** + * The test connection is used during the test suite. + */ + 'test' => [ + 'className' => 'Cake\Database\Connection', + 'driver' => 'Cake\Database\Driver\Postgres', + 'persistent' => false, + 'host' => 'localhost', + //'port' => 'non_standard_port_number', + 'username' => 'my_app', + 'password' => 'secret', + 'database' => 'test_myapp', + 'encoding' => 'utf8', + 'timezone' => 'UTC', + 'cacheMetadata' => true, + 'quoteIdentifiers' => false, + 'log' => false, + //'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'], + 'url' => env('DATABASE_TEST_URL', null), + ], + + /** + * For data migration from v4 to v5 + */ + 'transmogrify' => [ + 'className' => 'Cake\Database\Connection', + // Cake supports "Mysql", "Postgres", "Sqlite", or "Sqlserver", + // but Registry only supports the first two. + 'driver' => 'Cake\Database\Driver\Postgres', + 'persistent' => false, + 'host' => 'localhost', + 'username' => 'comanage', + 'password' => 'somepasswordhere', + 'database' => 'registryv4', + /** + * CakePHP will use the default DB port based on the driver selected + * MySQL on MAMP uses port 8889, MAMP users will want to uncomment + * the following line and set the port accordingly + */ + //'port' => 'non_standard_port_number', + 'encoding' => 'utf8', + 'timezone' => 'UTC', + 'flags' => [], + 'cacheMetadata' => true, + 'log' => false, + + /** + * Set identifier quoting to true if you are using reserved words or + * special characters in your table or column names. Enabling this + * setting will result in queries built using the Query Builder having + * identifiers quoted when creating SQL. It should be noted that this + * decreases performance because each query needs to be traversed and + * manipulated before being executed. + */ + // Set to true for MySQL + 'quoteIdentifiers' => false, + + /** + * During development, if using MySQL < 5.6, uncommenting the + * following line could boost the speed at which schema metadata is + * fetched from the database. It can also be set directly with the + * mysql configuration directive 'innodb_stats_on_metadata = 0' + * which is the recommended value in production environments + */ + //'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'], + + 'url' => env('DATABASE_URL', null), + ] + ] +]; diff --git a/app/config/schema/schema.json b/app/config/schema/schema.json index 2bd2f450c..3abdb34b2 100644 --- a/app/config/schema/schema.json +++ b/app/config/schema/schema.json @@ -28,6 +28,14 @@ }, "tables": { + "meta": { + "columns": { + "upgrade_version": { "type": "string", "size": 16 } + }, + "changelog": false, + "timestamps": false + }, + "cos": { "columns": { "id": {}, diff --git a/app/resources/locales/en_US/command.po b/app/resources/locales/en_US/command.po index 2035662b1..bf56a97ad 100644 --- a/app/resources/locales/en_US/command.po +++ b/app/resources/locales/en_US/command.po @@ -33,6 +33,12 @@ msgstr "Database schema update successful" msgid "db.schema" msgstr "Loading database schema from {0}" +msgid "opt.admin-family-name" +msgstr "Family Name of initial platform administrator" + +msgid "opt.admin-given-name" +msgstr "Given Name of initial platform administrator" + msgid "opt.admin-username" msgstr "Username of initial platform administrator" @@ -51,12 +57,21 @@ msgstr "Calculate changes but do not apply" msgid "se.already" msgstr "Setup appears to have already run" +msgid "se.db.cmpadmin" +msgstr "Creating the Platform Administrator" + msgid "se.db.co" msgstr "Creating COmanage CO" msgid "se.db.co.done" msgstr "COmanage CO created - CO Id: {0}" +msgid "se.done" +msgstr "Done" + +msgid "se.person_role.title" +msgstr "COmanage Platform Administrator" + msgid "se.salt" msgstr "Generating salt file" diff --git a/app/src/Command/SetupCommand.php b/app/src/Command/SetupCommand.php index fea8e4401..2d9f6ecf7 100644 --- a/app/src/Command/SetupCommand.php +++ b/app/src/Command/SetupCommand.php @@ -35,7 +35,7 @@ use Cake\Console\ConsoleOptionParser; use Cake\Utility\Security; use App\Lib\Enum\PermissionEnum; - +use App\Lib\Enum\SuspendableStatusEnum; class SetupCommand extends Command { @@ -52,6 +52,10 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar { $parser->addOption('admin-username', [ 'help' => __d('command', 'opt.admin-username'), + ])->addOption('admin-given-name', [ + 'help' => __d('command', 'opt.admin-given-name'), + ])->addOption('admin-family-name', [ + 'help' => __d('command', 'opt.admin-family-name'), ])->addOption('force', [ 'help' => __d('command', 'opt.force'), 'boolean' => true, @@ -76,7 +80,7 @@ public function execute(Arguments $args, ConsoleIo $io) // Check if the security salt file already exists, and if so abort. $securitySaltFile = LOCAL . DS . "config" . DS . "security.salt"; - + if(file_exists($securitySaltFile)) { $io->out(__d('command', 'se.already')); @@ -84,36 +88,81 @@ public function execute(Arguments $args, ConsoleIo $io) exit; } } - - // Set the salt now in case we need it. Normally this is done in bootstrap.php. - $salt = hash('sha256', Security::randomBytes(64)); - Security::setSalt($salt); - - // Write out the salt file - $io->out(__d('command', 'se.salt')); - - if(file_put_contents($securitySaltFile, $salt) === false) { - $err = error_get_last(); - throw new \RuntimeException($err[message]); + + // Collect the admin info before we try to do anything + + $givenName = $args->getOption('admin-given-name'); + $sn = $args->getOption('admin-family-name'); + $username = $args->getOption('admin-username'); + + if(empty($givenName)) { + $givenName = $io->ask(__d('command', 'opt.admin-given-name')); } - // We set 444 to prevent accidental changing of the salt, but also so the - // web server user can read it if this script is run by (say) root. - // We assume we're not installed on a shared, semi-public server. - chmod($securitySaltFile, 0444); - - // We need the following: - // - The COmanage CO - // - Register the current version for future upgrade purposes - - // Start with the COmanage CO - - $io->out(__d('command', 'se.db.co')); - - $coTable = $this->getTableLocator()->get("Cos"); - - $co_id = $coTable->setupCOmanageCO(); - if(!is_null($co_id)) { - $io->out(__d('command', 'se.db.co.done', [$co_id])); + + if(empty($sn)) { + $sn = $io->ask(__d('command', 'opt.admin-family-name')); + } + + if(empty($username)) { + $username = $io->ask(__d('command', 'opt.admin-username')); } + + $coTable = $this->getTableLocator()->get('Cos'); + + // Add the first CMP Administrator + + $io->out(__d('command', 'se.db.cmpadmin')); + + // We disable validation here because there may be dependencies on + // validation aspects that aren't set up yet or aren't available here + + $person = $coTable->People->newEntity([ + 'co_id' => $co_id, + 'status' => SuspendableStatusEnum::Active + ], + ['validate' => false]); + + $person->names = [$coTable->People->Names->newEntity([ + 'type_id' => $coTable->Types->getTypeId(coId: $co_id, + attribute: 'Names.type', + value: 'official'), + 'given' => $givenName, + 'family' => $sn, + 'primary_name' => true + ], + ['validate' => false])]; + + $person->identifiers = [$coTable->People->Identifiers->newEntity([ + 'type_id' => $coTable->Types->getTypeId(coId: $co_id, + attribute: 'Identifiers.type', + value: 'network'), + 'identifier' => $username, + 'login' => true, + 'status' => SuspendableStatusEnum::Active + ], + ['validate' => false])]; + + $person->person_roles = [$coTable->People->PersonRoles->newEntity([ + 'affiliation_type_id' => $coTable->Types->getTypeId(coId: $co_id, + attribute: 'PersonRoles.affiliation', + value: 'staff'), + 'title' => __d('command', 'se.person_role.title'), + 'status' => SuspendableStatusEnum::Active + ], + ['validate' => false])]; + + $person->group_members = [$coTable->People->GroupMembers->newEntity([ + 'group_id' => $coTable->Groups->getAdminGroupId(coId: $co_id) + ], + ['validate' => false])]; + + $person->group_owners = [$coTable->People->GroupOwners->newEntity([ + 'group_id' => $coTable->Groups->getAdminGroupId(coId: $co_id) + ], + ['validate' => false])]; + + $coTable->People->save($person); + + $io->out(__d('command', 'se.done')); } } \ No newline at end of file diff --git a/app/src/Command/TransmogrifyCommand.php b/app/src/Command/TransmogrifyCommand.php index 6b99b024c..c5cefc731 100644 --- a/app/src/Command/TransmogrifyCommand.php +++ b/app/src/Command/TransmogrifyCommand.php @@ -506,6 +506,13 @@ public function execute(Arguments $args, ConsoleIo $io) { $schemaPrefix = $outcfg['database'] . '.'; } + // Register the current version for future upgrade purposes + + $targetVersion = rtrim(file_get_contents(CONFIG . DS . "VERSION")); + + $metaTable = $this->getTableLocator()->get('Meta'); + $metaTable->setUpgradeVersion($targetVersion, true); + foreach(array_keys($this->tables) as $t) { // If we were given a list of tables see if this table is in the list if(!empty($atables) && !in_array($t, $atables)) diff --git a/app/src/Lib/Traits/HistoryTrait.php b/app/src/Lib/Traits/HistoryTrait.php index 33d9d9c1e..c952bb882 100644 --- a/app/src/Lib/Traits/HistoryTrait.php +++ b/app/src/Lib/Traits/HistoryTrait.php @@ -74,9 +74,14 @@ public function changesToString($entity): string { if($entity->isNew() || $entity->deleted) { // Generate a changeset of non-empty fields foreach($diffFields as $field) { + if(!is_string($field)) { + // This is a related model, skip + continue; + } + $newValue = $entity->get($field); - if(!empty($newValue)) { + if(!empty($newValue) && is_string($newValue)) { if($field == 'type_id') { $newValue = $Types->getTypeLabel((int)$newValue); } diff --git a/app/src/Model/Entity/Meta.php b/app/src/Model/Entity/Meta.php new file mode 100644 index 000000000..9564e4c50 --- /dev/null +++ b/app/src/Model/Entity/Meta.php @@ -0,0 +1,40 @@ + true, + 'id' => false, + 'slug' => false, + ]; +} \ No newline at end of file diff --git a/app/src/Model/Table/GroupsTable.php b/app/src/Model/Table/GroupsTable.php index 353a7cb8c..4673bfd32 100644 --- a/app/src/Model/Table/GroupsTable.php +++ b/app/src/Model/Table/GroupsTable.php @@ -280,6 +280,20 @@ public function findAdminGroup(Query $query, array $options): Query { ]); } + /** + * Get the Admin Group for a CO. + * + * @since COmanage Registry v5.0.0 + * @param int $coId CO ID + * @return int Group ID + */ + + public function getAdminGroupId(int $coId): int { + $g = $this->find('adminGroup', ['co_id' => $coId])->firstOrFail(); + + return $g->id; + } + /** * Obtain an iterator for all members of the requested Group. * diff --git a/app/src/Model/Table/MetaTable.php b/app/src/Model/Table/MetaTable.php new file mode 100644 index 000000000..72c873f92 --- /dev/null +++ b/app/src/Model/Table/MetaTable.php @@ -0,0 +1,91 @@ +setIsConfigurationTable(false); + + $this->setDisplayField('upgrade_version'); + } + + /** + * Determine the current "upgrade" version. + * + * @since COmanage Registry v5.0.0 + * @return Current version + */ + + public function getUpgradeVersion() { + $sql = "SELECT upgrade_version FROM meta"; + + $connection = ConnectionManager::get('default'); + $results = $connection->execute($sql)->fetchAll('assoc'); + + return $results['upgrade_version']; + } + + /** + * Update the current "upgrade" version. + * + * @since COmanage Registry v5.0.0 + * @param String $version New current version + * @param Boolean $insert Whether to assume an insert rather than an update + * @return Boolean True on success + */ + + public function setUpgradeVersion($version, $insert=false) { + $sql = null; + + if($insert) { + $sql = "INSERT INTO meta (upgrade_version) VALUES (:v)"; + } else { + $sql = "UPDATE meta SET upgrade_version = :v"; + } + + $connection = ConnectionManager::get('default'); + $results = $connection->execute($sql, ['v' => $version])->fetchAll('assoc'); + + return true; + } +} \ No newline at end of file diff --git a/app/src/Model/Table/TypesTable.php b/app/src/Model/Table/TypesTable.php index 17968662b..cacb8ed38 100644 --- a/app/src/Model/Table/TypesTable.php +++ b/app/src/Model/Table/TypesTable.php @@ -224,6 +224,28 @@ public function buildRules(RulesChecker $rules): RulesChecker { return $rules; } + /** + * Get the ID for a Type. + * + * @since COmanage Registry v5.0.0 + * @param int $coId CO ID + * @param string $attribute Attribute, in Models.attribute form + * @param string $value Value + * @return int Type ID + */ + + public function getTypeId(int $coId, string $attribute, string $value): int { + $t = $this->find() + ->where([ + 'Types.co_id' => $coId, + 'Types.attribute' => $attribute, + 'Types.value' => $value + ]) + ->firstOrFail(); + + return $t->id; + } + /** * Obtain the type label for a given type entity. *