Skip to content

Commit

Permalink
Feature equivalency work for CO MVC (CFM-18) and background CO deleti…
Browse files Browse the repository at this point in the history
…no (CFM-464)
  • Loading branch information
Benn Oshrin committed Apr 6, 2026
1 parent d53f97c commit 95ba807
Show file tree
Hide file tree
Showing 13 changed files with 370 additions and 45 deletions.
1 change: 1 addition & 0 deletions app/plugins/CoreJob/config/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"job": [
"AdopterJob",
"AssignerJob",
"DeletionJob",
"ProvisionerJob",
"SyncJob"
]
Expand Down
21 changes: 21 additions & 0 deletions app/plugins/CoreJob/resources/locales/en_US/core_job.po
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ msgstr "Source Keys to process, comma separated (requires external_identity_sour
msgid "opt.assigner.context"
msgstr "Identifier Assignment context"

msgid "opt.deletion.target_id"
msgstr "Record ID of Model to delete"

msgid "opt.deletion.target_model"
msgstr "Model to delete"

msgid "opt.entities"
msgstr "Comma separated list of entity IDs to process"

Expand Down Expand Up @@ -94,6 +100,21 @@ msgstr "Assigned {0}: {1}"
msgid "Assigner.start_summary"
msgstr "Assigning all Identifiers in context {0} for CO {1} ({2} entities)"

msgid "Deletion.error.co"
msgstr "DeletionJob can only be scheduled from the COmanage CO"

msgid "Deletion.error.model"
msgstr "Unsupported target model"

msgid "Deletion.register_summary"
msgstr "Requested hard delete of {0} {1}"

msgid "Deletion.start_summary"
msgstr "Beginning hard delete of {0} {1}"

msgid "Deletion.finish_summary"
msgstr "Deletion completed successfully"

msgid "Provisioner.cancel_summary"
msgstr "Job canceled after provisioning {0} entities ({1} errors)"

Expand Down
107 changes: 107 additions & 0 deletions app/plugins/CoreJob/src/Lib/Jobs/DeletionJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php
/**
* COmanage Registry Deletion Job
*
* Portions licensed to the University Corporation for Advanced Internet
* Development, Inc. ("UCAID") under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* UCAID licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @link https://www.internet2.edu/comanage COmanage Project
* @package registry
* @since COmanage Registry v5.2.0
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/

declare(strict_types = 1);

namespace CoreJob\Lib\Jobs;

use Cake\Datasource\ConnectionManager;
use Cake\ORM\TableRegistry;
use \App\Lib\Enum\JobStatusEnum;
use \App\Lib\Enum\SyncModeEnum;

class DeletionJob {
use \App\Lib\Traits\LabeledLogTrait;

/**
* Obtain the list of parameters supported by this Job.
*
* @since COmanage Registry v5.2.0
* @return Array Array of supported parameters.
* @throws \InvalidArgumentException
* @throws \RuntimeException
*/

public function parameterFormat(): array {
return [
'target_model' => [
'help' => __d('core_job', 'opt.deletion.target_model'),
'type' => 'select',
'choices' => ['Cos'],
'required' => true
],
'target_id' => [
'help' => __d('core_job', 'opt.deletion.target_id'),
'type' => 'integer',
'required' => true
]
];
}

/**
* Run the requested Job.
*
* @since COmanage Registry v5.2.0
* @param JobsTable $JobsTable Jobs table, for updating the Job status
* @param JobHistoryRecordsTable $JobHistoryRecordsTable Job History Records table, for recording additional history
* @param Job $job Job entity
* @param array $parameters Parameters for this Job
* @throws InvalidArgumentException
*/

