Skip to content

CO-2962_EnvSource_configuration_should_not_use_Shibboleth_SimpleSAMLphp #369

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/plugins/EnvSource/config/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"id": {},
"external_identity_source_id": {},
"redirect_on_duplicate": { "type": "url" },
"sp_mode": { "type": "enum" },
"mva_delimiter": { "type": "string", "size": "5" },
"sync_on_login": { "type": "boolean"},
"default_affiliation_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } },
"address_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } },
Expand Down
7 changes: 5 additions & 2 deletions app/plugins/EnvSource/resources/locales/en_US/env_source.po
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ msgstr "Source Key {0} is already attached to External Identity {1}"
msgid "field.EnvSources.address_type_id"
msgstr "Address Type"

msgid "field.EnvSources.mva_delimiter_common"
msgstr "Common Delimiters"

msgid "field.EnvSources.default_affiliation_type_id"
msgstr "Default Affiliation Type"

Expand Down Expand Up @@ -145,8 +148,8 @@ msgstr "Path to lookaside file, intended for testing only"
msgid "field.EnvSources.redirect_on_duplicate"
msgstr "Redirect on Duplicate"

msgid "field.EnvSources.sp_mode"
msgstr "Web Server Service Provider"
msgid "field.EnvSources.mva_delimiter"
msgstr "Service Provider Multi Value Delimiter"

