diff --git a/app/src/Lib/Traits/PluggableModelTrait.php b/app/src/Lib/Traits/PluggableModelTrait.php index fc102ec78..486e4e0be 100644 --- a/app/src/Lib/Traits/PluggableModelTrait.php +++ b/app/src/Lib/Traits/PluggableModelTrait.php @@ -88,11 +88,13 @@ public function afterMarshal( * * @since COmanage Registry v5.2.0 * @param EntityInterface $original Original entity + * @param int $targetCoId Target CO ID * @param string $targetDataSource Target DataSource connection name */ public function checkCloneDependencies( \Cake\Datasource\EntityInterface $original, + int $targetCoId, string $targetDataSource='default' ) { // Verify the plugin in use is active in the target database. If we're on the same diff --git a/app/src/Lib/Util/SearchUtilities.php b/app/src/Lib/Util/SearchUtilities.php index 346d88a89..4e8953bf2 100644 --- a/app/src/Lib/Util/SearchUtilities.php +++ b/app/src/Lib/Util/SearchUtilities.php @@ -36,8 +36,8 @@ class SearchUtilities { // To add a new clonable model, see https://spaces.at.internet2.edu/x/DIBuFQ // Because this list is used by CloneCommand, it should be sorted in dependency order. static protected $clonableModels = [ - 'Servers', 'Types', + 'Servers', 'Cous', 'Groups', 'ApiUsers', diff --git a/app/src/Model/Table/CousTable.php b/app/src/Model/Table/CousTable.php index 589b459f7..80c672cbe 100644 --- a/app/src/Model/Table/CousTable.php +++ b/app/src/Model/Table/CousTable.php @@ -183,7 +183,7 @@ public function getCloneSuccessors( // Pull the set of non-automatic COU Groups and return their UUIDs $clonableGroups = $this->Groups - ->find('nonAutomaticGroups', ['cou_id' => $original->id]) + ->find('nonAutomaticGroups', cou_id: $original->id) ->all(); return $clonableGroups->extract('uuid')->toArray(); @@ -278,9 +278,10 @@ public function postClone( $TargetGroups->addDefaults( coId: $clone->co_id, - couId: $clone->cou_id, + couId: $clone->id, rename: true, // Allow renaming on updates - autoOnly: true + autoOnly: true, + dataSource: $targetDataSource ); } diff --git a/app/src/Model/Table/GroupsTable.php b/app/src/Model/Table/GroupsTable.php index 14dc14260..ef3a43a75 100644 --- a/app/src/Model/Table/GroupsTable.php +++ b/app/src/Model/Table/GroupsTable.php @@ -296,7 +296,7 @@ public function addDefaults( $co = $Cos->get($coId); } catch(\Cake\Datasource\Exception\RecordNotFoundException $e) { - throw new \InvalidArgumentException(__d('error', __d('controller', 'Cos', [1]))); + throw new \InvalidArgumentException(__d('error', 'notfound', __d('controller', 'Cos', [1]))); } $couName = null; @@ -317,7 +317,9 @@ public function addDefaults( $couName = $cou->name; } - // The names get prefixed "CO" or "CO:COU:", as appropriate + // The names get prefixed "CO" or "CO:COU:", as appropriate. + // If the set of $defaultGroups is updated, checkCloneDependencies() + // may also need to be updated. $defaultGroups = [ ':admins' => [ @@ -553,11 +555,13 @@ public function buildRules(RulesChecker $rules): RulesChecker { * * @since COmanage Registry v5.2.0 * @param EntityInterface $original Original entity + * @param int $targetCoId Target CO ID * @param string $targetDataSource Target DataSource connection name */ public function checkCloneDependencies( EntityInterface $original, + int $targetCoId, string $targetDataSource='default' ) { // We don't clone Automatic Groups. Those should be created when the related structure @@ -568,6 +572,81 @@ public function checkCloneDependencies( // in CloneCommand output throw new \InvalidArgumentException("Group " . $original->id . " is an automatic group, skipping..."); } + + // Default Groups are automatically created when the CO is created, so (eg) + // CO:admins, CO:approvers, and CO:mfaexempt will already exist on the target + // CO. We need to make sure the UUIDs are in sync before proceeding. + // Note this does not need to be applied to COU default Groups, since those are + // cloned (and will therefore have the correct UUID). + + $syncUuid = false; + + if(!$original->cou_id) { + if(in_array($original->group_type, [ + GroupTypeEnum::Admins, + GroupTypeEnum::Approvers, + GroupTYpeEnum::MfaExempt + ])) { + $syncUuid = true; + } + + // We also need to sync the UUID on the Owners Group for the same type of Groups. + // We can't check the Owners Group when we process the original Group because + // we may process the Owners Group first (depending on the order returned from + // the database), so instead for each Owners Group we pull the base Group and + // see if it's one we're interested in. + + if($original->group_type == GroupTypeEnum::Owners) { + $baseGroup = $this->find()->where(['owners_group_id' => $original->id])->first(); + + if($baseGroup && + in_array($baseGroup->group_type, [ + GroupTypeEnum::Admins, + GroupTypeEnum::Approvers, + GroupTYpeEnum::MfaExempt + ])) { + // We'll sync $baseGroup later (or maybe we did it already), for now we + // only worry about $original. + $syncUuid = true; + } + } + } + + if($syncUuid) { + $TargetGroups = TableUtilities::getTableWithDataSource( + tableName: 'Groups', + connectionName: $targetDataSource + ); + + // We ignore the do_not_clone flag because all we're doing is syncing + // the UUID, and we have to make sure these Groups are linked. + + $whereClause = [ + 'co_id' => $targetCoId, + 'cou_id IS' => null, + 'group_type' => $original->group_type + ]; + + if($original->group_type == GroupTypeEnum::Owners) { + // For Owners Groups we have to use the name to find the Group, hopefully + // the admin didn't rename it. + + $whereClause['name'] = $original->name; + } + + $targetGroup = $TargetGroups->find()->where($whereClause)->first(); + + if($targetGroup) { + if($original->uuid != $targetGroup->uuid) { + $this->llog('trace', "Updating uuid on target Group " . $targetGroup->id . " to " . $original->uuid); + + $targetGroup->uuid = $original->uuid; + + // We don't want to run afterSave callbacks + $TargetGroups->save($targetGroup, ['clone' => true]); + } + } + } } /**