From 22e96792bca2ce21c738acec2129f2742120fdb7 Mon Sep 17 00:00:00 2001 From: Benn Oshrin Date: Thu, 25 Jul 2024 19:26:52 -0400 Subject: [PATCH] Initial implementation of Person Role Mapper Pipeline Plugin (CFM-371) --- NOTICE | 2 +- .../PipelineToolkit/README.md | 11 + .../PipelineToolkit/composer.json | 24 +++ .../PipelineToolkit/phpunit.xml.dist | 30 +++ .../locales/en_US/pipeline_toolkit.po | 50 +++++ .../src/Controller/AppController.php | 10 + .../PersonRoleMappersController.php | 40 ++++ .../PersonRoleMappingsController.php | 40 ++++ .../src/Model/Entity/PersonRoleMapper.php | 49 +++++ .../src/Model/Entity/PersonRoleMapping.php | 112 ++++++++++ .../Model/Table/PersonRoleMappersTable.php | 158 ++++++++++++++ .../Model/Table/PersonRoleMappingsTable.php | 194 ++++++++++++++++++ .../src/PipelineToolkitPlugin.php | 93 +++++++++ .../PipelineToolkit/src/config/plugin.json | 40 ++++ .../PersonRoleMappers/fields-links.inc | 39 ++++ .../PersonRoleMappers/fields-nav.inc | 31 +++ .../templates/PersonRoleMappers/fields.inc | 30 +++ .../templates/PersonRoleMappings/columns.inc | 54 +++++ .../templates/PersonRoleMappings/fields.inc | 77 +++++++ .../PipelineToolkit/tests/bootstrap.php | 55 +++++ .../PipelineToolkit/tests/schema.sql | 1 + .../PipelineToolkit/webroot/.gitkeep | 0 app/composer.json | 2 + app/config/schema/schema.json | 14 ++ app/resources/locales/en_US/controller.po | 3 + app/resources/locales/en_US/enumeration.po | 36 ++++ app/resources/locales/en_US/error.po | 3 + app/src/Controller/FlangesController.php | 41 ++++ .../Controller/StandardPluginController.php | 14 +- app/src/Lib/Enum/ComparisonEnum.php | 89 ++++++++ app/src/Lib/Enum/FlangeModeEnum.php | 36 ++++ app/src/Model/Entity/Flange.php | 42 ++++ app/src/Model/Table/FlangesTable.php | 134 ++++++++++++ app/src/Model/Table/PipelinesTable.php | 45 +++- app/src/View/Helper/FieldHelper.php | 10 +- app/templates/Flanges/columns.inc | 63 ++++++ app/templates/Flanges/fields.inc | 41 ++++ app/templates/Pipelines/fields-nav.inc | 38 ++++ app/templates/element/form/unorderedList.php | 8 +- app/vendor/composer/autoload_psr4.php | 2 + app/vendor/composer/autoload_static.php | 10 + 41 files changed, 1758 insertions(+), 13 deletions(-) create mode 100644 app/availableplugins/PipelineToolkit/README.md create mode 100644 app/availableplugins/PipelineToolkit/composer.json create mode 100644 app/availableplugins/PipelineToolkit/phpunit.xml.dist create mode 100644 app/availableplugins/PipelineToolkit/resources/locales/en_US/pipeline_toolkit.po create mode 100644 app/availableplugins/PipelineToolkit/src/Controller/AppController.php create mode 100644 app/availableplugins/PipelineToolkit/src/Controller/PersonRoleMappersController.php create mode 100644 app/availableplugins/PipelineToolkit/src/Controller/PersonRoleMappingsController.php create mode 100644 app/availableplugins/PipelineToolkit/src/Model/Entity/PersonRoleMapper.php create mode 100644 app/availableplugins/PipelineToolkit/src/Model/Entity/PersonRoleMapping.php create mode 100644 app/availableplugins/PipelineToolkit/src/Model/Table/PersonRoleMappersTable.php create mode 100644 app/availableplugins/PipelineToolkit/src/Model/Table/PersonRoleMappingsTable.php create mode 100644 app/availableplugins/PipelineToolkit/src/PipelineToolkitPlugin.php create mode 100644 app/availableplugins/PipelineToolkit/src/config/plugin.json create mode 100644 app/availableplugins/PipelineToolkit/templates/PersonRoleMappers/fields-links.inc create mode 100644 app/availableplugins/PipelineToolkit/templates/PersonRoleMappers/fields-nav.inc create mode 100644 app/availableplugins/PipelineToolkit/templates/PersonRoleMappers/fields.inc create mode 100644 app/availableplugins/PipelineToolkit/templates/PersonRoleMappings/columns.inc create mode 100644 app/availableplugins/PipelineToolkit/templates/PersonRoleMappings/fields.inc create mode 100644 app/availableplugins/PipelineToolkit/tests/bootstrap.php create mode 100644 app/availableplugins/PipelineToolkit/tests/schema.sql create mode 100644 app/availableplugins/PipelineToolkit/webroot/.gitkeep create mode 100644 app/src/Controller/FlangesController.php create mode 100644 app/src/Lib/Enum/ComparisonEnum.php create mode 100644 app/src/Lib/Enum/FlangeModeEnum.php create mode 100644 app/src/Model/Entity/Flange.php create mode 100644 app/src/Model/Table/FlangesTable.php create mode 100644 app/templates/Flanges/columns.inc create mode 100644 app/templates/Flanges/fields.inc create mode 100644 app/templates/Pipelines/fields-nav.inc diff --git a/NOTICE b/NOTICE index 38057cd82..962e0147b 100644 --- a/NOTICE +++ b/NOTICE @@ -1,6 +1,6 @@ COmanage Registry -Copyright (C) 2010-2020 +Copyright (C) 2010-2024 University Corporation for Advanced Internet Development, Inc. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/app/availableplugins/PipelineToolkit/README.md b/app/availableplugins/PipelineToolkit/README.md new file mode 100644 index 000000000..fcfac335b --- /dev/null +++ b/app/availableplugins/PipelineToolkit/README.md @@ -0,0 +1,11 @@ +# PipelineToolkit plugin for CakePHP + +## Installation + +You can install this plugin into your CakePHP application using [composer](https://getcomposer.org). + +The recommended way to install composer packages is: + +``` +composer require your-name-here/pipeline-toolkit +``` diff --git a/app/availableplugins/PipelineToolkit/composer.json b/app/availableplugins/PipelineToolkit/composer.json new file mode 100644 index 000000000..3d2ec201f --- /dev/null +++ b/app/availableplugins/PipelineToolkit/composer.json @@ -0,0 +1,24 @@ +{ + "name": "your-name-here/pipeline-toolkit", + "description": "PipelineToolkit plugin for CakePHP", + "type": "cakephp-plugin", + "license": "MIT", + "require": { + "php": ">=7.2", + "cakephp/cakephp": "4.5.*" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.3" + }, + "autoload": { + "psr-4": { + "PipelineToolkit\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "PipelineToolkit\\Test\\": "tests/", + "Cake\\Test\\": "vendor/cakephp/cakephp/tests/" + } + } +} diff --git a/app/availableplugins/PipelineToolkit/phpunit.xml.dist b/app/availableplugins/PipelineToolkit/phpunit.xml.dist new file mode 100644 index 000000000..514653539 --- /dev/null +++ b/app/availableplugins/PipelineToolkit/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + + + + + + + tests/TestCase/ + + + + + + + + + + + src/ + + + diff --git a/app/availableplugins/PipelineToolkit/resources/locales/en_US/pipeline_toolkit.po b/app/availableplugins/PipelineToolkit/resources/locales/en_US/pipeline_toolkit.po new file mode 100644 index 000000000..36de5007a --- /dev/null +++ b/app/availableplugins/PipelineToolkit/resources/locales/en_US/pipeline_toolkit.po @@ -0,0 +1,50 @@ +# COmanage Registry Localizations (pipeline_toolkit domain) +# +# 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.0.0 +# @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + +msgid "controller.PersonRoleMappers" +msgstr "{0,plural,=1{Person Role Mapper} other{Person Role Mappers}}" + +msgid "controller.PersonRoleMappings" +msgstr "{0,plural,=1{Person Role Mapping} other{Person Role Mappings}}" + +msgid "field.PersonRoleMappings.ad_hoc_tag" +msgstr "Ad Hoc Attribute Tag" + +msgid "field.PersonRoleMappings.affiliation_type_id" +msgstr "Affilation Type" + +msgid "field.PersonRoleMappings.comparison" +msgstr "Comparison" + +msgid "field.PersonRoleMappings.pattern" +msgstr "Pattern" + +msgid "field.PersonRoleMappings.pattern.desc" +msgstr "For regular expressions, including leading and trailing slashes (eg: /foo.*/)" + +msgid "field.PersonRoleMappings.target_cou_id" +msgstr "Target COU" + +msgid "field.PersonRoleMappings.target_affiliation_type_id" +msgstr "Target Affilation Type" diff --git a/app/availableplugins/PipelineToolkit/src/Controller/AppController.php b/app/availableplugins/PipelineToolkit/src/Controller/AppController.php new file mode 100644 index 000000000..ae09fba1f --- /dev/null +++ b/app/availableplugins/PipelineToolkit/src/Controller/AppController.php @@ -0,0 +1,10 @@ + [ + 'PersonRoleMappers.id' => 'asc' + ] + ]; +} diff --git a/app/availableplugins/PipelineToolkit/src/Controller/PersonRoleMappingsController.php b/app/availableplugins/PipelineToolkit/src/Controller/PersonRoleMappingsController.php new file mode 100644 index 000000000..00b3b16b3 --- /dev/null +++ b/app/availableplugins/PipelineToolkit/src/Controller/PersonRoleMappingsController.php @@ -0,0 +1,40 @@ + [ + 'PersonRoleMappings.id' => 'asc' + ] + ]; +} diff --git a/app/availableplugins/PipelineToolkit/src/Model/Entity/PersonRoleMapper.php b/app/availableplugins/PipelineToolkit/src/Model/Entity/PersonRoleMapper.php new file mode 100644 index 000000000..3629f6a4f --- /dev/null +++ b/app/availableplugins/PipelineToolkit/src/Model/Entity/PersonRoleMapper.php @@ -0,0 +1,49 @@ + + */ + protected $_accessible = [ + '*' => true, + 'id' => false, + 'slug' => false, + ]; +} diff --git a/app/availableplugins/PipelineToolkit/src/Model/Entity/PersonRoleMapping.php b/app/availableplugins/PipelineToolkit/src/Model/Entity/PersonRoleMapping.php new file mode 100644 index 000000000..c817649c3 --- /dev/null +++ b/app/availableplugins/PipelineToolkit/src/Model/Entity/PersonRoleMapping.php @@ -0,0 +1,112 @@ + + */ + protected $_accessible = [ + '*' => true, + 'id' => false, + 'slug' => false, + ]; + + /** + * Determine if the provided array of Person Role attributes matches the + * conditions of this Mapping. + * + * @since COmanage Registry v5.0.0 + * @param array $attributes Array of Person Role attributes, as currently assembled + * @param ExternalIdentityRole $eirdata External Identity Role data, from the backend + * @return bool true if the conditions match, false otherwise + */ + + public function matches(array $attributes, ExternalIdentityRole $eirdata): bool { + // Some conditions use special tests, most use ComparisonEnum. + // In general, we want to use the original EIR data for comparisons. + + if($this->attribute == 'AdHocAttribute.value') { + // We first need an AdHocAttribute that matches the configured tag. + // Note we only look at AdHocAttributes attached to the EIR, not the + // External Identity. + + if(!empty($this->ad_hoc_tag) && !empty($eirdata->ad_hoc_attributes)) { + // In the event there is more than one AdHocAttribute with the configured + // tag, we'll check each of them, and return true if any match. + foreach($eirdata->ad_hoc_attributes as $adhoc) { + if(!empty($adhoc->tag) && $this->ad_hoc_tag == $adhoc->tag) { + // Correct tag, now compare the value. We only return if compare() + // returns true, otherwise we keep iterating. + + if(ComparisonEnum::compare( + value: $adhoc->value, + comparison: $this->comparison, + pattern: $this->pattern + )) { + return true; + } + } + } + } + } elseif($this->attribute == 'ExternalIdentityRole.affiliation') { + // We should be given the affilation type id as mapped from the inbound EIR data, + // in which case we simply compare against the configured value + return (!empty($eirdata->affiliation_type_id) + && !empty($this->affiliation_type_id) + && ($eirdata->affiliation_type_id == $this->affiliation_type_id)); + } else { + // This is something like ExternalIdentityRole.title, etc + + $bits = explode('.', $this->attribute, 2); + $attr = $bits[1]; + + if(!empty($this->comparison) && !empty($this->pattern)) { + return ComparisonEnum::compare( + value: $eirdata->$attr, + comparison: $this->comparison, + pattern: $this->pattern + ); + } + } + + return false; + } +} diff --git a/app/availableplugins/PipelineToolkit/src/Model/Table/PersonRoleMappersTable.php b/app/availableplugins/PipelineToolkit/src/Model/Table/PersonRoleMappersTable.php new file mode 100644 index 000000000..9bdd1bf90 --- /dev/null +++ b/app/availableplugins/PipelineToolkit/src/Model/Table/PersonRoleMappersTable.php @@ -0,0 +1,158 @@ +addBehavior('Changelog'); + $this->addBehavior('Log'); + $this->addBehavior('Timestamp'); + + $this->setTableType(\App\Lib\Enum\TableTypeEnum::Configuration); + + // Define associations + $this->belongsTo('Flanges'); + + $this->hasMany('PipelineToolkit.PersonRoleMappings') + ->setDependent(true) + ->setCascadeCallbacks(true); + + $this->setDisplayField('attribute'); + + $this->setPrimaryLink(['flange_id']); + $this->setRequiresCO(true); + + $this->setPermissions([ + // Actions that operate over an entity (ie: require an $id) + 'entity' => [ + 'delete' => false, // Delete the pluggable object instead + 'edit' => ['platformAdmin', 'coAdmin'], + 'view' => ['platformAdmin', 'coAdmin'] + ], + // Actions that operate over a table (ie: do not require an $id) + 'table' => [ + 'add' => false, + 'index' => ['platformAdmin', 'coAdmin'] + ], + // Related models whose permissions we'll need, typically for table views + 'related' => [ + 'table' => [ + 'PipelineToolkit.PersonRoleMappings' + ] + ] + ]); + } + + /** + * Apply mappings for buildPersonRole. + * + * @since COmanage Registry v5.0.0 + * @param Flange $flange Flange, including top level plugin configuration + * @param array $newdata Array of new Person Role data + * @param ExternalIdentityRole $eirdata Original External Identity Role data + * @return array Updated array of new Person Role data + */ + + public function buildPersonRole( + \App\Model\Entity\Flange $flange, + array $newdata, + \App\Model\Entity\ExternalIdentityRole $eirdata + ): array { + $retdata = $newdata; + + // We need our Mapping configuration, which won't be in $flange + $mappings = $this->PersonRoleMappings->find() + ->where(['person_role_mapper_id' => $flange->person_role_mapper->id]) + ->order(['ordr' => 'ASC']) + ->all(); + + foreach($mappings as $mapping) { + if($mapping->matches($retdata, $eirdata)) { + // This Mapping matched, so update the return data + + if(!empty($mapping->target_affiliation_type_id)) { + $this->llog('trace', $flange->description . " mapping External Identity Role ID " + . $eirdata->id . " to Affiliation Type ID " + . $mapping->target_affiliation_type_id); + $retdata['affiliation_type_id'] = $mapping->target_affiliation_type_id; + } + + if(!empty($mapping->target_cou_id)) { + $this->llog('trace', $flange->description . " mapping External Identity Role ID " + . $eirdata->id . " to COU ID " + . $mapping->target_cou_id); + $retdata['cou_id'] = $mapping->target_cou_id; + } + } + } + + return $retdata; + } + + /** + * Set validation rules. + * + * @since COmanage Registry v5.0.0 + * @param Validator $validator Validator + * @return Validator Validator + * @throws InvalidArgumentException + * @throws RecordNotFoundException + */ + + public function validationDefault(Validator $validator): Validator { + $schema = $this->getSchema(); + + $validator->add('flange_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->notEmptyString('flange_id'); + + return $validator; + } +} \ No newline at end of file diff --git a/app/availableplugins/PipelineToolkit/src/Model/Table/PersonRoleMappingsTable.php b/app/availableplugins/PipelineToolkit/src/Model/Table/PersonRoleMappingsTable.php new file mode 100644 index 000000000..f2f98d16f --- /dev/null +++ b/app/availableplugins/PipelineToolkit/src/Model/Table/PersonRoleMappingsTable.php @@ -0,0 +1,194 @@ +addBehavior('Changelog'); + $this->addBehavior('Log'); + $this->addBehavior('Orderable'); + $this->addBehavior('Timestamp'); + + $this->setTableType(\App\Lib\Enum\TableTypeEnum::Configuration); + + // Define associations + $this->belongsTo('PipelineToolkit.PersonRoleMappers'); + + $this->belongsTo('AffiliationTypes') + ->setClassName('Types') + ->setForeignKey('affiliation_type_id') + ->setProperty('affiliation_type'); + $this->belongsTo('TargetCous') + ->setClassName('Cous') + ->setForeignKey('target_cou_id') + ->setProperty('target_cou'); + $this->belongsTo('TargetAffiliationTypes') + ->setClassName('Types') + ->setForeignKey('target_affiliation_type_id') + ->setProperty('target_affiliation_type'); + + $this->setDisplayField('description'); + + $this->setPrimaryLink(['PipelineToolkit.person_role_mapper_id']); + $this->setRequiresCO(true); + $this->setRedirectGoal('index'); + + $this->setAutoViewVars([ + 'affiliationTypes' => [ + 'type' => 'type', + 'attribute' => 'PersonRoles.affiliation_type' + ], + 'attributes' => [ + 'type' => 'array', + 'array' => $this->getMappableAttributes() + ], + 'comparisons' => [ + 'type' => 'enum', + 'class' => 'ComparisonEnum' + ], + 'targetAffiliationTypes' => [ + 'type' => 'type', + 'attribute' => 'PersonRoles.affiliation_type' + ], + 'targetCous' => [ + 'type' => 'select', + 'model' => 'Cous' + ] + ]); + + $this->setPermissions([ + // Actions that operate over an entity (ie: require an $id) + 'entity' => [ + 'delete' => ['platformAdmin', 'coAdmin'], + 'edit' => ['platformAdmin', 'coAdmin'], + 'view' => ['platformAdmin', 'coAdmin'] + ], + // Actions that operate over a table (ie: do not require an $id) + 'table' => [ + 'add' => ['platformAdmin', 'coAdmin'], + 'index' => ['platformAdmin', 'coAdmin'] + ] + ]); + } + + /** + * Obtain the set of mappable attributes, in Model.attribute format. + * + * @since COmanage Registry v5.0.0 + * @return array Mappable attributes + */ + + public function getMappableAttributes(): array { + return [ + 'AdHocAttribute.value', + 'ExternalIdentityRole.affiliation', + 'ExternalIdentityRole.department', + 'ExternalIdentityRole.organization', + 'ExternalIdentityRole.title', + ]; + } + + /** + * Set validation rules. + * + * @since COmanage Registry v5.0.0 + * @param Validator $validator Validator + * @return Validator Validator + * @throws InvalidArgumentException + * @throws RecordNotFoundException + */ + + public function validationDefault(Validator $validator): Validator { + $schema = $this->getSchema(); + + $validator->add('person_role_mapper_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->notEmptyString('person_role_mapper_id'); + + $validator->add('attribute', [ + 'content' => ['rule' => ['inList', $this->getMappableAttributes()]] + ]); + $validator->notEmptyString('attribute'); + + $validator->add('affiliation_type_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->notEmptyString('affiliation_type_id'); + + // The required fields here aren't ideal, since they vary by attribute + $this->registerStringValidation($validator, $schema, 'ad_hoc_tag', false); + + $validator->add('comparison', [ + 'content' => ['rule' => ['inList', ComparisonEnum::getConstValues()]] + ]); + $validator->allowEmptyString('comparison'); + + $this->registerStringValidation($validator, $schema, 'pattern', false); + + $validator->add('ordr', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->allowEmptyString('ordr'); + + $validator->add('target_cou_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->notEmptyString('target_cou_id'); + + $validator->add('target_affiliation_type_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->notEmptyString('target_affiliation_type_id'); + + return $validator; + } +} \ No newline at end of file diff --git a/app/availableplugins/PipelineToolkit/src/PipelineToolkitPlugin.php b/app/availableplugins/PipelineToolkit/src/PipelineToolkitPlugin.php new file mode 100644 index 000000000..737262b79 --- /dev/null +++ b/app/availableplugins/PipelineToolkit/src/PipelineToolkitPlugin.php @@ -0,0 +1,93 @@ +plugin( + 'PipelineToolkit', + ['path' => '/pipeline-toolkit'], + function (RouteBuilder $builder) { + // Add custom routes here + + $builder->fallbacks(); + } + ); + parent::routes($routes); + } + + /** + * Add middleware for the plugin. + * + * @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to update. + * @return \Cake\Http\MiddlewareQueue + */ + public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue + { + // Add your middlewares here + + return $middlewareQueue; + } + + /** + * Add commands for the plugin. + * + * @param \Cake\Console\CommandCollection $commands The command collection to update. + * @return \Cake\Console\CommandCollection + */ + public function console(CommandCollection $commands): CommandCollection + { + // Add your commands here + + $commands = parent::console($commands); + + return $commands; + } + + /** + * Register application container services. + * + * @param \Cake\Core\ContainerInterface $container The Container to update. + * @return void + * @link https://book.cakephp.org/4/en/development/dependency-injection.html#dependency-injection + */ + public function services(ContainerInterface $container): void + { + // Add your services here + } +} diff --git a/app/availableplugins/PipelineToolkit/src/config/plugin.json b/app/availableplugins/PipelineToolkit/src/config/plugin.json new file mode 100644 index 000000000..5ef8ae600 --- /dev/null +++ b/app/availableplugins/PipelineToolkit/src/config/plugin.json @@ -0,0 +1,40 @@ +{ + "types": { + "pipeline": [ + "PersonRoleMappers" + ] + }, + "schema": { + "tables": { + "person_role_mappers": { + "columns": { + "id": {}, + "flange_id": { "type": "integer", "foreignkey": { "table": "flanges", "column": "id" } } + }, + "indexes": { + "person_role_mappers_i1": { "columns": [ "flange_id" ] } + } + }, + "person_role_mappings": { + "columns": { + "id": {}, + "person_role_mapper_id": { "type": "integer", "foreignkey": { "table": "person_role_mappers", "column": "id" } }, + "attribute": { "type": "string", "size": 80 }, + "ad_hoc_tag": { "type": "string", "size": 128 }, + "affiliation_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } }, + "comparison": { "type": "string", "size": 4 }, + "pattern": { "type": "string", "size": 80 }, + "target_cou_id": { "type": "integer", "foreignkey": { "table": "cous", "column": "id" } }, + "target_affiliation_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } }, + "ordr": {} + }, + "indexes": { + "person_role_mappings_i1": { "columns": [ "person_role_mapper_id" ] }, + "person_role_mappings_i2": { "needed": false, "columns": [ "affiliation_type_id" ] }, + "person_role_mappings_i3": { "needed": false, "columns": [ "target_cou_id" ] }, + "person_role_mappings_i4": { "needed": false, "columns": [ "target_affiliation_type_id" ] } + } + } + } + } +} \ No newline at end of file diff --git a/app/availableplugins/PipelineToolkit/templates/PersonRoleMappers/fields-links.inc b/app/availableplugins/PipelineToolkit/templates/PersonRoleMappers/fields-links.inc new file mode 100644 index 000000000..d8c2ada5e --- /dev/null +++ b/app/availableplugins/PipelineToolkit/templates/PersonRoleMappers/fields-links.inc @@ -0,0 +1,39 @@ + 'transform', + 'order' => 'Default', + 'label' => __d('pipeline_toolkit', 'controller.PersonRoleMappings', [99]), + 'link' => [ + 'plugin' => 'PipelineToolkit', + 'controller' => 'PersonRoleMappings', + 'action' => 'index', + 'person_role_mapper_id' => $vv_obj->id + ], + 'class' => '' +]; diff --git a/app/availableplugins/PipelineToolkit/templates/PersonRoleMappers/fields-nav.inc b/app/availableplugins/PipelineToolkit/templates/PersonRoleMappers/fields-nav.inc new file mode 100644 index 000000000..fc4530862 --- /dev/null +++ b/app/availableplugins/PipelineToolkit/templates/PersonRoleMappers/fields-nav.inc @@ -0,0 +1,31 @@ + 'plugin', + 'active' => 'plugin' + ]; diff --git a/app/availableplugins/PipelineToolkit/templates/PersonRoleMappers/fields.inc b/app/availableplugins/PipelineToolkit/templates/PersonRoleMappers/fields.inc new file mode 100644 index 000000000..c50caa436 --- /dev/null +++ b/app/availableplugins/PipelineToolkit/templates/PersonRoleMappers/fields.inc @@ -0,0 +1,30 @@ + \ No newline at end of file diff --git a/app/availableplugins/PipelineToolkit/templates/PersonRoleMappings/columns.inc b/app/availableplugins/PipelineToolkit/templates/PersonRoleMappings/columns.inc new file mode 100644 index 000000000..8b442abea --- /dev/null +++ b/app/availableplugins/PipelineToolkit/templates/PersonRoleMappings/columns.inc @@ -0,0 +1,54 @@ + [ + 'type' => 'echo' + ], + 'attribute' => [ + 'type' => 'link' + ], + 'comparison' => [ + 'type' => 'enum', + 'class' => 'ComparisonEnum' + ], + 'pattern' => [ + 'type' => 'echo' + ], + 'affiliation_type_id' => [ + 'type' => 'fk', + 'label' => __d('field', 'affiliation'), + ] +]; + +/* +// When the $bulkActions variable exists in a columns.inc config, the "Bulk edit" switch will appear in the index. +$bulkActions = [ + // TODO: develop bulk actions. For now, use a placeholder. + 'delete' => true +]; +*/ diff --git a/app/availableplugins/PipelineToolkit/templates/PersonRoleMappings/fields.inc b/app/availableplugins/PipelineToolkit/templates/PersonRoleMappings/fields.inc new file mode 100644 index 000000000..ddd7dd9b0 --- /dev/null +++ b/app/availableplugins/PipelineToolkit/templates/PersonRoleMappings/fields.inc @@ -0,0 +1,77 @@ + + +element('form/listItem', [ + 'arguments' => [ + 'fieldName' => 'attribute', + 'fieldOptions' => [ + 'onChange' => 'updateGadgets()' + ], + 'fieldType' => 'select' + ] + ]); + + foreach (['ad_hoc_tag', + 'affiliation_type_id', + 'comparison', + 'pattern', + 'target_cou_id', + 'target_affiliation_type_id', + 'ordr' + ] as $field) { + print $this->element('form/listItem', [ + 'arguments' => [ + 'fieldName' => $field + ]]); + } +} diff --git a/app/availableplugins/PipelineToolkit/tests/bootstrap.php b/app/availableplugins/PipelineToolkit/tests/bootstrap.php new file mode 100644 index 000000000..737e7a8b4 --- /dev/null +++ b/app/availableplugins/PipelineToolkit/tests/bootstrap.php @@ -0,0 +1,55 @@ +loadSqlFiles('tests/schema.sql', 'test'); diff --git a/app/availableplugins/PipelineToolkit/tests/schema.sql b/app/availableplugins/PipelineToolkit/tests/schema.sql new file mode 100644 index 000000000..864af874d --- /dev/null +++ b/app/availableplugins/PipelineToolkit/tests/schema.sql @@ -0,0 +1 @@ +-- Test database schema for PipelineToolkit diff --git a/app/availableplugins/PipelineToolkit/webroot/.gitkeep b/app/availableplugins/PipelineToolkit/webroot/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/app/composer.json b/app/composer.json index 1122a447f..e50a631fd 100644 --- a/app/composer.json +++ b/app/composer.json @@ -34,6 +34,7 @@ "CoreAssigner\\": "plugins/CoreAssigner/src/", "CoreServer\\": "plugins/CoreServer/src/", "FileConnector\\": "availableplugins/FileConnector/src/", + "PipelineToolkit\\": "availableplugins/PipelineToolkit/src/", "SqlConnector\\": "availableplugins/SqlConnector/src/", "CoreJob\\": "plugins/CoreJob/src/" } @@ -46,6 +47,7 @@ "CoreAssigner\\Test\\": "plugins/CoreAssigner/tests/", "CoreServer\\Test\\": "plugins/CoreServer/tests/", "FileConnector\\Test\\": "availableplugins/FileConnector/tests/", + "PipelineToolkit\\Test\\": "availableplugins/PipelineToolkit/tests/", "SqlConnector\\Test\\": "availableplugins/SqlConnector/tests/", "CoreJob\\Test\\": "plugins/CoreJob/tests/" } diff --git a/app/config/schema/schema.json b/app/config/schema/schema.json index 54d0239a2..190826b33 100644 --- a/app/config/schema/schema.json +++ b/app/config/schema/schema.json @@ -693,6 +693,20 @@ } }, + "flanges": { + "columns": { + "id": {}, + "pipeline_id": { "type": "integer", "foreignkey": { "table": "pipelines", "column": "id" } }, + "description": {}, + "plugin": {}, + "status": {}, + "ordr": {} + }, + "indexes": { + "flanges_i1": { "columns": [ "pipeline_id" ] } + } + }, + "external_identity_sources": { "columns": { "id": {}, diff --git a/app/resources/locales/en_US/controller.po b/app/resources/locales/en_US/controller.po index 747cd44e0..714484a6e 100644 --- a/app/resources/locales/en_US/controller.po +++ b/app/resources/locales/en_US/controller.po @@ -63,6 +63,9 @@ msgstr "{0,plural,=1{External Identity Source} other{External Identity Sources}} msgid "ExtIdentitySourceRecords" msgstr "{0,plural,=1{External Identity Source Record} other{External Identity Source Records}}" +msgid "Flanges" +msgstr "{0,plural,=1{Flange} other{Flanges}}" + msgid "GroupMembers" msgstr "{0,plural,=1{Group Member} other{Group Members}}" diff --git a/app/resources/locales/en_US/enumeration.po b/app/resources/locales/en_US/enumeration.po index fb8519795..2d04220f4 100644 --- a/app/resources/locales/en_US/enumeration.po +++ b/app/resources/locales/en_US/enumeration.po @@ -36,6 +36,33 @@ msgstr "False" msgid "BooleanEnum.1" msgstr "True" +msgid "ComparisonEnum.CTS" +msgstr "Contains" + +msgid "ComparisonEnum.CTI" +msgstr "Contains (case insensitive)" + +msgid "ComparisonEnum.EQS" +msgstr "Is Exactly" + +msgid "ComparisonEnum.EQI" +msgstr "Is Exactly (case insensitive)" + +msgid "ComparisonEnum.NCT" +msgstr "Does Not Contain" + +msgid "ComparisonEnum.NCTI" +msgstr "Does Not Contain (case insensitive)" + +msgid "ComparisonEnum.NEQ" +msgstr "Is Not Exactly" + +msgid "ComparisonEnum.NEQI" +msgstr "Is Not Exactly (case insensitive)" + +msgid "ComparisonEnum.REGX" +msgstr "Matches Regular Expression" + msgid "DeletedRoleStatusEnum.D" msgstr "Archived" @@ -93,6 +120,15 @@ msgstr "Suspended" msgid "ExternalIdentityStatusEnum.DX" msgstr "Deleted" +msgid "FlangeModeEnum.EI" +msgstr "Retrieve External Identity" + +msgid "FlangeModeEnum.PR" +msgstr "Build Person Role" + +msgid "FlangeModeEnum.X" +msgstr "Disabled" + msgid "GroupTypeEnum.MA" msgstr "Active Members" diff --git a/app/resources/locales/en_US/error.po b/app/resources/locales/en_US/error.po index fbb338b83..fe8b193b9 100644 --- a/app/resources/locales/en_US/error.po +++ b/app/resources/locales/en_US/error.po @@ -271,6 +271,9 @@ msgstr "Permission Denied" msgid "PersonRoles.valid_from.after" msgstr "Valid From date must be earlier than Valid Through date" +msgid "Pipelines.plugin.notimpl" +msgstr "Pipeline plugin does not implement {0}" + msgid "Plugins.inactive" msgstr "The plugin {0} is not active" diff --git a/app/src/Controller/FlangesController.php b/app/src/Controller/FlangesController.php new file mode 100644 index 000000000..6dcffd261 --- /dev/null +++ b/app/src/Controller/FlangesController.php @@ -0,0 +1,41 @@ + [ + 'Flanges.description' => 'asc' + ] + ]; +} \ No newline at end of file diff --git a/app/src/Controller/StandardPluginController.php b/app/src/Controller/StandardPluginController.php index b15b29a9a..3b132fd14 100644 --- a/app/src/Controller/StandardPluginController.php +++ b/app/src/Controller/StandardPluginController.php @@ -31,6 +31,7 @@ // XXX not doing anything with Log yet use Cake\Log\Log; +use Cake\ORM\TableRegistry; use \App\Lib\Util\StringUtilities; use \App\Lib\Enum\SuspendableStatusEnum; @@ -86,11 +87,12 @@ public function beforeRender(\Cake\Event\EventInterface $event) { $link = $this->getPrimaryLink(true); - if(!empty($link->value)) { - $parentClassName = StringUtilities::foreignKeyToClassName($link->attr); - - $parentObj = $table->$parentClassName->get($link->value); - $parentDisplayField = $table->$parentClassName->getDisplayField(); + if(!empty($link->value) && !empty($link->model_name)) { + // This might be a plugin table in Plugin.Model notation + $parentTable = TableRegistry::getTableLocator()->get($link->model_name); + + $parentObj = $parentTable->get($link->value); + $parentDisplayField = $parentTable->getDisplayField(); $this->set('vv_bc_parent_obj', $parentObj); $this->set('vv_bc_parent_displayfield', $parentDisplayField); @@ -98,7 +100,7 @@ public function beforeRender(\Cake\Event\EventInterface $event) { // Override the title set in StandardController. Since that was set in edit() // which is called before the rendering hooks, this title will take precedence. - [$title, , ] = StringUtilities::entityAndActionToTitle($parentObj, $parentClassName, 'configure'); + [$title, , ] = StringUtilities::entityAndActionToTitle($parentObj, $link->model_name, 'configure'); $this->set('vv_title', $title); } diff --git a/app/src/Lib/Enum/ComparisonEnum.php b/app/src/Lib/Enum/ComparisonEnum.php new file mode 100644 index 000000000..a6b1324e4 --- /dev/null +++ b/app/src/Lib/Enum/ComparisonEnum.php @@ -0,0 +1,89 @@ + true, + 'id' => false, + 'slug' => false, + ]; +} \ No newline at end of file diff --git a/app/src/Model/Table/FlangesTable.php b/app/src/Model/Table/FlangesTable.php new file mode 100644 index 000000000..3bda87ded --- /dev/null +++ b/app/src/Model/Table/FlangesTable.php @@ -0,0 +1,134 @@ +addBehavior('Changelog'); + $this->addBehavior('Log'); + $this->addBehavior('Orderable'); + $this->addBehavior('Timestamp'); + + $this->setTableType(\App\Lib\Enum\TableTypeEnum::Configuration); + + // Define associations + $this->belongsTo('Pipelines'); + + $this->setPluginRelations(); + + $this->setDisplayField('description'); + + $this->setPrimaryLink('pipeline_id'); + $this->setRequiresCO(true); + + $this->setAutoViewVars([ + 'plugins' => [ + 'type' => 'plugin', + 'pluginType' => 'pipeline' + ], + 'statuses' => [ + 'type' => 'enum', + 'class' => 'FlangeModeEnum' + ] + ]); + + $this->setPermissions([ + // Actions that operate over an entity (ie: require an $id) + 'entity' => [ + 'configure' => ['platformAdmin', 'coAdmin'], + 'delete' => ['platformAdmin', 'coAdmin'], + 'edit' => ['platformAdmin', 'coAdmin'], + 'view' => ['platformAdmin', 'coAdmin'] + ], + // Actions that operate over a table (ie: do not require an $id) + 'table' => [ + 'add' => ['platformAdmin', 'coAdmin'], + 'index' => ['platformAdmin', 'coAdmin'] + ] + ]); + } + + /** + * Set validation rules. + * + * @since COmanage Registry v5.0.0 + * @param Validator $validator Validator + * @return Validator Validator + */ + + public function validationDefault(Validator $validator): Validator { + $schema = $this->getSchema(); + + $validator->add('pipeline_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->notEmptyString('pipeline_id'); + + $this->registerStringValidation($validator, $schema, 'description', false); + + $validator->add('status', [ + 'content' => ['rule' => ['inList', FlangeModeEnum::getConstValues()]] + ]); + $validator->notEmptyString('status'); + + $this->registerStringValidation($validator, $schema, 'plugin', true); + + $validator->add('ordr', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->allowEmptyString('ordr'); + + return $validator; + } +} \ No newline at end of file diff --git a/app/src/Model/Table/PipelinesTable.php b/app/src/Model/Table/PipelinesTable.php index 94213d9e4..9fb094cc8 100644 --- a/app/src/Model/Table/PipelinesTable.php +++ b/app/src/Model/Table/PipelinesTable.php @@ -44,6 +44,7 @@ use \App\Lib\Enum\ActionEnum; use \App\Lib\Enum\DeletedRoleStatusEnum; use \App\Lib\Enum\ExternalIdentityStatusEnum; +use \App\Lib\Enum\FlangeModeEnum; use \App\Lib\Enum\MatchStrategyEnum; use \App\Lib\Enum\ProvisioningContextEnum; use \App\Lib\Enum\StatusEnum; @@ -108,6 +109,7 @@ public function initialize(array $config): void { ->setProperty('sync_identifier_type'); $this->hasMany('ExternalIdentitySources'); + $this->hasMany('Flanges'); $this->setDisplayField('description'); @@ -166,6 +168,12 @@ public function initialize(array $config): void { 'table' => [ 'add' => ['platformAdmin', 'coAdmin'], 'index' => ['platformAdmin', 'coAdmin'] + ], + // Related models whose permissions we'll need, typically for table views + 'related' => [ + 'table' => [ + 'Flanges' + ] ] ]); } @@ -455,6 +463,15 @@ public function execute( $pipeline = $this->get($id); $eis = $this->ExternalIdentitySources->get($eisId); + // We'll pull Flanges separately to make it a bit clearer what we're doing, + // but we'll stuff it into the $pipeline configuration to make it easier to + // pass around. + $pipeline->flanges = $this->Flanges->find() + ->where(['pipeline_id' => $id]) + ->order(['Flanges.ordr' => 'ASC']) + ->contain($this->Flanges->getPluginRelations()) + ->all(); + // Start a Transaction $cxn = $this->getConnection(); $cxn->begin(); @@ -936,8 +953,11 @@ protected function obtainPerson( * Search for an existing Person using an attribute provided in the EIS Record. * * @since COmanage Registry v5.0.0 - * @param ExternalIdentitySource $eis External Identity Source - * XXX params/return + * @param ExternalIdentitySource $eis External Identity Source + * @param ExtIdentitySourceRecord $eisRecord External Identity Source Record + * @param string $matchStrategy MatchStrategyEnum + * @param int $attributeTypeId Attribute Type to search on + * @param array $attributes Attributes to use for searching * @return Person Person if found, null otherwise * @throws InvalidArgumentException */ @@ -1675,6 +1695,27 @@ protected function syncPerson( $newdata['status'] = StatusEnum::Active; } + if(!empty($pipeline->flanges)) { + // These should already be order by ordr. Note that ordr operates a bit + // unexpectedly here... the later flanges to get called can override the + // values returned by earlier flanges, since we always call all active + // flanges, so the later flanges take precedence over the earlier ones. + foreach($pipeline->flanges as $flange) { + if($flange->status == FlangeModeEnum::BuildPersonRole) { + $this->llog('trace', 'Running Flange ' . $flange->description . ' for BuildPersonRole'); + + $PluginTable = TableRegistry::getTableLocator()->get($flange->plugin); + + if(!method_exists($PluginTable, 'buildPersonRole')) { + throw new \RuntimeException(__d('Pipelines.plugin.notimpl', ['buildPersonRole'])); + } + + // The plugin should modify $newdata as needed, then return it + $newdata = $PluginTable->buildPersonRole($flange, $newdata, $eirentity); + } + } + } + // Do we have a corresponding record on the Person? $found = $curentities->firstMatch([$sourcefk => $eirentity->id]); diff --git a/app/src/View/Helper/FieldHelper.php b/app/src/View/Helper/FieldHelper.php index 544fbb7cd..e4095ca17 100644 --- a/app/src/View/Helper/FieldHelper.php +++ b/app/src/View/Helper/FieldHelper.php @@ -322,8 +322,14 @@ public function formField(string $fieldName, // Selects, Checkboxes, and Radio Buttons use "disabled" $fieldArgs['disabled'] = $fieldArgs['readonly']; - $fieldArgs['required'] = $this->isReqField($fieldName) - || (isset($fieldOptions['required']) && $fieldOptions['required']); + + // required can be overridden by the fields.inc, but start with the default expectation + $fieldArgs['required'] = $this->isReqField($fieldName); + + if(isset($fieldOptions['required'])) { + // This could be either true or false + $fieldArgs['required'] = $fieldOptions['required']; + } // Cause any select (except status) to render with a blank option, even // if the field is required. This makes it clear when a value needs to be set. diff --git a/app/templates/Flanges/columns.inc b/app/templates/Flanges/columns.inc new file mode 100644 index 000000000..e5b405913 --- /dev/null +++ b/app/templates/Flanges/columns.inc @@ -0,0 +1,63 @@ + [ + 'type' => 'link', + 'sortable' => true + ], + 'plugin' => [ + 'type' => 'echo', + 'sortable' => true + ], + 'ordr' => [ + 'type' => 'echo', + 'sortable' => true + ], + 'status' => [ + 'type' => 'enum', + 'class' => 'FlangeModeEnum', + 'sortable' => true + ] +]; + +// $rowActions appear as row-level menu items in the index view gear icon +$rowActions = [ + [ + 'action' => 'configure', + 'label' => __d('operation', 'configure.plugin'), + 'icon' => 'electrical_services' + ] +]; + +/* +// When the $bulkActions variable exists in a columns.inc config, the "Bulk edit" switch will appear in the index. +$bulkActions = [ + // TODO: develop bulk actions. For now, use a placeholder. + 'delete' => true +]; +*/ \ No newline at end of file diff --git a/app/templates/Flanges/fields.inc b/app/templates/Flanges/fields.inc new file mode 100644 index 000000000..ab041494e --- /dev/null +++ b/app/templates/Flanges/fields.inc @@ -0,0 +1,41 @@ + +element('form/listItem', [ + 'arguments' => [ + 'fieldName' => $field + ]]); + } +} diff --git a/app/templates/Pipelines/fields-nav.inc b/app/templates/Pipelines/fields-nav.inc new file mode 100644 index 000000000..c71fcd7d9 --- /dev/null +++ b/app/templates/Pipelines/fields-nav.inc @@ -0,0 +1,38 @@ + 'fork_left', // XXX CFM-404 this should be "valve" once we switch to Material Symbols + 'order' => 'Default', + 'label' => __d('controller', 'Flanges', [99]), + 'link' => [ + 'controller' => 'flanges', + 'action' => 'index', + 'pipeline_id' => $vv_obj->id + ], + 'class' => '' +]; diff --git a/app/templates/element/form/unorderedList.php b/app/templates/element/form/unorderedList.php index 4d3f8a9aa..9012de827 100644 --- a/app/templates/element/form/unorderedList.php +++ b/app/templates/element/form/unorderedList.php @@ -49,8 +49,12 @@ include($vv_template_path . DS . $fieldsFile); // Element ID print $this->element('form/entityID'); - // The Submit element will be printed only if we are adding or updating - print $this->element('form/submit', ['label' => __d('operation', 'save')]); + + if(!isset($suppress_submit) || !$suppress_submit) { + // The Submit element will be printed only if we are adding or updating, and if not + // suppressed by the field configuration + print $this->element('form/submit', ['label' => __d('operation', 'save')]); + } ?> diff --git a/app/vendor/composer/autoload_psr4.php b/app/vendor/composer/autoload_psr4.php index 461bc9bb0..6b5ff9040 100644 --- a/app/vendor/composer/autoload_psr4.php +++ b/app/vendor/composer/autoload_psr4.php @@ -40,6 +40,8 @@ 'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'), 'Psr\\Container\\' => array($vendorDir . '/psr/container/src'), 'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'), + 'PipelineToolkit\\Test\\' => array($baseDir . '/availableplugins/PipelineToolkit/tests'), + 'PipelineToolkit\\' => array($baseDir . '/availableplugins/PipelineToolkit/src'), 'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'), 'Phinx\\' => array($vendorDir . '/robmorgan/phinx/src/Phinx'), 'PHPStan\\PhpDocParser\\' => array($vendorDir . '/phpstan/phpdoc-parser/src'), diff --git a/app/vendor/composer/autoload_static.php b/app/vendor/composer/autoload_static.php index ea99e3f5f..5d92df701 100644 --- a/app/vendor/composer/autoload_static.php +++ b/app/vendor/composer/autoload_static.php @@ -92,6 +92,8 @@ class ComposerStaticInitb25f76eec921984aa94dcf4015a4846e 'Psr\\Http\\Client\\' => 16, 'Psr\\Container\\' => 14, 'Psr\\Cache\\' => 10, + 'PipelineToolkit\\Test\\' => 21, + 'PipelineToolkit\\' => 16, 'PhpParser\\' => 10, 'Phinx\\' => 6, 'PHPStan\\PhpDocParser\\' => 21, @@ -310,6 +312,14 @@ class ComposerStaticInitb25f76eec921984aa94dcf4015a4846e array ( 0 => __DIR__ . '/..' . '/psr/cache/src', ), + 'PipelineToolkit\\Test\\' => + array ( + 0 => __DIR__ . '/../..' . '/availableplugins/PipelineToolkit/tests', + ), + 'PipelineToolkit\\' => + array ( + 0 => __DIR__ . '/../..' . '/availableplugins/PipelineToolkit/src', + ), 'PhpParser\\' => array ( 0 => __DIR__ . '/..' . '/nikic/php-parser/lib/PhpParser',