Skip to content

Commit

Permalink
Implement AR-Identifier-2 (CFM-15)
Browse files Browse the repository at this point in the history
  • Loading branch information
Benn Oshrin committed Sep 25, 2024
1 parent 50b5a0e commit 31905e6
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 4 deletions.
3 changes: 3 additions & 0 deletions app/resources/locales/en_US/error.po
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ msgstr "No identifier of type \"{0}\" found"
msgid "IdentifierAssignments.type.none"
msgstr "No identifier type specified"

msgid "Identifiers.exists"
msgstr "Identifier already exists on {0} ID {1}"

msgid "Identifiers.login"
msgstr "Only Identifiers attached to a Person may be flagged for login"

Expand Down
6 changes: 6 additions & 0 deletions app/resources/locales/en_US/field.po
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,12 @@ msgstr "Telephone Number"
msgid "TelephoneNumbers.number.ext"
msgstr "x"

msgid "Types.case_insensitive"
msgstr "Case Insensitive"

msgid "Types.case_insensitive.desc"
msgstr "If ticked, uniqueness checks for this Identifier Type will be case insensitive"

msgid "Types.edupersonaffiliation.desc"
# XXX update link to PE wiki?
msgstr "Map the extended affiliation to this eduPersonAffiliation, see <a href="https://spaces.at.internet2.edu/display/COmanage/Extending+the+Registry+Data+Model#ExtendingtheRegistryDataModel-%7B%7BeduPersonAffiliation%7D%7DandExtendedAffiliations">eduPersonAffiliation and Extended Affiliations</a>"
Expand Down
2 changes: 1 addition & 1 deletion app/src/Model/Table/ExtIdentitySourceRecordsTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public function initialize(array $config): void {
$this->setRequiresCO(true);

// These are required for the link to work from the Artifacts page
$this->setAllowUnkeyedPrimaryCO(['index', 'view']);
$this->setAllowUnkeyedPrimaryCO(['index']);
$this->setAllowEmptyPrimaryLink(['index']);

$this->setIndexContains([
Expand Down
77 changes: 77 additions & 0 deletions app/src/Model/Table/IdentifiersTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

use Cake\Event\EventInterface;
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
use \App\Lib\Enum\SuspendableStatusEnum;
Expand All @@ -40,6 +41,7 @@ class IdentifiersTable extends Table {
use \App\Lib\Traits\ChangelogBehaviorTrait;
use \App\Lib\Traits\CoLinkTrait;
use \App\Lib\Traits\HistoryTrait;
use \App\Lib\Traits\LabeledLogTrait;
use \App\Lib\Traits\PermissionsTrait;
use \App\Lib\Traits\PrimaryLinkTrait;
use \App\Lib\Traits\ProvisionableTrait;
Expand Down Expand Up @@ -172,6 +174,24 @@ public function beforeMarshal(EventInterface $event, \ArrayObject $data, \ArrayO
}
}

/**
* Define business rules.
*
* @since COmanage Registry v5.0.0
* @param RulesChecker $rules RulesChecker object
* @return RulesChecker
*/

public function buildRules(RulesChecker $rules): RulesChecker {
// AR-Identifier-2 An Identifier must be unique for its Type and Entity (Person
// or Group) within the CO.
$rules->add([$this, 'ruleUniqueIdentifier'],
'uniqueIdentifier',
['errorField' => 'identifier']);

return $rules;
}

/**
* Callback after model save.
*
Expand Down Expand Up @@ -243,6 +263,63 @@ public function lookupPersonByLogin(int $coId, string $identifier): int {
return $id->person_id;
}

/**
* Application Rule to determine if an Identifier is already in use.
*
* @since COmanage Registry v5.0.0
* @param Entity $entity Entity to be validated
* @param array $options Application rule options
* @return boolean true if the Rule check passes, false otherwise
*/

public function ruleUniqueIdentifier($entity, $options) {
// Uniqueness constraints only apply to People and Groups

// In v4 we created a txn to ensure consistency, but it looks like Cake actually
// starts a transaction, so it appears we don't actially need to do that here.

if(!empty($entity->person_id) || !empty($entity->group_id)) {
if($entity->isNew()
|| $entity->isDirty('identifier')
|| $entity->isDirty('type_id')) {
// We need the Type configuration to see if uniqueness is case insensitive
$type = $this->Types->get($entity->type_id);

// Note we specifically do NOT check status, since a Suspended Identifier
// will still prevent duplicate assignment. (AR-Identifier-3)
$whereClause = [
'type_id' => $entity->type_id
];

if(isset($type->case_insensitive) && $type->case_insensitive) {
$whereClause['LOWER(identifier)'] = strtolower($entity->identifier);
} else {
$whereClause['identifier'] = $entity->identifier;
}

$identifier = $this->find()
->where($whereClause)
->epilog('FOR UPDATE')
->first();

if(!empty($identifier)) {
$inusect = !empty($identifier->person_id) ? __d('controller', 'People', 1) : __d('controller', 'Groups', 1);
$inuseid = !empty($identifier->person_id) ? $identifier->person_id : $identifier->group_id;

// If we fail in the middle of Identifier Assignment this returned message
// will get lost/superceded by a rollback error
$this->llog('rule', "AR-Identifier-2 Identifier " . $identifier->identifier . " is already in use on $inusect ID $inuseid");

return __d('error',
'Identifiers.exists',
[$inusect, $inuseid]);
}
}
}

return true;
}

/**
* Perform a keyword search.
*
Expand Down
5 changes: 5 additions & 0 deletions app/src/Model/Table/TypesTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,11 @@ public function validationDefault(Validator $validator): Validator {
]);
$validator->allowEmptyString('edupersonaffiliation');

$validator->add('case_insensitive', [
'content' => ['rule' => ['boolean']]
]);
$validator->allowEmptyString('case_insensitive');

$validator->add('status', [
'content' => ['rule' => ['inList', SuspendableStatusEnum::getConstValues()]]
]);
Expand Down
3 changes: 2 additions & 1 deletion app/templates/People/fields.inc
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ if($vv_action == 'add') {
'arguments' => [
'fieldName' => 'names.0.type_id',
'fieldOptions' => [
'default' => $vv_default_name_type
'default' => $vv_default_name_type,
'required' => true
],
'fieldType' => 'string'
]]);
Expand Down
9 changes: 7 additions & 2 deletions app/templates/Types/fields.inc
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,14 @@
var attr = document.getElementById('attribute').value;

// Handle page interaction
if(attr == 'PersonRoles.affiliation_type') {
if(attr == 'Identifiers.type') {
hideFields(['edupersonaffiliation'], isPageLoad);
showFields(['case-insensitive'], isPageLoad);
} else if(attr == 'PersonRoles.affiliation_type') {
hideFields(['case-insensitive'], isPageLoad);
showFields(['edupersonaffiliation'], isPageLoad);
} else {
hideFields(['edupersonaffiliation'], isPageLoad);
hideFields(['case-insensitive', 'edupersonaffiliation'], isPageLoad);
}
}

Expand All @@ -60,6 +64,7 @@ if($vv_action == 'add' || $vv_action == 'edit') {
foreach (['display_name',
'value',
'status',
'case_insensitive',
'edupersonaffiliation'
] as $field) {
print $this->element('form/listItem', [
Expand Down

0 comments on commit 31905e6

Please sign in to comment.