msgid "field.EnvSources.sync_on_login"
msgstr "Sync on Login"
Expand Down
27 changes: 9 additions & 18 deletions app/plugins/EnvSource/src/Model/Table/EnvSourcesTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -280,21 +280,15 @@ protected function resultToEntityData(

// Email Address
if(!empty($result['env_mail'])) {
$mails = [];

// We accept multiple values if supported by the configured SP software.

switch($EnvSource->sp_mode) {
case EnvSourceSpModeEnum::Shibboleth:
$mails = explode(";", $result['env_mail']);
break;
case EnvSourceSpModeEnum::SimpleSamlPhp:
$mails = explode(",", $result['env_mail']);
break;
default:
// We dont' try to tokenize the string
$mails = [ $result['env_mail' ]];
break;
$delimiter = $EnvSource->mva_delimiter;
$stringValue = $result['env_mail'];

// Check if the delimiter is provided and not an empty string
if (!empty($delimiter)) {
$mails = explode($delimiter, $stringValue);
} else {
$mails = [$stringValue];
}

foreach($mails as $m) {
Expand Down Expand Up @@ -443,10 +437,7 @@ public function validationDefault(Validator $validator): Validator {
]);
$validator->allowEmptyString('redirect_on_duplicate');

$validator->add('sp_mode', [
'content' => ['rule' => ['inList', EnvSourceSpModeEnum::getConstValues()]]
]);
$validator->notEmptyString('sp_mode');
$this->registerStringValidation($validator, $schema, 'mva_delimiter', true);

$validator->add('sync_on_login', [
'content' => ['rule' => 'boolean']
Expand Down
91 changes: 90 additions & 1 deletion app/plugins/EnvSource/templates/EnvSources/fields.inc
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,98 @@
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/

use EnvSource\Lib\Enum\EnvSourceSpModeEnum;

// Build the delimiter map dynamically from your $spModes array
$phpDelimiterMap = [];
$selectedValue = '';
foreach ($spModes as $key => $label) {
if ($key === EnvSourceSpModeEnum::Shibboleth) {
$phpDelimiterMap[$key] = ';';
if ($vv_obj?->mva_delimiter == ';') {
$selectedValue = EnvSourceSpModeEnum::Shibboleth;
}
} elseif ($key === EnvSourceSpModeEnum::SimpleSamlPhp) {
$phpDelimiterMap[$key] = ',';
if ($vv_obj?->mva_delimiter == ',') {
$selectedValue = EnvSourceSpModeEnum::SimpleSamlPhp;
}
} elseif ($key === EnvSourceSpModeEnum::Other) {
$phpDelimiterMap[$key] = '';
if (!in_array($vv_obj?->mva_delimiter, ['', ',', ';'])) {
$phpDelimiterMap[$key] = $vv_obj?->mva_delimiter;
$selectedValue = EnvSourceSpModeEnum::Other;
}
}
}

// Encode the array to JSON before inserting it into the Javascript block
$jsonDelimiterMap = json_encode($phpDelimiterMap);


$afterField = <<<JS
<script>
document.addEventListener('DOMContentLoaded', function() {
var select = document.getElementById('mva_delimiter_default');
var textbox = document.getElementById('mva_delimiter');
// Map the dropdown values to actual delimiter characters
var delimiterMap = $jsonDelimiterMap;
select.addEventListener('change', function() {
var selectedValue = select.value;
// Update the textbox with the corresponding delimiter
if (delimiterMap[selectedValue] !== undefined) {
textbox.value = delimiterMap[selectedValue];
} else {
// Fallback in case of unexpected values
textbox.value = selectedValue;
}
// Focus the textbox if "Other" is selected so we can immediately type
if (selectedValue === 'O') {
textbox.focus();
}
});
if (!textbox.value && select.value) {
textbox.value = delimiterMap[select.value] || '';
}
});
</script>
JS;


$fields = [
// 'duplicate_mode',
'sp_mode',
'mva_delimiter' => [
'fieldLabel' => __d('env_source','field.EnvSources.mva_delimiter'),
'groupedControls' => [
'mva_delimiter' => [
'type' => 'text',
'id' => 'mva_delimiter',
'label' => false,
'required' => true,
'class' => 'form-control mb-1',
'singleRowItem' => true,
],
'mva_delimiter_default' => [
'id' => 'mva_delimiter_default',
'label' => [
'text' => __d('env_source','field.EnvSources.mva_delimiter_common'),
'class' => 'mb-2'
],
'name' => 'mva_delimiter_default',
'required' => false,
'empty' => false,
'type' => 'select',
'value' => $selectedValue,
'options' => $spModes,
],
],
'afterField' => $afterField
],
'sync_on_login',
'address_type_id',
'default_affiliation_type_id',
Expand Down
6 changes: 6 additions & 0 deletions app/resources/locales/en_US/information.po
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,18 @@ msgstr "Executing post-database tasks for version {0}"
msgid "ug.tasks.pre"
msgstr "Executing pre-database tasks for version {0}"

msgid "ug.tasks.schema.reload"
msgstr "Force a global schema reload so that post-upgrade tasks are aware of the new schema"

msgid "ug.version.current"
msgstr "Current version: {0}"

msgid "ug.version.target"
msgstr "Target version: {0}"

msgid "ug.tasks.assignEnvSourceMvaDelimiter.co"
msgstr "Map the EnvSource SP Mode to Multi Value Delimitier for CO {0}"

msgid "ug.tasks.assignUuids"
msgstr "Assigning UUIDs (this may take a while for larger deployments)"

Expand Down
107 changes: 104 additions & 3 deletions app/src/Command/UpgradeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

namespace App\Command;

use Cake\Cache\Cache;
use Cake\Console\Arguments;
use Cake\Console\BaseCommand;
use Cake\Console\ConsoleIo;
Expand All @@ -37,13 +38,29 @@
use \App\Lib\Enum\GroupTypeEnum;
use \App\Lib\Util\PaginatedSqlIterator;
use \App\Lib\Util\SearchUtilities;
use Cake\ORM\TableRegistry;
use EnvSource\Lib\Enum\EnvSourceSpModeEnum;

class UpgradeCommand extends BaseCommand
{
use \Cake\ORM\Locator\LocatorAwareTrait;

protected $io = null;

/**
* Cache of EnvSource SP Modes partitioned by CO ID.
*
* This array temporarily stores the `sp_mode` values (e.g., 'SH', 'SS', 'O')
* from the database prior to schema migration. It allows post-migration tasks
* to accurately map the deprecated `sp_mode` column data into the new
* `mva_delimiter` column structure.
*
* Format: `[ coId => [ envSourceId => 'sp_mode_value' ] ]`
*
* @var array<int, array<int, string>>
*/
private array $EnvSourceSpMode = [];

// A list of known versions, must be semantic versioning compliant. The value
// is a "blocker" if it is a version that prevents an upgrade from happening.
// For example, if a user attempts to upgrade from 1.0.0 to 1.2.0, and 1.1.0
Expand Down Expand Up @@ -76,13 +93,15 @@ class UpgradeCommand extends BaseCommand
"5.2.0" => [
'block' => false,
'pre' => [
'checkGroupNames'
'checkGroupNames',
'cacheEnvSourcespMode'
],
'post' => [
'assignUuids',
'buildGroupTree',
'createDefaultGroups',
'installMostlyStaticPages'
'installMostlyStaticPages',
'assignEnvSourceMvaDelimiter',
]
]
];
Expand All @@ -95,7 +114,9 @@ class UpgradeCommand extends BaseCommand
'buildGroupTree' => ['global' => true],
'checkGroupNames' => ['global' => true],
'createDefaultGroups' => ['perCO' => true, 'perCOU' => true],
'installMostlyStaticPages' => ['perCO' => true]
'installMostlyStaticPages' => ['perCO' => true],
'cacheEnvSourcespMode' => ['perCO' => true],
'assignEnvSourceMvaDelimiter' => ['perCO' => true]
];

/**
Expand Down Expand Up @@ -235,6 +256,19 @@ public function execute(Arguments $args, ConsoleIo $io)
if(!$args->getOption('skipdatabase')) {
// Call database command
$this->executeCommand(DatabaseCommand::class);

// Force a global schema reload so that post-upgrade tasks are aware of the new schema
$this->io->out(__d('information', 'ug.tasks.schema.reload'));

// Clear all loaded table instances so they are re-instantiated with the new schema
TableRegistry::getTableLocator()->clear();

// Clear the CakePHP file/redis cache for models (this holds the schema arrays)
Cache::clear('_cake_model_');

// Disable schema caching for this specific process going forward
$connection = ConnectionManager::get('default');
$connection->cacheMetadata(false);
}

// Run appropriate post-database steps
Expand Down Expand Up @@ -378,6 +412,73 @@ protected function assignUuids() {
}
}


/**
* Cache the SP Mode for EnvSource instances associated with a given CO.
* This is used prior to database migrations where the sp_mode column is
* removed or altered, allowing post-migration tasks to map the old value
* to the new schema structure (e.g., mva_delimiter).
*
* @since COmanage Registry v5.2.0
* @param int $coId CO ID to cache the configurations for
* @return void
*/
protected function cacheEnvSourcespMode(int $coId): void
{
$MspsTable = $this->getTableLocator()->get('EnvSource.EnvSources');

$modes = $MspsTable->find('list', [
'keyField' => 'id',
'valueField' => 'sp_mode'
])
->innerJoinWith('ExternalIdentitySources')
->where(['ExternalIdentitySources.co_id' => $coId])
->applyOptions(['archived' => true])
->toArray();

$this->EnvSourceSpMode[$coId] = $modes;
}

/**
* Cache the SP Mode for EnvSource instances associated with a given CO.
* This is used prior to database migrations where the sp_mode column is
* removed or altered, allowing post-migration tasks to map the old value
* to the new schema structure (e.g., mva_delimiter).
*
* @param int $coId CO ID to cache the configurations for
* @return void
* @throws \Exception
* @since COmanage Registry v5.2.0
*/
protected function assignEnvSourceMvaDelimiter(int $coId): void
{
if (empty($this->EnvSourceSpMode[$coId])) {
// Nothing to migrate for this CO
return;
}

$EnvSourcesTable = $this->getTableLocator()->get('EnvSource.EnvSources');

// Iterate over the cached id => sp_mode array for this CO
foreach ($this->EnvSourceSpMode[$coId] as $envSourceId => $spMode) {
$delimiter = ';';

// Map the old SP Mode to the new multi-value delimiter
if ($spMode === EnvSourceSpModeEnum::Shibboleth) {
$delimiter = ';';
} elseif ($spMode === EnvSourceSpModeEnum::SimpleSamlPhp) {
$delimiter = ',';
}

// Use updateAll to execute a raw SQL UPDATE.
// This bypasses the ORM's beforeSave event and ChangelogBehavior's lock.
$EnvSourcesTable->updateAll(
['mva_delimiter' => $delimiter], // Fields to update
['id' => $envSourceId] // Conditions
);
}
}

/**
* Establish tree metadata for Groups.
*
Expand Down
5 changes: 3 additions & 2 deletions app/src/View/Helper/FieldHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ public function formField(string $fieldName,
string $fieldType = null,
array $fieldSelectOptions = null,
string $fieldNameAlias = null,
bool $labelIsTextOnly = null): string
bool $labelIsTextOnly = null): string
{
$fieldArgs = $fieldOptions ?? [];
$fieldArgs['label'] = $fieldOptions['label'] ?? false;
Expand Down Expand Up @@ -758,7 +758,8 @@ public function restructureFieldArguments(array $fieldArgs) {
'id',
'style',
'checked',
'label'
'label',
'name',
];

// Remove the top-level field options that are intended for Cake, and
Expand Down