public function run(
\App\Model\Table\JobsTable $JobsTable,
\App\Model\Table\JobHistoryRecordsTable $JobHistoryRecordsTable,
\App\Model\Entity\Job $job,
array $parameters
) {
// Check that the requesting CO is the COmanage CO.

$requestingCO = $JobsTable->Cos->get($job->co_id);

if(!$requestingCO->isCOmanageCO()) {
throw new \InvalidArgumentException(__d('core_job', 'Deletion.error.co'));
}

// We currently only support deleting COs.

if($parameters['target_model'] != 'Cos') {
throw new \InvalidArgumentException(__d('core_job', 'Deletion.error.model'));
}

// We need to pull the CO to call delete on it.

// get() will throw an exception on an invalid CO ID
$targetCO = $JobsTable->Cos->get($parameters['target_id']);

$JobsTable->start(job: $job, summary: __d('core_job', 'Deletion.start_summary', [$parameters['target_model'], $parameters['target_id']]));

$JobsTable->Cos->deleteOrFail($targetCO);

$JobsTable->finish(job: $job, summary: __d('core_job', 'Deletion.finish_summary'));
}
}
3 changes: 3 additions & 0 deletions app/resources/locales/en_US/information.po
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ msgstr "Changelog, expanded, button, click to collapse"
msgid "changelog.deleted"
msgstr "This record has been deleted"

msgid "cos.delete.sched"
msgstr "This CO is scheduled for deletion"

msgid "cos.none"
msgstr "You are not an active member in any collaboration. If your request for enrollment is still being processed, you will not be able to login until it is approved. Please contact an administrator for assistance."

Expand Down
8 changes: 7 additions & 1 deletion app/resources/locales/en_US/operation.po
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,13 @@ msgid "delete"
msgstr "Delete"

msgid "delete.confirm"
msgstr "Are you sure you wish to delete this record ({0})?"
msgstr "Are you sure you want to delete this record ({0})?"

msgid "delete.queue"
msgstr "Queue for Deletion"

msgid "delete.queue.confirm"
msgstr "Are you sure you want to schedule this record ({0}) for deletion?"

msgid "duplicate"
msgstr "Duplicate"
Expand Down
47 changes: 45 additions & 2 deletions app/src/Controller/CosController.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@

// XXX not doing anything with Log yet
use Cake\Log\Log;
//use \App\Lib\Enum\PermissionEnum;

use Cake\ORM\TableRegistry;

class CosController extends StandardController {
Expand Down Expand Up @@ -70,6 +68,51 @@ public function beforeRender(\Cake\Event\EventInterface $event) {
return parent::beforeRender($event);
}

/**
* Handle a delete action for a Standard object.
*
* @since COmanage Registry v5.2.0
* @param Integer $id Object ID
*/

public function delete($id) {
// XXX this could ultimately merge into StandardController
if(!empty($this->request->getQuery('queue'))
&& $this->request->getQuery('queue') == 'yes') {
// Register a Job to delete the requested entity

$JobTable = TableRegistry::getTableLocator()->get("Jobs");

try {
$comanageco = $this->Cos->find('COmanageCO')->firstOrFail();

$JobTable->register(
coId: $comanageco->id,
plugin: 'CoreJob.DeletionJob',
parameters: ['target_model' => 'Cos', 'target_id' => $id],
registerSummary: __d('core_job', 'Deletion.register_summary', ['Cos', $id])
);

// Because (unlike v4 Garbage Collection) Deletion Job doesn't use
// a special status, we update the entity description to provide a
// simple indicator to administrators. See also CFM-94.

$co = $this->Cos->get($id);
$co->description = __d('information', 'cos.delete.sched');
$this->Cos->save($co);

$this->Flash->success(__d('core_job', 'Deletion.register_summary', ['Cos', $id]));
}
catch(\Exception $e) {
$this->Flash->error($e->getMessage());
}

return $this->generateRedirect(null);
} else {
return parent::delete($id);
}
}

/*
* XXX implement, also REST API
*
Expand Down
69 changes: 69 additions & 0 deletions app/src/Lib/Traits/RuleTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php
/**
* COmanage Rule Trait
*
* Portions licensed to the University Corporation for Advanced Internet
* Development, Inc. ("UCAID") under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* UCAID licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @link https://www.internet2.edu/comanage COmanage Project
* @package registry
* @since COmanage Registry v5.2.0
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/

declare(strict_types = 1);

namespace App\Lib\Traits;

use Cake\ORM\TableRegistry;
use \App\Lib\Util\StringUtilities;

trait RuleTrait {
/**
* Case insensitive uniqueness rule.
*
* @since COmanage Registry v5.2.0
* @param Entity $entity Entity to be validated
* @param array $options Application rule options
* @return bool|string true if the Rule check passes, false otherwise
*/

public function ruleIsCaseInsensitiveUnique($entity, array $options): bool|string {
// Build a where clause of lowercased values of the current $entity
$whereClause = [];

if($entity->id) {
// Exclude the current record from the uniqueness check
$whereClause['id <>'] = $entity->id;
}

foreach($options['fields'] as $f) {
$whereClause['LOWER('.$f.')'] = strtolower($entity->$f);
}

$count = $this->find()
->where($whereClause)
->count();

if($count > 0) {
// XXX note this error lookup won't work for Plugins
return __d('error', 'exists', [__d('controller', StringUtilities::entityToClassName($entity), [1])]);
}

return true;
}
}
31 changes: 19 additions & 12 deletions app/src/Model/Behavior/ChangelogBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use Cake\Event\Event;
use Cake\ORM\Behavior;
use Cake\ORM\Query;
use Cake\ORM\TableRegistry;
use Cake\Utility\Inflector;

