diff --git a/app/src/Lib/Traits/ProvisionableTrait.php b/app/src/Lib/Traits/ProvisionableTrait.php index 7d32a6697..1fe837f42 100644 --- a/app/src/Lib/Traits/ProvisionableTrait.php +++ b/app/src/Lib/Traits/ProvisionableTrait.php @@ -86,7 +86,8 @@ public function requestProvisioning( $this->$parentTableName->requestProvisioning( id: $primaryLink->value, context: $context, - provisioningTargetId: $provisioningTargetId + provisioningTargetId: $provisioningTargetId, + job: $job ); } } diff --git a/app/src/Model/Table/GroupMembersTable.php b/app/src/Model/Table/GroupMembersTable.php index 120b7e2fd..1dff6545c 100644 --- a/app/src/Model/Table/GroupMembersTable.php +++ b/app/src/Model/Table/GroupMembersTable.php @@ -49,7 +49,6 @@ class GroupMembersTable extends Table { use \App\Lib\Traits\LayoutTrait; use \App\Lib\Traits\PermissionsTrait; use \App\Lib\Traits\PrimaryLinkTrait; - use \App\Lib\Traits\ProvisionableTrait; use \App\Lib\Traits\QueryModificationTrait; use \App\Lib\Traits\SearchFilterTrait; use \App\Lib\Traits\TableMetaTrait; @@ -292,6 +291,42 @@ public function localAfterSave(\Cake\Event\EventInterface $event, \Cake\Datasour return true; } + /** + * Request provisioning. + * + * @since COmanage Registry v5.2.0 + * @param int $id This table's entity ID to provision + * @param ProvisioningContextEnum $context Context in which provisioning is being requested + * @param int $provisioningTargetId If set, the Provisioning Target ID to request provisioning for (otherwise all) + * @param Job $job If called from a Job, the current Job entity + * @throws InvalidArgumentException + */ + + public function requestProvisioning( + int $id, + string $context, + ?int $provisioningTargetId=null, + ?Job $job=null, + ) { + // For GroupMembers we need to request provisioning on both the Group and the Person. + + $gm = $this->get($id); + + $this->People->requestProvisioning( + id: $gm->person_id, + context: $context, + provisioningTargetId: $provisioningTargetId, + job: $job + ); + + $this->Groups->requestProvisioning( + id: $gm->group_id, + context: $context, + provisioningTargetId: $provisioningTargetId, + job: $job + ); + } + /** * Application Rule to determine if the Person is already a member of the Group. * diff --git a/app/src/Model/Table/PeopleTable.php b/app/src/Model/Table/PeopleTable.php index 82fa658c6..43d4ecdb6 100644 --- a/app/src/Model/Table/PeopleTable.php +++ b/app/src/Model/Table/PeopleTable.php @@ -611,7 +611,7 @@ public function marshalProvisioningData(int $id): array { $entityData = $APlugin->marshalProvisioningData($authenticator, $id); - // Determine the entity name in order to populate the provisiosing data. + // Determine the entity name in order to populate the provisioning data. // We can calculate this because (unlike other Plugin types) there are // naming conventions for Authenticators. diff --git a/app/src/Model/Table/ProvisioningTargetsTable.php b/app/src/Model/Table/ProvisioningTargetsTable.php index 4fa4aeec1..28f3c7a74 100644 --- a/app/src/Model/Table/ProvisioningTargetsTable.php +++ b/app/src/Model/Table/ProvisioningTargetsTable.php @@ -33,10 +33,12 @@ use Cake\ORM\RulesChecker; use Cake\ORM\Table; use Cake\ORM\TableRegistry; +use Cake\Utility\Hash; use Cake\Utility\Inflector; use Cake\Validation\Validator; use App\Lib\Enum\ProvisionerModeEnum; use App\Lib\Enum\ProvisioningContextEnum; +use App\Lib\Enum\ProvisioningEligibilityEnum; use App\Lib\Enum\ProvisioningStatusEnum; use App\Lib\Enum\SuspendableStatusEnum; use App\Lib\Util\StringUtilities; @@ -93,6 +95,10 @@ public function initialize(array $config): void { $this->setRequiresCO(true); $this->setAllowLookupPrimaryLink(['provision', 'reprovision']); $this->setAllowLookupRelatedPrimaryLink(['status' => ['person_id', 'group_id']]); + + $this->setIndexContains([ + 'ProvisioningGroups' + ]); $this->setAutoViewVars([ 'plugins' => [ @@ -226,6 +232,36 @@ public function provision( continue; } + // If there is a Provisioning Group configured, check if this subject is eligible, + // and if not override the $eligibility that was passed in. Note we want to call the + // plugin even if deleted so deprovisioning can be performed. + + $celigibility = $eligibility; + + if(!empty($t->provisioning_group_id)) { + if($provisionedModel == 'People') { + $memberGids = Hash::extract($data, 'group_members.{n}.group_id'); + + if(!in_array($t->provisioning_group_id, $memberGids)) { + // AR-ProvisioningTarget-2 If a Person is removed as a member from a + // Provisioning Target's Provisioning Group, the Person will be reprovisioned + // with Deleted eligibility. We use Deleted because it is closer to the underlying + // intent -- the record should not (have) be(en) provisioned to the target at all, + // vs a formerly valid Person no longer being associated with the CO. + + $celigibility = ProvisioningEligibilityEnum::Deleted; + $this->llog('rule', "AR-ProvisioningTarget-2 Person " . $data->id . " not in Provisioning Group " . $t->provisioning_group_id . ", flagging as Deleted"); + } + } elseif($provisionedModel == 'Groups') { + if($t->provisioning_group_id != $data->id) { + // The requested Group is not the Provisioning Group, switch to a Delete + + $celigibility = ProvisioningEligibilityEnum::Deleted; + $this->llog('trace', "Group " . $data->id . " is not the configured Provisioning Group " . $t->provisioning_group_id . ", flagging as Deleted"); + } + } + } + $this->llog('trace', "Provisioning $provisionedModel for $pluginModel (context: $context)", $t->id); $requeue = false; @@ -236,7 +272,7 @@ public function provision( if($t->status != ProvisionerModeEnum::Queue || $context == ProvisioningContextEnum::Queue) { try { - $result = $this->$pluginModel->provision($t, $provisionedModel, $data, $eligibility); + $result = $this->$pluginModel->provision($t, $provisionedModel, $data, $celigibility); $this->alog('trace', $result); diff --git a/app/templates/ProvisioningTargets/columns.inc b/app/templates/ProvisioningTargets/columns.inc index d347109a4..ab1c98c6f 100644 --- a/app/templates/ProvisioningTargets/columns.inc +++ b/app/templates/ProvisioningTargets/columns.inc @@ -42,6 +42,13 @@ $indexColumns = [ 'type' => 'enum', 'class' => 'ProvisionerModeEnum', 'sortable' => true + ], + 'provisioning_group_id' => [ + 'type' => 'relatedLink', + 'label' => __d('field', 'ProvisioningTargets.provisioning_group_id'), + 'model' => 'provisioning_group', + 'field' => 'name', + 'sortable' => true ] ]; diff --git a/app/templates/ProvisioningTargets/fields.inc b/app/templates/ProvisioningTargets/fields.inc index 21cba05f9..9d25e1eaf 100644 --- a/app/templates/ProvisioningTargets/fields.inc +++ b/app/templates/ProvisioningTargets/fields.inc @@ -35,8 +35,7 @@ $fields = [ ], 'max_retry', 'plugin', - // todo: Not yet implemented (CFM-26) - // 'provisioning_group_id', + 'provisioning_group_id', 'ordr' ]; diff --git a/app/templates/Standard/index.php b/app/templates/Standard/index.php index d46b6c1a1..4380edc24 100644 --- a/app/templates/Standard/index.php +++ b/app/templates/Standard/index.php @@ -539,14 +539,16 @@ // We have a related model, eg actor_person.primary_name $sm = $cfg['submodel']; - if(!empty($entity->$m->$sm->$f)) { + // We use isset() tests here to allow for empty fields, + // eg $model->description = '' + if(isset($entity->$m->$sm->$f)) { $label = $entity->$m->$sm->$f . $suffix; } } else { - if(!empty($entity->$m->$f)) { + if(isset($entity->$m->$f)) { // HasOne $label = $entity->$m->$f . $suffix; - } elseif(!empty($entity->$m[0]->$f)) { + } elseif(isset($entity->$m[0]->$f)) { // HasMany, pick the first $label = $entity->$m[0]->$f . $suffix; }