Skip to content

Commit

Permalink
Implement Provisioning Groups (CFM-26)
Browse files Browse the repository at this point in the history
  • Loading branch information
Benn Oshrin committed Mar 21, 2026
1 parent 2d1fccd commit 3f60755
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 9 deletions.
3 changes: 2 additions & 1 deletion app/src/Lib/Traits/ProvisionableTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ public function requestProvisioning(
$this->$parentTableName->requestProvisioning(
id: $primaryLink->value,
context: $context,
provisioningTargetId: $provisioningTargetId
provisioningTargetId: $provisioningTargetId,
job: $job
);
}
}
Expand Down
37 changes: 36 additions & 1 deletion app/src/Model/Table/GroupMembersTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
Expand Down
2 changes: 1 addition & 1 deletion app/src/Model/Table/PeopleTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
38 changes: 37 additions & 1 deletion app/src/Model/Table/ProvisioningTargetsTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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' => [
Expand Down Expand Up @@ -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;
Expand All @@ -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);

Expand Down
7 changes: 7 additions & 0 deletions app/templates/ProvisioningTargets/columns.inc
Original file line number Diff line number Diff line change
Expand Up @@ -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
]
];

Expand Down
3 changes: 1 addition & 2 deletions app/templates/ProvisioningTargets/fields.inc
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ $fields = [
],
'max_retry',
'plugin',
// todo: Not yet implemented (CFM-26)
// 'provisioning_group_id',
'provisioning_group_id',
'ordr'
];

Expand Down
8 changes: 5 additions & 3 deletions app/templates/Standard/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down

0 comments on commit 3f60755

Please sign in to comment.