class ChangelogBehavior extends Behavior
Expand All @@ -48,16 +49,24 @@ class ChangelogBehavior extends Behavior
*/

public function beforeDelete(Event $event, $entity, \ArrayObject $options) {
$subject = $event->getSubject();
$tableName = $subject->getTable();
$alias = $subject->getAlias();
$parentfk = Inflector::singularize($tableName) . "_id";

if(isset($options['useHardDelete']) && $options['useHardDelete']) {
// Hard delete requested, so just return
// Hard delete requested. In order for a hard delete to succeed, any archive records
// must also (and first) be hard deleted, so we don't run into any (Changelog related)
// foreign key issues (GMR-7). We can just deleteAll since we neither want nor need
// callbacks.

$Table = TableRegistry::getTableLocator()->get($alias);

$Table->deleteAll([$parentfk => $entity->id]);

$event->setResult(true);
return;
}

$subject = $event->getSubject();
$table = $subject->getTable();
$alias = $subject->getAlias();
$parentfk = Inflector::singularize($table) . "_id";

// Before we do anything else, make sure we're not trying to update an archive record
if($entity->deleted || !empty($entity->$parentfk)) {
Expand Down Expand Up @@ -110,9 +119,9 @@ public function beforeFind(Event $event, Query $query, \ArrayObject $options, bo
}

$subject = $event->getSubject();
$table = $subject->getTable();
$tableName = $subject->getTable();
$alias = $subject->getAlias();
$parentfk = Inflector::singularize($table) . "_id";
$parentfk = Inflector::singularize($tableName) . "_id";

LogBehavior::strace($alias, 'Changelog altering find conditions');

Expand All @@ -132,8 +141,6 @@ public function beforeFind(Event $event, Query $query, \ArrayObject $options, bo
// that will be not-changelog but might become changelog?
$query->where([$alias . '.deleted IS NOT true'])
->where([$alias . '.' . $parentfk . ' IS NULL']);

// XXX need to also check parent key IS NULL
}

/**
Expand All @@ -153,9 +160,9 @@ public function beforeSave(Event $event, EntityInterface $entity, \ArrayObject $
}

$subject = $event->getSubject();
$table = $subject->getTable();
$tableName = $subject->getTable();
$alias = $subject->getAlias();
$parentfk = Inflector::singularize($table) . "_id";
$parentfk = Inflector::singularize($tableName) . "_id";

// Before we do anything else, make sure we're not trying to update an archive record
if($entity->deleted || !empty($entity->$parentfk)) {
Expand Down
2 changes: 2 additions & 0 deletions app/src/Model/Table/ApiUsersTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObj

public function buildRules(RulesChecker $rules): RulesChecker {
// AR-ApiUser-3 API usernames must be unique across the entire platform.
// Note we don't enforce case insensitive tests here, so we could have two
// different API Users called "co_2.apiuser" and "co_2.ApiUser".
$rules->add(
$rules->isUnique(['username']),
'usernameUnique',
Expand Down
Loading

0 comments on commit 95ba807

Please sign in to comment.