From 0b70f085410b466a64965e010ee658deee1ea35e Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Thu, 16 Oct 2025 13:02:14 +0300 Subject: [PATCH] Servers transmogrify --- .../Transmogrify/config/schema/tables.json | 94 ++++++++++++++++--- .../src/Command/TransmogrifyCommand.php | 16 ++-- .../src/Lib/Traits/CacheTrait.php | 17 ++++ .../src/Lib/Traits/TypeMapperTrait.php | 38 ++++++++ 4 files changed, 143 insertions(+), 22 deletions(-) diff --git a/app/plugins/Transmogrify/config/schema/tables.json b/app/plugins/Transmogrify/config/schema/tables.json index 24e2d832d..36ecf0939 100644 --- a/app/plugins/Transmogrify/config/schema/tables.json +++ b/app/plugins/Transmogrify/config/schema/tables.json @@ -32,6 +32,7 @@ }, "__NOTES__": "Common keys: source (DB table), displayField (for printing), addChangelog (add created/modified history), booleans (type coercion), cache (key(s) to cache rows by), sqlSelect (custom SELECT provider function), pre/postTable and pre/postRow (hook function names), fieldMap (left=new table column, right=source column name or &function). Use &mapNow to inject current timestamp, &mapExtendedType to resolve cm_co_extended_types, and mapping helpers found in Transmogrify traits. Note: There is no generic config-driven enforcement for required/defaults/unique; enforce uniqueness via database constraints." }, + "__NOTES__": "CONFIGURATION MIGRATIONS", "cos": { "source": "cm_cos", "displayField": "name", @@ -84,10 +85,6 @@ "t_and_c_return_url_allowlist": null } }, - "authentication_events": { - "source": "cm_authentication_events", - "displayField": "authenticated_identifier" - }, "api_users": { "source": "cm_api_users", "displayField": "username", @@ -101,6 +98,85 @@ "displayField": "name", "sqlSelect": "couSqlSelect" }, + "servers": { + "source": "cm_servers", + "displayField": "description", + "addChangelog": false, + "cache": ["co_id", "status"], + "fieldMap": { + "plugin": "&mapServerTypeToPlugin", + "server_type": null + } + }, + "http_servers": { + "source": "cm_http_servers", + "displayField": "serverurl", + "addChangelog": false, + "booleans": [ + "ssl_verify_host", + "ssl_verify_peer" + ], + "cache": ["server_id"], + "fieldMap": { + "serverurl": "url", + "ssl_verify_peer": "skip_ssl_verification", + "ssl_verify_host": null + } + }, + "oauth2_servers": { + "source": "cm_oauth2_servers", + "displayField": "serverurl", + "addChangelog": false, + "cache": ["server_id"], + "booleans": ["access_token_exp"], + "fieldMap": { + "serverurl": "url", + "proxy": null + } + }, + "sql_servers": { + "source": "cm_sql_servers", + "displayField": "hostname", + "cache": ["server_id"], + "addChangelog": false, + "fieldMap": { + "dbport": "port" + } + }, + "match_servers": { + "source": "cm_match_servers", + "displayField": "username", + "addChangelog": false, + "booleans": [ + "ssl_verify_peer", + "ssl_verify_host" + ], + "cache": ["server_id"], + "fieldMap": { + "serverurl": "url", + "auth_type": "?BA", + "ssl_verify_peer": "skip_ssl_verification", + "ssl_verify_host": null + } + }, + "match_server_attributes": { + "source": "cm_match_server_attributes", + "displayField": "attribute", + "addChangelog": false, + "booleans": [ + "required" + ], + "cache": ["match_server_id"], + "fieldMap": { + "type_id": "&mapMatchAttributeTypeId", + "type": null + } + }, + "__NOTES__": "DATA MIGRATIONS", + "authentication_events": { + "source": "cm_authentication_events", + "displayField": "authenticated_identifier" + }, "people": { "source": "cm_co_people", "displayField": "id", @@ -320,15 +396,5 @@ "co_person_id": "person_id", "org_identity_id": "external_identity_id" } - }, - "servers": { - "source": "cm_servers", - "displayField": "description", - "addChangelog": false, - "cache": ["co_id", "status"], - "fieldMap": { - "plugin": "&mapServerTypeToPlugin", - "server_type": null - } } } diff --git a/app/plugins/Transmogrify/src/Command/TransmogrifyCommand.php b/app/plugins/Transmogrify/src/Command/TransmogrifyCommand.php index e903e61e9..f6a049923 100644 --- a/app/plugins/Transmogrify/src/Command/TransmogrifyCommand.php +++ b/app/plugins/Transmogrify/src/Command/TransmogrifyCommand.php @@ -268,8 +268,8 @@ public function execute(Arguments $args, ConsoleIo $io): int foreach(array_keys($this->tables) as $t) { // Initializations per table migration - $modeltableEmpty = true; - $notSelected = false; + $outboundTableEmpty = true; + $skipTableTransmogrification = false; $inboundQualifiedTableName = $this->inconn->qualifyTableName($this->tables[$t]['source']); $outboundQualifiedTableName = $this->outconn->qualifyTableName($t); $Model = TableRegistry::getTableLocator()->get($t); @@ -292,7 +292,7 @@ public function execute(Arguments $args, ConsoleIo $io): int // Skip a table if already contains data if($Model->find()->count() > 0) { - $modeltableEmpty = false; + $outboundTableEmpty = false; $io->warning("Table (" . $t . ") is not empty. We will not overwrite existing data."); } @@ -303,12 +303,12 @@ public function execute(Arguments $args, ConsoleIo $io): int !empty($selected) && !in_array($t, $selected) ) { - $notSelected = true; + $skipTableTransmogrification = true; $io->warning("Skipping Transmogrification. Table ($t) is not in the selected subset."); } // Mark the table as skipped if it is not empty and not selected - $this->cache['skipInsert'][$outboundQualifiedTableName] = (!$modeltableEmpty && $notSelected); + $this->cache['skipInsert'][$outboundQualifiedTableName] = !$outboundTableEmpty || $skipTableTransmogrification; /* * End of checks @@ -371,7 +371,7 @@ public function execute(Arguments $args, ConsoleIo $io): int // and then skip insert from everywhere. $this->runPreRowHook($t, $origRow, $row); - // Step 3: Set changelog defaults (created/modified timestamps, user IDs) + // Set changelog defaults (created/modified timestamps, user IDs) // Must be done before boolean normalization as it adds new fields $this->populateChangelogDefaults( $t, @@ -407,7 +407,7 @@ public function execute(Arguments $args, ConsoleIo $io): int $warns++; $this->cache['rejected'][$outboundQualifiedTableName][$row['id']] = $row; $progress->warn("Skipping $t record " . $row['id'] . " due to invalid foreign key: " . $e->getMessage()); - $io->ask('Press to continue...'); + $progress->ask('Press to continue...'); } catch(\InvalidArgumentException $e) { // If we can't find a value for mapping we skip the record @@ -415,7 +415,7 @@ public function execute(Arguments $args, ConsoleIo $io): int $warns++; $this->cache['rejected'][$outboundQualifiedTableName][$row['id']] = $row; $progress->warn("Skipping $t record " . $row['id'] . ": " . $e->getMessage()); - $io->ask('Press to continue...'); + $progress->ask('Press to continue...'); } catch(\Exception $e) { $err++; diff --git a/app/plugins/Transmogrify/src/Lib/Traits/CacheTrait.php b/app/plugins/Transmogrify/src/Lib/Traits/CacheTrait.php index 44ea8934e..c5b55c05f 100644 --- a/app/plugins/Transmogrify/src/Lib/Traits/CacheTrait.php +++ b/app/plugins/Transmogrify/src/Lib/Traits/CacheTrait.php @@ -104,6 +104,8 @@ protected function findCoId(array $row): int isset($row['group_id']) => $this->getCoIdFromGroupId((int)$row['group_id']), + isset($row['match_server_id']) => $this->getCoIdFromMatchServer((int)$row['match_server_id']), + // Legacy/preRow: org_identity_id follows the same External Identity path isset($row['org_identity_id']) => $this->getCoIdFromExternalIdentityId((int)$row['org_identity_id']), @@ -179,6 +181,21 @@ private function getCoIdFromExternalIdentityId(int $externalIdentityId): ?int return $personId !== null ? $this->getCoIdFromPersonId($personId) : null; } + /** + * Resolve a CO ID from a Match Server ID via cache. + * + * @param int $matchServerId Match Server ID to resolve + * @return int|null CO ID if found, null otherwise + * @since COmanage Registry v5.2.0 + */ + private function getCoIdFromMatchServer(int $matchServerId): ?int + { + if (isset($this->cache['match_servers']['id'][$matchServerId]['server_id'])) { + $serverId = (int)$this->cache['match_servers']['id'][$matchServerId]['server_id']; + return $this->cache['servers']['id'][$serverId]['co_id'] ?? null; + } + return null; + } /** * Resolve a CO ID from a Person Role ID via cache. diff --git a/app/plugins/Transmogrify/src/Lib/Traits/TypeMapperTrait.php b/app/plugins/Transmogrify/src/Lib/Traits/TypeMapperTrait.php index 2b90c7c66..f96dec0aa 100644 --- a/app/plugins/Transmogrify/src/Lib/Traits/TypeMapperTrait.php +++ b/app/plugins/Transmogrify/src/Lib/Traits/TypeMapperTrait.php @@ -68,6 +68,19 @@ trait TypeMapperTrait 'SQ' => 'CoreServer.SqlServers', ]; + /** + * Map match attribute types to corresponding model names + * + * @var array + * @since COmanage Registry v5.2.0 + */ + public const MATCH_ATTRIBUTE_TYPE_MAP = [ + 'emailAddress' => 'EmailAddresses', + 'name' => 'Names', + 'identifier' => 'Identifiers', + 'dateOfBirth' => '', + ]; + /** * Map address type to corresponding type ID * @@ -145,6 +158,31 @@ protected function mapIdentifierType(array $row): ?int return $this->mapType($row, 'Identifiers.type', $this->findCoId($row)); } + + /** + * Map match attribute type to corresponding type ID based on attribute key + * + * @param array $row Row data containing match attribute + * @return int|null Mapped type ID or null if attribute not found/mapped + * @since COmanage Registry v5.2.0 + */ + protected function mapMatchAttributeTypeId(array $row): ?int + { + // Determine the model based on the match attribute map (eg, emailAddress -> EmailAddresses) + $attributeKey = $row['attribute'] ?? null; + if (!$attributeKey || !array_key_exists($attributeKey, self::MATCH_ATTRIBUTE_TYPE_MAP)) { + return null; + } + + $model = self::MATCH_ATTRIBUTE_TYPE_MAP[$attributeKey]; + + // If the mapping is empty (eg, dateOfBirth), return null + if ($model === '') { + return null; + } + return $this->mapType($row, $model . '.type', $this->findCoId($row)); + } + /** * Map login identifiers, in accordance with the configuration. *