Skip to content

Commit

Permalink
Additional fixes to Clone Command (CO-479)
Browse files Browse the repository at this point in the history
  • Loading branch information
Benn Oshrin committed Mar 6, 2026
1 parent 6b253a6 commit cb5b71d
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 6 deletions.
2 changes: 2 additions & 0 deletions app/src/Lib/Traits/PluggableModelTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion app/src/Lib/Util/SearchUtilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
7 changes: 4 additions & 3 deletions app/src/Model/Table/CousTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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
);
}

Expand Down
83 changes: 81 additions & 2 deletions app/src/Model/Table/GroupsTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -317,7 +317,9 @@ public function addDefaults(
$couName = $cou->name;
}

// The names get prefixed "CO" or "CO:COU:<couname>", as appropriate
// The names get prefixed "CO" or "CO:COU:<couname>", as appropriate.
// If the set of $defaultGroups is updated, checkCloneDependencies()
// may also need to be updated.

$defaultGroups = [
':admins' => [
Expand Down Expand Up @@ -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
Expand All @@ -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]);
}
}
}
}

/**
Expand Down

0 comments on commit cb5b71d

Please sign in to comment.