diff --git a/app/availableplugins/FileConnector/README.md b/app/availableplugins/FileConnector/README.md
new file mode 100644
index 000000000..8bb5edeb3
--- /dev/null
+++ b/app/availableplugins/FileConnector/README.md
@@ -0,0 +1,11 @@
+# FileProvisioner 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/file-provisioner
+```
diff --git a/app/availableplugins/FileConnector/composer.json b/app/availableplugins/FileConnector/composer.json
new file mode 100644
index 000000000..740d46354
--- /dev/null
+++ b/app/availableplugins/FileConnector/composer.json
@@ -0,0 +1,24 @@
+{
+ "name": "comanage-registry/file-provisioner",
+ "description": "FileConnector plugin for COmanage Registry",
+ "type": "cakephp-plugin",
+ "license": "MIT",
+ "require": {
+ "php": ">=7.2",
+ "cakephp/cakephp": "4.4.*"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.5 || ^9.3"
+ },
+ "autoload": {
+ "psr-4": {
+ "FileConnector\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "FileConnector\\Test\\": "tests/",
+ "Cake\\Test\\": "vendor/cakephp/cakephp/tests/"
+ }
+ }
+}
diff --git a/app/availableplugins/FileConnector/phpunit.xml.dist b/app/availableplugins/FileConnector/phpunit.xml.dist
new file mode 100644
index 000000000..9fb2429d3
--- /dev/null
+++ b/app/availableplugins/FileConnector/phpunit.xml.dist
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+ tests/TestCase/
+
+
+
+
+
+
+
+
+
+
+ src/
+
+
+
diff --git a/app/availableplugins/FileConnector/resources/locales/en_US/file_connector.po b/app/availableplugins/FileConnector/resources/locales/en_US/file_connector.po
new file mode 100644
index 000000000..970794dd0
--- /dev/null
+++ b/app/availableplugins/FileConnector/resources/locales/en_US/file_connector.po
@@ -0,0 +1,35 @@
+# COmanage Registry Localizations (file_provisioner 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.FileProvisioners"
+msgstr "{0,plural,=1{File Provisioner} other{File Provisioners}}"
+
+msgid "error.filename.writeable"
+msgstr "The file \"{0}\" is not writable"
+
+msgid "field.FileProvisioners.filename"
+msgstr "File Name"
+
+msgid "field.FileProvisioners.filename.desc"
+msgstr "Full path to file to write to, which must exist and be writeable"
\ No newline at end of file
diff --git a/app/availableplugins/FileConnector/src/Controller/AppController.php b/app/availableplugins/FileConnector/src/Controller/AppController.php
new file mode 100644
index 000000000..d1cf9843f
--- /dev/null
+++ b/app/availableplugins/FileConnector/src/Controller/AppController.php
@@ -0,0 +1,10 @@
+ [
+ 'FileProvisioners.id' => 'asc'
+ ]
+ ];
+}
diff --git a/app/availableplugins/FileConnector/src/FileConnectorPlugin.php b/app/availableplugins/FileConnector/src/FileConnectorPlugin.php
new file mode 100644
index 000000000..c3e445196
--- /dev/null
+++ b/app/availableplugins/FileConnector/src/FileConnectorPlugin.php
@@ -0,0 +1,93 @@
+plugin(
+ 'FileConnector',
+ ['path' => '/file-connector'],
+ 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/FileConnector/src/Model/Entity/FileProvisioner.php b/app/availableplugins/FileConnector/src/Model/Entity/FileProvisioner.php
new file mode 100644
index 000000000..36fc9b3e3
--- /dev/null
+++ b/app/availableplugins/FileConnector/src/Model/Entity/FileProvisioner.php
@@ -0,0 +1,49 @@
+
+ */
+ protected $_accessible = [
+ '*' => true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
diff --git a/app/availableplugins/FileConnector/src/Model/Table/FileProvisionersTable.php b/app/availableplugins/FileConnector/src/Model/Table/FileProvisionersTable.php
new file mode 100644
index 000000000..e8b7c7fa7
--- /dev/null
+++ b/app/availableplugins/FileConnector/src/Model/Table/FileProvisionersTable.php
@@ -0,0 +1,188 @@
+addBehavior('Changelog');
+ $this->addBehavior('Log');
+ $this->addBehavior('Timestamp');
+
+ $this->setTableType(\App\Lib\Enum\TableTypeEnum::Configuration);
+
+ // Define associations
+ $this->belongsTo('ProvisioningTargets');
+
+ $this->setDisplayField('filename');
+
+ $this->setPrimaryLink(['provisioning_target_id']);
+ $this->setRequiresCO(true);
+
+ $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' => false, //['platformAdmin', 'coAdmin'],
+ 'index' => ['platformAdmin', 'coAdmin']
+ ]
+ ]);
+
+ $this->setProvisionableModels([
+ 'People',
+ 'Groups'
+ ]);
+ }
+
+ /**
+ * Define business rules.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param RulesChecker $rules RulesChecker object
+ * @return RulesChecker
+ */
+
+ public function buildRules(RulesChecker $rules): RulesChecker {
+ // The requested file must exist and be writeable.
+
+ $rules->add([$this, 'ruleIsFileWriteable'],
+ 'isFileWriteable',
+ ['errorField' => 'filename']);
+
+ return $rules;
+ }
+
+ /**
+ * Provision object data to the provisioning target.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param SqlProvisioner $provisioningTarget FileProvisioner configuration
+ * @param string $className Class name of primary object being provisioned
+ * @param object $data Provisioning data in Entity format (eg: \App\Model\Entity\Person)
+ * @param ProvisioningEligibilityEnum $eligibility Provisioning Eligibility Enum
+ * @return array Array of status, comment, and optional identifier
+ */
+
+ public function provision(
+ \FileProvisioner\Model\Entity\FileProvisioner $provisioningTarget,
+ string $entityName,
+ object $data,
+ string $eligibility
+ ): array {
+ // Default output is an empty record
+ $output = [ 'id' => $data->id ];
+
+ if($eligibility == ProvisioningEligibilityEnum::Eligible) {
+ $output = $data;
+ }
+
+ if(file_put_contents(
+ filename: $provisioningTarget->filename,
+ data: json_encode($output, JSON_INVALID_UTF8_SUBSTITUTE) . "\n",
+ flags: FILE_APPEND
+ ) === false) {
+ throw new \RuntimeException("Write to " . $provisioningTarget->filename . " failed");
+ }
+
+ return [
+ 'status' => ProvisioningStatusEnum::Provisioned,
+ 'comment' => "Wrote 1 record to file",
+ 'identifier' => null
+ ];
+ }
+
+ /**
+ * Application Rule to determine if the current entity is a writeable file.
+ *
+ * @param Entity $entity Entity to be validated
+ * @param array $options Application rule options
+ *
+ * @return string|bool true if the Rule check passes, false otherwise
+ * @since COmanage Registry v5.0.0
+ */
+
+ public function ruleIsFileWriteable($entity, array $options): string|bool {
+ if(!is_writable($entity->filename)) {
+ return __d('file_provisioner', 'error.filename.writeable', [$entity->filename]);
+ }
+
+ return true;
+ }
+
+ /**
+ * 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('provisioning_target_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->notEmptyString('provisioning_target_id');
+
+ $this->registerStringValidation($validator, $schema, 'filename', true);
+
+ return $validator;
+ }
+}
\ No newline at end of file
diff --git a/app/availableplugins/FileConnector/src/config/plugin.json b/app/availableplugins/FileConnector/src/config/plugin.json
new file mode 100644
index 000000000..29079f98d
--- /dev/null
+++ b/app/availableplugins/FileConnector/src/config/plugin.json
@@ -0,0 +1,21 @@
+{
+ "types": {
+ "provisioner": [
+ "FileProvisioners"
+ ]
+ },
+ "schema": {
+ "tables": {
+ "file_provisioners": {
+ "columns": {
+ "id": {},
+ "provisioning_target_id": {},
+ "filename": { "type": "string", "size": 128 }
+ },
+ "indexes": {
+ "file_provisioners_i1": { "columns": [ "provisioning_target_id" ]}
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/availableplugins/FileConnector/templates/FileProvisioners/fields.inc b/app/availableplugins/FileConnector/templates/FileProvisioners/fields.inc
new file mode 100644
index 000000000..4980a687a
--- /dev/null
+++ b/app/availableplugins/FileConnector/templates/FileProvisioners/fields.inc
@@ -0,0 +1,31 @@
+Field->control('filename');
+}
diff --git a/app/availableplugins/FileConnector/tests/bootstrap.php b/app/availableplugins/FileConnector/tests/bootstrap.php
new file mode 100644
index 000000000..040ecb580
--- /dev/null
+++ b/app/availableplugins/FileConnector/tests/bootstrap.php
@@ -0,0 +1,55 @@
+loadSqlFiles('tests/schema.sql', 'test');
diff --git a/app/availableplugins/FileConnector/tests/schema.sql b/app/availableplugins/FileConnector/tests/schema.sql
new file mode 100644
index 000000000..ab2df5081
--- /dev/null
+++ b/app/availableplugins/FileConnector/tests/schema.sql
@@ -0,0 +1 @@
+-- Test database schema for FileProvisioner
diff --git a/app/availableplugins/FileConnector/webroot/.gitkeep b/app/availableplugins/FileConnector/webroot/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/availableplugins/SqlConnector/README.md b/app/availableplugins/SqlConnector/README.md
new file mode 100644
index 000000000..06ab084d6
--- /dev/null
+++ b/app/availableplugins/SqlConnector/README.md
@@ -0,0 +1,11 @@
+# SqlConnector 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/sql-connector
+```
diff --git a/app/availableplugins/SqlConnector/composer.json b/app/availableplugins/SqlConnector/composer.json
new file mode 100644
index 000000000..25e5666fe
--- /dev/null
+++ b/app/availableplugins/SqlConnector/composer.json
@@ -0,0 +1,24 @@
+{
+ "name": "comanage-registry/sql-connector",
+ "description": "SqlConnector plugin for COmanage Registry",
+ "type": "cakephp-plugin",
+ "license": "MIT",
+ "require": {
+ "php": ">=7.2",
+ "cakephp/cakephp": "4.4.*"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.5 || ^9.3"
+ },
+ "autoload": {
+ "psr-4": {
+ "SqlConnector\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "SqlConnector\\Test\\": "tests/",
+ "Cake\\Test\\": "vendor/cakephp/cakephp/tests/"
+ }
+ }
+}
diff --git a/app/availableplugins/SqlConnector/phpunit.xml.dist b/app/availableplugins/SqlConnector/phpunit.xml.dist
new file mode 100644
index 000000000..ed4972e70
--- /dev/null
+++ b/app/availableplugins/SqlConnector/phpunit.xml.dist
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+ tests/TestCase/
+
+
+
+
+
+
+
+
+
+
+ src/
+
+
+
diff --git a/app/availableplugins/SqlConnector/resources/locales/en_US/sql_connector.po b/app/availableplugins/SqlConnector/resources/locales/en_US/sql_connector.po
new file mode 100644
index 000000000..ceb0662fa
--- /dev/null
+++ b/app/availableplugins/SqlConnector/resources/locales/en_US/sql_connector.po
@@ -0,0 +1,59 @@
+# COmanage Registry Localizations (sql_connector 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.SqlProvisioners"
+msgstr "{0,plural,=1{SQL Provisioner} other{SQL Provisioners}}"
+
+msgid "error.table_prefix"
+msgstr "Table Name Prefix must be alphanumeric and end with an underscore"
+
+msgid "field.SqlProvisioners.table_prefix"
+msgstr "Table Name Prefix"
+
+msgid "field.SqlProvisioners.table_prefix.desc"
+msgstr "Prefix used when constructing table names, must be alphanumeric and end with an underscore (_)"
+
+msgid "operation.reapply"
+msgstr "Reapply Target Database Schema"
+
+msgid "operation.resync"
+msgstr "Resync Reference Data"
+
+msgid "result.prov.added"
+msgstr "New record published"
+
+msgid "result.prov.deleted"
+msgstr "Record deleted"
+
+msgid "result.prov.ineligible"
+msgstr "Record is not eligible for provisioning"
+
+msgid "result.prov.updated"
+msgstr "Record updated"
+
+msgid "result.reapply.ok"
+msgstr "Schema Reapplied"
+
+msgid "result.resync.ok"
+msgstr "Reference Data Synced"
\ No newline at end of file
diff --git a/app/availableplugins/SqlConnector/src/Controller/AppController.php b/app/availableplugins/SqlConnector/src/Controller/AppController.php
new file mode 100644
index 000000000..42c5e1b94
--- /dev/null
+++ b/app/availableplugins/SqlConnector/src/Controller/AppController.php
@@ -0,0 +1,10 @@
+ [
+ 'SqlProvisioners.id' => 'asc'
+ ]
+ ];
+
+ /**
+ * Reapply the target database schema.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param string $id SqlProvisioner ID
+ */
+
+ public function reapply(string $id) {
+ try {
+ $this->SqlProvisioners->applySchema((int)$id);
+ $this->Flash->success(__d('sql_connector', 'result.reapply.ok'));
+ }
+ catch(\Exception $e) {
+ $this->Flash->error($e->getMessage());
+ }
+
+ return $this->generateRedirect((int)$id);
+ }
+
+ /**
+ * Reapply all Reference Data, including Groups.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param string $id SqlProvisioner ID
+ */
+
+ public function resync(string $id) {
+ try {
+ $cur_co = $this->getCO();
+
+ $this->SqlProvisioners->syncReferenceData(id: $id);
+
+ $this->Flash->success(__d('sql_connector', 'result.resync.ok'));
+ }
+ catch(\Exception $e) {
+ $this->Flash->error($e->getMessage());
+ }
+
+ return $this->generateRedirect((int)$id);
+ }
+}
diff --git a/app/availableplugins/SqlConnector/src/Model/Entity/SqlProvisioner.php b/app/availableplugins/SqlConnector/src/Model/Entity/SqlProvisioner.php
new file mode 100644
index 000000000..5d5ff2247
--- /dev/null
+++ b/app/availableplugins/SqlConnector/src/Model/Entity/SqlProvisioner.php
@@ -0,0 +1,49 @@
+
+ */
+ protected $_accessible = [
+ '*' => true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
diff --git a/app/availableplugins/SqlConnector/src/Model/Table/SqlProvisionersTable.php b/app/availableplugins/SqlConnector/src/Model/Table/SqlProvisionersTable.php
new file mode 100644
index 000000000..2cebb53af
--- /dev/null
+++ b/app/availableplugins/SqlConnector/src/Model/Table/SqlProvisionersTable.php
@@ -0,0 +1,754 @@
+ [
+ 'table' => 'people',
+ 'name' => 'SpPeople',
+ 'source' => 'People',
+ 'source_table' => 'people',
+ 'related' => [
+ 'AdHocAttributes',
+ 'Addresses',
+ 'EmailAddresses',
+ 'ExternalIdentities',
+ 'GroupMembers',
+ 'Identifiers',
+ 'Names',
+ 'PersonRoles',
+ 'Pronouns',
+ 'TelephoneNumbers',
+ 'Urls'
+ ]
+ ],
+ 'Groups' => [
+ 'table' => 'groups',
+ 'name' => 'SpGroups',
+ 'source' => 'Groups',
+ 'source_table' => 'groups',
+ 'related' => [
+ 'GroupMembers'
+ ]
+ ]
+ ];
+
+ // Secondary models that are provisioned along with one or more other models
+ protected $secondaryModels = [
+ 'AdHocAttributes' => [
+ 'table' => 'ad_hoc_attributes',
+ 'name' => 'SpAdHocAttributes',
+ 'source' => 'AdHocAttributes',
+ 'source_table' => 'ad_hoc_attributes',
+ 'related' => []
+ ],
+ 'Addresses' => [
+ 'table' => 'addresses',
+ 'name' => 'SpAddresses',
+ 'source' => 'Addresses',
+ 'source_table' => 'addresses',
+ 'related' => []
+ ],
+ 'EmailAddresses' => [
+ 'table' => 'email_addresses',
+ 'name' => 'SpEmailAddresses',
+ 'source' => 'EmailAddresses',
+ 'source_table' => 'email_addresses',
+ 'related' => []
+ ],
+ 'ExternalIdentities' => [
+ 'table' => 'external_identities',
+ 'name' => 'SpExternalIdentities',
+ 'source' => 'ExternalIdentities',
+ 'source_table' => 'external_identities',
+ 'related' => [
+ 'AdHocAttributes',
+ 'Addresses',
+ 'EmailAddresses',
+ 'ExternalIdentityRoles',
+ 'Identifiers',
+ 'Names',
+ 'Pronouns',
+ 'TelephoneNumbers',
+ 'Urls'
+ ]
+ ],
+ 'ExternalIdentityRoles' => [
+ 'table' => 'external_identity_roles',
+ 'name' => 'SpExternalIdentityRoles',
+ 'source' => 'ExternalIdentityRoles',
+ 'source_table' => 'external_identity_roles',
+ 'related' => [
+ 'AdHocAttributes',
+ 'Addresses',
+ 'TelephoneNumbers'
+ ]
+ ],
+ 'GroupMembers' => [
+ 'table' => 'group_members',
+ 'name' => 'SpGroupMembers',
+ 'source' => 'GroupMembers',
+ 'source_table' => 'group_members',
+ 'related' => []
+ ],
+// XXX Not implementing GroupOwners pending resolution of CO-2508
+ 'Identifiers' => [
+ 'table' => 'identifiers',
+ 'name' => 'SpIdentifiers',
+ 'source' => 'Identifiers',
+ 'source_table' => 'identifiers',
+ 'related' => []
+ ],
+ 'Names' => [
+ 'table' => 'names',
+ 'name' => 'SpNames',
+ 'source' => 'Names',
+ 'source_table' => 'names',
+ 'related' => []
+ ],
+ 'PersonRoles' => [
+ 'table' => 'person_roles',
+ 'name' => 'SpPersonRoles',
+ 'source' => 'PersonRoles',
+ 'source_table' => 'person_roles',
+ 'related' => [
+ 'AdHocAttributes',
+ 'Addresses',
+ 'TelephoneNumbers'
+ ]
+ ],
+ 'Pronouns' => [
+ 'table' => 'pronouns',
+ 'name' => 'SpPronouns',
+ 'source' => 'Pronouns',
+ 'source_table' => 'pronouns',
+ 'related' => []
+ ],
+ 'TelephoneNumbers' => [
+ 'table' => 'telephone_numbers',
+ 'name' => 'SpTelephoneNumbers',
+ 'source' => 'TelephoneNumbers',
+ 'source_table' => 'telephone_numbers',
+ 'related' => []
+ ],
+ 'Urls' => [
+ 'table' => 'urls',
+ 'name' => 'SpUrls',
+ 'source' => 'Urls',
+ 'source_table' => 'urls',
+ 'related' => []
+ ]
+ ];
+
+ // Models holding reference data
+ protected $referenceModels = [
+ 'Cous' => [
+ 'table' => 'cous',
+ 'name' => 'SpCous',
+ 'source' => 'Cous',
+ 'source_table' => 'cous',
+// XXX Note as of right now syncReferenceData doesn't look at 'related'
+// - if we need it to, that'll break Groups
+ 'related' => []
+ ],
+ 'Types' => [
+ 'table' => 'types',
+ 'name' => 'SpTypes',
+ 'source' => 'Types',
+ 'source_table' => 'types',
+ 'related' => []
+ ]
+/* XXX not yet implemented
+ [
+ 'table' => 'co_terms_and_conditions',
+ // Ordinarily we'd call this SpCoTermsAndConditions, but it's not worth
+ // fighting cake's inflector
+ 'name' => 'SpCoTermsAndCondition',
+ 'source' => 'CoTermsAndConditions',
+ 'source_table' => 'co_terms_and_conditions'
+ ],
+ [
+ 'table' => 'org_identity_sources',
+ 'name' => 'SpOrgIdentitySource',
+ 'source' => 'OrgIdentitySource',
+ 'source_table' => 'org_identity_sources'
+ ]*/
+ ];
+
+ /**
+ * Perform Cake Model initialization.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param array $config Configuration options passed to constructor
+ */
+
+ public function initialize(array $config): void {
+ // Timestamp behavior handles created/modified updates
+ $this->addBehavior('Changelog');
+ $this->addBehavior('Log');
+ $this->addBehavior('Timestamp');
+
+ $this->setTableType(\App\Lib\Enum\TableTypeEnum::Configuration);
+
+ // Define associations
+ $this->belongsTo('ProvisioningTargets');
+ $this->belongsTo('Servers');
+
+ $this->setDisplayField('server_id');
+
+ $this->setPrimaryLink(['provisioning_target_id']);
+ $this->setRequiresCO(true);
+ $this->setAllowLookupPrimaryLink(['reapply', 'resync']);
+
+ $this->setAutoViewVars([
+ 'servers' => [
+ 'type' => 'select',
+ 'model' => 'Servers',
+ 'where' => ['plugin' => 'CoreServer.SqlServers']
+ ]
+ ]);
+
+ $this->setPermissions([
+ // Actions that operate over an entity (ie: require an $id)
+ 'entity' => [
+ 'delete' => ['platformAdmin', 'coAdmin'],
+ 'edit' => ['platformAdmin', 'coAdmin'],
+ 'reapply' => ['platformAdmin', 'coAdmin'],
+ 'resync' => ['platformAdmin', 'coAdmin'],
+ 'view' => ['platformAdmin', 'coAdmin']
+ ],
+ // Actions that operate over a table (ie: do not require an $id)
+ 'table' => [
+ 'add' => false, //['platformAdmin', 'coAdmin'],
+ 'index' => ['platformAdmin', 'coAdmin']
+ ]
+ ]);
+
+ $this->setProvisionableModels(
+ array_merge(
+ array_keys($this->referenceModels),
+ array_keys($this->primaryModels)
+ )
+ );
+ }
+
+ /**
+ * Apply the Target Database Schema.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param integer $id SQL Provisioner ID
+ * @throws InvalidArgumentException
+ * @throws RuntimeException
+ */
+
+ public function applySchema($id) {
+ // In order to apply the schema, we need to find the underlying
+ // SqlConnector configuration. There should only be (at most) one...
+
+ $Plugins = TableRegistry::getTableLocator()->get('Plugins');
+
+ $targetSchema = $Plugins->getPluginConfig(plugin: "SqlConnector", key: "target-schema");
+
+ if(empty($targetSchema)) {
+ throw new \RuntimeException("Could not find SqlProvisioner target schema definition");
+ }
+
+ // Pull our configuration
+
+ $spcfg = $this->get($id);
+
+ $this->Servers->SqlServers->connect($spcfg->server_id, 'targetdb');
+
+ $SchemaManager = new SchemaManager(connection: 'targetdb');
+
+ $SchemaManager->applySchemaObject(
+ schemaObject: $targetSchema,
+ tablePrefix: $spcfg->table_prefix
+ );
+
+ return true;
+ }
+
+ /**
+ * Callback after model save.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param EventInterface $event Event
+ * @param EntityInterface $entity Entity (ie: Co)
+ * @param ArrayObject $options Save options
+ * @return bool True on success
+ */
+
+ public function localAfterSave(\Cake\Event\EventInterface $event, \Cake\Datasource\EntityInterface $entity, \ArrayObject $options): bool {
+ // Apply the database schema (PAR-SqlProvisioner-1)
+ $this->llog('rule', "PAR-SqlProvisioner-1 Applying database schema for SqlProvisioner " . $entity->id);
+ $this->applySchema($entity->id);
+
+ // Populate or update the reference data (PAR-SqlProvisioner-2)
+ $this->llog('rule', "PAR-SqlProvisioner-2 Syncing reference data for SqlProvisioner " . $entity->id);
+ $this->syncReferenceData($entity->id);
+
+ return true;
+ }
+
+ /**
+ * Provision object data to the provisioning target.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param SqlProvisioner $provisioningTarget SqlProvisioner configuration
+ * @param string $className Class name of primary object being provisioned
+ * @param object $data Provisioning data in Entity format (eg: \App\Model\Entity\Person)
+ * @param ProvisioningEligibilityEnum $eligibility Provisioning Eligibility Enum
+ * @return array Array of status, comment, and optional identifier
+ */
+
+ public function provision(
+ \SqlConnector\Model\Entity\SqlProvisioner $provisioningTarget,
+ string $className,
+ object $data, // $data is currently only \App\Model\Entity\Person, but that might change
+ string $eligibility
+ ): array {
+ // Connect to the target database
+ $this->Servers->SqlServers->connect($provisioningTarget->server_id, 'targetdb');
+
+ return $this->syncEntity(
+ $provisioningTarget,
+ $entityName,
+ $data,
+ $eligibility
+ );
+ }
+
+ /**
+ * Sync an entity to the target database schema.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param SqlProvisioner $SqlProvisioner SqlProvisioner configuration
+ * @param string $entityName Entity name of primary object being provisioned
+ * @param object $data Provisioning data in Entity format (eg: \App\Model\Entity\Person)
+ * @param ProvisioningEligibilityEnum $eligibility Provisioning Eligibility Enum
+ * @param string $dataSource Datasource to provision to
+ * @return array Array of status, comment, and optional identifier
+ */
+
+ protected function syncEntity(
+ \SqlConnector\Model\Entity\SqlProvisioner $SqlProvisioner,
+ string $entityName,
+ $data,
+ string $eligibility,
+ string $dataSource='targetdb'): array {
+ // Find the model config, which may vary depending on the type of entity.
+ // We don't check secondaryModels because those aren't directly provisioned.
+ $mconfig = $this->primaryModels[$entityName]
+ ?? ($this->referenceModels[$entityName] ?? null);
+
+ if(!$mconfig) {
+ throw new \RuntimeException("Model configuration for $entityName not defined");
+ }
+
+ // Pull the current target record
+// XXX similar code in syncReferenceData, refactor?
+ $options = [
+ 'table' => $SqlProvisioner->table_prefix . $mconfig['table'],
+ 'alias' => $mconfig['name'],
+ 'connection' => ConnectionManager::get($dataSource)
+ ];
+
+ $SpTable = TableRegistry::get(alias: $mconfig['name'], options: $options);
+
+ try {
+ $curEntity = $SpTable->get($data->id);
+
+ if($eligibility == ProvisioningEligibilityEnum::Eligible) {
+ // We have a currently provisioned record and the subject is Eligible,
+ // patch it with $data and try saving.
+ $patchedEntity = $SpTable->patchEntity($curEntity, $data->toArray(), ['validate' => false]);
+
+ $SpTable->saveOrFail(
+ $patchedEntity,
+ [
+ 'validate' => false,
+ 'checkRules' => false
+ ]
+ );
+
+ if(!empty($mconfig['related'])) {
+ // Process related models
+ foreach($mconfig['related'] as $rmodel) {
+ $this->syncRelatedEntities(
+ SqlProvisioner: $SqlProvisioner,
+ parentEntityName: $entityName,
+ relatedEntityName: $rmodel,
+ parentData: $data,
+ eligibility: $eligibility,
+ dataSource: $dataSource
+ );
+ }
+ }
+
+ return [
+ 'status' => ProvisioningStatusEnum::Provisioned,
+ 'comment' => __d('sql_connector', 'result.prov.updated'),
+ 'identifier' => null
+ ];
+ } else {
+ // The subject record is deleted or otherwise Ineligible, remove the
+ // current entity. Remove the related models before the entity.
+
+ if(!empty($mconfig['related'])) {
+ // Process related models
+ foreach($mconfig['related'] as $rmodel) {
+ $this->syncRelatedEntities(
+ SqlProvisioner: $SqlProvisioner,
+ parentEntityName: $entityName,
+ relatedEntityName: $rmodel,
+ parentData: $data,
+ eligibility: $eligibility,
+ dataSource: $dataSource
+ );
+ }
+ }
+
+ $SpTable->delete($curEntity);
+
+ return [
+ 'status' => ProvisioningStatusEnum::NotProvisioned,
+ 'comment' => __d('sql_connector', 'result.prov.deleted'),
+ 'identifier' => null
+ ];
+ }
+ }
+ catch(\Cake\Datasource\Exception\RecordNotFoundException $e) {
+ // The record is not yet in the SP table (probably a new record)
+ if($eligibility == ProvisioningEligibilityEnum::Eligible) {
+ // The subject is eligible, so provision the record
+ $newEntity = $SpTable->newEntity($data->toArray(), ['validate' => false]);
+
+ $SpTable->saveOrFail(
+ $newEntity,
+ [
+ 'validate' => false,
+ 'checkRules' => false
+ ]
+ );
+
+ if(!empty($mconfig['related'])) {
+ // Process related models
+ foreach($mconfig['related'] as $rmodel) {
+ $this->syncRelatedEntities(
+ SqlProvisioner: $SqlProvisioner,
+ parentEntityName: $entityName,
+ relatedEntityName: $rmodel,
+ parentData: $data,
+ eligibility: $eligibility,
+ dataSource: $dataSource
+ );
+ }
+ }
+
+ return [
+ 'status' => ProvisioningStatusEnum::Provisioned,
+ 'comment' => __d('sql_connector', 'result.prov.added'),
+ 'identifier' => null
+ ];
+ } else {
+ // The subject record is deleted or otherwise Ineligible, nothing to do
+
+ return [
+ 'status' => ProvisioningStatusEnum::NotProvisioned,
+ 'comment' => __d('sql_connector', 'result.prov.ineligible'),
+ 'identifier' => null
+ ];
+ }
+ }
+ catch(\Exception $e) {
+ return [
+ 'status' => ProvisioningStatusEnum::Unknown,
+ 'comment' => $e->getMessage(),
+ 'identifier' => null
+ ];
+ }
+ }
+
+ /**
+ * Synchronize reference data to the target database.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param int $id SQL Provisioner ID
+ * @param string $dataSource DataSource label
+ */
+
+ public function syncReferenceData($id, $dataSource='targetdb') {
+ $spcfg = $this->get($id, ['contain' => ['ProvisioningTargets']]);
+
+ $this->Servers->SqlServers->connect($spcfg->server_id, $dataSource);
+
+ // We treat Groups as Reference Models since they may be referred to
+ // by other entities. We do NOT sync Group Members here, just the Groups.
+
+ foreach(
+ // PAR-SqlProvisioner-3 When Reference Data is resynced, Groups are also resynced.
+ array_merge($this->referenceModels, ['Groups' => $this->primaryModels['Groups']])
+ as $mname => $m
+ ) {
+ // First construct the model reflecting the target database
+
+ $options = [
+ 'table' => $spcfg->table_prefix . $m['table'],
+ 'alias' => $m['name'],
+ 'connection' => ConnectionManager::get($dataSource)
+ ];
+
+ $SpTable = TableRegistry::get(alias: $m['name'], options: $options);
+
+ // Next get the source table model
+
+// XXX don't we need to use the "plugin" datasource here and elsewhere?
+// (test with job shell - maybe this is an RFE for Reprovision All)
+ $SrcTable = TableRegistry::get($m['source']);
+
+ // Pull the source records and then sync them to the target table.
+ // We expect reference data to be no larger than O(100) or maybe
+ // O(1000) so we don't bother with PaginatedSqlIterator here.
+
+ $srcRecords = [];
+
+ foreach($SrcTable->find()
+ ->where(['co_id' => $spcfg->provisioning_target->co_id])
+ ->toArray() as $r) {
+ // We shouldn't have to manually convert the entities to arrays
+ // but toArray() is returning an array of objects instead of an
+ // array of arrays... (and we only need this because the second
+ // parameter to patchEntities expects an array since it's typically
+ // used to process form data)
+
+ // We key on record ID for use in delete, below
+ $srcRecords[$r->id] = $r->toArray();
+ }
+
+ // Pull the current target records
+ $curRecords = $SpTable->find()->all();
+
+ // Patch the target with the source. Note this will handle add and
+ // insert correctly, but will ignore any records from $curRecords that
+ // are not in $srcRecords.
+ $patchedRecords = $SpTable->patchEntities($curRecords, $srcRecords, ['validate' => false]);
+
+ $SpTable->saveManyOrFail($patchedRecords, ['validate' => false, 'checkRules' => false]);
+
+ // patchEntities will handle inserts and updates, but not deletes.
+
+ $toDelete = [];
+
+ foreach($curRecords as $c) {
+ if(!isset($srcRecords[$c->id])) {
+ $toDelete[] = $c;
+ }
+ }
+
+ if(!empty($toDelete)) {
+ $SpTable->deleteMany($toDelete);
+ }
+ }
+ }
+
+ /**
+ * Sync related entities to the target database schema.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param SqlProvisioner $SqlProvisioner SqlProvisioner configuration
+ * @param string $parentEntityName Entity name of primary object being provisioned
+ * @param string $relatedEntityName Entity name of related object being provisioned
+ * @param object $parentData Provisioning data in Entity format (eg: \App\Model\Entity\Person) for parent
+ * @param ProvisioningEligibilityEnum $eligibility Provisioning Eligibility Enum
+ * @param string $dataSource Datasource to provision to
+ */
+
+ protected function syncRelatedEntities(
+ \SqlConnector\Model\Entity\SqlProvisioner $SqlProvisioner,
+ string $parentEntityName,
+ string $relatedEntityName,
+ $parentData,
+ string $eligibility,
+ string $dataSource='targetdb') {
+ // eg: person_id
+ $parentFk = StringUtilities::entityToForeignKey($parentData);
+ // eg: names
+ $relatedTable = Inflector::tableize($relatedEntityName);
+
+ // $parentData will have the "new" values for the related model,
+ // we need to pull the current values from the SP tables
+
+ $mconfig = $this->secondaryModels[$relatedEntityName];
+
+ if(!$mconfig) {
+ throw new \RuntimeException("Model configuration for $relatedEntityName not defined");
+ }
+
+ $options = [
+ 'table' => $SqlProvisioner->table_prefix . $mconfig['table'],
+ 'alias' => $mconfig['name'],
+ 'connection' => ConnectionManager::get($dataSource)
+ ];
+
+ $SpTable = TableRegistry::get(alias: $mconfig['name'], options: $options);
+
+ // We have the source values, but we need to convert them to arrays
+ // for patchEntities
+ $srcEntities = [];
+
+ foreach($parentData->$relatedTable as $r) {
+ $srcEntities[$r->id] = $r->toArray();
+ }
+
+ // Pull the current provisioned data
+
+ $curEntities = $SpTable->find()
+ ->where([$parentFk => $parentData->id])
+ ->all();
+
+ if($eligibility == ProvisioningEligibilityEnum::Eligible) {
+ // Patch the target with the source. Note this will handle add and
+ // insert correctly, but will ignore any records from $curEntities that
+ // are not in $srcEntities.
+ $patchedEntities = $SpTable->patchEntities($curEntities, $srcEntities, ['validate' => false]);
+
+ $SpTable->saveManyOrFail($patchedEntities, ['validate' => false, 'checkRules' => false]);
+ } else {
+ // Delete all currently provisioned entries, which will force by
+ // clearing $srcEntities
+
+ $srcEntities = [];
+ }
+
+ // Sync any related entities. We need to do this after save for Eligible
+ // records (above) and before delition of ineligible records (below).
+ // We have to do this once per instance of the parent related model.
+ // eg: If we're currently syncing parent model People and related model
+ // PersonRoles, we need to syncRelatedEntities on PersonRoles once for
+ // _each_ roles attached to the Person.
+
+ if(!empty($mconfig['related'])) {
+ // Process related models
+ foreach($mconfig['related'] as $rmodel) {
+ if(!empty($parentData->$relatedTable)) {
+ foreach($parentData->$relatedTable as $rmdata) {
+ $this->syncRelatedEntities(
+ SqlProvisioner: $SqlProvisioner,
+ parentEntityName: $relatedEntityName,
+ relatedEntityName: $rmodel,
+ parentData: $rmdata,
+ eligibility: $eligibility,
+ dataSource: $dataSource
+ );
+ }
+ }
+ }
+ }
+
+ // Delete any dropped related entities
+
+ $toDelete = [];
+
+ foreach($curEntities as $c) {
+ if(!isset($srcEntities[$c->id])) {
+ $toDelete[] = $c;
+ }
+ }
+
+ if(!empty($toDelete)) {
+ $SpTable->deleteMany($toDelete);
+ }
+
+ // We don't currently return errors up the stack, should we?
+ }
+
+ /**
+ * 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('provisioning_target_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->notEmptyString('provisioning_target_id');
+
+ $validator->add('server_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->notEmptyString('server_id');
+
+ $this->registerStringValidation($validator, $schema, 'table_prefix', true);
+
+ // Table prefixes must be alphanumeric and end in an underscore
+ $validator->add('table_prefix', [
+ 'format' => [
+ 'rule' => function ($value, $context) {
+ return (preg_match('/[\w]+_/', $value) ? true : __d('sql_connector', 'error.table_prefix'));
+ }
+ ]
+ ]);
+
+ return $validator;
+ }
+}
\ No newline at end of file
diff --git a/app/availableplugins/SqlConnector/src/SqlConnectorPlugin.php b/app/availableplugins/SqlConnector/src/SqlConnectorPlugin.php
new file mode 100644
index 000000000..a0bcdf884
--- /dev/null
+++ b/app/availableplugins/SqlConnector/src/SqlConnectorPlugin.php
@@ -0,0 +1,93 @@
+plugin(
+ 'SqlConnector',
+ ['path' => '/sql-connector'],
+ 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/SqlConnector/src/config/plugin.json b/app/availableplugins/SqlConnector/src/config/plugin.json
new file mode 100644
index 000000000..04a3c185c
--- /dev/null
+++ b/app/availableplugins/SqlConnector/src/config/plugin.json
@@ -0,0 +1,304 @@
+{
+ "types": {
+ "provisioner": [
+ "SqlProvisioners"
+ ]
+ },
+ "schema": {
+ "tables": {
+ "sql_provisioners": {
+ "columns": {
+ "id": {},
+ "provisioning_target_id": {},
+ "server_id": { "notnull": false },
+ "table_prefix": { "type": "string", "size": 32 }
+ },
+ "indexes": {
+ "sql_provisioners_i1": { "columns": [ "provisioning_target_id" ]}
+ }
+ }
+ }
+ },
+ "target-schema": {
+ "not-yet-implemented-tables": {
+ "terms_and_conditions": "CFM-200",
+ "external_identity_sources": {
+ "JIRA": "CFM-265",
+ "fk from": [ "names" ]
+ }
+ },
+
+ "tables": {
+ "types": {
+ "columns": {
+ "id": {},
+ "attribute": { "type": "string", "size": 32, "notnull": true },
+ "display_name": { "type": "string", "size": 64, "notnull": true },
+ "value": { "type": "string", "size": 32, "notnull": true },
+ "edupersonaffiliation": { "type": "string", "size": 32 },
+ "status": {}
+ },
+ "indexes": {
+ "types_i2": { "columns": [ "attribute" ] }
+ }
+ },
+
+ "cous": {
+ "columns": {
+ "id": {},
+ "name": {},
+ "description": {},
+ "parent_id": { "type": "integer", "foreignkey": { "table": "cous", "column": "id" } }
+ },
+ "indexes": {
+ "cous_i3": { "columns": [ "parent_id" ] }
+ },
+ "changelog": false
+ },
+
+ "people": {
+ "columns": {
+ "id": {},
+ "status": {},
+ "timezone": { "type": "string", "size": 80 },
+ "date_of_birth": { "type": "date" }
+ },
+ "indexes": {
+ },
+ "changelog": false
+ },
+
+ "person_roles": {
+ "columns": {
+ "id": {},
+ "person_id": { "notnull": true },
+ "status": {},
+ "ordr": {},
+ "cou_id": {},
+ "affiliation_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } },
+ "title": { "type": "string", "size": 128 },
+ "organization": { "type": "string", "size": 128 },
+ "department": { "type": "string", "size": 128 },
+ "manager_person_id": { "type": "integer", "foreignkey": { "table": "people", "column": "id" } },
+ "sponsor_person_id": { "type": "integer", "foreignkey": { "table": "people", "column": "id" } },
+ "valid_from": {},
+ "valid_through": {}
+ },
+ "indexes": {
+ "person_roles_i1": { "columns": [ "person_id" ] },
+ "person_roles_i2": { "columns": [ "sponsor_person_id" ] },
+ "person_roles_i3": { "columns": [ "cou_id" ] },
+ "person_roles_i4": { "columns": [ "affiliation_type_id" ] },
+ "person_roles_i5": { "columns": [ "manager_person_id" ] }
+ },
+ "changelog": false
+ },
+
+ "external_identities": {
+ "columns": {
+ "id": {},
+ "person_id": { "notnull": true },
+ "status": {},
+ "date_of_birth": { "type": "date" }
+ },
+ "indexes": {
+ "external_identities_i1": { "columns": [ "person_id" ] }
+ },
+ "changelog": false
+ },
+
+ "external_identity_roles": {
+ "columns": {
+ "id": {},
+ "external_identity_id": { "notnull": true },
+ "status": {},
+ "ordr": {},
+ "affiliation_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } },
+ "title": { "type": "string", "size": 128 },
+ "organization": { "type": "string", "size": 128 },
+ "department": { "type": "string", "size": 128 },
+ "manager_identifier": { "type": "string", "size": 512 },
+ "sponsor_identifier": { "type": "string", "size": 512 },
+ "valid_from": {},
+ "valid_through": {}
+ },
+ "indexes": {
+ "external_identity_roles_i1": { "columns": [ "external_identity_id" ] },
+ "external_identity_roles_i2": { "columns": [ "affiliation_type_id" ] }
+ },
+ "changelog": false
+ },
+
+ "groups": {
+ "columns": {
+ "id": {},
+ "cou_id": {},
+ "name": {},
+ "description": { "size": 256 },
+ "open": { "type": "boolean" },
+ "status": {},
+ "group_type": { "type": "string", "size": 2 }
+ },
+ "indexes": {
+ "groups_i5": { "columns": [ "cou_id" ]}
+ },
+ "changelog": false
+ },
+
+ "ad_hoc_attributes": {
+ "columns": {
+ "id": {},
+ "tag": { "type": "string", "size": 128 },
+ "value": { "type": "string", "size": 256 }
+ },
+ "indexes": {
+ },
+ "mvea": [ "person", "person_role", "external_identity", "external_identity_role" ],
+ "changelog": false
+ },
+
+ "addresses": {
+ "columns": {
+ "id": {},
+ "street": { "type": "text" },
+ "room": { "type": "string", "size": 64 },
+ "locality": { "type": "string", "size": 128 },
+ "state": { "type": "string", "size": 128 },
+ "postal_code": { "type": "string", "size": 16 },
+ "country": { "type": "string", "size": 128 },
+ "description": {},
+ "type_id": {},
+ "language": {}
+ },
+ "indexes": {
+ "addresses_i1": { "columns": [ "type_id" ] }
+ },
+ "mvea": [ "person", "person_role", "external_identity", "external_identity_role" ],
+ "changelog": false
+ },
+
+ "email_addresses": {
+ "columns": {
+ "id": {},
+ "mail": { "type": "string", "size": 256 },
+ "description": {},
+ "type_id": {},
+ "verified": { "type": "boolean" }
+ },
+ "indexes": {
+ "email_addresses_i3": { "columns": [ "type_id" ] }
+ },
+ "mvea": [ "person", "external_identity" ],
+ "changelog": false
+ },
+
+ "identifiers": {
+ "columns": {
+ "id": {},
+ "identifier": { "type": "string", "size": 512 },
+ "type_id": {},
+ "login": { "type": "boolean" },
+ "status": {}
+ },
+ "indexes": {
+ "identifiers_i3": { "columns": [ "type_id" ] }
+ },
+ "mvea": [ "person", "external_identity", "group" ],
+ "changelog": false
+ },
+
+ "names": {
+ "columns": {
+ "id": {},
+ "honorific": { "type": "string", "size": 32 },
+ "given": { "type": "string", "size": 128 },
+ "middle": { "type": "string", "size": 128 },
+ "family": { "type": "string", "size": 128 },
+ "suffix": { "type": "string", "size": 32 },
+ "type_id": {},
+ "language": {},
+ "primary_name": { "type": "boolean" },
+ "display_name": { "type": "string", "size": 256 }
+ },
+ "indexes": {
+ "names_i1": { "columns": [ "type_id" ] }
+ },
+ "mvea": [ "person", "external_identity" ],
+ "changelog": false
+ },
+
+ "pronouns": {
+ "columns": {
+ "id": {},
+ "pronouns": { "type": "string", "size": 64 },
+ "language": {},
+ "type_id": {}
+ },
+ "indexes": {
+ "pronouns_i1": { "columns": [ "type_id" ] }
+ },
+ "mvea": [ "person", "external_identity" ],
+ "changelog": false
+ },
+
+ "telephone_numbers": {
+ "columns": {
+ "id": {},
+ "country_code": { "type": "string", "size": 3 },
+ "area_code": { "type": "string", "size": 8 },
+ "number": { "type": "string", "size": 64 },
+ "extension": { "type": "string", "size": 16 },
+ "description": {},
+ "type_id": {}
+ },
+ "indexes": {
+ "telephone_numbers_i1": { "columns": [ "type_id" ] }
+ },
+ "mvea": [ "person", "person_role", "external_identity", "external_identity_role" ],
+ "changelog": false
+ },
+
+ "urls": {
+ "columns": {
+ "id": {},
+ "url": { "type": "string", "size": 256 },
+ "description": {},
+ "type_id": {}
+ },
+ "indexes": {
+ "urls_i1": { "columns": [ "type_id" ] }
+ },
+ "mvea": [ "person", "external_identity" ],
+ "changelog": false
+ },
+
+ "group_members": {
+ "columns": {
+ "id": {},
+ "group_id": {},
+ "person_id": {},
+ "valid_from": {},
+ "valid_through": {}
+ },
+ "indexes": {
+ "group_members_i1": { "columns": [ "group_id" ]},
+ "group_members_i2": { "columns": [ "person_id" ]}
+ },
+ "changelog": false
+ },
+
+ "group_owners": {
+ "columns": {
+ "id": {},
+ "group_id": {},
+ "person_id": {}
+ },
+ "indexes": {
+ "group_owners_i1": { "columns": [ "group_id" ]},
+ "group_owners_i2": { "columns": [ "person_id" ]}
+ },
+ "changelog": false
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/availableplugins/SqlConnector/templates/SqlProvisioners/fields-links.inc b/app/availableplugins/SqlConnector/templates/SqlProvisioners/fields-links.inc
new file mode 100644
index 000000000..6788eb97a
--- /dev/null
+++ b/app/availableplugins/SqlConnector/templates/SqlProvisioners/fields-links.inc
@@ -0,0 +1,48 @@
+ 'history',
+ 'order' => 'Default',
+ 'label' => __d('sql_connector', 'operation.reapply'),
+ 'link' => [
+ 'action' => 'reapply',
+ $vv_obj->id
+ ],
+ 'class' => ''
+];
+
+$topLinks[] = [
+ 'icon' => 'history',
+ 'order' => 'Default',
+ 'label' => __d('sql_connector', 'operation.resync'),
+ 'link' => [
+ 'action' => 'resync',
+ $vv_obj->id
+ ],
+ 'class' => ''
+];
diff --git a/app/availableplugins/SqlConnector/templates/SqlProvisioners/fields.inc b/app/availableplugins/SqlConnector/templates/SqlProvisioners/fields.inc
new file mode 100644
index 000000000..2b23c2634
--- /dev/null
+++ b/app/availableplugins/SqlConnector/templates/SqlProvisioners/fields.inc
@@ -0,0 +1,33 @@
+Field->control('server_id');
+
+ print $this->Field->control('table_prefix', ['default' => 'sp_']);
+}
diff --git a/app/availableplugins/SqlConnector/tests/bootstrap.php b/app/availableplugins/SqlConnector/tests/bootstrap.php
new file mode 100644
index 000000000..7539568b5
--- /dev/null
+++ b/app/availableplugins/SqlConnector/tests/bootstrap.php
@@ -0,0 +1,55 @@
+loadSqlFiles('tests/schema.sql', 'test');
diff --git a/app/availableplugins/SqlConnector/tests/schema.sql b/app/availableplugins/SqlConnector/tests/schema.sql
new file mode 100644
index 000000000..b3c02dbcb
--- /dev/null
+++ b/app/availableplugins/SqlConnector/tests/schema.sql
@@ -0,0 +1 @@
+-- Test database schema for SqlConnector
diff --git a/app/availableplugins/SqlConnector/webroot/.gitkeep b/app/availableplugins/SqlConnector/webroot/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/composer.json b/app/composer.json
index 85310beb1..6c29c7fbb 100644
--- a/app/composer.json
+++ b/app/composer.json
@@ -29,13 +29,19 @@
},
"autoload": {
"psr-4": {
- "App\\": "src/"
+ "App\\": "src/",
+ "CoreServer\\": "plugins/CoreServer/src/",
+ "FileConnector\\": "availableplugins/FileConnector/src/",
+ "SqlConnector\\": "availableplugins/SqlConnector/src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Test\\": "tests/",
- "Cake\\Test\\": "vendor/cakephp/cakephp/tests/"
+ "Cake\\Test\\": "vendor/cakephp/cakephp/tests/",
+ "CoreServer\\Test\\": "plugins/CoreServer/tests/",
+ "FileConnector\\Test\\": "availableplugins/FileConnector/tests/",
+ "SqlConnector\\Test\\": "availableplugins/SqlConnector/tests/"
}
},
"scripts": {
diff --git a/app/config/schema/schema.json b/app/config/schema/schema.json
index 3a5b7bb3b..652a772d7 100644
--- a/app/config/schema/schema.json
+++ b/app/config/schema/schema.json
@@ -19,9 +19,13 @@
"id": { "type": "integer", "autoincrement": true, "primarykey": true },
"language": { "type": "string", "size": 16 },
"name": { "type": "string", "size": 128, "notnull": true },
+ "ordr": { "type": "integer" },
"person_id": { "type": "integer", "foreignkey": { "table": "people", "column": "id" } },
"person_role_id": { "type": "integer", "foreignkey": { "table": "person_roles", "column": "id" } },
"plugin": { "type": "string", "size": 80 },
+ "provisioning_target_id": { "type": "integer", "foreignkey": { "table": "provisioning_targets", "column": "id" }, "notnull": true },
+ "report_id": { "type": "integer", "foreignkey": { "table": "reports", "column": "id" }, "notnull": true },
+ "server_id": { "type": "integer", "foreignkey": { "table": "servers", "column": "id" }, "notnull": true },
"status": { "type": "string", "size": 2 },
"type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" }, "notnull": true },
"valid_from": { "type": "datetime" },
@@ -202,7 +206,7 @@
"id": {},
"person_id": { "notnull": true },
"status": {},
- "ordr": { "type": "integer" },
+ "ordr": {},
"cou_id": {},
"affiliation_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } },
"title": { "type": "string", "size": 128 },
@@ -239,7 +243,7 @@
"id": {},
"external_identity_id": { "notnull": true },
"status": {},
- "ordr": { "type": "integer" },
+ "ordr": {},
"affiliation_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } },
"title": { "type": "string", "size": 128 },
"organization": { "type": "string", "size": 128 },
@@ -391,18 +395,55 @@
"sourced": true
},
+ "provisioning_targets": {
+ "columns": {
+ "id": {},
+ "co_id": {},
+ "description": {},
+ "plugin": {},
+ "status": {},
+ "provisioning_group_id": { "type": "integer", "foreignkey": { "table": "groups", "column": "id" } },
+ "retry_interval": { "type": "integer" },
+ "ordr": {}
+ },
+ "indexes": {
+ "provisioning_targets_i1": { "columns": [ "co_id" ]},
+ "provisioning_targets_i2": { "needed": false, "columns": [ "provisioning_group_id" ] }
+ }
+ },
+
+ "provisioning_history_records": {
+ "columns": {
+ "id": {},
+ "provisioning_target_id": {},
+ "subject_model": { "type": "string", "size": 80 },
+ "subjectid": { "type": "integer", "comment": "This is a foreign key, but not at the database level" },
+ "person_id": {},
+ "group_id": {},
+ "status": {},
+ "comment": {}
+ },
+ "indexes": {
+ "provisioning_history_records_i1": { "columns": [ "provisioning_target_id" ] },
+ "provisioning_history_records_i2": { "columns": [ "person_id" ] },
+ "provisioning_history_records_i3": { "columns": [ "group_id" ] }
+ }
+ },
+
"identifiers": {
"columns": {
"id": {},
"identifier": { "type": "string", "size": 512 },
"type_id": {},
"login": { "type": "boolean" },
- "status": {}
+ "status": {},
+ "provisioning_target_id": { "notnull": false }
},
"indexes": {
"identifiers_i1": { "columns": [ "identifier", "type_id", "person_id" ] },
"identifiers_i2": { "columns": [ "identifier", "type_id", "external_identity_id" ] },
- "identifiers_i3": { "columns": [ "type_id" ] }
+ "identifiers_i3": { "columns": [ "type_id" ] },
+ "identifiers_i4": { "needed": false, "columns": [ "provisioning_target_id" ] }
},
"mvea": [ "person", "external_identity", "group" ],
"sourced": true
@@ -524,6 +565,19 @@
"job_history_records_i3": { "columns": [ "external_identity_id" ] },
"job_history_records_i4": { "columns": [ "job_id", "record_key" ] }
}
+ },
+
+ "servers": {
+ "columns": {
+ "id": {},
+ "co_id": {},
+ "description": {},
+ "plugin": {},
+ "status": {}
+ },
+ "indexes": {
+ "servers_i1": { "columns": [ "co_id" ] }
+ }
}
},
diff --git a/app/plugins/CoreServer/README.md b/app/plugins/CoreServer/README.md
new file mode 100644
index 000000000..984a690c6
--- /dev/null
+++ b/app/plugins/CoreServer/README.md
@@ -0,0 +1,11 @@
+# CoreServer 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/core-server
+```
diff --git a/app/plugins/CoreServer/composer.json b/app/plugins/CoreServer/composer.json
new file mode 100644
index 000000000..471af66a7
--- /dev/null
+++ b/app/plugins/CoreServer/composer.json
@@ -0,0 +1,24 @@
+{
+ "name": "comanage-registry/core-server",
+ "description": "CoreServer plugin for COmanage Registry",
+ "type": "cakephp-plugin",
+ "license": "MIT",
+ "require": {
+ "php": ">=7.2",
+ "cakephp/cakephp": "4.4.*"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.5 || ^9.3"
+ },
+ "autoload": {
+ "psr-4": {
+ "CoreServer\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "CoreServer\\Test\\": "tests/",
+ "Cake\\Test\\": "vendor/cakephp/cakephp/tests/"
+ }
+ }
+}
diff --git a/app/plugins/CoreServer/phpunit.xml.dist b/app/plugins/CoreServer/phpunit.xml.dist
new file mode 100644
index 000000000..dc201acf9
--- /dev/null
+++ b/app/plugins/CoreServer/phpunit.xml.dist
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+ tests/TestCase/
+
+
+
+
+
+
+
+
+
+
+ src/
+
+
+
diff --git a/app/plugins/CoreServer/resources/locales/en_US/core_server.po b/app/plugins/CoreServer/resources/locales/en_US/core_server.po
new file mode 100644
index 000000000..5a8660f67
--- /dev/null
+++ b/app/plugins/CoreServer/resources/locales/en_US/core_server.po
@@ -0,0 +1,58 @@
+# COmanage Registry Localizations (core_server 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-plugins
+# @since COmanage Registry v5.0.0
+# @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+
+msgid "controller.SqlServers"
+msgstr "{0,plural,=1{SQL Server} other{SQL Servers}}"
+
+msgid "enumeration.RdbmsTypeEnum.LT"
+msgstr "SQLite"
+
+msgid "enumeration.RdbmsTypeEnum.MA"
+msgstr "MariaDB"
+
+msgid "enumeration.RdbmsTypeEnum.MS"
+msgstr "MS SQL Server"
+
+msgid "enumeration.RdbmsTypeEnum.MY"
+msgstr "MySQL"
+
+# XXX Not yet supported
+#msgid "enumeration.RdbmsTypeEnum.OR"
+#msgstr "Oracle"
+
+msgid "enumeration.RdbmsTypeEnum.PG"
+msgstr "Postgres"
+
+msgid "field.SqlServers.databas"
+msgstr "Database Name"
+
+msgid "field.SqlServers.hostname"
+msgstr "Hostname"
+
+# XXX Temporary?
+msgid "field.SqlServers.password"
+msgstr "Password"
+
+msgid "field.SqlServers.type"
+msgstr "RDBMS Type"
diff --git a/app/plugins/CoreServer/src/Controller/AppController.php b/app/plugins/CoreServer/src/Controller/AppController.php
new file mode 100644
index 000000000..554055701
--- /dev/null
+++ b/app/plugins/CoreServer/src/Controller/AppController.php
@@ -0,0 +1,10 @@
+ [
+ 'SqlServers.hostname' => 'asc'
+ ]
+ ];
+}
diff --git a/app/plugins/CoreServer/src/CoreServerPlugin.php b/app/plugins/CoreServer/src/CoreServerPlugin.php
new file mode 100644
index 000000000..6711a5cf7
--- /dev/null
+++ b/app/plugins/CoreServer/src/CoreServerPlugin.php
@@ -0,0 +1,93 @@
+plugin(
+ 'CoreServer',
+ ['path' => '/core-server'],
+ 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/plugins/CoreServer/src/Lib/Enum/RdbmsTypeEnum.php b/app/plugins/CoreServer/src/Lib/Enum/RdbmsTypeEnum.php
new file mode 100644
index 000000000..a6be23625
--- /dev/null
+++ b/app/plugins/CoreServer/src/Lib/Enum/RdbmsTypeEnum.php
@@ -0,0 +1,46 @@
+
+ */
+ protected $_accessible = [
+ '*' => true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
diff --git a/app/plugins/CoreServer/src/Model/Table/SqlServersTable.php b/app/plugins/CoreServer/src/Model/Table/SqlServersTable.php
new file mode 100644
index 000000000..fc52131a3
--- /dev/null
+++ b/app/plugins/CoreServer/src/Model/Table/SqlServersTable.php
@@ -0,0 +1,171 @@
+addBehavior('Changelog');
+ $this->addBehavior('Log');
+ // Timestamp behavior handles created/modified updates
+ $this->addBehavior('Timestamp');
+
+ $this->setTableType(\App\Lib\Enum\TableTypeEnum::Configuration);
+
+ // Define associations
+ $this->belongsTo('Servers');
+
+ $this->setDisplayField('hostname');
+
+ $this->setPrimaryLink('server_id');
+ $this->setRequiresCO(true);
+
+ $this->setAutoViewVars([
+ 'types' => [
+ 'type' => 'enum',
+ 'class' => 'CoreServer.RdbmsTypeEnum'
+ ]
+ ]);
+
+ $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' => ['platformAdmin', 'coAdmin'],
+ 'index' => ['platformAdmin', 'coAdmin']
+ ]
+ ]);
+ }
+
+ /**
+ * Establish a connection (via Cake's ConnectionManager) to the specified SQL server.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param int $serverId Server ID (NOT SqlServer ID)
+ * @param string $name Connection name, used for subsequent access via Models
+ * @return bool true on success
+ * @throws Exception
+ */
+
+ public function connect(int $serverId, string $name): bool {
+ // Note if you're looking to add support for tablePrefix here (eg: "cm_")
+ // Cake basically dropped support for that in v3. As an alternate,
+ // individual models can be configured to use alternate table names,
+ // which is basically what the SQL Provisioner does.
+
+ // Pull our configuration via the parent Server object.
+ $server = $this->Servers->get($serverId, ['contain' => ['SqlServers']]);
+
+ $dbmap = [
+ RdbmsTypeEnum::MariaDB => 'Mysql',
+ RdbmsTypeEnum::MySQL => 'Mysql',
+ RdbmsTypeEnum::Postgres => 'Postgres',
+ RdbmsTypeEnum::SQLite => 'Sqlite',
+ RdbmsTypeEnum::SqlServer => 'Sqlserver'
+ ];
+
+ $dbconfig = [
+ 'className' => 'Cake\Database\Connection',
+ 'driver' => "Cake\Database\Driver\\" . $dbmap[$server->sql_server->type],
+ 'persistent' => false,
+ 'host' => $server->sql_server->hostname,
+ 'username' => $server->sql_server->username,
+ 'password' => $server->sql_server->password,
+ 'database' => $server->sql_server->databas,
+ 'quoteIdentifiers' => false,
+ 'encoding' => 'utf8',
+ 'timezone' => 'UTC'
+ ];
+
+ ConnectionManager::setConfig('targetdb', $dbconfig);
+
+ return true;
+ }
+
+ /**
+ * 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('server_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->notEmptyString('server_id');
+
+ $validator->add('type', [
+ 'content' => ['rule' => ['inList', RdbmsTypeEnum::getConstValues()]]
+ ]);
+ $validator->notEmptyString('type');
+
+ $this->registerStringValidation($validator, $schema, 'hostname', true);
+
+ $this->registerStringValidation($validator, $schema, 'databas', true);
+
+ $this->registerStringValidation($validator, $schema, 'username', false);
+
+ $this->registerStringValidation($validator, $schema, 'password', false);
+
+ return $validator;
+ }
+}
diff --git a/app/plugins/CoreServer/src/config/plugin.json b/app/plugins/CoreServer/src/config/plugin.json
new file mode 100644
index 000000000..876ade57f
--- /dev/null
+++ b/app/plugins/CoreServer/src/config/plugin.json
@@ -0,0 +1,25 @@
+{
+ "types": {
+ "server": [
+ "SqlServers"
+ ]
+ },
+ "schema": {
+ "tables": {
+ "sql_servers": {
+ "columns": {
+ "id": {},
+ "server_id": {},
+ "type": { "type": "string", "size": 2 },
+ "hostname": { "type": "string", "size": 128 },
+ "databas": { "type": "string", "size": 128 },
+ "username": { "type": "string", "size": 128 },
+ "password": { "type": "string", "size": 80 }
+ },
+ "indexes": {
+ "sql_servers_i1": { "columns": [ "server_id" ]}
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/plugins/CoreServer/templates/SqlServers/fields.inc b/app/plugins/CoreServer/templates/SqlServers/fields.inc
new file mode 100644
index 000000000..aaa97647e
--- /dev/null
+++ b/app/plugins/CoreServer/templates/SqlServers/fields.inc
@@ -0,0 +1,39 @@
+Field->control('type');
+
+ print $this->Field->control('hostname');
+
+ print $this->Field->control('databas');
+
+ print $this->Field->control('username');
+
+ print $this->Field->control('password');
+}
diff --git a/app/plugins/CoreServer/tests/bootstrap.php b/app/plugins/CoreServer/tests/bootstrap.php
new file mode 100644
index 000000000..d27d0ae8c
--- /dev/null
+++ b/app/plugins/CoreServer/tests/bootstrap.php
@@ -0,0 +1,55 @@
+loadSqlFiles('tests/schema.sql', 'test');
diff --git a/app/plugins/CoreServer/tests/schema.sql b/app/plugins/CoreServer/tests/schema.sql
new file mode 100644
index 000000000..f5df4568c
--- /dev/null
+++ b/app/plugins/CoreServer/tests/schema.sql
@@ -0,0 +1 @@
+-- Test database schema for CoreServer
diff --git a/app/plugins/CoreServer/webroot/.gitkeep b/app/plugins/CoreServer/webroot/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/resources/locales/en_US/command.po b/app/resources/locales/en_US/command.po
index 5a3303979..d67e5fbc4 100644
--- a/app/resources/locales/en_US/command.po
+++ b/app/resources/locales/en_US/command.po
@@ -132,6 +132,15 @@ msgstr "COmanage Platform Administrator"
msgid "se.salt"
msgstr "Generating salt file {0}"
+msgid "opt.test.database.ok"
+msgstr "Database connection established"
+
+msgid "opt.test.database.source"
+msgstr "For database test, the datasource to use"
+
+msgid "opt.test.test"
+msgstr "Test to perform"
+
msgid "tm.epilog"
msgstr "An optional, space separated list of tables to transmogrify may be specified"
diff --git a/app/resources/locales/en_US/controller.po b/app/resources/locales/en_US/controller.po
index 6df39e05b..1e3d2425d 100644
--- a/app/resources/locales/en_US/controller.po
+++ b/app/resources/locales/en_US/controller.po
@@ -96,6 +96,15 @@ msgstr "{0,plural,=1{Pronoun Preference} other{Pronouns}}"
msgid "Plugins"
msgstr "{0,plural,=1{Plugin} other{Plugins}}"
+msgid "ProvisioningHistoryRecords"
+msgstr "{0,plural,=1{Provisioning History Record} other{Provisioning History Records}}"
+
+msgid "ProvisioningTargets"
+msgstr "{0,plural,=1{Provisioning Target} other{Provisioning Targets}}"
+
+msgid "Servers"
+msgstr "{0,plural,=1{Server} other{Servers}}"
+
msgid "TelephoneNumbers"
msgstr "{0,plural,=1{Telephone Number} other{Telephone Numbers}}"
diff --git a/app/resources/locales/en_US/defaultType.po b/app/resources/locales/en_US/defaultType.po
index 93cceccdc..3cd270731 100644
--- a/app/resources/locales/en_US/defaultType.po
+++ b/app/resources/locales/en_US/defaultType.po
@@ -93,8 +93,9 @@ msgstr "OpenID"
msgid "Identifiers.type.orcid"
msgstr "ORCiD"
+# This is coded as "provisioningtarget" for compatibility with v4
msgid "Identifiers.type.provisioningtarget"
-msgstr "Provisioning Target"
+msgstr "Provisioning Key"
msgid "Identifiers.type.reference"
msgstr "Match Reference"
diff --git a/app/resources/locales/en_US/enumeration.po b/app/resources/locales/en_US/enumeration.po
index fb01ca1b2..7634d6b41 100644
--- a/app/resources/locales/en_US/enumeration.po
+++ b/app/resources/locales/en_US/enumeration.po
@@ -249,6 +249,36 @@ msgstr "Country Code, Area Code, Number"
msgid "PermittedTelephoneNumberFieldsEnum.country_code,area_code,number,extension"
msgstr "Country Code, Area Code, Number, Extension"
+msgid "ProvisionerModeEnum.A"
+msgstr "Immediate"
+
+msgid "ProvisionerModeEnum.E"
+msgstr "Enrollment Only"
+
+msgid "ProvisionerModeEnum.M"
+msgstr "Manual"
+
+msgid "ProvisionerModeEnum.Q"
+msgstr "Queue"
+
+msgid "ProvisionerModeEnum.QE"
+msgstr "Queue on Error"
+
+msgid "ProvisionerModeEnum.X"
+msgstr "Disabled"
+
+msgid "ProvisioningStatusEnum.N"
+msgstr "Not Provisioned"
+
+msgid "ProvisioningStatusEnum.P"
+msgstr "Provisioned"
+
+msgid "ProvisioningStatusEnum.Q"
+msgstr "Queued"
+
+msgid "ProvisioningStatusEnum.X"
+msgstr "Unknown"
+
msgid "RequiredAddressFieldsEnum.country"
msgstr "Country"
diff --git a/app/resources/locales/en_US/field.po b/app/resources/locales/en_US/field.po
index 28273eb27..c11e25e06 100644
--- a/app/resources/locales/en_US/field.po
+++ b/app/resources/locales/en_US/field.po
@@ -216,6 +216,9 @@ msgstr "(Jr, III, etc)"
msgid "tag"
msgstr "Tag"
+msgid "timestamp"
+msgstr "Timestamp"
+
msgid "title"
msgstr "Title"
@@ -408,6 +411,30 @@ msgstr "Plugin"
msgid "Plugins.location"
msgstr "Location"
+msgid "ProvisioningHistoryRecords.subject_model"
+msgstr "Subject Object Type"
+
+msgid "ProvisioningHistoryRecords.subjectid"
+msgstr "Subject Object ID"
+
+msgid "ProvisioningTargets.ordr.desc"
+msgstr "The order in which this provisioner will be run when provisioning occurs (leave blank to run after all current provisioners)"
+
+msgid "ProvisioningTargets.provisioning_group_id"
+msgstr "Provisioning Group"
+
+msgid "ProvisioningTargets.provisioning_group_id.desc"
+msgstr "If set, only members of the specified Group will be provisioned to this target"
+
+msgid "ProvisioningTargets.retry_interval"
+msgstr "Retry Interval"
+
+msgid "ProvisioningTargets.retry_interval.desc"
+msgstr "If the provisioning action fails, it will be automatically retried after this interval (in seconds), default is 900 seconds. Set to 0 to not try again. (To stop retrying, cancel the job in the Job Queue.)"
+
+msgid "ProvisioningTargets.status"
+msgstr "Provisioning Mode"
+
msgid "TelephoneNumbers.formatted_number"
msgstr "Telephone Number"
diff --git a/app/resources/locales/en_US/menu.po b/app/resources/locales/en_US/menu.po
index a875c0671..d15a079c4 100644
--- a/app/resources/locales/en_US/menu.po
+++ b/app/resources/locales/en_US/menu.po
@@ -24,6 +24,9 @@
# Menu Messages
+msgid "artifacts"
+msgstr "Available {0} Artifacts"
+
msgid "co.Attributes"
msgstr "Attributes"
@@ -138,9 +141,11 @@ msgstr "All Groups"
msgid "co.switch"
msgstr "Switch CO"
+msgid "registries"
+msgstr "Available {0} Registries"
+
msgid "related.configurations"
msgstr "Related Configurations"
msgid "related.links"
-msgstr "Related Links"
-
+msgstr "Related Links"
\ No newline at end of file
diff --git a/app/resources/locales/en_US/operation.po b/app/resources/locales/en_US/operation.po
index fc01b4b65..e01e38929 100644
--- a/app/resources/locales/en_US/operation.po
+++ b/app/resources/locales/en_US/operation.po
@@ -57,6 +57,9 @@ msgstr "Close"
msgid "confirm"
msgstr "Confirm"
+msgid "configure.a"
+msgstr "Configure {0}"
+
msgid "configure.plugin"
msgstr "Configure Plugin"
@@ -117,6 +120,12 @@ msgstr "Previous"
msgid "primary"
msgstr "Make Primary"
+msgid "provision"
+msgstr "Provision Now"
+
+msgid "provisioning.status"
+msgstr "Provisioning Status"
+
msgid "Types.restore"
msgstr "Add/Restore Default Types"
diff --git a/app/src/Controller/ApiV2Controller.php b/app/src/Controller/ApiV2Controller.php
index 78c5fb7c8..62c5a88a9 100644
--- a/app/src/Controller/ApiV2Controller.php
+++ b/app/src/Controller/ApiV2Controller.php
@@ -36,6 +36,7 @@
use Cake\ORM\TableRegistry;
use Cake\Utility\Inflector;
+use \App\Lib\Enum\ProvisioningContextEnum;
use \App\Lib\Enum\SuspendableStatusEnum;
class ApiV2Controller extends AppController {
@@ -94,6 +95,12 @@ public function add() {
if($this->$modelsName->saveOrFail($obj)) {
$results[] = ['id' => $obj->id];
+
+ // Trigger provisioning, letting errors bubble up (AR-GMR-5)
+ if(method_exists($table, "requestProvisioning")) {
+ $this->llog('rule', "AR-GMR-5 Requesting provisioning for $modelsName " . $obj->id);
+ $table->requestProvisioning(id: $obj->id, context: ProvisioningContextEnum::Automatic);
+ }
}
}
catch(\Exception $e) {
@@ -146,6 +153,16 @@ public function delete($id) {
// note similar logic in StandardController
$this->$modelsName->deleteOrFail($obj);
+ if(method_exists($obj, "isReadOnly") && $obj->isReadOnly()) {
+ throw new BadRequestException(__d('error', 'edit.readonly'));
+ }
+
+ // Trigger provisioning, letting errors bubble up (AR-GMR-5)
+ if(method_exists($table, "requestProvisioning")) {
+ $this->llog('rule', "AR-GMR-5 Requesting provisioning for deleted entity $modelsName " . $obj->id);
+ $table->requestProvisioning(id: $obj->id, context: ProvisioningContextEnum::Automatic);
+ }
+
// Render an empty view
$this->render('/Standard/api/v2/json/delete');
}
@@ -190,6 +207,12 @@ public function edit($id) {
$this->$modelsName->saveOrFail($obj);
+ // Trigger provisioning, letting errors bubble up (AR-GMR-5)
+ if(method_exists($table, "requestProvisioning")) {
+ $this->llog('rule', "AR-GMR-5 Requesting provisioning for $modelsName " . $obj->id);
+ $table->requestProvisioning(id: $obj->id, context: ProvisioningContextEnum::Automatic);
+ }
+
// Let the view render
$this->render('/Standard/api/v2/json/add-edit');
}
diff --git a/app/src/Controller/AppController.php b/app/src/Controller/AppController.php
index 50851736a..58c231870 100644
--- a/app/src/Controller/AppController.php
+++ b/app/src/Controller/AppController.php
@@ -78,6 +78,9 @@ public function initialize(): void {
// COmanage specific component that handles authn/z processintg
$this->loadComponent('RegistryAuth');
+ // Breadcrumb Manager
+ $this->loadComponent('Breadcrumb');
+
$ChangelogEventListener = new ChangelogEventListener($this->RegistryAuth);
EventManager::instance()->on($ChangelogEventListener);
@@ -145,13 +148,6 @@ public function beforeRender(\Cake\Event\EventInterface $event) {
$this->set('vv_menu_permissions', $this->RegistryAuth->getMenuPermissions($this->getCOID()));
}
- // For breadcrumbs, do we have a target model, and if so is it a configuration
- // model (eg: ApiUsers) or an object model (eg: CoPeople)?
- if(isset($this->$modelsName) // May not be set under certain error conditions
- && method_exists($this->$modelsName, "getIsConfigurationTable")) {
- $this->set('vv_is_configuration_model', $this->$modelsName->getIsConfigurationTable());
- }
-
return parent::beforeRender($event);
}
@@ -194,7 +190,7 @@ public function getCOID(): ?int {
* @throws \RuntimeException
*/
- protected function getPrimaryLink(bool $lookup=false) {
+ public function getPrimaryLink(bool $lookup=false) {
// Did we already figure this out? (But only if $lookup)
if($lookup && isset($this->cur_pl->value)) {
return $this->cur_pl;
diff --git a/app/src/Controller/CoSettingsController.php b/app/src/Controller/CoSettingsController.php
index 626372ab5..0ca438200 100644
--- a/app/src/Controller/CoSettingsController.php
+++ b/app/src/Controller/CoSettingsController.php
@@ -33,6 +33,19 @@
use Cake\Log\Log;
class CoSettingsController extends StandardController {
+ /**
+ * Perform Controller initialization.
+ *
+ * @since COmanage Registry v5.0.0
+ */
+
+ public function initialize(): void {
+ parent::initialize();
+
+ // Configure breadcrumb rendering
+ $this->Breadcrumb->skipParents(['/^\/co-settings\/edit/']);
+ }
+
/**
* Manage CO Settings.
*
diff --git a/app/src/Controller/Component/BreadcrumbComponent.php b/app/src/Controller/Component/BreadcrumbComponent.php
new file mode 100644
index 000000000..eeb22f2df
--- /dev/null
+++ b/app/src/Controller/Component/BreadcrumbComponent.php
@@ -0,0 +1,229 @@
+getSubject();
+ $request = $controller->getRequest();
+ $modelsName = $controller->getName();
+
+ if(!$request->is('restful')) {
+ // Determine the request target, but strip off query params
+ $requestTarget = $request->getRequestTarget(false);
+
+ $skipAll = false;
+ $skipConfig = false;
+
+ foreach($this->skipAllPaths as $p) {
+ if(preg_match($p, $requestTarget)) {
+ $skipAll = true;
+ break;
+ }
+ }
+
+ foreach($this->skipConfigPaths as $p) {
+ if(preg_match($p, $requestTarget)) {
+ $skipConfig = true;
+ break;
+ }
+ }
+
+ // Determine if the current request maps to a path where
+ // breadcrumb rendering should be skipped in whole or in part
+ $controller->set('vv_bc_skip', $skipAll);
+
+ $controller->set('vv_bc_skip_config', $skipConfig);
+
+ // Do we have a target model, and if so is it a configuration
+ // model (eg: ApiUsers) or an object model (eg: CoPeople)?
+ if(isset($controller->$modelsName) // May not be set under certain error conditions
+ && method_exists($controller->$modelsName, "isConfigurationTable")) {
+ $controller->set('vv_bc_configuration_link', $controller->$modelsName->isConfigurationTable());
+ } else {
+ $controller->set('vv_bc_configuration_link', false);
+ }
+
+ // Build a list of intermediate parent links, starting with any
+ // injected parents. This overrides $skipParentPaths.
+ $parents = $this->injectParents;
+
+ $skipParent = false;
+
+ foreach($this->skipParentPaths as $p) {
+ if(preg_match($p, $requestTarget)) {
+ $skipParent = true;
+ break;
+ }
+ }
+
+ if(!$skipParent) {
+ // For non-index views, insert a link back to the index.
+ $action = $request->getParam('action');
+ $primaryLink = $controller->getPrimaryLink(true);
+
+ if($action != 'index') {
+ $target = [
+ 'plugin' => null,
+ 'controller' => $modelsName,
+ 'action' => 'index'
+ ];
+
+ if(!empty($primaryLink->attr)) {
+ $target['?'] = [$primaryLink->attr => $primaryLink->value];
+ }
+
+ $parents[] = [
+ 'label' => __d('controller', $modelsName, [99]),
+ 'target' => $target
+ ];
+ }
+ }
+
+ $controller->set('vv_bc_parents', $parents);
+ }
+ }
+
+ /**
+ * Inject the primary link into the breadcrumb path.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param object link Primary Link (as returned by getPrimaryLink())
+ */
+
+ public function injectPrimaryLink(object $link) {
+ // eg: "People"
+ $modelsName = StringUtilities::foreignKeyToClassName($link->attr);
+
+ $contain = [];
+
+ if($modelsName == 'People' || $modelsName == 'ExternalIdentities') {
+ // We need the Primary Name to render it
+ $contain[] = 'PrimaryName';
+ }
+
+ $linkTable = TableRegistry::getTableLocator()->get($modelsName);
+ $linkObj = $linkTable->get($link->value, ['contain' => $contain]);
+ $displayField = $linkTable->getDisplayField();
+
+ $this->injectParents[] = [
+ 'target' => [
+ 'plugin' => null,
+ 'controller' => $modelsName,
+ 'action' => 'index',
+ '?' => [
+ 'co_id' => $link->co_id
+ ]
+ ],
+ 'label' => __d('controller', $modelsName, [99])
+ ];
+
+ $label = $linkObj->$displayField;
+
+ if(!empty($linkObj->primary_name)) {
+ $label = $linkObj->primary_name->full_name;
+ }
+
+ // If we don't have a visible label use the record ID
+ if(empty($label)) {
+ $label = $linkObj->id;
+ }
+
+ $this->injectParents[] = [
+ 'target' => [
+ 'plugin' => null,
+ 'controller' => $modelsName,
+ 'action' => 'edit',
+ $linkObj->id
+ ],
+ 'label' => $label
+ ];
+ }
+
+ /**
+ * Set the set of paths that should be skipped when rendering breadcrumbs.
+ * Paths are specified as regular expressions, eg: '/^\/cos\/select/'
+ *
+ * @since COmanage Registry v5.0.0
+ * @param array $skipPaths Array of regular expressions describing paths to be skipped
+ */
+
+ public function skipAll(array $skipPaths) {
+ $this->skipAllPaths = $skipPaths;
+ }
+
+ /**
+ * Set the set of paths which should not get a "configuration" breadcrumb even
+ * though they might otherwise ordinarily get one (by being configuration objects).
+ * Paths are specified as regular expressions, eg: '/^\/provisioning-targets\/status/'
+ *
+ * @since COmanage Registry v5.0.0
+ * @param array $skipPaths Array of regular expressions describing paths
+ */
+
+ public function skipConfig(array $skipPaths) {
+ $this->skipConfigPaths = $skipPaths;
+ }
+
+ /**
+ * Set the set of paths that should not automatically get a link back to their parent.
+ * Paths are specified as regular expressions, eg: '/^\/co-settings\/edit/'
+ *
+ * @since COmanage Registry v5.0.0
+ * @param array $skipPaths Array of regular expressions describing paths
+ */
+
+ public function skipParents(array $skipPaths) {
+ $this->skipParentPaths = $skipPaths;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Controller/CosController.php b/app/src/Controller/CosController.php
index 604d58339..5156077a1 100644
--- a/app/src/Controller/CosController.php
+++ b/app/src/Controller/CosController.php
@@ -42,6 +42,19 @@ class CosController extends StandardController {
]
];
+ /**
+ * Perform Controller initialization.
+ *
+ * @since COmanage Registry v5.0.0
+ */
+
+ public function initialize(): void {
+ parent::initialize();
+
+ // Configure breadcrumb rendering
+ $this->Breadcrumb->skipAll(['/^\/cos\/select/']);
+ }
+
/**
* Callback run prior to the view rendering.
*
diff --git a/app/src/Controller/DashboardsController.php b/app/src/Controller/DashboardsController.php
index 970494723..5e074eb98 100644
--- a/app/src/Controller/DashboardsController.php
+++ b/app/src/Controller/DashboardsController.php
@@ -37,6 +37,57 @@
//use \App\Lib\Enum\PermissionEnum;
class DashboardsController extends StandardController {
+ /**
+ * Perform Controller initialization.
+ *
+ * @since COmanage Registry v5.0.0
+ */
+
+ public function initialize(): void {
+ parent::initialize();
+
+ // Configure breadcrumb rendering
+ $this->Breadcrumb->skipConfig([
+ '/^\/dashboards\/artifacts/',
+ '/^\/dashboards\/dashboard/',
+ '/^\/dashboards\/registries/'
+ ]);
+ // There is currently no inventory of dashboards, so we skip parents
+ // for configuration, dashboard, and registries actions
+ $this->Breadcrumb->skipParents(['/^\/dashboards/']);
+ }
+
+ /**
+ * Render the CO Registries Dashboard.
+ *
+ * @since COmanage Registry v5.0.0
+ */
+
+ public function artifacts() {
+ $cur_co = $this->getCO();
+
+ $this->set('vv_title', __d('menu', 'artifacts', [$cur_co->name]));
+
+ // Construct the set of primary registry objects, which
+ // we want to order by the localized text string.
+
+ // We're assuming that the permission for each of these items is the same as for
+ // registries() itself, ie: CMP or CO Admin. But plausibly some of this stuff
+ // could be delegated to (eg) a COU Admin at some point...
+
+ $artifactMenuItems = [
+ __d('controller', 'Jobs', [99]) => [
+ 'icon' => 'assignment',
+ 'controller' => 'jobs',
+ 'action' => 'index'
+ ]
+ ];
+
+ ksort($artifactMenuItems);
+
+ $this->set('vv_artifacts_menu_items', $artifactMenuItems);
+ }
+
/**
* Render the CO Configuration Dashboard.
*
@@ -66,6 +117,12 @@ public function configuration() {
'controller' => 'cous',
'action' => 'index'
],
+ // XXX External Identity Sources should use "cloud_download" for the icon
+ __d('controller', 'ProvisioningTargets', [99]) => [
+ 'icon' => 'cloud_upload',
+ 'controller' => 'provisioning_targets',
+ 'action' => 'index'
+ ],
__d('controller', 'Reports', [99]) => [
'icon' => 'summarize',
'controller' => 'reports',
@@ -128,6 +185,47 @@ public function dashboard(?int $id=null) {
// XXX placeholder
}
+ /**
+ * Render the CO Registries Dashboard.
+ *
+ * @since COmanage Registry v5.0.0
+ */
+
+ public function registries() {
+ $cur_co = $this->getCO();
+
+ $this->set('vv_title', __d('menu', 'registries', [$cur_co->name]));
+
+ // Construct the set of primary registry objects, which
+ // we want to order by the localized text string.
+
+ // We're assuming that the permission for each of these items is the same as for
+ // registries() itself, ie: CMP or CO Admin. But plausibly some of this stuff
+ // could be delegated to (eg) a COU Admin at some point...
+
+ $registryMenuItems = [
+ __d('controller', 'Groups', [99]) => [
+ 'icon' => 'people',
+ 'controller' => 'groups',
+ 'action' => 'index'
+ ],
+ __d('controller', 'People', [99]) => [
+ 'icon' => 'person',
+ 'controller' => 'people',
+ 'action' => 'index'
+ ],
+ __d('controller', 'Servers', [99]) => [
+ 'icon' => 'computer',
+ 'controller' => 'servers',
+ 'action' => 'index'
+ ]
+ ];
+
+ ksort($registryMenuItems);
+
+ $this->set('vv_registries_menu_items', $registryMenuItems);
+ }
+
/**
* Perform a cross model search.
*
diff --git a/app/src/Controller/MVEAController.php b/app/src/Controller/MVEAController.php
index 7a5fb97f0..4d1204b30 100644
--- a/app/src/Controller/MVEAController.php
+++ b/app/src/Controller/MVEAController.php
@@ -33,8 +33,45 @@
use Cake\Log\Log;
use Cake\ORM\TableRegistry;
use Cake\Utility\Inflector;
+use \App\Lib\Util\StringUtilities;
class MVEAController extends StandardController {
+ /**
+ * Callback run prior to the request action.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param EventInterface $event Cake Event
+ * @return \Cake\Http\Response HTTP Response
+ */
+
+ public function beforeFilter(\Cake\Event\EventInterface $event) {
+ // $this->name = Models
+ $modelsName = $this->name;
+
+ if(!$this->request->is('restful')) {
+ // Provide additional hints to BreadcrumbsComponent. This needs to be here
+ // and not in beforeRender because the component beforeRender will run first.
+
+ // This is all we need where person_id is the primary link, but for MVEAs
+ // that are more deeply linked (to person_role_id, external_identity_id,
+ // or external_identity_role_id) we need to look up the further links.
+ $primaryLink = $this->getPrimaryLink(true);
+
+ if($primaryLink->attr == 'person_id' || $primaryLink->attr == 'group_id') {
+ $this->Breadcrumb->injectPrimaryLink($primaryLink);
+ } else {
+ $parentModel = StringUtilities::foreignKeyToClassName($primaryLink->attr);
+
+ $parentPrimaryLink = $this->$modelsName->$parentModel->findPrimaryLink((int)$primaryLink->value);
+
+ $this->Breadcrumb->injectPrimaryLink($parentPrimaryLink);
+ $this->Breadcrumb->injectPrimaryLink($primaryLink);
+ }
+ }
+
+ return parent::beforeFilter($event);
+ }
+
/**
* Callback run prior to the request render.
*
@@ -49,67 +86,6 @@ public function beforeRender(\Cake\Event\EventInterface $event) {
$fieldName = Inflector::underscore(Inflector::singularize($modelsName));
if(!$this->request->is('restful')) {
- // Use the PrimaryLink to set information for breadcrumbs
-
- $link = $this->getPrimaryLink(true);
-
- if(!empty($link->value)) {
- $this->set('vv_primary_link_attr', $link->attr);
- $this->set('vv_primary_link_id', $link->value);
-
- $Names = $this->getTableLocator()->get('Names');
-
- switch($link->attr) {
- case 'external_identity_role_id':
- $ExternalIdentityRoles = $this->getTableLocator()->get('ExternalIdentityRoles');
- $roleEntity = $ExternalIdentityRoles->findById((int)$link->value)->firstOrFail();
-
- // Note this is a string, but vv_person_name is an entity
- $this->set('vv_ei_role', $ExternalIdentityRoles->generateDisplayField($roleEntity));
- $this->set('vv_ei_role_id', $link->value);
- // fall through
- case 'external_identity_id':
- $ExternalIdentity = $this->getTableLocator()->get('ExternalIdentities');
-
- // What's the Person ID for the ExternalIdentity?
- $eiId = isset($roleEntity) ? $roleEntity->external_identity_id : $link->value;
-
- $externalIdentity = $ExternalIdentity->findById($eiId)->firstOrFail();
-
- // What's the primary name for the Extarnal Identity?
- $this->set('vv_ei_name', $Names->primaryName($externalIdentity->id, 'external_identity'));
- $this->set('vv_ei_id', $externalIdentity->id);
-
- // What's the primary name of the Person?
- $personName = $Names->primaryName($externalIdentity->person_id);
- $this->set('vv_person_name', $personName);
- $this->set('vv_supertitle', $personName->full_name);
- $this->set('vv_person_id', $externalIdentity->person_id);
- break;
- case 'person_role_id':
- $PersonRoles = $this->getTableLocator()->get('PersonRoles');
- $roleEntity = $PersonRoles->findById((int)$link->value)->firstOrFail();
- // Note this is a string, but vv_person_name is an entity
- $this->set('vv_person_role', $PersonRoles->generateDisplayField($roleEntity));
- $this->set('vv_person_role_id', $link->value);
-
- // Also set a name
- $personName = $Names->primaryName($roleEntity->person_id);
- $this->set('vv_person_name', $personName);
- $this->set('vv_supertitle', $personName->full_name);
- $this->set('vv_person_id', $roleEntity->person_id);
- break;
- case 'person_id':
- $personName = $Names->primaryName((int)$link->value);
- $this->set('vv_person_name', $personName);
- $this->set('vv_supertitle', $personName->full_name);
- $this->set('vv_person_id', $link->value);
- break;
- default;
- break;
- }
- }
-
// If there is a default type setting for this model, pass it to the view
if($this->$modelsName->getSchema()->hasColumn('type_id')) {
$defaultTypeField = "default_" . $fieldName . "_type_id";
diff --git a/app/src/Controller/PagesController.php b/app/src/Controller/PagesController.php
index 5ad47405e..6dc732847 100644
--- a/app/src/Controller/PagesController.php
+++ b/app/src/Controller/PagesController.php
@@ -30,7 +30,22 @@
* @link https://book.cakephp.org/4/en/controllers/pages-controller.html
*/
class PagesController extends AppController
-{
+{
+ /**
+ * Perform Cake Model initialization.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param array $config Configuration options passed to constructor
+ */
+
+ public function initialize(): void
+ {
+ parent::initialize();
+
+ // Configure breadcrumb rendering
+ $this->Breadcrumb->skipAll(['/^\/$/']);
+ }
+
/**
* Displays a view
*
diff --git a/app/src/Controller/ProvisioningHistoryRecordsController.php b/app/src/Controller/ProvisioningHistoryRecordsController.php
new file mode 100644
index 000000000..888e2f031
--- /dev/null
+++ b/app/src/Controller/ProvisioningHistoryRecordsController.php
@@ -0,0 +1,61 @@
+ [
+ 'ProvisioningHistoryRecords.id' => 'desc'
+ ]
+ ];
+
+ /**
+ * Callback run prior to the request action.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param EventInterface $event Cake Event
+ * @return \Cake\Http\Response HTTP Response
+ */
+
+ public function beforeFilter(\Cake\Event\EventInterface $event) {
+ if(!$this->request->is('restful')) {
+ // Provide additional hints to BreadcrumbsComponent. This needs to be here
+ // and not in beforeRender because the component beforeRender will run first.
+
+ $this->Breadcrumb->injectPrimaryLink($this->getPrimaryLink(true));
+ }
+
+ return parent::beforeFilter($event);
+ }
+}
\ No newline at end of file
diff --git a/app/src/Controller/ProvisioningTargetsController.php b/app/src/Controller/ProvisioningTargetsController.php
new file mode 100644
index 000000000..0f9f8fef2
--- /dev/null
+++ b/app/src/Controller/ProvisioningTargetsController.php
@@ -0,0 +1,101 @@
+ [
+ 'ProvisioningTargets.description' => 'asc'
+ ]
+ ];
+
+ /**
+ * Perform Controller initialization.
+ *
+ * @since COmanage Registry v5.0.0
+ */
+
+ public function initialize(): void {
+ parent::initialize();
+
+ // Configure breadcrumb rendering
+ $this->Breadcrumb->skipConfig(['/^\/provisioning-targets\/status/']);
+ $this->Breadcrumb->skipParents(['/^\/provisioning-targets\/status/']);
+ }
+
+ /**
+ * Callback run prior to the request action.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param EventInterface $event Cake Event
+ * @return \Cake\Http\Response HTTP Response
+ */
+
+ public function beforeFilter(\Cake\Event\EventInterface $event) {
+ if(!$this->request->is('restful')) {
+ // Provide additional hints to BreadcrumbsComponent. This needs to be here
+ // and not in beforeRender because the component beforeRender will run first.
+
+ if($this->request->getParam('action') == 'status') {
+ $this->Breadcrumb->injectPrimaryLink($this->getPrimaryLink(true));
+ }
+ }
+
+ return parent::beforeFilter($event);
+ }
+
+ /**
+ * Generate a status index.
+ *
+ * @since COmanage Registry v5.0.0
+ */
+
+ public function status() {
+ // PrimaryLinkTrait - Look up our primary link to see which object type we're
+ // working with, an also get our CO ID
+ $link = $this->getPrimaryLink(true);
+
+ if($link->attr == 'person_id') {
+ $statuses = $this->ProvisioningTargets->status(coId: $link->co_id, personId: (int)$link->value);
+ } elseif($link->attr == 'group_id') {
+ $statuses = $this->ProvisioningTargets->status(coId: $link->co_id, groupId: (int)$link->value);
+ }
+
+ $this->set('vv_provisioning_statuses', $statuses);
+
+ if(!$this->request->is('restful')) {
+ $this->set('vv_title', __d('operation', 'provisioning.status'));
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/Controller/ServersController.php b/app/src/Controller/ServersController.php
new file mode 100644
index 000000000..9cb5931f2
--- /dev/null
+++ b/app/src/Controller/ServersController.php
@@ -0,0 +1,41 @@
+ [
+ 'Servers.description' => 'asc'
+ ]
+ ];
+}
\ No newline at end of file
diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php
index e3490a4ad..f88cd1eaa 100644
--- a/app/src/Controller/StandardController.php
+++ b/app/src/Controller/StandardController.php
@@ -31,6 +31,7 @@
use InvalidArgumentException;
use \Cake\Http\Exception\BadRequestException;
+use \App\Lib\Enum\ProvisioningContextEnum;
use \App\Lib\Enum\SuspendableStatusEnum;
use \App\Lib\Util\StringUtilities;
@@ -60,6 +61,12 @@ public function add() {
if($table->save($obj)) {
$this->Flash->success(__d('result', 'saved'));
+ // Trigger provisioning, letting errors bubble up (AR-GMR-5)
+ if(method_exists($table, "requestProvisioning")) {
+ $this->llog('rule', "AR-GMR-5 Requesting provisioning for $modelsName " . $obj->id);
+ $table->requestProvisioning(id: $obj->id, context: ProvisioningContextEnum::Automatic);
+ }
+
// If this is a Pluggable Model, instantiate the plugin and redirect
// into the Entry Point Model
if(!empty($obj->plugin) && method_exists($this, "instantiatePlugin")) {
@@ -209,6 +216,14 @@ public function delete($id) {
$this->Flash->success(__d('result', 'deleted'));
}
+ // Trigger provisioning, letting errors bubble up (AR-GMR-5)
+ // In general, tables should check that they were passed a deleted
+ // record and martial data/set eligibility appropriately
+ if(method_exists($table, "requestProvisioning")) {
+ $this->llog('rule', "AR-GMR-5 Requesting provisioning for deleted entity $modelsName " . $obj->id);
+ $table->requestProvisioning(id: (int)$id, context: ProvisioningContextEnum::Automatic);
+ }
+
// Return to index since there is no delete view
return $this->generateRedirect(null);
}
@@ -313,6 +328,12 @@ public function edit(string $id) {
if($table->save($saveObj)) {
$this->Flash->success(__d('result', 'saved'));
+ // Trigger provisioning, letting errors bubble up (AR-GMR-5)
+ if(method_exists($table, "requestProvisioning")) {
+ $this->llog('rule', "AR-GMR-5 Requesting provisioning for $modelsName " . $obj->id);
+ $table->requestProvisioning(id: (int)$id, context: ProvisioningContextEnum::Automatic);
+ }
+
return $this->generateRedirect((int)$id);
}
@@ -593,8 +614,13 @@ protected function populateAutoViewVars(object $obj=null) {
$this->set($vvar, array_combine($avv['array'], $avv['array']));
break;
case 'enum':
- // We just want the localized text strings for the defined constants
+ // We just want the localized text strings for the defined constants.
$class = '\\App\\Lib\\Enum\\'.$avv['class'];
+ // We support plugin notation for plugin defined enumerations.
+ if(strstr($avv['class'], ".")) {
+ $bits = explode('.', $avv['class'], 2);
+ $class = '\\'.$bits[0].'\\Lib\\Enum\\'.$bits[1];
+ }
$this->set($vvar, $class::getLocalizedConsts());
break;
// "auxiliary" and "select" do basically the same thing, but the former
@@ -695,11 +721,56 @@ protected function populateAutoViewVars(object $obj=null) {
}
}
+ /**
+ * Handle a provisioning request for a Standard object.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param string $id Object ID
+ */
+
+ public function provision($id) {
+ // $this->name = Models
+ $modelsName = $this->name;
+ // $table = the actual table object
+ $table = $this->$modelsName;
+ // $tableName = models
+ $tableName = $table->getTable();
+
+ // Note that only Primary Models support provisioning, but those that
+ // don't won't have permission to execute this function.
+
+ try {
+ $table->requestProvisioning(
+ id: (int)$id,
+ context: ProvisioningContextEnum::Manual,
+ provisioningTargetId: (int)$this->getRequest()->getQuery('provisioning_target_id')
+ );
+ }
+ catch(\Exception $e) {
+ $this->Flash->error($e->getMessage());
+ }
+
+ // We don't render any flash messages since they could get complex
+ // depending on what was provisioned, so instead we redirect into the
+ // provisioning status index for the object.
+ // Redirect to the provisioning status view
+
+ $redirect = [
+ 'controller' => 'ProvisioningTargets',
+ 'action' => 'status',
+ '?' => [
+ StringUtilities::tableToForeignKey($table) => $id
+ ]
+ ];
+
+ return $this->redirect($redirect);
+ }
+
/**
* Handle a view action for a Standard object.
*
* @since COmanage Registry v5.0.0
- * @param Integer $id Object ID
+ * @param string $id Object ID
*/
public function view($id = null) {
diff --git a/app/src/Controller/StandardPluggableController.php b/app/src/Controller/StandardPluggableController.php
index 0268a3126..0fb8234b7 100644
--- a/app/src/Controller/StandardPluggableController.php
+++ b/app/src/Controller/StandardPluggableController.php
@@ -57,7 +57,7 @@ public function configure(string $id) {
$pluginTable = $this->getTableLocator()->get($parentObj->plugin);
$pluginObj = $pluginTable->find()
- ->where(['report_id' => $parentId])
+ ->where([StringUtilities::tableToForeignKey($table) => $parentId])
->firstOrFail();
return $this->redirect([
@@ -85,8 +85,11 @@ protected function instantiatePlugin(object $obj) {
// For now, we just populate the foreign key from the instantiated plugin
// to its parent object, but we might want to allow the plugin model to
// set some default values.
+ $created = new \Datetime('now');
+
$iValues = [
- $parentKey => $obj->id
+ $parentKey => $obj->id,
+ 'created' => $created->format('Y-m-d H:i:s')
];
$pTable = $this->getTableLocator()->get($obj->plugin);
diff --git a/app/src/Controller/StandardPluginController.php b/app/src/Controller/StandardPluginController.php
index 6f705c2d7..4a5553948 100644
--- a/app/src/Controller/StandardPluginController.php
+++ b/app/src/Controller/StandardPluginController.php
@@ -36,6 +36,74 @@
use \App\Lib\Enum\SuspendableStatusEnum;
class StandardPluginController extends StandardController {
+ /**
+ * Callback run prior to the request action.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param EventInterface $event Cake Event
+ * @return \Cake\Http\Response HTTP Response
+ */
+
+ public function beforeFilter(\Cake\Event\EventInterface $event) {
+ // $this->name = Models
+ $modelsName = $this->name;
+
+ if(!$this->request->is('restful')) {
+ // Provide additional hints to BreadcrumbsComponent. This needs to be here
+ // and not in beforeRender because the component beforeRender will run first.
+
+ // This is all we need where person_id is the primary link, but for MVEAs
+ // that are more deeply linked (to person_role_id, external_identity_id,
+ // or external_identity_role_id) we need to look up the further links.
+ $primaryLink = $this->getPrimaryLink(true);
+
+ $this->Breadcrumb->skipParents(['/^\/[a-zA-Z0-9-]+\/[a-zA-Z0-9-]+\/edit\//']);
+
+ if($primaryLink->attr == 'server_id') {
+ // Servers shouldn't show up as configuration, so automatically hide it
+ // eg for server plugins
+ $this->Breadcrumb->skipConfig(['/^\//']);
+ }
+
+ $this->Breadcrumb->injectPrimaryLink($primaryLink);
+ }
+
+ return parent::beforeFilter($event);
+ }
+
+ /**
+ * Callback run prior to the request render.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param EventInterface $event Cake Event
+ */
+
+ public function beforeRender(\Cake\Event\EventInterface $event) {
+ // $this->name = Models (ie: from ModelsTable, eg FileProvisionersTable)
+ $modelsName = $this->name;
+ // $table = the actual table object
+ $table = $this->$modelsName;
+
+ $link = $this->getPrimaryLink(true);
+
+ if(!empty($link->value)) {
+ $parentClassName = StringUtilities::foreignKeyToClassName($link->attr);
+
+ $parentObj = $table->$parentClassName->get($link->value);
+ $parentDisplayField = $table->$parentClassName->getDisplayField();
+
+ $this->set('vv_bc_parent_obj', $parentObj);
+ $this->set('vv_bc_parent_displayfield', $parentDisplayField);
+
+ // Override the title set in StandardController. Since that was set in edit()
+ // which is called before the rendering hooks, this title will take precedence.
+
+ $this->set('vv_title', __d('operation', 'configure.a', $parentObj->$parentDisplayField));
+ }
+
+ return parent::beforeRender($event);
+ }
+
/**
* Determine the filesystem path to a file within a plugin.
*
diff --git a/app/src/Lib/Enum/ProvisionerModeEnum.php b/app/src/Lib/Enum/ProvisionerModeEnum.php
new file mode 100644
index 000000000..ec48a4d37
--- /dev/null
+++ b/app/src/Lib/Enum/ProvisionerModeEnum.php
@@ -0,0 +1,39 @@
+getConstants();
- $className = substr(strrchr(get_called_class(), '\\'), 1);
-
- foreach(array_values($consts) as $key) {
- $ret[$key] = __d('enumeration', $className.'.'.$key);
+ // get_called_class() will return something like App\Lib\Enum\StatusEnum
+ // or CoreServer\Lib\Enum\RdbmsTypeEnum
+ $classBits = explode('\\', get_called_class(), 4);
+
+ if($classBits[0] == 'App') {
+ foreach(array_values($consts) as $key) {
+ $ret[$key] = __d('enumeration', $classBits[3].'.'.$key);
+ }
+ } else {
+ $pluginDomain = Inflector::underscore($classBits[0]);
+
+ foreach(array_values($consts) as $key) {
+ $ret[$key] = __d($pluginDomain, 'enumeration.'.$classBits[3].'.'.$key);
+ }
}
return $ret;
diff --git a/app/src/Lib/Traits/PluggableModelTrait.php b/app/src/Lib/Traits/PluggableModelTrait.php
index bd31c5ae4..8bb384fc8 100644
--- a/app/src/Lib/Traits/PluggableModelTrait.php
+++ b/app/src/Lib/Traits/PluggableModelTrait.php
@@ -35,6 +35,9 @@
use App\Lib\Util\StringUtilities;
trait PluggableModelTrait {
+ // The set of plugin entry point models used in configurations for this model
+ protected $_pluginModels = [];
+
/**
* Determine the plugin type used by this Pluggable Model. This is the lowercased
* singular prefix of the Pluggable Model Table name. eg: For "ReportsTable" the
@@ -73,7 +76,7 @@ protected function instantiatePluginModel(string $pmodel, string $path) {
// models that do not represent Cake Tables, we can't use the TableLocator here,
// we just use plain PHP "new".
- $pluginClassName = "\\" . $pluginName . "\\" . $path . "\\" . $pluginModel;
+ $pluginClassName = "\\" . $pluginName . $path . "\\" . $pluginModel;
$pClass = new $pluginClassName();
return $pClass;
@@ -111,12 +114,22 @@ protected function setPluginRelations() {
->all();
foreach($models as $m) {
- $this->hasMany($m->plugin)
+ // In general, a model with a "plugin" field has a 1-1 relation
+ // with the instantiated plugin configuration. eg: One instance
+ // of a Server has exactly one SqlServer associated with it.
+ $this->hasOne($m->plugin)
->setDependent(true)
->setCascadeCallbacks(true);
+
+ // Cache the list of entry points that we found
+ $this->_pluginModels[] = $m->plugin;
}
- if($this->isConfigurationTable()) {
+ // isArtifactTable() might not be the exact right test here...
+ // for now, we only want to exclude Jobs (since there's nothing
+ // to configure) but this may change.
+
+ if(!$this->isArtifactTable()) {
$this->setAllowLookupPrimaryLink(['configure']);
}
}
diff --git a/app/src/Lib/Traits/PrimaryLinkTrait.php b/app/src/Lib/Traits/PrimaryLinkTrait.php
index 0ee4f1eba..3e4f4b800 100644
--- a/app/src/Lib/Traits/PrimaryLinkTrait.php
+++ b/app/src/Lib/Traits/PrimaryLinkTrait.php
@@ -200,7 +200,11 @@ public function findPrimaryLink(int $id) {
// should be set. Return the first one we find.
foreach(array_keys($this->primaryLinks) as $plKey) {
if(!empty($obj->$plKey)) {
- return (object)['attr' => $plKey, 'value' => $obj->$plKey];
+ return (object)[
+ 'attr' => $plKey,
+ 'value' => $obj->$plKey,
+ 'co_id' => $this->calculateCoForRecord($obj)
+ ];
}
}
@@ -407,7 +411,7 @@ public function setAllowEmptyPrimaryLink(bool $allowEmpty) {
* Set whether the primary link can be resolved via the object ID in the URL.
*
* @since COmanage Registry v5.0.0
- * @param boolean $allowEmpty true if the primary link can be resolved via the URL ID
+ * @param array $actions Actions where the primary link can be obtained by looking up the record ID
*/
public function setAllowLookupPrimaryLink(array $actions) {
diff --git a/app/src/Lib/Traits/ProvisionableTrait.php b/app/src/Lib/Traits/ProvisionableTrait.php
new file mode 100644
index 000000000..349a33146
--- /dev/null
+++ b/app/src/Lib/Traits/ProvisionableTrait.php
@@ -0,0 +1,83 @@
+marshalProvisioningData($id);
+
+ // Invocation of the plugins is handled by the Pluggable table
+ $ProvisioningTargets = TableRegistry::getTableLocator()->get('ProvisioningTargets');
+
+ $ProvisioningTargets->provision(
+ data: $data['data'],
+ eligibility: $data['eligibility'],
+ context: $context,
+ id: $provisioningTargetId
+ );
+ } else {
+ // This is a secondary model, eg Names. We need to figure out the primary model
+ // and then request provisioning on that one instead.
+
+ $primaryLink = $this->findPrimaryLink($id);
+
+ $parentTableName = StringUtilities::foreignKeyToClassName($primaryLink->attr);
+
+ $this->$parentTableName->requestProvisioning(
+ id: $primaryLink->value,
+ context: $context,
+ provisioningTargetId: $provisioningTargetId
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/Lib/Traits/ProvisionerTrait.php b/app/src/Lib/Traits/ProvisionerTrait.php
new file mode 100644
index 000000000..e229e679f
--- /dev/null
+++ b/app/src/Lib/Traits/ProvisionerTrait.php
@@ -0,0 +1,69 @@
+provisonableModels;
+ }
+
+ /**
+ * Determine if the requested Model is supported by this Provisioner.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param string $model Model to check
+ * @return bool True if $model is supported, false otherwise
+ */
+
+ public function isProvisionableModel(string $model): bool {
+ return in_array($model, $this->provisionableModels);
+ }
+
+ /**
+ * Set the supported Provisionable Models.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param array $models Array of supported Provisionable Models
+ */
+
+ public function setProvisionableModels(array $models) {
+ $this->provisionableModels = $models;
+ }
+}
diff --git a/app/src/Lib/Traits/ValidationTrait.php b/app/src/Lib/Traits/ValidationTrait.php
index d3a3862cc..fdad31de5 100644
--- a/app/src/Lib/Traits/ValidationTrait.php
+++ b/app/src/Lib/Traits/ValidationTrait.php
@@ -73,6 +73,7 @@ public function registerPrimaryKeyValidation(Validator $validator, array $primar
* @param TableSchemaInterface $schema Cake Schema
* @param string $field Field name
* @param bool $required Whether this field is required
+ * @param string $prefix Require the value to start with $prefix
* @return Validator Cake Validator
*/
diff --git a/app/src/Lib/Util/SchemaManager.php b/app/src/Lib/Util/SchemaManager.php
index 888ba8f07..36647ed06 100644
--- a/app/src/Lib/Util/SchemaManager.php
+++ b/app/src/Lib/Util/SchemaManager.php
@@ -65,14 +65,15 @@ class SchemaManager {
* Construct a new SchemaManager.
*
* @since COmanage Registry v5.0.0
- * @param ConsoleIo $io Cake ConsoleIo object
+ * @param ConsoleIo $io Cake ConsoleIo object
+ * @param string $connection Database connection name
*/
- public function __construct(?ConsoleIo $io=null) {
+ public function __construct(?ConsoleIo $io=null, string $connection='default') {
if($io) $this->io = $io;
// Use the ConnectionManager to get the database config to pass to DBAL.
- $db = ConnectionManager::get('default');
+ $db = ConnectionManager::get($connection);
// $db is a ConnectionInterface object
$cfg = $db->config();
@@ -100,12 +101,18 @@ public function __construct(?ConsoleIo $io=null) {
* Apply a schema file.
*
* @since COmanage Registry v5.0.0
- * @param string $schemaFile Schema file to apply
- * @param bool $parseOnly If true, attempt to parse the file only, but perform no other actions
- * @param bool $diffOnly If true, generate a diff against the current database state, but do not apply it
+ * @param string $schemaFile Schema file to apply
+ * @param bool $parseOnly If true, attempt to parse the file only, but perform no other actions
+ * @param bool $diffOnly If true, generate a diff against the current database state, but do not apply it
+ * @param string $tablePrefix String to prefix to table names
*/
- public function applySchemaFile(string $schemaFile, bool $parseOnly=false, bool $diffOnly=false) {
+ public function applySchemaFile(
+ string $schemaFile,
+ bool $parseOnly=false,
+ bool $diffOnly=false,
+ string $tablePrefix=""
+ ) {
if(!is_readable($schemaFile)) {
throw new \RuntimeException(__d('error', 'file', [$schemaFile]));
}
@@ -141,16 +148,17 @@ public function applySchemaFile(string $schemaFile, bool $parseOnly=false, bool
*
* @since COmanage Registry v5.0.0
* @param object $schemaObject Schema object
+ * @param string $tablePrefix String to prefix to table names
*/
- public function applySchemaObject(object $schemaObject) {
+ public function applySchemaObject(object $schemaObject, string $tablePrefix="") {
if(!$this->columnLibrary) {
// We need the column library from the core config
$this->applySchemaFile(schemaFile: ROOT . DS . 'config' . DS . 'schema' . DS . 'schema.json',
parseOnly: true);
}
- $this->processSchema(schemaConfig: $schemaObject);
+ $this->processSchema(schemaConfig: $schemaObject, tablePrefix: $tablePrefix);
}
/**
@@ -159,15 +167,20 @@ public function applySchemaObject(object $schemaObject) {
* @since COmanage Registry v5.0.0
* @param object $schemaConfig Schema object
* @param bool $diffOnly If true, generate a diff against the current database state, but do not apply it
+ * @param string $tablePrefix String to prefix to table names
*/
- protected function processSchema(object $schemaConfig, bool $diffOnly=false) {
+ protected function processSchema(
+ object $schemaConfig,
+ bool $diffOnly=false,
+ string $tablePrefix=""
+ ) {
$schema = new Schema();
// Walk through $schemaConfig and build our schema in DBAL format.
foreach($schemaConfig->tables as $tName => $tCfg) {
- $table = $schema->createTable($tName);
+ $table = $schema->createTable($tablePrefix.$tName);
foreach($tCfg->columns as $cName => $cCfg) {
// We allow "inherited" definitions from the fieldLibrary, so merge together
@@ -206,13 +219,13 @@ protected function processSchema(object $schemaConfig, bool $diffOnly=false) {
}
if(isset($colCfg->foreignkey)) {
- $table->addForeignKeyConstraint($colCfg->foreignkey->table,
+ $table->addForeignKeyConstraint($tablePrefix.$colCfg->foreignkey->table,
[$cName],
[$colCfg->foreignkey->column],
[],
// We name our foreign keys the same way they
// were previously named by adodb
- $tName . "_" . $cName . "_fkey");
+ $tablePrefix.$tName . "_" . $cName . "_fkey");
}
}
@@ -229,8 +242,8 @@ protected function processSchema(object $schemaConfig, bool $diffOnly=false) {
// Insert a foreign key to this model and index it
$table->addColumn($mColumn, "integer", ['notnull' => false]);
- $table->addForeignKeyConstraint($fkTable, [$mColumn], ['id'], [], $tName . "_" . $mColumn . "_fkey");
- $table->addIndex([$mColumn], $tName . "_im" . $i++);
+ $table->addForeignKeyConstraint($tablePrefix.$fkTable, [$mColumn], ['id'], [], $tablePrefix.$tName . "_" . $mColumn . "_fkey");
+ $table->addIndex([$mColumn], $tablePrefix.$tName . "_im" . $i++);
}
}
@@ -255,12 +268,12 @@ protected function processSchema(object $schemaConfig, bool $diffOnly=false) {
// an Org Identity Source, so we need a foreign key into ourself.
if(isset($tCfg->sourced) && $tCfg->sourced) {
- $sColumn = "source_" . \Cake\Utility\Inflector::singularize($tName) . "_id";
+ $sColumn = "source_" . $tablePrefix.\Cake\Utility\Inflector::singularize($tName) . "_id";
// Insert a foreign key to this model and index it
$table->addColumn($sColumn, "integer", ['notnull' => false]);
- $table->addForeignKeyConstraint($table, [$sColumn], ['id'], [], $tName . "_" . $sColumn . "_fkey");
- $table->addIndex([$sColumn], $tName . "_im" . $i++);
+ $table->addForeignKeyConstraint($tablePrefix.$tName, [$sColumn], ['id'], [], $tablePrefix.$tName . "_" . $sColumn . "_fkey");
+ $table->addIndex([$sColumn], $tablePrefix.$tName . "_im" . $i++);
}
// Default is to insert timestamp and changelog fields, unless disabled
@@ -280,7 +293,7 @@ protected function processSchema(object $schemaConfig, bool $diffOnly=false) {
$table->addColumn("actor_identifier", "string", ['length' => 256, 'notnull' => false]);
$table->addForeignKeyConstraint($table, [$clColumn], ['id'], [], $tName . "_" . $clColumn . "_fkey");
- $table->addIndex([$clColumn], $tName . "_icl", [], []);
+ $table->addIndex([$clColumn], $tablePrefix.$tName . "_icl", [], []);
}
}
@@ -330,6 +343,7 @@ protected function processSchema(object $schemaConfig, bool $diffOnly=false) {
}
catch(\Exception $e) {
if($this->io) $this->io->out($e->getMessage());
+ else throw new \RuntimeException($e->getMessage());
}
// We might run bin/cake schema_cache clear or
diff --git a/app/src/Lib/Util/StringUtilities.php b/app/src/Lib/Util/StringUtilities.php
index c7056b827..7fd786286 100644
--- a/app/src/Lib/Util/StringUtilities.php
+++ b/app/src/Lib/Util/StringUtilities.php
@@ -119,6 +119,27 @@ public static function foreignKeyToClassName(string $s): string {
return Inflector::camelize(Inflector::pluralize(substr($s, 0, strlen($s)-3)));
}
+ /**
+ * Localize a controller name, accounting for plugins.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param string $controllerName Name of controller to localize
+ * @param string $pluginName Plugin name, if appropriate
+ * @param bool $plural Whether to use plural localization
+ * @return string Localized text string
+ */
+
+ public static function localizeController(string $controllerName, ?string $pluginName, bool $plural=false): string {
+ if($pluginName) {
+ // Localize via plugin
+ return __d(Inflector::underscore($pluginName), 'controller.'.$controllerName, [$plural ? 99 : 1]);
+ } else {
+ // Standard localization
+
+ return __d('controller', $modelsName, [$plural ? 99 : 1]);
+ }
+ }
+
/**
* Determine the model component of a Plugin path.
*
@@ -161,6 +182,21 @@ public static function tableToEntityName($table): string {
return substr($classPath, strrpos($classPath, '\\')+1);
}
+ /**
+ * Determine the foreign key name to point to a Cake Entity (eg: foo_id for FooTable).
+ *
+ * @since COmanage Registry v5.0.0
+ * @param Entity $entity Entity
+ * @return string Foreign key name
+ */
+
+ public static function tableToForeignKey($table): string {
+ // $classPath will be something like App\Model\Entity\Name, but we want to return "name_id"
+ $classPath = $table->getEntityClass();
+
+ return Inflector::underscore(Inflector::singularize(substr($classPath, strrpos($classPath, '\\')+1))) . "_id";
+ }
+
// The following two utilities provide base64 encoding and decoding for
// strings that might contain special characters that could interfere with
// URLs. base64 can generate reserved characters, so we handle those specially
diff --git a/app/src/Model/Behavior/OrderableBehavior.php b/app/src/Model/Behavior/OrderableBehavior.php
new file mode 100644
index 000000000..917bec5bb
--- /dev/null
+++ b/app/src/Model/Behavior/OrderableBehavior.php
@@ -0,0 +1,66 @@
+getSubject();
+
+ $query = $Table->find();
+ $query->select(['maxorder' => $query->func()->max('ordr', ['ordr'])]);
+
+ $row = $query->first();
+
+ if(!empty($row->maxorder)) {
+ $data['ordr'] = $row->maxorder+1;
+ } else {
+ $data['ordr'] = 1;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Entity/Group.php b/app/src/Model/Entity/Group.php
index b7e170923..e47027272 100644
--- a/app/src/Model/Entity/Group.php
+++ b/app/src/Model/Entity/Group.php
@@ -39,11 +39,22 @@ class Group extends Entity {
'slug' => false,
];
+ /**
+ * Determine if this is the All Members group.
+ *
+ * @since COmanage Registry v5.0.0
+ * @return bool true if this is the All Members group, false otherwise.
+ */
+
+ public function isAllMembers(): bool {
+ return $this->group_type == GroupTypeEnum::AllMembers;
+ }
+
/**
* Determine if this is an automatic group.
*
* @since COmanage Registry v5.0.0
- * @return bool true if this is not an automatic group, false otherwise.
+ * @return bool true if this is an automatic group, false otherwise.
*/
public function isAutomatic(): bool {
diff --git a/app/src/Model/Entity/GroupMember.php b/app/src/Model/Entity/GroupMember.php
index 3b0dd4b24..21471edf6 100644
--- a/app/src/Model/Entity/GroupMember.php
+++ b/app/src/Model/Entity/GroupMember.php
@@ -37,4 +37,14 @@ class GroupMember extends Entity {
'id' => false,
'slug' => false,
];
+
+ /**
+ * Determine if this Group Membership is valid, meaning it has validity dates
+ * that are current.
+ */
+
+ public function isValid(): bool {
+ return (!$this->valid_from || $this->valid_from->isPast())
+ && (!$this->valid_through || $this->valid_through->isFuture());
+ }
}
\ No newline at end of file
diff --git a/app/src/Model/Entity/Person.php b/app/src/Model/Entity/Person.php
index 5e64536ef..b81d904a0 100644
--- a/app/src/Model/Entity/Person.php
+++ b/app/src/Model/Entity/Person.php
@@ -51,4 +51,15 @@ class Person extends Entity {
public function isActive(): bool {
return in_array($this->status, [StatusEnum::Active, StatusEnum::GracePeriod]);
}
+
+ /**
+ * Determine if this Person is Locked).
+ *
+ * @since COmanage Registry v5.0.0
+ * @return bool true if Person is Active or GracePeriod, false otherwise
+ */
+
+ public function isLocked(): bool {
+ return $this->status == StatusEnum::Locked;
+ }
}
\ No newline at end of file
diff --git a/app/src/Model/Entity/PersonRole.php b/app/src/Model/Entity/PersonRole.php
index 4254c8267..6662d037e 100644
--- a/app/src/Model/Entity/PersonRole.php
+++ b/app/src/Model/Entity/PersonRole.php
@@ -51,4 +51,22 @@ class PersonRole extends Entity {
public function isActive(): bool {
return in_array($this->status, [StatusEnum::Active, StatusEnum::GracePeriod]);
}
+
+ /**
+ * Determine if this Person Role is valid. A valid record isActive() AND also
+ * has validity dates that are current.
+ *
+ * @since COmange Registry v5.0.0
+ * @return bool true if the Person Role is valid, false otherwise
+ */
+
+ public function isValid(): bool {
+ // AR-PersonRole-3 A Person Role is considered valid (and provisionable) if
+ // (1) the Person Role is in Active or Grace Period status,
+ // (2) the valid from date is unspecified or in the past, and
+ // (3) the valid through date is unspecified or in the future.
+ return $this->isActive()
+ && (!$this->valid_from || $this->valid_from->isPast())
+ && (!$this->valid_through || $this->valid_through->isFuture());
+ }
}
\ No newline at end of file
diff --git a/app/src/Model/Entity/ProvisioningHistoryRecord.php b/app/src/Model/Entity/ProvisioningHistoryRecord.php
new file mode 100644
index 000000000..946439233
--- /dev/null
+++ b/app/src/Model/Entity/ProvisioningHistoryRecord.php
@@ -0,0 +1,54 @@
+ true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+
+ /**
+ * Determine if this entity is Read Only.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param Entity $entity Cake Entity
+ * @return boolean true if the entity is read only, false otherwise
+ */
+
+ public function isReadOnly(): bool {
+ // History records can't be altered once created
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Entity/ProvisioningTarget.php b/app/src/Model/Entity/ProvisioningTarget.php
new file mode 100644
index 000000000..09dae98f7
--- /dev/null
+++ b/app/src/Model/Entity/ProvisioningTarget.php
@@ -0,0 +1,42 @@
+ true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
\ No newline at end of file
diff --git a/app/src/Model/Entity/Server.php b/app/src/Model/Entity/Server.php
new file mode 100644
index 000000000..30a1b3fa4
--- /dev/null
+++ b/app/src/Model/Entity/Server.php
@@ -0,0 +1,42 @@
+ true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
\ No newline at end of file
diff --git a/app/src/Model/Table/AdHocAttributesTable.php b/app/src/Model/Table/AdHocAttributesTable.php
index b7857f8d2..c0b95f022 100644
--- a/app/src/Model/Table/AdHocAttributesTable.php
+++ b/app/src/Model/Table/AdHocAttributesTable.php
@@ -38,6 +38,7 @@ class AdHocAttributesTable extends Table {
use \App\Lib\Traits\HistoryTrait;
use \App\Lib\Traits\PermissionsTrait;
use \App\Lib\Traits\PrimaryLinkTrait;
+ use \App\Lib\Traits\ProvisionableTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\ValidationTrait;
use \App\Lib\Traits\SearchFilterTrait;
@@ -73,7 +74,6 @@ public function initialize(array $config): void {
'entity' => [
'delete' => ['platformAdmin', 'coAdmin'],
'edit' => ['platformAdmin', 'coAdmin'],
- 'primary' => ['platformAdmin', 'coAdmin'],
'view' => ['platformAdmin', 'coAdmin']
],
// Actions that operate over a table (ie: do not require an $id)
diff --git a/app/src/Model/Table/AddressesTable.php b/app/src/Model/Table/AddressesTable.php
index a149dd445..e81d5e2f1 100644
--- a/app/src/Model/Table/AddressesTable.php
+++ b/app/src/Model/Table/AddressesTable.php
@@ -41,6 +41,7 @@ class AddressesTable extends Table {
use \App\Lib\Traits\HistoryTrait;
use \App\Lib\Traits\PermissionsTrait;
use \App\Lib\Traits\PrimaryLinkTrait;
+ use \App\Lib\Traits\ProvisionableTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\TypeTrait;
use \App\Lib\Traits\ValidationTrait;
@@ -101,7 +102,6 @@ public function initialize(array $config): void {
'entity' => [
'delete' => ['platformAdmin', 'coAdmin'],
'edit' => ['platformAdmin', 'coAdmin'],
- 'primary' => ['platformAdmin', 'coAdmin'],
'view' => ['platformAdmin', 'coAdmin']
],
// Actions that operate over a table (ie: do not require an $id)
diff --git a/app/src/Model/Table/CousTable.php b/app/src/Model/Table/CousTable.php
index f3ef2f1df..bbb1d4316 100644
--- a/app/src/Model/Table/CousTable.php
+++ b/app/src/Model/Table/CousTable.php
@@ -35,12 +35,15 @@
use Cake\ORM\TableRegistry;
use Cake\Validation\Validator;
+use \App\Lib\Enum\ProvisioningEligibilityEnum;
+
class CousTable extends Table {
use \App\Lib\Traits\AutoViewVarsTrait;
use \App\Lib\Traits\ChangelogBehaviorTrait;
use \App\Lib\Traits\CoLinkTrait;
use \App\Lib\Traits\PermissionsTrait;
use \App\Lib\Traits\PrimaryLinkTrait;
+ use \App\Lib\Traits\ProvisionableTrait;
use \App\Lib\Traits\SearchFilterTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\ValidationTrait;
@@ -63,13 +66,18 @@ public function initialize(array $config): void {
// Define associations
$this->belongsTo('Cos');
+ // AR-COU-2 A COU may not be deleted if it has any children.
$this->belongsTo('Cous')
->setForeignKey('parent_id')
// Property is set so ruleValidateCO can find it. We don't use the
// _id suffix to match Cake's default pattern.
->setProperty('parent');
- $this->hasMany('Groups');
+ // AR-COU-6 If a COU is deleted, the special groups associated with the COU will also be deleted.
+ $this->hasMany('Groups')
+ ->setDependent(true)
+ ->setCascadeCallbacks(true);
+ // AR-COU-1 A COU may not be deleted if it has any members.
$this->hasMany('PersonRoles');
$this->setDisplayField('name');
@@ -142,7 +150,6 @@ public function localAfterSave(\Cake\Event\EventInterface $event, \Cake\Datasour
}
}
-
if($entity->isNew() && !empty($entity->id)) {
// Run setup for new COU
@@ -152,6 +159,32 @@ public function localAfterSave(\Cake\Event\EventInterface $event, \Cake\Datasour
return true;
}
+ /**
+ * Marshal object data for provisioning.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param int $id Entity ID
+ * @return array An array of provisionable data and eligibility
+ */
+
+ public function marshalProvisioningData(int $id): array {
+ $ret = [];
+ // We need the archived record on delete to properly deprovision
+ $ret['data'] = $this->get($id, ['archived' => true]);
+
+ // Provisioning Eligibility is
+ // - Deleted if the changelog deleted flag is true
+ // - Eligible otherwise (COUs don't currently have a suspended status)
+
+ $ret['eligibility'] = ProvisioningEligibilityEnum::Eligible;
+
+ if($ret['data']->deleted) {
+ $ret['eligibility'] = ProvisioningEligibilityEnum::Deleted;
+ }
+
+ return $ret;
+ }
+
/**
* Assemble the set of potential parent COUs.
*
diff --git a/app/src/Model/Table/DashboardsTable.php b/app/src/Model/Table/DashboardsTable.php
index c6f164438..8a10f22f2 100644
--- a/app/src/Model/Table/DashboardsTable.php
+++ b/app/src/Model/Table/DashboardsTable.php
@@ -59,7 +59,13 @@ public function initialize(array $config): void {
$this->setPrimaryLink('co_id');
$this->setRequiresCO(true);
- $this->setAllowUnkeyedPrimaryCO(['configuration', 'dashboard', 'search']);
+ $this->setAllowUnkeyedPrimaryCO([
+ 'artifacts',
+ 'configuration',
+ 'dashboard',
+ 'registries',
+ 'search']
+ );
$this->setPermissions([
// Actions that operate over an entity (ie: require an $id)
@@ -71,9 +77,11 @@ public function initialize(array $config): void {
],
// Actions that operate over a table (ie: do not require an $id)
'table' => [
+ 'artifacts' => ['platformAdmin', 'coAdmin'],
'configuration' => ['platformAdmin', 'coAdmin'],
// XXX CFM-230 This needs to be updated for actual Dashboard permissions
'dashboard' => ['platformAdmin', 'coAdmin', 'coMember'],
+ 'registries' => ['platformAdmin', 'coAdmin'],
'search' => ['platformAdmin', 'coAdmin']
/* 'add' => ['platformAdmin', 'coAdmin'],
'index' => ['platformAdmin', 'coAdmin']*/
diff --git a/app/src/Model/Table/EmailAddressesTable.php b/app/src/Model/Table/EmailAddressesTable.php
index 7d48c5218..c8a21fa54 100644
--- a/app/src/Model/Table/EmailAddressesTable.php
+++ b/app/src/Model/Table/EmailAddressesTable.php
@@ -39,6 +39,7 @@ class EmailAddressesTable extends Table {
use \App\Lib\Traits\HistoryTrait;
use \App\Lib\Traits\PermissionsTrait;
use \App\Lib\Traits\PrimaryLinkTrait;
+ use \App\Lib\Traits\ProvisionableTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\TypeTrait;
use \App\Lib\Traits\ValidationTrait;
@@ -96,7 +97,6 @@ public function initialize(array $config): void {
'entity' => [
'delete' => ['platformAdmin', 'coAdmin'],
'edit' => ['platformAdmin', 'coAdmin'],
- 'primary' => ['platformAdmin', 'coAdmin'],
'view' => ['platformAdmin', 'coAdmin']
],
// Actions that operate over a table (ie: do not require an $id)
diff --git a/app/src/Model/Table/GroupMembersTable.php b/app/src/Model/Table/GroupMembersTable.php
index 9fd0a85c4..c5a697a72 100644
--- a/app/src/Model/Table/GroupMembersTable.php
+++ b/app/src/Model/Table/GroupMembersTable.php
@@ -45,6 +45,7 @@ class GroupMembersTable extends Table {
use \App\Lib\Traits\LabeledLogTrait;
use \App\Lib\Traits\PermissionsTrait;
use \App\Lib\Traits\PrimaryLinkTrait;
+ use \App\Lib\Traits\ProvisionableTrait;
use \App\Lib\Traits\QueryModificationTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\ValidationTrait;
diff --git a/app/src/Model/Table/GroupsTable.php b/app/src/Model/Table/GroupsTable.php
index f12c98e2a..4abd3c4ca 100644
--- a/app/src/Model/Table/GroupsTable.php
+++ b/app/src/Model/Table/GroupsTable.php
@@ -37,6 +37,7 @@
use \App\Lib\Util\PaginatedSqlIterator;
use \App\Lib\Enum\ActionEnum;
use \App\Lib\Enum\GroupTypeEnum;
+use \App\Lib\Enum\ProvisioningEligibilityEnum;
use \App\Lib\Enum\StatusEnum;
use \App\Lib\Enum\SuspendableStatusEnum;
@@ -48,6 +49,7 @@ class GroupsTable extends Table {
use \App\Lib\Traits\LabeledLogTrait;
use \App\Lib\Traits\PermissionsTrait;
use \App\Lib\Traits\PrimaryLinkTrait;
+ use \App\Lib\Traits\ProvisionableTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\ValidationTrait;
use \App\Lib\Traits\SearchFilterTrait;
@@ -86,11 +88,14 @@ public function initialize(array $config): void {
$this->hasMany('Identifiers')
->setDependent(true)
->setCascadeCallbacks(true);
-
+ $this->hasMany('ProvisioningHistoryRecords')
+ ->setDependent(true)
+ ->setCascadeCallbacks(true);
+
$this->setDisplayField('name');
$this->setPrimaryLink('co_id');
- $this->setAllowLookupPrimaryLink(['reconcile']);
+ $this->setAllowLookupPrimaryLink(['provision', 'reconcile']);
$this->setRequiresCO(true);
$this->setAutoViewVars([
@@ -110,6 +115,7 @@ public function initialize(array $config): void {
'entity' => [
'delete' => ['platformAdmin', 'coAdmin'],
'edit' => ['platformAdmin', 'coAdmin'],
+ 'provision' => ['platformAdmin', 'coAdmin'],
'reconcile' => ['platformAdmin', 'coAdmin'],
'view' => ['platformAdmin', 'coAdmin']
],
@@ -130,7 +136,8 @@ public function initialize(array $config): void {
'GroupNestings',
'GroupOwners',
'HistoryRecords',
- 'Identifiers'
+ 'Identifiers',
+ 'ProvisioningTargets'
]
]);
}
@@ -368,6 +375,88 @@ public function localAfterSave(\Cake\Event\EventInterface $event, \Cake\Datasour
return true;
}
+ /**
+ * Marshal object data for provisioning.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param int $id Entity ID
+ * @return array An array of provisionable data and eligibility
+ */
+
+ public function marshalProvisioningData(int $id): array {
+ $ret = [];
+
+ $ret['data'] = $this->get($id, [
+ // We need archives for handling deleted records
+ 'archived' => 'true',
+ 'contain' => [
+ 'GroupMembers',
+ 'Identifiers'
+ ]
+ ]);
+
+ // Provisioning Eligibility is
+ // - Deleted if the changelog deleted flag is true
+ // - Eligible if the status is Active
+ // - Ineligible otherwise
+
+ $ret['eligibility'] = ProvisioningEligibilityEnum::Ineligible;
+
+ // We filter various attributes depending on the status of the record.
+
+ if($ret['data']->deleted) {
+ $ret['eligibility'] = ProvisioningEligibilityEnum::Deleted;
+
+ // For deleted or archived records, we remove all Group Members,
+ // but we leave the Identifiers in place.
+
+ $ret['data']->group_members = [];
+ } elseif($ret['data']->status == SuspendableStatusEnum::Active) {
+ $ret['eligibility'] = ProvisioningEligibilityEnum::Eligible;
+
+ // For Eligible, we still need to remove Group Memberships that are
+ // invalid, and Identifiers that are suspended.
+
+ $groupMembers = [];
+
+ foreach($ret['data']->group_members as $gm) {
+ if($gm->isValid()) {
+ $groupMembers[] = $gm;
+ }
+ }
+
+ $ret['data']->group_members = $groupMembers;
+
+ $identifiers = [];
+
+ foreach($ret['data']->identifiers as $id) {
+ if($id->status == SuspendableStatusEnum::Active) {
+ $identifiers[] = $id;
+ }
+ }
+
+ $ret['data']->identifiers = $identifiers;
+ } else {
+ $ret['eligibility'] = ProvisioningEligibilityEnum::Ineligible;
+ // For Ineligible records, we remove the group memberships, and
+ // any suspended Identifiers.
+
+ $ret['data']->group_members = [];
+
+ $identifiers = [];
+
+ foreach($ret['data']->identifiers as $id) {
+ if($id->status == SuspendableStatusEnum::Active) {
+ $identifiers[] = $id;
+ }
+ }
+
+ $ret['data']->identifiers = $identifiers;
+ }
+
+ return $ret;
+ }
+
/**
* Reconcile the members of an automatic or nested Group.
*
diff --git a/app/src/Model/Table/HistoryRecordsTable.php b/app/src/Model/Table/HistoryRecordsTable.php
index c10585b81..fa9325f7a 100644
--- a/app/src/Model/Table/HistoryRecordsTable.php
+++ b/app/src/Model/Table/HistoryRecordsTable.php
@@ -77,7 +77,6 @@ public function initialize(array $config): void {
// XXX note primary link is external_identity_id when set...
// or the other fields as we add them
$this->setPrimaryLink(['external_identity_id', 'group_id', 'person_id']);
- $this->setAllowLookupPrimaryLink(['primary']);
$this->setRequiresCO(true);
// XXX does some of this stuff really belong in the controller?
diff --git a/app/src/Model/Table/IdentifiersTable.php b/app/src/Model/Table/IdentifiersTable.php
index 241eef6cb..899ebefc6 100644
--- a/app/src/Model/Table/IdentifiersTable.php
+++ b/app/src/Model/Table/IdentifiersTable.php
@@ -40,6 +40,7 @@ class IdentifiersTable extends Table {
use \App\Lib\Traits\HistoryTrait;
use \App\Lib\Traits\PermissionsTrait;
use \App\Lib\Traits\PrimaryLinkTrait;
+ use \App\Lib\Traits\ProvisionableTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\TypeTrait;
use \App\Lib\Traits\ValidationTrait;
@@ -89,6 +90,7 @@ public function initialize(array $config): void {
$this->belongsTo('ExternalIdentities');
$this->belongsTo('Groups');
$this->belongsTo('People');
+ $this->belongsTo('ProvisioningTargets');
$this->belongsTo('Types');
$this->setDisplayField('identifier');
@@ -113,7 +115,6 @@ public function initialize(array $config): void {
'entity' => [
'delete' => ['platformAdmin', 'coAdmin'],
'edit' => ['platformAdmin', 'coAdmin'],
- 'primary' => ['platformAdmin', 'coAdmin'],
'view' => ['platformAdmin', 'coAdmin']
],
// Actions that operate over a table (ie: do not require an $id)
diff --git a/app/src/Model/Table/JobHistoryRecordsTable.php b/app/src/Model/Table/JobHistoryRecordsTable.php
index 11743f374..ced38a86f 100644
--- a/app/src/Model/Table/JobHistoryRecordsTable.php
+++ b/app/src/Model/Table/JobHistoryRecordsTable.php
@@ -67,7 +67,6 @@ public function initialize(array $config): void {
$this->setDisplayField('comment');
$this->setPrimaryLink(['job_id']);
- $this->setAllowLookupPrimaryLink(['primary']);
$this->setRequiresCO(true);
$this->setAutoViewVars([
@@ -91,7 +90,7 @@ public function initialize(array $config): void {
],
// Actions that operate over a table (ie: do not require an $id)
'table' => [
- 'add' => false, //['platformAdmin', 'coAdmin'],
+ 'add' => false,
'index' => ['platformAdmin', 'coAdmin']
]
]);
diff --git a/app/src/Model/Table/JobsTable.php b/app/src/Model/Table/JobsTable.php
index 601fec6c8..cdf55aec5 100644
--- a/app/src/Model/Table/JobsTable.php
+++ b/app/src/Model/Table/JobsTable.php
@@ -77,7 +77,7 @@ public function initialize(array $config): void {
$this->hasMany('JobHistoryRecords')
->setDependent(true)
- ->setCascadeCallbacks(true);;
+ ->setCascadeCallbacks(true);
$this->setPluginRelations();
diff --git a/app/src/Model/Table/NamesTable.php b/app/src/Model/Table/NamesTable.php
index a09777c64..e748c622c 100644
--- a/app/src/Model/Table/NamesTable.php
+++ b/app/src/Model/Table/NamesTable.php
@@ -45,6 +45,7 @@ class NamesTable extends Table {
use \App\Lib\Traits\LabeledLogTrait;
use \App\Lib\Traits\PermissionsTrait;
use \App\Lib\Traits\PrimaryLinkTrait;
+ use \App\Lib\Traits\ProvisionableTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\TypeTrait;
use \App\Lib\Traits\ValidationTrait;
diff --git a/app/src/Model/Table/PeopleTable.php b/app/src/Model/Table/PeopleTable.php
index fd131ee95..b925f728b 100644
--- a/app/src/Model/Table/PeopleTable.php
+++ b/app/src/Model/Table/PeopleTable.php
@@ -35,6 +35,8 @@
use Cake\Validation\Validator;
use \App\Lib\Enum\GroupTypeEnum;
use \App\Lib\Enum\StatusEnum;
+use \App\Lib\Enum\SuspendableStatusEnum;
+use \App\Lib\Enum\ProvisioningEligibilityEnum;
use \App\Lib\Util\PaginatedSqlIterator;
class PeopleTable extends Table {
@@ -45,6 +47,7 @@ class PeopleTable extends Table {
use \App\Lib\Traits\LabeledLogTrait;
use \App\Lib\Traits\PermissionsTrait;
use \App\Lib\Traits\PrimaryLinkTrait;
+ use \App\Lib\Traits\ProvisionableTrait;
use \App\Lib\Traits\QueryModificationTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\ValidationTrait;
@@ -107,6 +110,9 @@ public function initialize(array $config): void {
$this->hasMany('Pronouns')
->setDependent(true)
->setCascadeCallbacks(true);
+ $this->hasMany('ProvisioningHistoryRecords')
+ ->setDependent(true)
+ ->setCascadeCallbacks(true);
$this->hasMany('TelephoneNumbers')
->setDependent(true)
->setCascadeCallbacks(true);
@@ -120,6 +126,7 @@ public function initialize(array $config): void {
$this->setPrimaryLink('co_id');
$this->setRequiresCO(true);
$this->setRedirectGoal('self');
+ $this->setAllowLookupPrimaryLink(['provision']);
// XXX does some of this stuff really belong in the controller?
$this->setEditContains([
@@ -150,10 +157,10 @@ public function initialize(array $config): void {
// Actions that operate over an entity (ie: require an $id)
// See also CFM-126
'entity' => [
- 'delete' => ['platformAdmin', 'coAdmin'],
- 'edit' => ['platformAdmin', 'coAdmin'],
- 'canvas' => ['platformAdmin', 'coAdmin'],
- 'view' => ['platformAdmin', 'coAdmin']
+ 'delete' => ['platformAdmin', 'coAdmin'],
+ 'edit' => ['platformAdmin', 'coAdmin'],
+ 'provision' => ['platformAdmin', 'coAdmin'],
+ 'view' => ['platformAdmin', 'coAdmin']
],
// Actions that operate over a table (ie: do not require an $id)
'table' => [
@@ -170,6 +177,7 @@ public function initialize(array $config): void {
'HistoryRecords',
'Identifiers',
'PersonRoles',
+ 'ProvisioningTargets',
'TelephoneNumbers',
'Urls'
]
@@ -207,27 +215,6 @@ public function beforeDelete(\Cake\Event\Event $event, $entity, \ArrayObject $op
return true;
}
- /**
- * Callback after model save.
- *
- * @since COmanage Registry v5.0.0
- * @param EventInterface $event Event
- * @param EntityInterface $entity Entity (ie: Co)
- * @param ArrayObject $options Save options
- * @return bool True on success
- */
-
- public function localAfterSave(\Cake\Event\EventInterface $event, \Cake\Datasource\EntityInterface $entity, \ArrayObject $options): bool {
- $this->recordHistory($entity);
-
- // XXX implement this eventually?
- //$provision = (isset($options['provision']) ? $options['provision'] : true);
-
- $this->reconcileCoMembersGroupMemberships($entity);
-
- return true;
- }
-
/**
* Table specific logic to generate a display field.
*
@@ -261,6 +248,177 @@ public function getMembers(int $coId): PaginatedSqlIterator {
return new PaginatedSqlIterator($this, $conditions);
}
+ /**
+ * Callback after model save.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param EventInterface $event Event
+ * @param EntityInterface $entity Entity (ie: Co)
+ * @param ArrayObject $options Save options
+ * @return bool True on success
+ */
+
+ public function localAfterSave(\Cake\Event\EventInterface $event, \Cake\Datasource\EntityInterface $entity, \ArrayObject $options): bool {
+ $this->recordHistory($entity);
+
+ // XXX implement this eventually?
+ //$provision = (isset($options['provision']) ? $options['provision'] : true);
+
+ $this->reconcileCoMembersGroupMemberships($entity);
+
+ return true;
+ }
+
+ /**
+ * Marshal object data for provisioning.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param int $id Entity ID
+ * @return array An array of provisionable data and eligibility
+ */
+
+ public function marshalProvisioningData(int $id): array {
+ $ret = [];
+
+ $ret['data'] = $this->get($id, [
+ // We need archives for handling deleted records
+ 'archived' => 'true',
+ 'contain' => [
+ 'PrimaryName' => [ 'Types' ],
+ 'Addresses' => [ 'Types' ],
+ 'AdHocAttributes',
+ 'EmailAddresses' => [ 'Types' ],
+ 'ExternalIdentities' => [
+ 'PrimaryName' => [ 'Types' ],
+ 'Addresses' => [ 'Types' ],
+ 'AdHocAttributes',
+ 'EmailAddresses' => [ 'Types' ],
+ 'ExternalIdentityRoles' => [
+ 'Addresses' => [ 'Types' ],
+ 'AdHocAttributes',
+ 'TelephoneNumbers' => [ 'Types' ],
+ 'Types'
+ ],
+ 'Identifiers' => [ 'Types' ],
+ 'Names' => [ 'Types' ],
+ 'Pronouns',
+ 'TelephoneNumbers' => [ 'Types' ],
+ 'Urls' => [ 'Types' ]
+ ],
+ 'GroupMembers' => [ 'Groups' ],
+ 'GroupOwners' => [ 'Groups' ],
+ 'Identifiers' => [ 'Types' ],
+ 'Names' => [ 'Types' ],
+ 'PersonRoles' => [
+ 'Addresses' => [ 'Types' ],
+ 'AdHocAttributes',
+ 'Cous',
+ 'ManagerPeople' => [ 'PrimaryName' ],
+ 'SponsorPeople' => [ 'PrimaryName' ],
+ 'TelephoneNumbers' => [ 'Types' ],
+ 'Types'
+ ],
+ 'Pronouns',
+ 'TelephoneNumbers' => [ 'Types' ],
+ 'Urls' => [ 'Types' ]
+ ]
+ ]);
+
+ // Provisioning Eligibility is
+ // - Deleted if the changelog deleted flag is true OR status is Archived
+ // - Eligible if entity->isActive()
+ // - Ineligible otherwise
+
+ // Most statuses don't provision anything
+ $ret['eligibility'] = ProvisioningEligibilityEnum::Deleted;
+
+ // We filter various attributes depending on the status of the record.
+
+ if($ret['data']->deleted || $ret['data']->status == StatusEnum::Archived) {
+ $ret['eligibility'] = ProvisioningEligibilityEnum::Deleted;
+
+ // For deleted or archived records, we remove everything except names
+ // and identifiers, which might be useful for error reporting and record keeping.
+ // Unlike Ineligible, we *don't* keep the All Members groups.
+
+ $ret['data']->ad_hoc_attributes = [];
+ $ret['data']->addresses = [];
+ $ret['data']->email_addresses = [];
+ $ret['data']->external_identities = [];
+ $ret['data']->group_members = [];
+ $ret['data']->group_owners = [];
+ $ret['data']->person_roles = [];
+ $ret['data']->pronouns = [];
+ $ret['data']->telephone_numbers = [];
+ $ret['data']->urls = [];
+ } elseif($ret['data']->isActive()) {
+ $ret['eligibility'] = ProvisioningEligibilityEnum::Eligible;
+
+ // For Eligible, we still need to remove Person Roles and Group Memberships
+ // that are invalid, and Identifiers that are suspended.
+
+ $personRoles = [];
+
+ foreach($ret['data']->person_roles as $pr) {
+ if($pr->isValid()) {
+ $personRoles[] = $pr;
+ }
+ }
+
+ $ret['data']->person_roles = $personRoles;
+
+ $groupMembers = [];
+
+ foreach($ret['data']->group_members as $gm) {
+ if($gm->isValid()) {
+ $groupMembers[] = $gm;
+ }
+ }
+
+ $ret['data']->group_members = $groupMembers;
+
+ $identifiers = [];
+
+ foreach($ret['data']->identifiers as $id) {
+ if($id->status == SuspendableStatusEnum::Active) {
+ $identifiers[] = $id;
+ }
+ }
+
+ $ret['data']->identifiers = $identifiers;
+ } else {
+ $ret['eligibility'] = ProvisioningEligibilityEnum::Ineligible;
+ // For Ineligible records, we remove the items that may be used for eligibilities,
+ // specifically group memberships/ownerships and PersonRoles. We leave the
+ // All Members group in place. We also remove any suspended Identifiers.
+
+ $groupMembers = [];
+
+ foreach($ret['data']->group_members as $gm) {
+ if($gm->group->isAllMembers()) {
+ $groupMembers[] = $gm;
+ }
+ }
+
+ $ret['data']->group_members = $groupMembers;
+
+ $identifiers = [];
+
+ foreach($ret['data']->identifiers as $id) {
+ if($id->status == SuspendableStatusEnum::Active) {
+ $identifiers[] = $id;
+ }
+ }
+
+ $ret['data']->identifiers = $identifiers;
+
+ $ret['data']->group_owners = [];
+ $ret['data']->person_roles = [];
+ }
+
+ return $ret;
+ }
+
/**
* Reconcile memberships in CO members groups based on the Person entity.
*
diff --git a/app/src/Model/Table/PersonRolesTable.php b/app/src/Model/Table/PersonRolesTable.php
index 0278a59b7..900f727ff 100644
--- a/app/src/Model/Table/PersonRolesTable.php
+++ b/app/src/Model/Table/PersonRolesTable.php
@@ -45,6 +45,7 @@ class PersonRolesTable extends Table {
use \App\Lib\Traits\LabeledLogTrait;
use \App\Lib\Traits\PermissionsTrait;
use \App\Lib\Traits\PrimaryLinkTrait;
+ use \App\Lib\Traits\ProvisionableTrait;
use \App\Lib\Traits\QueryModificationTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\TypeTrait;
@@ -112,7 +113,7 @@ public function initialize(array $config): void {
->setDependent(true)
->setCascadeCallbacks(true);
- $this->setDisplayField('id');
+ $this->setDisplayField('title');
$this->setPrimaryLink('person_id');
$this->setRequiresCO(true);
diff --git a/app/src/Model/Table/PluginsTable.php b/app/src/Model/Table/PluginsTable.php
index edc3296ff..de90a5a6e 100644
--- a/app/src/Model/Table/PluginsTable.php
+++ b/app/src/Model/Table/PluginsTable.php
@@ -241,6 +241,28 @@ public function getActivePluginModels(string $type): array {
return array_combine($active, $active);
}
+ /**
+ * Read the value for a configuration key for a plugin, which must be Active.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param string $plugin Plugin name
+ * @param string $key Configuration key
+ * @param array Array of configuration information
+ */
+
+ public function getPluginConfig(string $plugin, string $key) {
+ // While most calls to this table accept a plugin object, this one takes
+ // a string to simplify code that needs a value out of plugin.json.
+ $pObj = $this->find()
+ ->where([
+ 'plugin' => $plugin,
+ 'status' => SuspendableStatusEnum::Active
+ ])
+ ->firstOrFail();
+
+ return $this->readPluginConfig($pObj, $key);
+ }
+
/**
* Obtain the Entry Point Models implemented by a plugin for a specific plugin type.
*
@@ -302,9 +324,9 @@ public function pluginPath(\App\Model\Entity\Plugin $plugin, string $file): stri
return $fileName;
}
- $this->llog('error', "Could not find $plugin");
+ $this->llog('error', "Could not find $fileName");
- throw new \InvalidArgumentException("Could not find $plugin");
+ throw new \InvalidArgumentException("Could not find $fileName");
}
/**
@@ -439,7 +461,7 @@ public function syncPluginRegistry() {
// Create an array of the already registered plugins
foreach($registered as $rp) {
- $registeredIndex[$rp->plugin] = $rp->location;
+ $registeredIndex[$rp->plugin] = $rp;
}
// Insert rows for any plugin not currently in the Registry.
@@ -450,6 +472,7 @@ public function syncPluginRegistry() {
foreach(array_keys($plugins) as $pluginType) {
foreach($plugins[$pluginType] as $p) {
if(!isset($registeredIndex[$p])) {
+ // This is a new plugin
$obj = $this->newEntity([
'plugin' => $p,
'location' => $pluginType,
@@ -461,6 +484,24 @@ public function syncPluginRegistry() {
]);
$this->saveOrFail($obj);
+ } elseif($registeredIndex[$p]->location != $pluginIndex[$p]) {
+ // The plugin location moved. This won't typically happen, but might
+ // if a developer moves a plugin around.
+
+ $rp = $registeredIndex[$p];
+
+ if($rp->location == PluginLocationEnum::Core) {
+ // If the old location was core, update the comment but leave the plugin as active
+ $rp->comment = __d('information', 'plugin.active');
+ } elseif($pluginIndex[$p] == PluginLocationEnum::Core) {
+ // If the new location is core, make sure the plugin is active
+ $rp->status = SuspendableStatusEnum::Active;
+ $rp->comment = __d('information', 'plugin.active.only');
+ }
+
+ $rp->location = $pluginIndex[$p];
+
+ $this->saveOrFail($rp);
}
}
}
diff --git a/app/src/Model/Table/PronounsTable.php b/app/src/Model/Table/PronounsTable.php
index b7b1bc702..6f6fb511c 100644
--- a/app/src/Model/Table/PronounsTable.php
+++ b/app/src/Model/Table/PronounsTable.php
@@ -40,6 +40,7 @@ class PronounsTable extends Table {
use \App\Lib\Traits\HistoryTrait;
use \App\Lib\Traits\PermissionsTrait;
use \App\Lib\Traits\PrimaryLinkTrait;
+ use \App\Lib\Traits\ProvisionableTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\TypeTrait;
use \App\Lib\Traits\ValidationTrait;
@@ -94,7 +95,6 @@ public function initialize(array $config): void {
'entity' => [
'delete' => ['platformAdmin', 'coAdmin'],
'edit' => ['platformAdmin', 'coAdmin'],
- 'primary' => ['platformAdmin', 'coAdmin'],
'view' => ['platformAdmin', 'coAdmin']
],
// Actions that operate over a table (ie: do not require an $id)
diff --git a/app/src/Model/Table/ProvisioningHistoryRecordsTable.php b/app/src/Model/Table/ProvisioningHistoryRecordsTable.php
new file mode 100644
index 000000000..e9e0cdc81
--- /dev/null
+++ b/app/src/Model/Table/ProvisioningHistoryRecordsTable.php
@@ -0,0 +1,218 @@
+addBehavior('Changelog');
+ $this->addBehavior('Log');
+ $this->addBehavior('Timestamp');
+
+ $this->setTableType(\App\Lib\Enum\TableTypeEnum::Artifact);
+
+ // Define associations
+ $this->belongsTo('ProvisioningTargets');
+ $this->belongsTo('People');
+ $this->belongsTo('Groups');
+
+ $this->setDisplayField('comment');
+
+ // We list provisioning_target_id last so breadcrumbs don't try to use it
+ $this->setPrimaryLink(['person_id', 'group_id', 'provisioning_target_id']);
+ //$this->setAllowLookupPrimaryLink(['primary']);
+ $this->setRequiresCO(true);
+
+ $this->setAutoViewVars([
+ 'statuses' => [
+ 'type' => 'enum',
+ 'class' => 'ProvisioningStatusEnum'
+ ]
+ ]);
+
+ $this->setIndexContains(['ProvisioningTargets']);
+
+ $this->setViewContains([
+ 'People' => ['PrimaryName'],
+ 'Groups'
+ ]);
+
+ $this->setPermissions([
+ // Actions that operate over an entity (ie: require an $id)
+ 'entity' => [
+ 'delete' => false,
+ 'edit' => false,
+ 'view' => ['platformAdmin', 'coAdmin']
+ ],
+ // Actions that operate over a table (ie: do not require an $id)
+ 'table' => [
+ 'add' => false,
+ 'index' => ['platformAdmin', 'coAdmin']
+ ]
+ ]);
+ }
+
+ /**
+ * Table specific logic to generate a display field.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param JobHistoryRecord $entity Entity to generate display field for
+ * @return string Display field
+ */
+
+ public function generateDisplayField(\App\Model\Entity\ProvisioningHistoryRecord $entity): string {
+ // Comments may be too long to render, so we just use the model name
+ // (which will get appended with the record ID)
+
+ return __d('controller', 'ProvisioningHistoryRecords', [1]);
+ }
+
+ /**
+ * Record a Provisioning History Record.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param int $provisioningTargetId Provisioning Target ID
+ * @param string $comment Comment
+ * @param string $status ProvisioningStatusEnum
+ * @param string $subjectModel The provisioned model
+ * @param int $subjectId The provisioned entity id (of type $subjectModel)
+ * @return int Provisioning History Record ID
+ */
+
+ public function record(int $provisioningTargetId,
+ string $comment,
+ string $status,
+ string $subjectModel,
+ int $subjectId): int {
+ // We record all models and foreign keys, but only select (primary) models
+ // have database level foreign key relations (for viewing history) so we
+ // populate the correct foreign key if supported
+
+ $personId = null;
+ $groupId = null;
+
+ switch($subjectModel) {
+ case 'Groups':
+ $groupId = $subjectId;
+ break;
+ case 'People':
+ $personId = $subjectId;
+ break;
+ default:
+ break;
+ }
+
+ $obj = $this->newEntity([
+ 'provisioning_target_id' => $provisioningTargetId,
+ 'comment' => $comment,
+ 'status' => $status,
+ 'subject_model' => $subjectModel,
+ 'subjectid' => $subjectId,
+ 'person_id' => $personId,
+ 'group_id' => $groupId
+ ]);
+
+ $this->saveOrFail($obj);
+
+// XXX trace this too? (below is copy/paste from Job History)
+ // For now, always trace log Job History. We might do something more complicated later.
+ // eg: Make it configurable whether we create Job History, log, or both?
+ // This is documented at https://spaces.at.internet2.edu/display/COmanage/Registry+PE+Jobs#RegistryPEJobs-RegistryJobHistory
+// $this->llog('trace', $comment, "{$jobId}:{$recordKey}");
+
+ return $obj->id;
+ }
+
+ /**
+ * 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('provisioning_target_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->notEmptyString('provisioning_target_id');
+
+ $this->registerStringValidation($validator, $schema, 'comment', true);
+
+ $validator->add('status', [
+ 'content' => ['rule' => ['inList', ProvisioningStatusEnum::getConstValues()]]
+ ]);
+ $validator->notEmptyString('status');
+
+ $this->registerStringValidation($validator, $schema, 'subject_model', true);
+
+ $validator->add('subjectid', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->allowEmptyString('subjectid');
+
+ $validator->add('person_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->allowEmptyString('person_id');
+
+ $validator->add('group', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->allowEmptyString('group');
+
+ return $validator;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Table/ProvisioningTargetsTable.php b/app/src/Model/Table/ProvisioningTargetsTable.php
new file mode 100644
index 000000000..e0a3d76e7
--- /dev/null
+++ b/app/src/Model/Table/ProvisioningTargetsTable.php
@@ -0,0 +1,341 @@
+addBehavior('Changelog');
+ $this->addBehavior('Log');
+ $this->addBehavior('Orderable');
+ $this->addBehavior('Timestamp');
+
+ $this->setTableType(\App\Lib\Enum\TableTypeEnum::Configuration);
+
+ // Define associations
+ $this->belongsTo('Cos');
+ $this->belongsTo('ProvisioningGroups')
+ ->setClassName('Groups')
+ ->setForeignKey('provisioning_group_id')
+ // Property is set so ruleValidateCO can find it. We don't use the
+ // _id suffix to match Cake's default pattern.
+ ->setProperty('provisioning_group');
+
+ $this->hasMany('ProvisioningHistoryRecords')
+ ->setDependent(true)
+ ->setCascadeCallbacks(true);
+
+ $this->setPluginRelations();
+
+ $this->setDisplayField('description');
+
+ $this->setPrimaryLink(['co_id', 'group_id', 'person_id']);
+ $this->setRequiresCO(true);
+ $this->setAllowUnkeyedPrimaryLink(['status']);
+
+ $this->setAutoViewVars([
+ 'plugins' => [
+ 'type' => 'plugin',
+ 'pluginType' => 'provisioner'
+ ],
+ 'provisioningGroups' => [
+ 'type' => 'select',
+ 'model' => 'ProvisioningGroups'
+ ],
+ 'statuses' => [
+ 'type' => 'enum',
+ 'class' => 'ProvisionerModeEnum'
+ ]
+ ]);
+
+ $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'],
+ 'status' => ['platformAdmin', 'coAdmin']
+ ]
+ ]);
+ }
+
+ /**
+ * Invoke provisioning. This function is intended to be called via ProvisionableTrait.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param mixed $data Provisioning object data, eg as returned by Table::get()
+ * @param ProvisioningEligibilityEnum $eligibility Provisioning eligibility
+ * @param ProvisioningContextEnum $context Provisioning context
+ * @param int $id Provisioning Target ID, or null to provision all targets
+ */
+
+ public function provision(
+ mixed $data,
+ string $eligibility,
+ string $context,
+ ?int $id=null
+ ) {
+ // Convert the primary data object to the primary provisioned object name
+ // (eg: People or Cous)
+ $provisionedModel = StringUtilities::entityToClassName($data);
+
+ $query = $this->find()
+ ->where([
+ 'ProvisioningTargets.co_id' => $data->co_id,
+// XXX how do we know which mode's worth of provisioners we want?
+ 'ProvisioningTargets.status <>' => ProvisionerModeEnum::Disabled
+ ]);
+
+ if($id) {
+ $query = $query->where(['ProvisioningTargets.id' => $id]);
+ }
+
+ $targets = $query->order(['ProvisioningTargets.ordr' => 'ASC'])
+ ->contain($this->getContainableModels())
+ ->all();
+
+ foreach($targets as $t) {
+ // Compare our $context against the target's $status. There are three possible
+ // contexts, with their corresponding provisionable statuses:
+ // Automatic: Immediate, Queue, QuueOnError
+ // Enrollment: Enrollment, Immediate, Queue, QueueOnError
+ // Manual: Enrollment, Immediate, Manual, Queue, QueueOnError
+// XXX do we need ARs or PARs for this? add appropriate logging along with ARs
+
+ switch($context) {
+ case ProvisioningContextEnum::Automatic:
+ if(!in_array($t->status, [
+ ProvisionerModeEnum::Immediate,
+ ProvisionerModeEnum::Queue,
+ ProvisionerModeEnum::QueueOnError
+ ])) {
+ $this->llog('trace', "Skipping Provisioning Target with mode " . $t->status . " (automatic context)", $t->id);
+ continue 2;
+ }
+ break;
+ case ProvisioningContextEnum::Enrollment:
+ if($t->status == ProvisionerModeEnum::Manual) {
+ $this->llog('trace', "Skipping Provisioning Target with mode " . $t->status . " (enrollment context)", $t->id);
+ continue 2;
+ }
+ break;
+ case ProvisioningContextEnum::Manual:
+ // Manual provisioning is permitted regardless of target status
+ break;
+ }
+
+ $pluginModel = StringUtilities::pluginModel($t->plugin);
+ // The model in underscore format, eg file_provisioner
+ $uPluginModel = Inflector::underscore(Inflector::singularize($pluginModel));
+
+ // Does this plugin support this model?
+ if(!$this->$pluginModel->isProvisionableModel($provisionedModel)) {
+ $this->llog('trace', "Skipping $provisionedModel for $pluginModel (not supported)", $t->id);
+ continue;
+ }
+
+ try {
+ $this->llog('trace', "Provisioning $provisionedModel for $pluginModel (context: $context)", $t->id);
+
+ $result = $this->$pluginModel->provision($t->$uPluginModel, $provisionedModel, $data, $eligibility);
+
+ $this->ProvisioningHistoryRecords->record(
+ provisioningTargetId: $t->id,
+ comment: $result['comment'],
+ status: $result['status'],
+ subjectModel: $provisionedModel,
+ subjectId: $data->id
+ );
+ }
+ catch(\Exception $e) {
+ $this->ProvisioningHistoryRecords->record(
+ provisioningTargetId: $t->id,
+ comment: $e->getMessage(),
+ status: ProvisioningStatusEnum::NotProvisioned,
+ subjectModel: $provisionedModel,
+ subjectId: $data->id
+ );
+ }
+ }
+ }
+
+ /**
+ * Obtain provisioning status. (Either $groupId or $personId must be requested.)
+ *
+ * @since COmanage Registry v5.0.0
+ * @param int $coId CO ID
+ * @param int $groupId Group ID
+ * @param int $personId Person ID
+ */
+
+ public function status(int $coId, int $groupId=null, int $personId=null): array {
+ $ret = [];
+
+ // Start by pulling the set of active provisioning targets
+
+ $targets = $this->find()
+ ->where([
+ 'ProvisioningTargets.co_id' => $coId,
+ 'ProvisioningTargets.status <>' => ProvisionerModeEnum::Disabled
+ ])
+ ->all();
+
+ if(!empty($targets)) {
+ foreach($targets as $t) {
+ // For each target, get the status of the target for the requested subject.
+ // If the plugin implements a status() function we'll call it, otherwise
+ // we'll get the status from ProvisioningHistory.
+
+ $pluginModel = StringUtilities::pluginModel($t->plugin);
+
+ if(method_exists($this->$pluginModel, 'status')) {
+ // XXX define interface and call (implement with SqlProvisioner)
+ throw new \RuntimeException('NOT IMPLEMENTED');
+ } else {
+ $subjectFK = null;
+ $subjectID = null;
+
+ if(!empty($personId)) {
+ $subjectFK = 'person_id';
+ $subjectID = $personId;
+ } elseif(!empty($groupId)) {
+ $subjectFK = 'group_id';
+ $subjectID = $groupId;
+ } else {
+ throw new \InvalidArgumentException("NOT IMPKEMENTED");
+ }
+
+ $rec = $this->ProvisioningHistoryRecords->find()
+ ->where([
+ 'provisioning_target_id' => $t->id,
+ $subjectFK => $subjectID
+ ])
+ ->order(['id' => 'DESC'])
+ ->first();
+
+ if(!empty($rec)) {
+ $ret[] = [
+ 'target' => $t,
+ 'status' => $rec->status,
+ 'comment' => $rec->comment,
+ // XXX where does identifier come from?
+ //'identifier' => '?',
+ 'timestamp' => $rec->created
+ ];
+ } else {
+ $ret[] = [
+ 'target' => $t,
+ 'status' => ProvisioningStatusEnum::NotProvisioned,
+ 'comment' => __d('enumeration', 'ProvisioningStatusEnum.'.ProvisioningStatusEnum::NotProvisioned)
+ ];
+ }
+ }
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * 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('co_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->notEmptyString('co_id');
+
+ $this->registerStringValidation($validator, $schema, 'description', false);
+
+ $validator->add('status', [
+ 'content' => ['rule' => ['inList', ProvisionerModeEnum::getConstValues()]]
+ ]);
+ $validator->notEmptyString('status');
+
+ $this->registerStringValidation($validator, $schema, 'plugin', true);
+
+ $validator->add('provisioning_group_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->allowEmptyString('provisioning_group_id');
+
+ $validator->add('retry_interval', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->allowEmptyString('retry_interval');
+
+ $validator->add('ordr', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->allowEmptyString('ordr');
+
+ return $validator;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Table/ServersTable.php b/app/src/Model/Table/ServersTable.php
new file mode 100644
index 000000000..6c70d8156
--- /dev/null
+++ b/app/src/Model/Table/ServersTable.php
@@ -0,0 +1,163 @@
+addBehavior('Changelog');
+ $this->addBehavior('Log');
+ $this->addBehavior('Timestamp');
+
+ $this->setTableType(\App\Lib\Enum\TableTypeEnum::Primary);
+
+ // Define associations
+ $this->belongsTo('Cos');
+
+// XXX Note this will bind to (eg) CoreServer but not (eg) SqlProvisioner
+ $this->setPluginRelations();
+
+ $this->setDisplayField('description');
+
+ $this->setPrimaryLink('co_id');
+ $this->setRequiresCO(true);
+
+ $this->setAutoViewVars([
+ 'plugins' => [
+ 'type' => 'plugin',
+ 'pluginType' => 'server'
+ ],
+ 'statuses' => [
+ 'type' => 'enum',
+ 'class' => 'SuspendableStatusEnum'
+ ]
+ ]);
+
+ $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']
+ ]
+ ]);
+ }
+
+ /**
+ * Define business rules.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param RulesChecker $rules RulesChecker object
+ * @return RulesChecker
+ */
+
+ public function buildRules(RulesChecker $rules): RulesChecker {
+ // AR-Server-1 A Server cannot be deleted if it is referenced from
+ // a Configuration object (including plugins).
+
+ $rules->addDelete([$this, 'ruleInUse'],
+ 'serverInUse',
+ ['errorField' => 'status']);
+
+ return $rules;
+ }
+
+ /**
+ * Application Rule to determine if the server is in use.
+ *
+ * @since COmanage Registyr 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 ruleInUse($entity, $options) {
+ // XXX CFM-281 we need to do something here
+
+ return true;
+ }
+
+ /**
+ * 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('co_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->notEmptyString('co_id');
+
+ $this->registerStringValidation($validator, $schema, 'description', false);
+
+ $validator->add('status', [
+ 'content' => ['rule' => ['inList', SuspendableStatusEnum::getConstValues()]]
+ ]);
+ $validator->notEmptyString('status');
+
+ $this->registerStringValidation($validator, $schema, 'plugin', false);
+
+ return $validator;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Table/TelephoneNumbersTable.php b/app/src/Model/Table/TelephoneNumbersTable.php
index 3044435ed..ca8022826 100644
--- a/app/src/Model/Table/TelephoneNumbersTable.php
+++ b/app/src/Model/Table/TelephoneNumbersTable.php
@@ -40,6 +40,7 @@ class TelephoneNumbersTable extends Table {
use \App\Lib\Traits\HistoryTrait;
use \App\Lib\Traits\PermissionsTrait;
use \App\Lib\Traits\PrimaryLinkTrait;
+ use \App\Lib\Traits\ProvisionableTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\TypeTrait;
use \App\Lib\Traits\ValidationTrait;
@@ -97,7 +98,6 @@ public function initialize(array $config): void {
'entity' => [
'delete' => ['platformAdmin', 'coAdmin'],
'edit' => ['platformAdmin', 'coAdmin'],
- 'primary' => ['platformAdmin', 'coAdmin'],
'view' => ['platformAdmin', 'coAdmin']
],
// Actions that operate over a table (ie: do not require an $id)
diff --git a/app/src/Model/Table/TypesTable.php b/app/src/Model/Table/TypesTable.php
index b62b4c5fa..2f763c65f 100644
--- a/app/src/Model/Table/TypesTable.php
+++ b/app/src/Model/Table/TypesTable.php
@@ -37,11 +37,14 @@
use \App\Lib\Enum\EduPersonAffiliationEnum;
use \App\Lib\Enum\SuspendableStatusEnum;
+use \App\Lib\Enum\ProvisioningEligibilityEnum;
+
class TypesTable extends Table {
use \App\Lib\Traits\AutoViewVarsTrait;
use \App\Lib\Traits\CoLinkTrait;
use \App\Lib\Traits\PermissionsTrait;
use \App\Lib\Traits\PrimaryLinkTrait;
+ use \App\Lib\Traits\ProvisionableTrait;
use \App\Lib\Traits\SearchFilterTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\ValidationTrait;
@@ -263,6 +266,35 @@ public function getTypeLabel(int $id): string {
return $type->value;
}
+ /**
+ * Marshal object data for provisioning.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param int $id Entity ID
+ * @return array An array of provisionable data and eligibility
+ */
+
+ public function marshalProvisioningData(int $id): array {
+ $ret = [];
+ // We need the archived record on delete to properly deprovision
+ $ret['data'] = $this->get($id, ['archived' => true]);
+
+ // Provisioning Eligibility is
+ // - Deleted if the changelog deleted flag is true
+ // - Eligible if status is Active
+ // - Ineligible otherwise
+
+ $ret['eligibility'] = ProvisioningEligibilityEnum::Ineligible;
+
+ if($ret['data']->deleted) {
+ $ret['eligibility'] = ProvisioningEligibilityEnum::Deleted;
+ } elseif($ret['data']->status == SuspendableStatusEnum::Active) {
+ $ret['eligibility'] = ProvisioningEligibilityEnum::Eligible;
+ }
+
+ return $ret;
+ }
+
/**
* Determine if this type is in use.
*
diff --git a/app/src/Model/Table/UrlsTable.php b/app/src/Model/Table/UrlsTable.php
index 914d051ee..1c446eca5 100644
--- a/app/src/Model/Table/UrlsTable.php
+++ b/app/src/Model/Table/UrlsTable.php
@@ -39,6 +39,7 @@ class UrlsTable extends Table {
use \App\Lib\Traits\HistoryTrait;
use \App\Lib\Traits\PermissionsTrait;
use \App\Lib\Traits\PrimaryLinkTrait;
+ use \App\Lib\Traits\ProvisionableTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\TypeTrait;
use \App\Lib\Traits\ValidationTrait;
@@ -90,7 +91,6 @@ public function initialize(array $config): void {
'entity' => [
'delete' => ['platformAdmin', 'coAdmin'],
'edit' => ['platformAdmin', 'coAdmin'],
- 'primary' => ['platformAdmin', 'coAdmin'],
'view' => ['platformAdmin', 'coAdmin']
],
// Actions that operate over a table (ie: do not require an $id)
diff --git a/app/src/View/Helper/FieldHelper.php b/app/src/View/Helper/FieldHelper.php
index 02f014cdb..da303680d 100644
--- a/app/src/View/Helper/FieldHelper.php
+++ b/app/src/View/Helper/FieldHelper.php
@@ -45,6 +45,9 @@ class FieldHelper extends Helper {
// Our current model name
protected $modelName = null;
+ // The plugin we are rendering within, if set
+ protected $pluginName = null;
+
// The list of required fields
protected $reqFields = [];
@@ -130,7 +133,7 @@ public function control(string $fieldName,
'label' => __d('operation', 'configure.plugin'),
'url' => [
'plugin' => null,
- 'controller' => 'reports',
+ 'controller' => StringUtilities::entityToClassname($vv_obj),
'action' => 'configure',
$vv_obj->id
]
@@ -332,25 +335,54 @@ protected function formNameDiv(string $fieldName, string $labelText=null): strin
// First try to autogenerate the field label (if we weren't given one).
+ $pluginDomain = (!empty($this->pluginName)
+ ? Inflector::underscore($this->pluginName)
+ : null);
+
if(!$label) {
// We autogenerate field labels and descriptions from the field name.
- // Fields of the form foo_id map to the singular form of registry.ct.foos.
- // All others map first to registry.fd.Model.foo, then to registry.fd.foo
- // if no Model specific key is found.
-
- $label = __d('field', $mn.".".$fn);
- if($label == $mn.".".$fn) {
- // Model specific label not found, try again
-
- $f = null;
-
- if(preg_match('/^(.*?)_id$/', $fn, $f)) {
- // Map foreign keys (foo_id) to the controller label
- $label = __d('controller', Inflector::camelize(Inflector::pluralize($f[1])), [1]);
+ // We loop over the field generation logic twice, first for a plugin
+ // context (if set) and then generally (if no plugin localization was found).
+
+ // We use $core as the variable for this loop, so the rest of the code
+ // is easier to read (!$core = plugin)
+ for($core = 0;$core < 2;$core++) {
+ if(!$core && empty($this->pluginName)) {
+ // No plugin set, just go to the core field checks
+ continue;
+ }
+
+ // Is there a model specific key? For plugins, this will be in field.Model.Field
+
+ $key = (!$core ? "field." : "") . "$mn.$fn";
+ $label = __d(($core ? 'field' : $pluginDomain), $key);
+
+ if($label == $key) {
+ // Model specific label not found, try again for a general label
+
+ $f = null;
+
+ if(preg_match('/^(.*?)_id$/', $fn, $f)) {
+ // Map foreign keys (foo_id) to the controller label
+ $key = (!$core ? "controller." : "") . Inflector::camelize(Inflector::pluralize($f[1]));
+ $label = __d(($core ? 'controller' : $pluginDomain), $key, [1]);
+
+ if($key != $label) {
+ break;
+ }
+ } else {
+ // Just look up the key
+ $key = (!$core ? "field." : "") . $fn;
+ $label = __d(($core ? 'field' : $pluginDomain), $key);
+
+ if($key != $label) {
+ break;
+ }
+ }
} else {
- // Just look up the key
- $label = __d('field', $fn);
+ // If we found a key, break the loop
+ break;
}
}
}
@@ -358,15 +390,21 @@ protected function formNameDiv(string $fieldName, string $labelText=null): strin
// We try to automagically determine if a description for the field exists by
// looking for the corresponding .desc language translation.
- $desc = __d('field', $mn.".".$fn.".desc");
-
- if($desc == $mn.".".$fn.".desc") {
- $desc = __d('field', $fn.".desc");
- }
-
- // If the description is the literal key we just generated, there is no description
- if($desc == $fn.".desc") {
- $desc = null;
+ for($core = 0;$core < 2;$core++) {
+ if(!$core && empty($this->pluginName)) {
+ // No plugin set, just go to the core field checks
+ continue;
+ }
+
+ $key = (!$core ? "field." : "") . "$mn.$fn.desc";
+ $desc = __d(($core ? 'field' : $pluginDomain), $key);
+
+ // If the description is the literal key we just generated, there is no description
+ if($desc == $key) {
+ $desc = null;
+ } else {
+ break;
+ }
}
return '
@@ -444,14 +482,16 @@ public function statusControl(string $fieldName,
public function startControlSet(string $modelName,
string $action,
bool $editable,
- array $reqFields,
- $entity=null): string {
+ array $reqFields,
+ $entity=null,
+ ?string $pluginName=null): string {
$this->editable = $editable;
$this->modelName = $modelName;
+ $this->pluginName = $pluginName;
$this->reqFields = $reqFields;
$this->entity = $entity;
$this->action = $action;
-
+
return '
-
+
$this->Menu->getMenuOrder('Default'),
'icon' => 'history',
'url' => $actionUrl,
- 'label' => __d('operation', 'HistoryRecords')
+ 'label' => __d('controller', 'HistoryRecords', [99])
+ );
+ // provisioning actions
+ $actionUrl = $this->Url->build(
+ [
+ 'controller' => 'provisioning_targets',
+ 'action' => 'status',
+ '?' => [
+ 'person_id' => $curId
+ ]
+ ]
+ );
+ $action_args['vv_actions'][] = array(
+ 'order' => $this->Menu->getMenuOrder('Default'),
+ 'icon' => 'cloud_sync',
+ 'url' => $actionUrl,
+ 'label' => __d('operation', 'provisioning.status')
);
// delete
$actionPostBtnArray = ['action' => 'delete', $curId];
diff --git a/app/templates/layout/default.php b/app/templates/layout/default.php
index ee2b7377a..2f220b2f7 100644
--- a/app/templates/layout/default.php
+++ b/app/templates/layout/default.php
@@ -168,13 +168,9 @@
-
-
-
- = $this->element('breadcrumbs') ?>
-
-
+
+ = $this->element('breadcrumbs') ?>
+
diff --git a/app/vendor/autoload.php b/app/vendor/autoload.php
index 3e8e561ee..3b4acb469 100644
--- a/app/vendor/autoload.php
+++ b/app/vendor/autoload.php
@@ -2,6 +2,24 @@
// autoload.php @generated by Composer
+if (PHP_VERSION_ID < 50600) {
+ if (!headers_sent()) {
+ header('HTTP/1.1 500 Internal Server Error');
+ }
+ $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
+ if (!ini_get('display_errors')) {
+ if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+ fwrite(STDERR, $err);
+ } elseif (!headers_sent()) {
+ echo $err;
+ }
+ }
+ trigger_error(
+ $err,
+ E_USER_ERROR
+ );
+}
+
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitb25f76eec921984aa94dcf4015a4846e::getLoader();
diff --git a/app/vendor/cakephp-plugins.php b/app/vendor/cakephp-plugins.php
index 8f0549d56..5b33c03b7 100644
--- a/app/vendor/cakephp-plugins.php
+++ b/app/vendor/cakephp-plugins.php
@@ -5,6 +5,7 @@
'plugins' => [
'Bake' => $baseDir . '/vendor/cakephp/bake/',
'Cake/TwigView' => $baseDir . '/vendor/cakephp/twig-view/',
+ 'CoreServer' => $baseDir . '/plugins/CoreServer/',
'DebugKit' => $baseDir . '/vendor/cakephp/debug_kit/',
'Migrations' => $baseDir . '/vendor/cakephp/migrations/',
'TestWidget' => $baseDir . '/plugins/TestWidget/',
diff --git a/app/vendor/composer/ClassLoader.php b/app/vendor/composer/ClassLoader.php
index 0cd6055d1..a72151c77 100644
--- a/app/vendor/composer/ClassLoader.php
+++ b/app/vendor/composer/ClassLoader.php
@@ -42,6 +42,9 @@
*/
class ClassLoader
{
+ /** @var \Closure(string):void */
+ private static $includeFile;
+
/** @var ?string */
private $vendorDir;
@@ -106,6 +109,7 @@ class ClassLoader
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
+ self::initializeIncludeClosure();
}
/**
@@ -149,7 +153,7 @@ public function getFallbackDirsPsr4()
/**
* @return string[] Array of classname => path
- * @psalm-var array
+ * @psalm-return array
*/
public function getClassMap()
{
@@ -425,7 +429,8 @@ public function unregister()
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
- includeFile($file);
+ $includeFile = self::$includeFile;
+ $includeFile($file);
return true;
}
@@ -555,18 +560,26 @@ private function findFileWithExtension($class, $ext)
return false;
}
-}
-/**
- * Scope isolated include.
- *
- * Prevents access to $this/self from included files.
- *
- * @param string $file
- * @return void
- * @private
- */
-function includeFile($file)
-{
- include $file;
+ /**
+ * @return void
+ */
+ private static function initializeIncludeClosure()
+ {
+ if (self::$includeFile !== null) {
+ return;
+ }
+
+ /**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ *
+ * @param string $file
+ * @return void
+ */
+ self::$includeFile = \Closure::bind(static function($file) {
+ include $file;
+ }, null, null);
+ }
}
diff --git a/app/vendor/composer/autoload_classmap.php b/app/vendor/composer/autoload_classmap.php
index 03201ebda..17572c4b0 100644
--- a/app/vendor/composer/autoload_classmap.php
+++ b/app/vendor/composer/autoload_classmap.php
@@ -2,7 +2,7 @@
// autoload_classmap.php @generated by Composer
-$vendorDir = dirname(dirname(__FILE__));
+$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
diff --git a/app/vendor/composer/autoload_files.php b/app/vendor/composer/autoload_files.php
index c6ab79d61..8cb5843bd 100644
--- a/app/vendor/composer/autoload_files.php
+++ b/app/vendor/composer/autoload_files.php
@@ -2,23 +2,17 @@
// autoload_files.php @generated by Composer
-$vendorDir = dirname(dirname(__FILE__));
+$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
- '6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
- 'ec07570ca5a812141189b1fa81503674' => $vendorDir . '/phpunit/phpunit/src/Framework/Assert/Functions.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
- 'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
- '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
+ 'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
+ '34122c0574b76bf21c9a8db62b5b9cf3' => $vendorDir . '/cakephp/chronos/src/carbon_compat.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
- 'ad155f8f1cf0d418fe49e248db8c661b' => $vendorDir . '/react/promise/src/functions_include.php',
- 'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
- '23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.php',
- '0d59ee240a4cd96ddbb4ff164fccea4d' => $vendorDir . '/symfony/polyfill-php73/bootstrap.php',
'07d7f1a47144818725fd8d91a907ac57' => $vendorDir . '/laminas/laminas-diactoros/src/functions/create_uploaded_file.php',
'da94ac5d3ca7d2dbab84ce561ce72bfd' => $vendorDir . '/laminas/laminas-diactoros/src/functions/marshal_headers_from_sapi.php',
'3d97c8dcdfba8cb85d3b34f116bb248b' => $vendorDir . '/laminas/laminas-diactoros/src/functions/marshal_method_from_sapi.php',
@@ -35,11 +29,17 @@
'cc8e14526dc240491e17a838cb78508c' => $vendorDir . '/laminas/laminas-diactoros/src/functions/normalize_server.legacy.php',
'786bf90caabc9e09b6ad4cc5ca8f0e30' => $vendorDir . '/laminas/laminas-diactoros/src/functions/normalize_uploaded_files.legacy.php',
'751a5a3f463e4be759be31748b61737c' => $vendorDir . '/laminas/laminas-diactoros/src/functions/parse_cookie_header.legacy.php',
- '34122c0574b76bf21c9a8db62b5b9cf3' => $vendorDir . '/cakephp/chronos/src/carbon_compat.php',
'c720f792236cd163ece8049879166850' => $vendorDir . '/cakephp/cakephp/src/Core/functions.php',
'ede59e3a405fb689cd1cebb7bb1db3fb' => $vendorDir . '/cakephp/cakephp/src/Collection/functions.php',
'90236b492da7ca2983a2ad6e33e4152e' => $vendorDir . '/cakephp/cakephp/src/I18n/functions.php',
'2cb76c05856dfb60ada40ef54138d49a' => $vendorDir . '/cakephp/cakephp/src/Routing/functions.php',
'b1fc73705e1bec51cd2b20a32cf1c60a' => $vendorDir . '/cakephp/cakephp/src/Utility/bootstrap.php',
+ 'ad155f8f1cf0d418fe49e248db8c661b' => $vendorDir . '/react/promise/src/functions_include.php',
+ '0d59ee240a4cd96ddbb4ff164fccea4d' => $vendorDir . '/symfony/polyfill-php73/bootstrap.php',
+ 'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
+ '23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.php',
+ '6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
+ '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
+ 'ec07570ca5a812141189b1fa81503674' => $vendorDir . '/phpunit/phpunit/src/Framework/Assert/Functions.php',
'801c31d8ed748cfa537fa45402288c95' => $vendorDir . '/psy/psysh/src/functions.php',
);
diff --git a/app/vendor/composer/autoload_namespaces.php b/app/vendor/composer/autoload_namespaces.php
index 7be99901a..136acb8a1 100644
--- a/app/vendor/composer/autoload_namespaces.php
+++ b/app/vendor/composer/autoload_namespaces.php
@@ -2,7 +2,7 @@
// autoload_namespaces.php @generated by Composer
-$vendorDir = dirname(dirname(__FILE__));
+$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
diff --git a/app/vendor/composer/autoload_psr4.php b/app/vendor/composer/autoload_psr4.php
index c6aec9d22..1fd70561a 100644
--- a/app/vendor/composer/autoload_psr4.php
+++ b/app/vendor/composer/autoload_psr4.php
@@ -2,7 +2,7 @@
// autoload_psr4.php @generated by Composer
-$vendorDir = dirname(dirname(__FILE__));
+$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
@@ -25,6 +25,8 @@
'Symfony\\Component\\Filesystem\\' => array($vendorDir . '/symfony/filesystem'),
'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'),
'Symfony\\Component\\Config\\' => array($vendorDir . '/symfony/config'),
+ 'SqlConnector\\Test\\' => array($baseDir . '/availableplugins/SqlConnector/tests'),
+ 'SqlConnector\\' => array($baseDir . '/availableplugins/SqlConnector/src'),
'SlevomatCodingStandard\\' => array($vendorDir . '/slevomat/coding-standard/SlevomatCodingStandard'),
'Seld\\Signal\\' => array($vendorDir . '/seld/signal-handler/src'),
'Seld\\PharUtils\\' => array($vendorDir . '/seld/phar-utils/src'),
@@ -49,6 +51,8 @@
'Laminas\\Diactoros\\' => array($vendorDir . '/laminas/laminas-diactoros/src'),
'JsonSchema\\' => array($vendorDir . '/justinrainbow/json-schema/src/JsonSchema'),
'Jasny\\Twig\\' => array($vendorDir . '/jasny/twig-extensions/src'),
+ 'FileConnector\\Test\\' => array($baseDir . '/availableplugins/FileConnector/tests'),
+ 'FileConnector\\' => array($baseDir . '/availableplugins/FileConnector/src'),
'Doctrine\\Instantiator\\' => array($vendorDir . '/doctrine/instantiator/src/Doctrine/Instantiator'),
'Doctrine\\Deprecations\\' => array($vendorDir . '/doctrine/deprecations/lib/Doctrine/Deprecations'),
'Doctrine\\DBAL\\' => array($vendorDir . '/doctrine/dbal/src'),
@@ -57,6 +61,8 @@
'DeepCopy\\' => array($vendorDir . '/myclabs/deep-copy/src/DeepCopy'),
'DebugKit\\Test\\Fixture\\' => array($vendorDir . '/cakephp/debug_kit/tests/Fixture'),
'DebugKit\\' => array($vendorDir . '/cakephp/debug_kit/src'),
+ 'CoreServer\\Test\\' => array($baseDir . '/plugins/CoreServer/tests'),
+ 'CoreServer\\' => array($baseDir . '/plugins/CoreServer/src'),
'CoreReport\\Test\\' => array($baseDir . '/availableplugins/CoreReport/tests'),
'CoreReport\\' => array($baseDir . '/availableplugins/CoreReport/src'),
'Composer\\XdebugHandler\\' => array($vendorDir . '/composer/xdebug-handler/src'),
diff --git a/app/vendor/composer/autoload_real.php b/app/vendor/composer/autoload_real.php
index 0209e6f28..8072a483a 100644
--- a/app/vendor/composer/autoload_real.php
+++ b/app/vendor/composer/autoload_real.php
@@ -25,51 +25,26 @@ public static function getLoader()
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInitb25f76eec921984aa94dcf4015a4846e', 'loadClassLoader'), true, true);
- self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
+ self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInitb25f76eec921984aa94dcf4015a4846e', 'loadClassLoader'));
- $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
- if ($useStaticLoader) {
- require __DIR__ . '/autoload_static.php';
+ require __DIR__ . '/autoload_static.php';
+ call_user_func(\Composer\Autoload\ComposerStaticInitb25f76eec921984aa94dcf4015a4846e::getInitializer($loader));
- call_user_func(\Composer\Autoload\ComposerStaticInitb25f76eec921984aa94dcf4015a4846e::getInitializer($loader));
- } else {
- $map = require __DIR__ . '/autoload_namespaces.php';
- foreach ($map as $namespace => $path) {
- $loader->set($namespace, $path);
- }
+ $loader->register(true);
- $map = require __DIR__ . '/autoload_psr4.php';
- foreach ($map as $namespace => $path) {
- $loader->setPsr4($namespace, $path);
- }
+ $filesToLoad = \Composer\Autoload\ComposerStaticInitb25f76eec921984aa94dcf4015a4846e::$files;
+ $requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
+ if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
+ $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
- $classMap = require __DIR__ . '/autoload_classmap.php';
- if ($classMap) {
- $loader->addClassMap($classMap);
+ require $file;
}
- }
-
- $loader->register(true);
-
- if ($useStaticLoader) {
- $includeFiles = Composer\Autoload\ComposerStaticInitb25f76eec921984aa94dcf4015a4846e::$files;
- } else {
- $includeFiles = require __DIR__ . '/autoload_files.php';
- }
- foreach ($includeFiles as $fileIdentifier => $file) {
- composerRequireb25f76eec921984aa94dcf4015a4846e($fileIdentifier, $file);
+ }, null, null);
+ foreach ($filesToLoad as $fileIdentifier => $file) {
+ $requireFile($fileIdentifier, $file);
}
return $loader;
}
}
-
-function composerRequireb25f76eec921984aa94dcf4015a4846e($fileIdentifier, $file)
-{
- if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
- require $file;
-
- $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
- }
-}
diff --git a/app/vendor/composer/autoload_static.php b/app/vendor/composer/autoload_static.php
index 2d4c1836d..b446bd07a 100644
--- a/app/vendor/composer/autoload_static.php
+++ b/app/vendor/composer/autoload_static.php
@@ -7,19 +7,13 @@
class ComposerStaticInitb25f76eec921984aa94dcf4015a4846e
{
public static $files = array (
- '6124b4c8570aa390c21fafd04a26c69f' => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
- 'ec07570ca5a812141189b1fa81503674' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Assert/Functions.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
- 'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php',
- '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
+ 'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
+ '34122c0574b76bf21c9a8db62b5b9cf3' => __DIR__ . '/..' . '/cakephp/chronos/src/carbon_compat.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php',
- 'ad155f8f1cf0d418fe49e248db8c661b' => __DIR__ . '/..' . '/react/promise/src/functions_include.php',
- 'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
- '23c18046f52bef3eea034657bafda50f' => __DIR__ . '/..' . '/symfony/polyfill-php81/bootstrap.php',
- '0d59ee240a4cd96ddbb4ff164fccea4d' => __DIR__ . '/..' . '/symfony/polyfill-php73/bootstrap.php',
'07d7f1a47144818725fd8d91a907ac57' => __DIR__ . '/..' . '/laminas/laminas-diactoros/src/functions/create_uploaded_file.php',
'da94ac5d3ca7d2dbab84ce561ce72bfd' => __DIR__ . '/..' . '/laminas/laminas-diactoros/src/functions/marshal_headers_from_sapi.php',
'3d97c8dcdfba8cb85d3b34f116bb248b' => __DIR__ . '/..' . '/laminas/laminas-diactoros/src/functions/marshal_method_from_sapi.php',
@@ -36,12 +30,18 @@ class ComposerStaticInitb25f76eec921984aa94dcf4015a4846e
'cc8e14526dc240491e17a838cb78508c' => __DIR__ . '/..' . '/laminas/laminas-diactoros/src/functions/normalize_server.legacy.php',
'786bf90caabc9e09b6ad4cc5ca8f0e30' => __DIR__ . '/..' . '/laminas/laminas-diactoros/src/functions/normalize_uploaded_files.legacy.php',
'751a5a3f463e4be759be31748b61737c' => __DIR__ . '/..' . '/laminas/laminas-diactoros/src/functions/parse_cookie_header.legacy.php',
- '34122c0574b76bf21c9a8db62b5b9cf3' => __DIR__ . '/..' . '/cakephp/chronos/src/carbon_compat.php',
'c720f792236cd163ece8049879166850' => __DIR__ . '/..' . '/cakephp/cakephp/src/Core/functions.php',
'ede59e3a405fb689cd1cebb7bb1db3fb' => __DIR__ . '/..' . '/cakephp/cakephp/src/Collection/functions.php',
'90236b492da7ca2983a2ad6e33e4152e' => __DIR__ . '/..' . '/cakephp/cakephp/src/I18n/functions.php',
'2cb76c05856dfb60ada40ef54138d49a' => __DIR__ . '/..' . '/cakephp/cakephp/src/Routing/functions.php',
'b1fc73705e1bec51cd2b20a32cf1c60a' => __DIR__ . '/..' . '/cakephp/cakephp/src/Utility/bootstrap.php',
+ 'ad155f8f1cf0d418fe49e248db8c661b' => __DIR__ . '/..' . '/react/promise/src/functions_include.php',
+ '0d59ee240a4cd96ddbb4ff164fccea4d' => __DIR__ . '/..' . '/symfony/polyfill-php73/bootstrap.php',
+ 'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
+ '23c18046f52bef3eea034657bafda50f' => __DIR__ . '/..' . '/symfony/polyfill-php81/bootstrap.php',
+ '6124b4c8570aa390c21fafd04a26c69f' => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
+ '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
+ 'ec07570ca5a812141189b1fa81503674' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Assert/Functions.php',
'801c31d8ed748cfa537fa45402288c95' => __DIR__ . '/..' . '/psy/psysh/src/functions.php',
);
@@ -70,6 +70,8 @@ class ComposerStaticInitb25f76eec921984aa94dcf4015a4846e
'Symfony\\Component\\Filesystem\\' => 29,
'Symfony\\Component\\Console\\' => 26,
'Symfony\\Component\\Config\\' => 25,
+ 'SqlConnector\\Test\\' => 18,
+ 'SqlConnector\\' => 13,
'SlevomatCodingStandard\\' => 23,
'Seld\\Signal\\' => 12,
'Seld\\PharUtils\\' => 15,
@@ -110,6 +112,11 @@ class ComposerStaticInitb25f76eec921984aa94dcf4015a4846e
'JsonSchema\\' => 11,
'Jasny\\Twig\\' => 11,
),
+ 'F' =>
+ array (
+ 'FileConnector\\Test\\' => 19,
+ 'FileConnector\\' => 14,
+ ),
'D' =>
array (
'Doctrine\\Instantiator\\' => 22,
@@ -123,6 +130,8 @@ class ComposerStaticInitb25f76eec921984aa94dcf4015a4846e
),
'C' =>
array (
+ 'CoreServer\\Test\\' => 16,
+ 'CoreServer\\' => 11,
'CoreReport\\Test\\' => 16,
'CoreReport\\' => 11,
'Composer\\XdebugHandler\\' => 23,
@@ -229,6 +238,14 @@ class ComposerStaticInitb25f76eec921984aa94dcf4015a4846e
array (
0 => __DIR__ . '/..' . '/symfony/config',
),
+ 'SqlConnector\\Test\\' =>
+ array (
+ 0 => __DIR__ . '/../..' . '/availableplugins/SqlConnector/tests',
+ ),
+ 'SqlConnector\\' =>
+ array (
+ 0 => __DIR__ . '/../..' . '/availableplugins/SqlConnector/src',
+ ),
'SlevomatCodingStandard\\' =>
array (
0 => __DIR__ . '/..' . '/slevomat/coding-standard/SlevomatCodingStandard',
@@ -327,6 +344,14 @@ class ComposerStaticInitb25f76eec921984aa94dcf4015a4846e
array (
0 => __DIR__ . '/..' . '/jasny/twig-extensions/src',
),
+ 'FileConnector\\Test\\' =>
+ array (
+ 0 => __DIR__ . '/../..' . '/availableplugins/FileConnector/tests',
+ ),
+ 'FileConnector\\' =>
+ array (
+ 0 => __DIR__ . '/../..' . '/availableplugins/FileConnector/src',
+ ),
'Doctrine\\Instantiator\\' =>
array (
0 => __DIR__ . '/..' . '/doctrine/instantiator/src/Doctrine/Instantiator',
@@ -359,6 +384,14 @@ class ComposerStaticInitb25f76eec921984aa94dcf4015a4846e
array (
0 => __DIR__ . '/..' . '/cakephp/debug_kit/src',
),
+ 'CoreServer\\Test\\' =>
+ array (
+ 0 => __DIR__ . '/../..' . '/plugins/CoreServer/tests',
+ ),
+ 'CoreServer\\' =>
+ array (
+ 0 => __DIR__ . '/../..' . '/plugins/CoreServer/src',
+ ),
'CoreReport\\Test\\' =>
array (
0 => __DIR__ . '/../..' . '/availableplugins/CoreReport/tests',