Merge pull request #558 from nextcloud/enh/noid/settings-table

Move SAML configurations to a table of their own
This commit is contained in:
Vincent Petry 2022-04-07 23:16:55 +02:00 committed by GitHub
commit d47892d0b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1237 additions and 480 deletions

View file

@ -1,6 +1,16 @@
# Changelog
All notable changes to this project will be documented in this file.
## 5.0.0
### Changed
- store configurations in a separate database table, not appconfig
### Added
- occ commands for modifying SAML configurations
### Removed
- Ability to change SAML configuration with occ app-config, use the new occ commands instead
## 4.1.0
### Added
- Nextcloud 22 support

View file

@ -38,12 +38,8 @@ try {
\OC::$server->getLogger()->logException($e);
return;
}
$samlSettings = new \OCA\User_SAML\SAMLSettings(
$urlGenerator,
$config,
$request,
$session
);
$samlSettings = \OC::$server->query(\OCA\User_SAML\SAMLSettings::class);
$userData = new \OCA\User_SAML\UserData(
new \OCA\User_SAML\UserResolver(\OC::$server->getUserManager()),
@ -72,11 +68,6 @@ $returnScript = false;
$type = '';
switch ($config->getAppValue('user_saml', 'type')) {
case 'saml':
try {
$oneLoginSettings = new \OneLogin\Saml2\Settings($samlSettings->getOneLoginSettingsArray(1));
} catch (\OneLogin\SAML2\Error $e) {
$returnScript = true;
}
$type = 'saml';
break;
case 'environment-variable':

View file

@ -16,7 +16,7 @@ The following providers are supported and tested at the moment:
* Any other provider that authenticates using the environment variable
While theoretically any other authentication provider implementing either one of those standards is compatible, we like to note that they are not part of any internal test matrix.]]></description>
<version>4.3.0</version>
<version>5.0.0</version>
<licence>agpl</licence>
<author>Lukas Reschke</author>
<namespace>User_SAML</namespace>
@ -36,6 +36,10 @@ While theoretically any other authentication provider implementing either one of
<nextcloud min-version="21" max-version="24" />
</dependencies>
<commands>
<command>OCA\User_SAML\Command\ConfigCreate</command>
<command>OCA\User_SAML\Command\ConfigDelete</command>
<command>OCA\User_SAML\Command\ConfigGet</command>
<command>OCA\User_SAML\Command\ConfigSet</command>
<command>OCA\User_SAML\Command\GetMetadata</command>
</commands>
<settings>

View file

@ -69,20 +69,47 @@ return [
'url' => '/saml/selectUserBackEnd',
'verb' => 'GET',
],
[
'name' => 'Settings#getSamlProviderIds',
'url' => '/settings/providers',
'verb' => 'GET',
],
[
'name' => 'Settings#getSamlProviderSettings',
'url' => '/settings/providerSettings/{providerId}',
'verb' => 'GET',
'defaults' => [
'providerId' => '1'
'providerId' => 1
],
'requirements' => [
'providerId' => '\d+'
]
],
[
'name' => 'Settings#setProviderSetting',
'url' => '/settings/providerSettings/{providerId}',
'verb' => 'PUT',
'defaults' => [
'providerId' => 1
],
'requirements' => [
'providerId' => '\d+'
]
],
[
'name' => 'Settings#newSamlProviderSettingsId',
'url' => '/settings/providerSettings',
'verb' => 'POST',
],
[
'name' => 'Settings#deleteSamlProviderSettings',
'url' => '/settings/providerSettings/{providerId}',
'verb' => 'DELETE',
'defaults' => [
'providerId' => '1'
'providerId' => 1
],
'requirements' => [
'providerId' => '\d+'
]
],
[

View file

@ -8,20 +8,30 @@
currentConfig: '1',
providerIds: '1',
_getAppConfig: function (key) {
return $.ajax({
type: 'GET',
url: OC.linkToOCS('apps/provisioning_api/api/v1', 2) + 'config/apps' + '/user_saml/' + key + '?format=json'
});
},
init: function(callback) {
this._getAppConfig('providerIds').done(function (data){
if (data.ocs.data.data !== '') {
OCA.User_SAML.Admin.providerIds = data.ocs.data.data;
OCA.User_SAML.Admin.currentConfig = OCA.User_SAML.Admin.providerIds.split(',').sort()[0];
var xhr = new XMLHttpRequest();
xhr.open('GET', OC.generateUrl('/apps/user_saml/settings/providers'));
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('requesttoken', OC.requestToken);
xhr.onload = function () {
var response = JSON.parse(xhr.response);
if (xhr.status >= 200 && xhr.status < 300) {
if (response.providerIds !== "") {
OCA.User_SAML.Admin.providerIds += ',' + response.providerIds;
OCA.User_SAML.Admin.currentConfig = OCA.User_SAML.Admin.providerIds.split(',')[0];
}
callback();
} else {
console.error("Could not fetch new provider ID");
}
callback();
});
};
xhr.onerror = function () {
console.error("Could not fetch new provider ID");
}
xhr.send();
},
chooseEnv: function() {
if (OC.PasswordConfirmation.requiresPasswordConfirmation()) {
@ -60,23 +70,49 @@
/**
* Add a new provider
* @returns {number} id of the provider
*/
addProvider: function(callback) {
var providerIds = OCA.User_SAML.Admin.providerIds.split(',');
var nextId = 1;
if (providerIds.indexOf('1') >= 0) {
nextId = 2;
while ($.inArray('' + nextId, providerIds) >= 0) {
nextId++;
addProvider: function (callback) {
var xhr = new XMLHttpRequest();
xhr.open('POST', OC.generateUrl('/apps/user_saml/settings/providerSettings'));
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.setRequestHeader('requesttoken', OC.requestToken)
xhr.onload = function () {
var response = JSON.parse(xhr.response)
if (xhr.status >= 200 && xhr.status < 300) {
OCA.User_SAML.Admin.providerIds += ',' + response.id;
callback(response.id)
} else {
console.error("Could not fetch new provider ID")
}
}
OCP.AppConfig.setValue('user_saml', 'providerIds', OCA.User_SAML.Admin.providerIds + ',' + nextId, {
success: function () {
OCA.User_SAML.Admin.providerIds += ',' + nextId;
callback(nextId)
};
xhr.onerror = function () {
console.error("Could not fetch new provider ID");
};
xhr.send();
},
updateProvider: function (configKey, configValue, successCb, errorCb) {
var xhr = new XMLHttpRequest();
xhr.open('PUT', OC.generateUrl('/apps/user_saml/settings/providerSettings/' + this.currentConfig));
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('requesttoken', OC.requestToken);
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
successCb();
} else {
console.error("Could not update config");
errorCb();
}
});
};
xhr.onerror = function () {
console.error("Could not update config");
errorCb();
};
xhr.send(JSON.stringify({configKey: configKey, configValue: configValue.trim()}));
},
removeProvider: function(callback) {
@ -86,17 +122,8 @@
if (index > -1) {
providerIds.splice(index, 1);
}
var config = this.currentConfig;
$.ajax({ url: OC.generateUrl('/apps/user_saml/settings/providerSettings/' + this.currentConfig), type: 'DELETE'})
.done(function(data) {
OCP.AppConfig.setValue('user_saml', 'providerIds', providerIds.join(','), {
success: function () {
OCA.User_SAML.Admin.providerIds = providerIds.join(',');
callback(config);
}
});
});
.done(callback(this.currentConfig));
}
},
@ -105,14 +132,22 @@
OC.PasswordConfirmation.requirePasswordConfirmation(_.bind(this.setSamlConfigValue, this, category, setting, value));
return;
}
// store global config flags without idp prefix
var configIdentifier = this.getConfigIdentifier();
if (global === true) {
configIdentifier = '';
}
OC.msg.startSaving('#user-saml-save-indicator');
OCP.AppConfig.setValue('user_saml', configIdentifier + category + '-' + setting, value.trim());
OC.msg.finishedSaving('#user-saml-save-indicator', {status: 'success', data: {message: t('user_saml', 'Saved')}});
var callbacks = {
success: function () {
OC.msg.finishedSaving('#user-saml-save-indicator', {status: 'success', data: {message: t('user_saml', 'Saved')}});
},
error: function() {
OC.msg.finishedSaving('#user-saml-save-indicator', {status: 'error', data: {message: t('user_saml', 'Could not save')}});
}
};
if (global) {
OCP.AppConfig.setValue('user_saml', category + '-' + setting, value, callbacks);
return;
}
this.updateProvider(category + '-' + setting, value, callbacks.success, callbacks.error);
}
}
})(OCA);
@ -175,35 +210,48 @@ $(function() {
$('.account-list li[data-id="' + providerId + '"]').addClass('active');
OCA.User_SAML.Admin.currentConfig = '' + providerId;
$.get(OC.generateUrl('/apps/user_saml/settings/providerSettings/' + providerId)).done(function(data) {
Object.keys(data).forEach(function(category, index){
document.querySelectorAll('#user-saml-settings input[type="text"], #user-saml-settings textarea').forEach(function (inputNode) {
inputNode.value = '';
});
document.querySelectorAll('#user-saml-settings input[type="checkbox"]').forEach(function (inputNode) {
inputNode.checked = false;
inputNode.setAttribute('value', '0');
});
document.querySelectorAll('#user-saml-settings select option').forEach(function (inputNode) {
inputNode.selected = false;
});
Object.keys(data).forEach(function(category){
var entries = data[category];
Object.keys(entries).forEach(function (configKey) {
var element = $('#user-saml-settings *[data-key="' + configKey + '"]');
if ($('#user-saml-settings #user-saml-' + category + ' #user-saml-' + configKey).length) {
element = $('#user-saml-' + category + ' #user-saml-' + configKey);
var htmlElement = document.querySelector('#user-saml-settings *[data-key="' + configKey + '"]')
|| document.querySelector('#user-saml-' + category + ' #user-saml-' + configKey)
|| document.querySelector('#user-saml-' + category + ' [name="' + configKey + '"]');
if (!htmlElement) {
console.log("could not find element for " + configKey);
return;
}
if ($('#user-saml-settings #user-saml-' + category + ' [name="' + configKey + '"]').length) {
element = $('#user-saml-' + category + ' [name="' + configKey + '"]');
}
if(element.is('input') && element.prop('type') === 'text') {
element.val(entries[configKey])
}
else if(element.is('textarea')) {
element.val(entries[configKey]);
}
else if(element.prop('type') === 'checkbox') {
var value = entries[configKey] === '1' ? '1' : '0';
element.val(value);
if ((htmlElement.tagName === 'INPUT' && htmlElement.getAttribute('type') === 'text')
|| htmlElement.tagName === 'TEXTAREA'
) {
htmlElement.nodeValue = entries[configKey];
htmlElement.value = entries[configKey];
} else if (htmlElement.tagName === 'INPUT' && htmlElement.getAttribute('type') === 'checkbox') {
htmlElement.checked = entries[configKey] === '1';
htmlElement.value = entries[configKey] === '1' ? '1' : '0';
} else if (htmlElement.tagName === 'SELECT') {
htmlElement.querySelector('[value="' + entries[configKey] + '"]').selected = true;
} else {
console.log('unable to find element for ' + configKey);
console.error("Could not handle " + configKey + " Tag is " + htmlElement.tagName + " and type is " + htmlElement.getAttribute("type"));
}
});
});
$('input:checkbox[value="1"]').attr('checked', true);
$('input:checkbox[value="0"]').attr('checked', false);
var xmlDownloadButton = $('#get-metadata');
var url = xmlDownloadButton.data('base') + '?idp=' + providerId;
xmlDownloadButton.attr('href', url);
var xmlDownloadButton = document.getElementById('get-metadata');
var url = xmlDownloadButton.dataset.base + '?idp=' + providerId;
xmlDownloadButton.setAttribute('href', url);
});
};

View file

@ -23,6 +23,7 @@ namespace OCA\User_SAML\AppInfo;
use OCA\User_SAML\DavPlugin;
use OCA\User_SAML\Middleware\OnlyLoggedInMiddleware;
use OCA\User_SAML\SAMLSettings;
use OCP\AppFramework\App;
use OCP\AppFramework\IAppContainer;
use OCP\SabrePluginEvent;
@ -48,7 +49,8 @@ class Application extends App {
return new DavPlugin(
$server->getSession(),
$server->getConfig(),
$_SERVER
$_SERVER,
$server->get(SAMLSettings::class)
);
});

View file

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace OCA\User_SAML\Command;
use OC\Core\Command\Base;
use OCA\User_SAML\SAMLSettings;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ConfigCreate extends Base {
/** @var SAMLSettings */
private $samlSettings;
public function __construct(SAMLSettings $samlSettings) {
parent::__construct();
$this->samlSettings = $samlSettings;
}
protected function configure() {
$this->setName('saml:config:create');
$this->setDescription('Creates a new config and prints the new provider ID');
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$output->writeln($this->samlSettings->getNewProviderId());
return 0;
}
}

View file

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace OCA\User_SAML\Command;
use OC\Core\Command\Base;
use OCA\User_SAML\SAMLSettings;
use OCP\DB\Exception;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ConfigDelete extends Base {
/** @var SAMLSettings */
private $samlSettings;
public function __construct(SAMLSettings $samlSettings) {
parent::__construct();
$this->samlSettings = $samlSettings;
}
protected function configure() {
$this->setName('saml:config:delete');
$this->addArgument(
'providerId',
InputArgument::REQUIRED,
'ProviderID of the SAML config to edit'
);
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$pId = (int)$input->getArgument('providerId');
if ((string)$pId !== $input->getArgument('providerId')) {
// Make sure we don't delete provider with id 0 by error
$output->writeln('<error>providerId argument needs to be an number. Got: ' . $pId . '</error>');
return 1;
}
try {
$this->samlSettings->delete($pId);
$output->writeln('Provider deleted.');
} catch (Exception $e) {
$output->writeln('<error>Provider with id: ' . $providerId . ' does not exist.</error>');
return 1;
}
return 0;
}
}

74
lib/Command/ConfigGet.php Normal file
View file

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace OCA\User_SAML\Command;
use OC\Core\Command\Base;
use OCA\User_SAML\SAMLSettings;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class ConfigGet extends Base {
/** @var SAMLSettings */
private $samlSettings;
public function __construct(SAMLSettings $samlSettings) {
parent::__construct();
$this->samlSettings = $samlSettings;
}
protected function configure() {
$this->setName('saml:config:get');
$this->addOption(
'providerId',
'p',
InputOption::VALUE_REQUIRED,
'ProviderID of a SAML config to print'
);
parent::configure();
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$providerId = (int)$input->getOption('providerId');
if (!empty($providerId)) {
$providerIds = [$providerId];
} else {
$providerIds = array_keys($this->samlSettings->getListOfIdps());
}
$settings = [];
foreach ($providerIds as $pid) {
$settings[$pid] = $this->samlSettings->get($pid);
}
$this->writeArrayInOutputFormat($input, $output, $settings);
return 0;
}
}

97
lib/Command/ConfigSet.php Normal file
View file

@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace OCA\User_SAML\Command;
use OC\Core\Command\Base;
use OCA\User_SAML\SAMLSettings;
use OCP\DB\Exception;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class ConfigSet extends Base {
/** @var SAMLSettings */
private $samlSettings;
public function __construct(SAMLSettings $samlSettings) {
parent::__construct();
$this->samlSettings = $samlSettings;
}
protected function configure() {
$this->setName('saml:config:set');
$this->addArgument(
'providerId',
InputArgument::REQUIRED,
'ProviderID of the SAML config to edit'
);
foreach (SAMLSettings::IDP_CONFIG_KEYS as $key) {
$this->addOption(
$key,
null,
InputOption::VALUE_REQUIRED,
);
}
parent::configure();
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$pId = (int)$input->getArgument('providerId');
if ((string)$pId !== $input->getArgument('providerId')) {
// Make sure we don't delete provider with id 0 by error
$output->writeln('<error>providerId argument needs to be an number. Got: ' . $pId . '</error>');
return 1;
}
try {
$settings = $this->samlSettings->get($pId);
} catch (Exception $e) {
$output->writeln('<error>Provider with id: ' . $providerId . ' does not exist.</error>');
return 1;
}
foreach ($input->getOptions() as $key => $value) {
if (!in_array($key, SAMLSettings::IDP_CONFIG_KEYS) || $value === null) {
continue;
}
if ($value === '') {
unset($settings[$key]);
continue;
}
$settings[$key] = $value;
}
$this->samlSettings->set($pId, $settings);
$output->writeln('The provider\'s config was updated.');
return 0;
}
}

View file

@ -26,6 +26,7 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use OCA\User_SAML\SAMLSettings;
use OneLogin\Saml2\Error;
use OneLogin\Saml2\Settings;
class GetMetadata extends Command {
@ -67,7 +68,7 @@ EOT
* @return void
*/
protected function execute(InputInterface $input, OutputInterface $output) {
$idp = $input->getArgument('idp');
$idp = (int)$input->getArgument('idp');
$settings = new Settings($this->SAMLSettings->getOneLoginSettingsArray($idp));
$metadata = $settings->getSPMetadata();
$errors = $settings->validateMetadata($metadata);

View file

@ -51,7 +51,7 @@ class SAMLController extends Controller {
/** @var IUserSession */
private $userSession;
/** @var SAMLSettings */
private $SAMLSettings;
private $samlSettings;
/** @var UserBackend */
private $userBackend;
/** @var IConfig */
@ -76,7 +76,7 @@ class SAMLController extends Controller {
* @param IRequest $request
* @param ISession $session
* @param IUserSession $userSession
* @param SAMLSettings $SAMLSettings
* @param SAMLSettings $samlSettings
* @param UserBackend $userBackend
* @param IConfig $config
* @param IURLGenerator $urlGenerator
@ -84,24 +84,24 @@ class SAMLController extends Controller {
* @param IL10N $l
*/
public function __construct(
$appName,
IRequest $request,
ISession $session,
IUserSession $userSession,
SAMLSettings $SAMLSettings,
UserBackend $userBackend,
IConfig $config,
$appName,
IRequest $request,
ISession $session,
IUserSession $userSession,
SAMLSettings $samlSettings,
UserBackend $userBackend,
IConfig $config,
IURLGenerator $urlGenerator,
ILogger $logger,
IL10N $l,
UserResolver $userResolver,
UserData $userData,
ICrypto $crypto
ILogger $logger,
IL10N $l,
UserResolver $userResolver,
UserData $userData,
ICrypto $crypto
) {
parent::__construct($appName, $request);
$this->session = $session;
$this->userSession = $userSession;
$this->SAMLSettings = $SAMLSettings;
$this->samlSettings = $samlSettings;
$this->userBackend = $userBackend;
$this->config = $config;
$this->urlGenerator = $urlGenerator;
@ -171,7 +171,7 @@ class SAMLController extends Controller {
$type = $this->config->getAppValue($this->appName, 'type');
switch ($type) {
case 'saml':
$auth = new Auth($this->SAMLSettings->getOneLoginSettingsArray($idp));
$auth = new Auth($this->samlSettings->getOneLoginSettingsArray($idp));
$ssoUrl = $auth->login(null, [], false, false, true);
$response = new Http\RedirectResponse($ssoUrl);
@ -242,7 +242,7 @@ class SAMLController extends Controller {
* @throws Error
*/
public function getMetadata($idp) {
$settings = new Settings($this->SAMLSettings->getOneLoginSettingsArray($idp));
$settings = new Settings($this->samlSettings->getOneLoginSettingsArray($idp));
$metadata = $settings->getSPMetadata();
$errors = $settings->validateMetadata($metadata);
if (empty($errors)) {
@ -304,7 +304,7 @@ class SAMLController extends Controller {
return new Http\RedirectResponse($this->urlGenerator->getAbsoluteURL('/'));
}
$auth = new Auth($this->SAMLSettings->getOneLoginSettingsArray($idp));
$auth = new Auth($this->samlSettings->getOneLoginSettingsArray($idp));
$auth->processResponse($AuthNRequestID);
$this->logger->debug('Attributes send by the IDP: ' . json_encode($auth->getAttributes()));
@ -411,14 +411,14 @@ class SAMLController extends Controller {
if ($pass) {
$idp = $this->session->get('user_saml.Idp');
$auth = new Auth($this->SAMLSettings->getOneLoginSettingsArray($idp));
$auth = new Auth($this->samlSettings->getOneLoginSettingsArray($idp));
$stay = true ; // $auth will return the redirect URL but won't perform the redirect himself
if ($isFromIDP) {
$keepLocalSession = true ; // do not let processSLO to delete the entire session. Let userSession->logout do the job
$targetUrl = $auth->processSLO(
$keepLocalSession,
null,
$this->SAMLSettings->usesSloWebServerDecode(),
$this->samlSettings->usesSloWebServerDecode($idp),
null,
$stay
);
@ -432,7 +432,6 @@ class SAMLController extends Controller {
}
} else {
// If request is not from IDP, we send the logout request to the IDP
$parameters = [];
$nameId = $this->session->get('user_saml.samlNameId');
$nameIdFormat = $this->session->get('user_saml.samlNameIdFormat');
$nameIdNameQualifier = $this->session->get('user_saml.samlNameIdNameQualifier');
@ -490,7 +489,7 @@ class SAMLController extends Controller {
public function selectUserBackEnd($redirectUrl) {
$attributes = ['loginUrls' => []];
if ($this->SAMLSettings->allowMultipleUserBackEnds()) {
if ($this->samlSettings->allowMultipleUserBackEnds()) {
$displayName = $this->l->t('Direct log in');
$customDisplayName = $this->config->getAppValue('user_saml', 'directLoginName', '');
@ -520,7 +519,7 @@ class SAMLController extends Controller {
*/
private function getIdps($redirectUrl) {
$result = [];
$idps = $this->SAMLSettings->getListOfIdps();
$idps = $this->samlSettings->getListOfIdps();
foreach ($idps as $idpId => $displayName) {
$result[] = [
'url' => $this->getSSOUrl($redirectUrl, $idpId),

View file

@ -23,11 +23,14 @@
namespace OCA\User_SAML\Controller;
use OCA\User_SAML\SAMLSettings;
use OCA\User_SAML\Settings\Admin;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\Response;
use OCP\IConfig;
use OCP\IRequest;
use OneLogin\Saml2\Constants;
class SettingsController extends Controller {
@ -35,21 +38,32 @@ class SettingsController extends Controller {
private $config;
/** @var Admin */
private $admin;
/** @var SAMLSettings */
private $samlSettings;
public function __construct($appName,
IRequest $request,
IConfig $config,
Admin $admin) {
public function __construct(
$appName,
IRequest $request,
IConfig $config,
Admin $admin,
SAMLSettings $samlSettings
) {
parent::__construct($appName, $request);
$this->config = $config;
$this->admin = $admin;
$this->samlSettings = $samlSettings;
}
public function getSamlProviderIds(): DataResponse {
$keys = array_keys($this->samlSettings->getListOfIdps());
return new DataResponse([ 'providerIds' => implode(',', $keys)]);
}
/**
* @param $providerId
* @return array of categories containing entries for each config parameter with their value
*/
public function getSamlProviderSettings($providerId) {
public function getSamlProviderSettings(int $providerId) {
/**
* This uses the list of available config parameters from the admin section
* and extends it with fields that are not coming from \OCA\User_SAML\Settings\Admin
@ -63,13 +77,15 @@ class SettingsController extends Controller {
'x509cert' => ['required' => false],
];
/* Fetch all config values for the given providerId */
$settings = [];
// initialize settings with default value for option box (others are left empty)
$settings['sp']['name-id-format'] = Constants::NAMEID_UNSPECIFIED;
$storedSettings = $this->samlSettings->get($providerId);
foreach ($params as $category => $content) {
if (empty($content) || $category === 'providers' || $category === 'type') {
continue;
}
foreach ($content as $setting => $details) {
$prefix = $providerId === '1' ? '' : $providerId . '-';
/* use security as category instead of security-* */
if (strpos($category, 'security-') === 0) {
$category = 'security';
@ -78,42 +94,41 @@ class SettingsController extends Controller {
// as this is the only category that has the saml- prefix on config keys
if (strpos($category, 'attribute-mapping') === 0) {
$category = 'attribute-mapping';
$key = $prefix . 'saml-attribute-mapping' . '-' . $setting;
$key = 'saml-attribute-mapping' . '-' . $setting;
} elseif ($category === 'name-id-formats') {
if ($setting === $storedSettings['sp-name-id-format']) {
$settings['sp']['name-id-format'] = $storedSettings['sp-name-id-format'];
//continue 2;
}
continue;
} else {
$key = $prefix . $category . '-' . $setting;
$key = $category . '-' . $setting;
}
if (isset($details['global']) && $details['global']) {
// Read legacy data from oc_appconfig
$settings[$category][$setting] = $this->config->getAppValue('user_saml', $key, '');
} else {
$settings[$category][$setting] = $storedSettings[$key] ?? '';
}
$settings[$category][$setting] = $this->config->getAppValue('user_saml', $key, '');
}
}
return $settings;
}
public function deleteSamlProviderSettings($providerId) {
$params = $this->admin->getForm()->getParams();
$params['idp'] = [
'singleLogoutService.url' => null,
'singleLogoutService.responseUrl' => null,
'singleSignOnService.url' => null,
'idp-entityId' => null,
];
/* Fetch all config values for the given providerId */
foreach ($params as $category => $content) {
if (!is_array($content) || $category === 'providers') {
continue;
}
foreach ($content as $setting => $details) {
if (isset($details['global']) && $details['global'] === true) {
continue;
}
$prefix = $providerId === '1' ? '' : $providerId . '-';
$key = $prefix . $category . '-' . $setting;
/* use security as category instead of security-* */
if (strpos($category, 'security-') === 0) {
$category = 'security';
}
$this->config->deleteAppValue('user_saml', $key);
}
}
$this->samlSettings->delete($providerId);
return new Response();
}
public function setProviderSetting(int $providerId, string $configKey, string $configValue) {
$configuration = $this->samlSettings->get($providerId);
$configuration[$configKey] = $configValue;
$this->samlSettings->set($providerId, $configuration);
return new Response();
}
public function newSamlProviderSettingsId() {
return new DataResponse(['id' => $this->samlSettings->getNewProviderId()]);
}
}

View file

@ -35,11 +35,14 @@ class DavPlugin extends ServerPlugin {
private $auth;
/** @var Server */
private $server;
/** @var SAMLSettings */
private $samlSettings;
public function __construct(ISession $session, IConfig $config, array $auth) {
public function __construct(ISession $session, IConfig $config, array $auth, SAMLSettings $samlSettings) {
$this->session = $session;
$this->config = $config;
$this->auth = $auth;
$this->samlSettings = $samlSettings;
}
@ -54,7 +57,7 @@ class DavPlugin extends ServerPlugin {
$this->config->getAppValue('user_saml', 'type') === 'environment-variable' &&
!$this->session->exists('user_saml.samlUserData')
) {
$uidMapping = $this->config->getAppValue('user_saml', 'general-uid_mapping');
$uidMapping = $this->samlSettings->get(1)['general-uid_mapping'];
if (isset($this->auth[$uidMapping])) {
$this->session->set(Auth::DAV_AUTHENTICATED, $this->auth[$uidMapping]);
$this->session->set('user_saml.samlUserData', $this->auth);

View file

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace OCA\User_SAML\Db;
use OCP\AppFramework\Db\Entity;
use function json_decode;
use function json_encode;
/**
* @method string getName()
* @method void setName(string $value)
* @method void setConfiguration(string $value)
* @method string getConfiguration()
*/
class ConfigurationsEntity extends Entity {
/** @var string */
public $name;
/** @var string */
public $configuration;
public function __construct() {
$this->addType('name', 'string');
$this->addType('configuration', 'string');
}
/**
* sets also the name, because it is a shorthand to 'general-idp0_display_name'
*
* @throws \JsonException
*/
public function importConfiguration(array $configuration): void {
$this->setConfiguration(json_encode($configuration, JSON_THROW_ON_ERROR));
$this->setName($configuration['general-idp0_display_name'] ?? '');
}
public function getConfigurationArray(): array {
return json_decode($this->configuration, true) ?? [];
}
public function asArray(): array {
return [
'id' => $this->getId(),
'name' => $this->getName(),
'configuration' => $this->getConfigurationArray()
];
}
}

View file

@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace OCA\User_SAML\Db;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
class ConfigurationsMapper extends QBMapper {
public function __construct(IDBConnection $db) {
parent::__construct($db, 'user_saml_configurations', ConfigurationsEntity::class);
}
public function set(int $id, array $configuration): void {
$entity = new ConfigurationsEntity();
$entity->setId($id);
$entity->importConfiguration($configuration);
$this->insertOrUpdate($entity);
}
public function deleteById(int $id): void {
$entity = new ConfigurationsEntity();
$entity->setId($id);
$this->delete($entity);
}
public function getAll(): array {
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'configuration')
->from('user_saml_configurations')
->orderBy('id', 'ASC');
/** @var ConfigurationsEntity $entity */
$entities = $this->findEntities($qb);
$result = [];
foreach ($entities as $entity) {
$result[$entity->getId()] = $entity->getConfigurationArray();
}
return $result;
}
public function get(int $idp): array {
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'configuration')
->from('user_saml_configurations')
->where($qb->expr()->eq('id', $qb->createNamedParameter($idp, IQueryBuilder::PARAM_INT)));
/** @var ConfigurationsEntity $entity */
try {
$entity = $this->findEntity($qb);
} catch (DoesNotExistException $e) {
return [];
}
return $entity->getConfigurationArray();
}
public function reserveId(): int {
$qb = $this->db->getQueryBuilder();
$qb->select('id')
->from('user_saml_configurations')
->orderBy('id', 'DESC')
->setMaxResults(1);
try {
$entity = $this->findEntity($qb);
$newId = $entity->getId() + 1; // autoincrement manually
} catch (DoesNotExistException $e) {
$newId = 1;
}
$newEntity = new ConfigurationsEntity();
$newEntity->setId($newId);
$newEntity->importConfiguration([]);
return $this->insert($newEntity)->getId();
}
}

View file

@ -0,0 +1,231 @@
<?php
declare(strict_types=1);
namespace OCA\User_SAML\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\DB\Types;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
/**
* Auto-generated migration step: Please modify to your needs!
*/
class Version5000Date20211025124248 extends SimpleMigrationStep {
private const IDP_CONFIG_KEYS = [
'general-idp0_display_name',
'general-uid_mapping',
'idp-entityId',
'idp-singleLogoutService.responseUrl',
'idp-singleLogoutService.url',
'idp-singleSignOnService.url',
'idp-x509cert',
'security-authnRequestsSigned',
'security-general',
'security-logoutRequestSigned',
'security-logoutResponseSigned',
'security-lowercaseUrlencoding',
'security-nameIdEncrypted',
'security-offer',
'security-required',
'security-signatureAlgorithm',
'security-signMetadata',
'security-sloWebServerDecode',
'security-wantAssertionsEncrypted',
'security-wantAssertionsSigned',
'security-wantMessagesSigned',
'security-wantNameId',
'security-wantNameIdEncrypted',
'security-wantXMLValidation',
'saml-attribute-mapping-displayName_mapping',
'saml-attribute-mapping-email_mapping',
'saml-attribute-mapping-group_mapping',
'saml-attribute-mapping-home_mapping',
'saml-attribute-mapping-quota_mapping',
'sp-x509cert',
'sp-name-id-format',
'sp-privateKey',
];
/** @var IDBConnection */
private $dbc;
/** @var ?IQueryBuilder */
private $insertQuery;
/** @var ?IQueryBuilder */
private $deleteQuery;
/** @var ?IQueryBuilder */
private $readQuery;
public function __construct(IDBConnection $dbc) {
$this->dbc = $dbc;
}
/**
* @param IOutput $output
* @param Closure():ISchemaWrapper $schemaClosure
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('user_saml_configurations')) {
$table = $schema->createTable('user_saml_configurations');
$table->addColumn('id', Types::INTEGER, [
'notnull' => true,
]);
$table->addColumn('name', Types::STRING, [
'length' => 256,
'notnull' => false,
'default' => '',
]);
$table->addColumn('configuration', Types::TEXT, [
'notnull' => true,
]);
$table->setPrimaryKey(['id'], 'idx_user_saml_config');
}
return $schema;
}
/**
* @param IOutput $output
* @param Closure():IschemaWrapper $schemaClosure
* @param array $options
*/
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
$prefixes = $this->fetchPrefixes();
foreach ($prefixes as $prefix) {
$keyStart = $prefix === 1 ? '' : $prefix . '-';
$configKeys = array_reduce(
self::IDP_CONFIG_KEYS,
function (array $carry, string $rawConfigKey) use ($keyStart): array {
$carry[] = $keyStart . $rawConfigKey;
return $carry;
},
[]
);
$configData = $keysToDelete = [];
$gRows = $this->readConfiguration($configKeys);
while ($row = $gRows->current()) {
$configData[$this->normalizeKey($row['configkey'])] = $row['configvalue'];
$keysToDelete[] = $row['configkey'];
$gRows->next();
}
if (empty($configData)) {
continue; // No config found
}
if ($this->insertConfiguration($prefix, $configData) && !empty($keysToDelete)) {
$this->deleteOldConfiguration($keysToDelete);
}
}
$this->deletePrefixes();
}
/**
* @psalm-param list<string> $keys the list of keys to delete
*/
protected function deleteOldConfiguration(array $keys): bool {
if (!$this->deleteQuery) {
$this->deleteQuery = $this->dbc->getQueryBuilder();
$this->deleteQuery->delete('appconfig')
->where($this->deleteQuery->expr()->eq('appid', $this->deleteQuery->createNamedParameter('user_saml')))
->andWhere($this->deleteQuery->expr()->in('configkey', $this->deleteQuery->createParameter('cfgKeys')));
}
$deletedRows = $this->deleteQuery
->setParameter('cfgKeys', $keys, IQueryBuilder::PARAM_STR_ARRAY)
->execute();
return $deletedRows > 0;
}
/**
* @param array<string, string> $configData The key-value map of config to save
*/
protected function insertConfiguration(int $id, array $configData): bool {
if (!$this->insertQuery) {
$this->insertQuery = $this->dbc->getQueryBuilder();
$this->insertQuery->insert('user_saml_configurations')
->values([
'id' => $this->insertQuery->createParameter('configId'),
'name' => $this->insertQuery->createParameter('displayName'),
'configuration' => $this->insertQuery->createParameter('configuration'),
]);
}
$insertedRows = $this->insertQuery
->setParameter('configId', $id)
->setParameter('displayName', $configData['general-idp0_display_name'] ?? '')
->setParameter('configuration', \json_encode($configData, JSON_THROW_ON_ERROR))
->execute();
return $insertedRows > 0;
}
/** @psalm-param list<string> $configKeys */
protected function readConfiguration(array $configKeys): \Generator {
if (!$this->readQuery) {
$this->readQuery = $this->dbc->getQueryBuilder();
$this->readQuery->select('configkey', 'configvalue')
->from('appconfig')
->where($this->readQuery->expr()->eq('appid', $this->readQuery->createNamedParameter('user_saml')))
->andWhere($this->readQuery->expr()->in('configkey', $this->readQuery->createParameter('cfgKeys')));
}
$r = $this->readQuery->setParameter('cfgKeys', $configKeys, IQueryBuilder::PARAM_STR_ARRAY)
->execute();
while ($row = $r->fetch()) {
yield $row;
}
$r->closeCursor();
}
protected function normalizeKey(string $prefixedKey): string {
$isPrefixed = \preg_match('/^[0-9]*-/', $prefixedKey, $matches);
if ($isPrefixed === 0) {
return $prefixedKey;
} elseif ($isPrefixed === 1) {
return \substr($prefixedKey, strlen($matches[0]));
}
throw new \RuntimeException('Invalid regex pattern');
}
/** @psalm-return list<int> */
protected function fetchPrefixes(): array {
$q = $this->dbc->getQueryBuilder();
$q->select('configvalue')
->from('appconfig')
->where($q->expr()->eq('appid', $q->createNamedParameter('user_saml')))
->andWhere($q->expr()->eq('configkey', $q->createNamedParameter('providerIds')));
$r = $q->execute();
$prefixes = $r->fetchOne();
if ($prefixes === false) {
return [1]; // 1 is the default value for providerIds
}
return array_map('intval', explode(',', $prefixes));
}
protected function deletePrefixes(): void {
$q = $this->dbc->getQueryBuilder();
$q->delete('appconfig')
->where($q->expr()->eq('appid', $q->createNamedParameter('user_saml')))
->andWhere($q->expr()->eq('configkey', $q->createNamedParameter('providerIds')))
->execute();
}
}

View file

@ -21,6 +21,9 @@
namespace OCA\User_SAML;
use InvalidArgumentException;
use OCA\User_SAML\Db\ConfigurationsMapper;
use OCP\DB\Exception;
use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
@ -28,16 +31,59 @@ use OCP\IURLGenerator;
use OneLogin\Saml2\Constants;
class SAMLSettings {
private const LOADED_NONE = 0;
private const LOADED_CHOSEN = 1;
private const LOADED_ALL = 2;
public const IDP_CONFIG_KEYS = [
'general-idp0_display_name',
'general-uid_mapping',
'idp-entityId',
'idp-singleLogoutService.responseUrl',
'idp-singleLogoutService.url',
'idp-singleSignOnService.url',
'idp-x509cert',
'security-authnRequestsSigned',
'security-general',
'security-logoutRequestSigned',
'security-logoutResponseSigned',
'security-lowercaseUrlencoding',
'security-nameIdEncrypted',
'security-offer',
'security-required',
'security-signatureAlgorithm',
'security-signMetadata',
'security-sloWebServerDecode',
'security-wantAssertionsEncrypted',
'security-wantAssertionsSigned',
'security-wantMessagesSigned',
'security-wantNameId',
'security-wantNameIdEncrypted',
'security-wantXMLValidation',
'saml-attribute-mapping-displayName_mapping',
'saml-attribute-mapping-email_mapping',
'saml-attribute-mapping-group_mapping',
'saml-attribute-mapping-home_mapping',
'saml-attribute-mapping-quota_mapping',
'sp-x509cert',
'sp-name-id-format',
'sp-privateKey',
];
/** @var IURLGenerator */
private $urlGenerator;
/** @var IConfig */
private $config;
/** @var IRequest */
private $request;
/** @var ISession */
private $session;
/** @var array list of global settings which are valid for every idp */
private $globalSettings = ['general-require_provisioned_account', 'general-allow_multiple_user_back_ends', 'general-use_saml_auth_for_desktop'];
/** @var array<int, array<string, string>> */
private $configurations;
/** @var int */
private $configurationsLoadedState = self::LOADED_NONE;
/** @var ConfigurationsMapper */
private $mapper;
/**
* @param IURLGenerator $urlGenerator
@ -45,148 +91,167 @@ class SAMLSettings {
* @param IRequest $request
* @param ISession $session
*/
public function __construct(IURLGenerator $urlGenerator,
IConfig $config,
IRequest $request,
ISession $session) {
public function __construct(
IURLGenerator $urlGenerator,
IConfig $config,
ISession $session,
ConfigurationsMapper $mapper
) {
$this->urlGenerator = $urlGenerator;
$this->config = $config;
$this->request = $request;
$this->session = $session;
$this->mapper = $mapper;
}
/**
* get list of the configured IDPs
* Get list of the configured IDPs
*
* @return array
* @return array<int, string>
* @throws Exception
*/
public function getListOfIdps() {
public function getListOfIdps(): array {
$this->ensureConfigurationsLoaded();
$result = [];
$providerIds = explode(',', $this->config->getAppValue('user_saml', 'providerIds', '1'));
natsort($providerIds);
foreach ($providerIds as $id) {
$prefix = $id === '1' ? '' : $id .'-';
$result[$id] = $this->config->getAppValue('user_saml', $prefix . 'general-idp0_display_name', '');
foreach ($this->configurations as $configID => $config) {
// no fancy array_* method, because there might be thousands
$result[$configID] = $config['general-idp0_display_name'] ?? '';
}
asort($result);
return $result;
}
/**
* check if multiple user back ends are allowed
*
* @return bool
* Check if multiple user back ends are allowed
*/
public function allowMultipleUserBackEnds() {
public function allowMultipleUserBackEnds(): bool {
$type = $this->config->getAppValue('user_saml', 'type');
$setting = $this->config->getAppValue('user_saml', 'general-allow_multiple_user_back_ends', '0');
return ($setting === '1' && $type === 'saml');
return ($setting === '1' && $type === 'saml');
}
public function usesSloWebServerDecode() : bool {
return $this->config->getAppValue('user_saml', 'security-sloWebServerDecode', '0') === '1';
public function usesSloWebServerDecode(int $idp): bool {
$config = $this->get($idp);
return ($config['security-sloWebServerDecode'] ?? false) === '1';
}
/**
* get config for given IDP
* Get config for given IDP
*
* @param int $idp
* @return array
* @throws Exception
*/
public function getOneLoginSettingsArray($idp) {
$prefix = '';
if ($idp > 1) {
$prefix = $idp . '-';
}
public function getOneLoginSettingsArray(int $idp): array {
$this->ensureConfigurationsLoaded($idp);
$settings = [
'strict' => true,
'debug' => $this->config->getSystemValue('debug', false),
'baseurl' => $this->urlGenerator->linkToRouteAbsolute('user_saml.SAML.base'),
'security' => [
'nameIdEncrypted' => ($this->config->getAppValue('user_saml', $prefix . 'security-nameIdEncrypted', '0') === '1') ? true : false,
'authnRequestsSigned' => ($this->config->getAppValue('user_saml', $prefix . 'security-authnRequestsSigned', '0') === '1') ? true : false,
'logoutRequestSigned' => ($this->config->getAppValue('user_saml', $prefix . 'security-logoutRequestSigned', '0') === '1') ? true : false,
'logoutResponseSigned' => ($this->config->getAppValue('user_saml', $prefix . 'security-logoutResponseSigned', '0') === '1') ? true : false,
'signMetadata' => ($this->config->getAppValue('user_saml', $prefix . 'security-signMetadata', '0') === '1') ? true : false,
'wantMessagesSigned' => ($this->config->getAppValue('user_saml', $prefix . 'security-wantMessagesSigned', '0') === '1') ? true : false,
'wantAssertionsSigned' => ($this->config->getAppValue('user_saml', $prefix . 'security-wantAssertionsSigned', '0') === '1') ? true : false,
'wantAssertionsEncrypted' => ($this->config->getAppValue('user_saml', $prefix . 'security-wantAssertionsEncrypted', '0') === '1') ? true : false,
'wantNameId' => ($this->config->getAppValue('user_saml', $prefix . 'security-wantNameId', '0') === '1') ? true : false,
'wantNameIdEncrypted' => ($this->config->getAppValue('user_saml', $prefix . 'security-wantNameIdEncrypted', '0') === '1') ? true : false,
'wantXMLValidation' => ($this->config->getAppValue('user_saml', $prefix . 'security-wantXMLValidation', '0') === '1') ? true : false,
'nameIdEncrypted' => ($this->configurations[$idp]['security-nameIdEncrypted'] ?? '0') === '1',
'authnRequestsSigned' => ($this->configurations[$idp]['security-authnRequestsSigned'] ?? '0') === '1',
'logoutRequestSigned' => ($this->configurations[$idp]['security-logoutRequestSigned'] ?? '0') === '1',
'logoutResponseSigned' => ($this->configurations[$idp]['security-logoutResponseSigned'] ?? '0') === '1',
'signMetadata' => ($this->configurations[$idp]['security-signMetadata'] ?? '0') === '1',
'wantMessagesSigned' => ($this->configurations[$idp]['security-wantMessagesSigned'] ?? '0') === '1',
'wantAssertionsSigned' => ($this->configurations[$idp]['security-wantAssertionsSigned'] ?? '0') === '1',
'wantAssertionsEncrypted' => ($this->configurations[$idp]['security-wantAssertionsEncrypted'] ?? '0') === '1',
'wantNameId' => ($this->configurations[$idp]['security-wantNameId'] ?? '0') === '1',
'wantNameIdEncrypted' => ($this->configurations[$idp]['security-wantNameIdEncrypted'] ?? '0') === '1',
'wantXMLValidation' => ($this->configurations[$idp]['security-wantXMLValidation'] ?? '0') === '1',
'requestedAuthnContext' => false,
'lowercaseUrlencoding' => ($this->config->getAppValue('user_saml', $prefix . 'security-lowercaseUrlencoding', '0') === '1') ? true : false,
'signatureAlgorithm' => $this->config->getAppValue('user_saml', $prefix . 'security-signatureAlgorithm', null)
'lowercaseUrlencoding' => ($this->configurations[$idp]['security-lowercaseUrlencoding'] ?? '0') === '1',
'signatureAlgorithm' => $this->configurations[$idp]['security-signatureAlgorithm'] ?? null,
// "sloWebServerDecode" is not expected to be passed to the OneLogin class
],
'sp' => [
'entityId' => $this->urlGenerator->linkToRouteAbsolute('user_saml.SAML.getMetadata'),
'assertionConsumerService' => [
'url' => $this->urlGenerator->linkToRouteAbsolute('user_saml.SAML.assertionConsumerService'),
],
'NameIDFormat' => $this->config->getAppValue('user_saml', $prefix . 'sp-name-id-format', Constants::NAMEID_UNSPECIFIED)
'NameIDFormat' => $this->configurations[$idp]['sp-name-id-format'] ?? Constants::NAMEID_UNSPECIFIED,
'x509cert' => $this->configurations[$idp]['sp-x509cert'] ?? '',
'privateKey' => $this->configurations[$idp]['sp-privateKey'] ?? '',
],
'idp' => [
'entityId' => $this->config->getAppValue('user_saml', $prefix . 'idp-entityId', ''),
'entityId' => $this->configurations[$idp]['idp-entityId'] ?? '',
'singleSignOnService' => [
'url' => $this->config->getAppValue('user_saml', $prefix . 'idp-singleSignOnService.url', ''),
'url' => $this->configurations[$idp]['idp-singleSignOnService.url'] ?? '',
],
'x509cert' => $this->configurations[$idp]['idp-x509cert'] ?? '',
],
];
$spx509cert = $this->config->getAppValue('user_saml', $prefix . 'sp-x509cert', '');
$spxprivateKey = $this->config->getAppValue('user_saml', $prefix . 'sp-privateKey', '');
if ($spx509cert !== '') {
$settings['sp']['x509cert'] = $spx509cert;
}
if ($spxprivateKey !== '') {
$settings['sp']['privateKey'] = $spxprivateKey;
}
$idpx509cert = $this->config->getAppValue('user_saml', $prefix . 'idp-x509cert', '');
if ($idpx509cert !== '') {
$settings['idp']['x509cert'] = $idpx509cert;
}
$slo = $this->config->getAppValue('user_saml', $prefix . 'idp-singleLogoutService.url', '');
if ($slo !== '') {
$settings['idp']['singleLogoutService'] = [
'url' => $this->config->getAppValue('user_saml', $prefix . 'idp-singleLogoutService.url', ''),
];
$settings['sp']['singleLogoutService'] = [
'url' => $this->urlGenerator->linkToRouteAbsolute('user_saml.SAML.singleLogoutService'),
];
$sloResponseUrl = $this->config->getAppValue('user_saml', $prefix . 'idp-singleLogoutService.responseUrl', '');
if ($sloResponseUrl !== '') {
$settings['idp']['singleLogoutService']['responseUrl'] = $sloResponseUrl;
// must be added only if configured
if (($this->configurations[$idp]['idp-singleLogoutService.url'] ?? '') !== '') {
$settings['idp']['singleLogoutService'] = ['url' => $this->configurations[$idp]['idp-singleLogoutService.url']];
$settings['sp']['singleLogoutService'] = ['url' => $this->urlGenerator->linkToRouteAbsolute('user_saml.SAML.singleLogoutService')];
if (($this->configurations[$idp]['idp-singleLogoutService.responseUrl'] ?? '') !== '') {
$settings['idp']['singleLogoutService']['responseUrl'] = $this->configurations[$idp]['idp-singleLogoutService.responseUrl'];
}
}
return $settings;
}
public function getProviderId(): int {
// defaults to 1, needed for env-mode
return (int)($this->session->get('user_saml.Idp') ?? 1);
}
public function getNewProviderId(): int {
return $this->mapper->reserveId();
}
/**
* calculate prefix for config values
*
* @param string name of the setting
* @return string
* @throws Exception
*/
public function getPrefix($setting = '') {
$prefix = '';
if (!empty($setting) && in_array($setting, $this->globalSettings)) {
return $prefix;
public function get(int $id): array {
$this->ensureConfigurationsLoaded($id);
return $this->configurations[$id] ?? [];
}
/**
* @throws InvalidArgumentException
*/
public function set(int $id, array $settings): void {
foreach (array_keys($settings) as $configKey) {
if (!in_array($configKey, self::IDP_CONFIG_KEYS)) {
throw new InvalidArgumentException('Invalid config key');
}
}
$idp = $this->session->get('user_saml.Idp');
if ((int)$idp > 1) {
$prefix = $idp . '-';
$this->mapper->set($id, $settings);
}
/**
* @throws Exception
*/
public function delete(int $id): void {
$this->mapper->deleteById($id);
}
/**
* @throws Exception
*/
protected function ensureConfigurationsLoaded(int $idp = -1): void {
if (self::LOADED_ALL === $this->configurationsLoadedState
|| (self::LOADED_CHOSEN === $this->configurationsLoadedState
&& isset($this->configurations[$idp])
)
) {
return;
}
return $prefix;
if ($idp !== -1) {
$this->configurations[$idp] = $this->mapper->get($idp);
} else {
$configs = $this->mapper->getAll();
foreach ($configs as $id => $config) {
$this->configurations[$id] = $config;
}
}
$this->configurationsLoadedState = $idp === -1 ? self::LOADED_ALL : self::LOADED_CHOSEN;
}
}

View file

@ -23,6 +23,7 @@
namespace OCA\User_SAML\Settings;
use OCA\User_SAML\SAMLSettings;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\Defaults;
use OCP\IConfig;
@ -37,30 +38,33 @@ class Admin implements ISettings {
private $defaults;
/** @var IConfig */
private $config;
/** @var SAMLSettings */
private $samlSettings;
/**
* @param IL10N $l10n
* @param Defaults $defaults
* @param IConfig $config
*/
public function __construct(IL10N $l10n,
Defaults $defaults,
IConfig $config) {
public function __construct(
IL10N $l10n,
Defaults $defaults,
IConfig $config,
SAMLSettings $samlSettings
) {
$this->l10n = $l10n;
$this->defaults = $defaults;
$this->config = $config;
$this->samlSettings = $samlSettings;
}
/**
* @return TemplateResponse
*/
public function getForm() {
$providerIds = explode(',', $this->config->getAppValue('user_saml', 'providerIds', '1'));
natsort($providerIds);
$providerIds = $this->samlSettings->getListOfIdps();
$providers = [];
foreach ($providerIds as $id) {
$prefix = $id === '1' ? '' : $id .'-';
$name = $this->config->getAppValue('user_saml', $prefix . 'general-idp0_display_name', '');
foreach ($providerIds as $id => $name) {
$providers[] = [
'id' => $id,
'name' => $name === '' ? $this->l10n->t('Provider ') . $id : $name
@ -134,45 +138,51 @@ class Admin implements ISettings {
];
$selectedNameIdFormat = $this->config->getAppValue('user_saml', 'sp-name-id-format', Constants::NAMEID_UNSPECIFIED);
$firstIdPConfig = isset($providers[0]) ? $this->samlSettings->get($providers[0]['id']) : null;
$nameIdFormats = [
Constants::NAMEID_EMAIL_ADDRESS => [
'label' => $this->l10n->t('Email address'),
'selected' => $selectedNameIdFormat === Constants::NAMEID_EMAIL_ADDRESS,
'selected' => false,
],
Constants::NAMEID_ENCRYPTED => [
'label' => $this->l10n->t('Encrypted'),
'selected' => $selectedNameIdFormat === Constants::NAMEID_ENCRYPTED,
'selected' => false,
],
Constants::NAMEID_ENTITY => [
'label' => $this->l10n->t('Entity'),
'selected' => $selectedNameIdFormat === Constants::NAMEID_ENTITY,
'selected' => false,
],
Constants::NAMEID_KERBEROS => [
'label' => $this->l10n->t('Kerberos'),
'selected' => $selectedNameIdFormat === Constants::NAMEID_KERBEROS,
'selected' => false,
],
Constants::NAMEID_PERSISTENT => [
'label' => $this->l10n->t('Persistent'),
'selected' => $selectedNameIdFormat === Constants::NAMEID_PERSISTENT,
'selected' => false,
],
Constants::NAMEID_TRANSIENT => [
'label' => $this->l10n->t('Transient'),
'selected' => $selectedNameIdFormat === Constants::NAMEID_TRANSIENT,
'selected' => false,
],
Constants::NAMEID_UNSPECIFIED => [
'label' => $this->l10n->t('Unspecified'),
'selected' => $selectedNameIdFormat === Constants::NAMEID_UNSPECIFIED,
'selected' => false,
],
Constants::NAMEID_WINDOWS_DOMAIN_QUALIFIED_NAME => [
'label' => $this->l10n->t('Windows domain qualified name'),
'selected' => $selectedNameIdFormat === Constants::NAMEID_WINDOWS_DOMAIN_QUALIFIED_NAME,
'selected' => false,
],
Constants::NAMEID_X509_SUBJECT_NAME => [
'label' => $this->l10n->t('X509 subject name'),
'selected' => $selectedNameIdFormat === Constants::NAMEID_X509_SUBJECT_NAME,
'selected' => false,
],
];
$chosenFormat = $firstIdPConfig['sp-name-id-format'] ?? '';
if ($firstIdPConfig !== null && isset($nameIdFormats[$chosenFormat])) {
$nameIdFormats[$chosenFormat]['selected'] = true;
} else {
$nameIdFormats[Constants::NAMEID_UNSPECIFIED]['selected'] = true;
}
$type = $this->config->getAppValue('user_saml', 'type');
if ($type === 'saml') {
@ -203,7 +213,8 @@ class Admin implements ISettings {
'attribute-mapping' => $attributeMappingSettings,
'name-id-formats' => $nameIdFormats,
'type' => $type,
'providers' => $providers
'providers' => $providers,
'config' => $firstIdPConfig,
];
return new TemplateResponse('user_saml', 'admin', $params);

View file

@ -394,8 +394,10 @@ class UserBackend implements IApacheBackend, UserInterface, IUserBackend {
* {@inheritdoc}
*/
public function getLogoutUrl() {
$prefix = $this->settings->getPrefix();
$slo = $this->config->getAppValue('user_saml', $prefix . 'idp-singleLogoutService.url', '');
$id = $this->settings->getProviderId();
$settings = $this->settings->get($id);
$slo = $settings['idp-singleLogoutService.url'] ?? '';
if ($slo === '') {
return '';
}
@ -543,9 +545,12 @@ class UserBackend implements IApacheBackend, UserInterface, IUserBackend {
self::$backends = $backends;
}
/**
* @throws \OCP\DB\Exception
*/
private function getAttributeKeys($name) {
$prefix = $this->settings->getPrefix($name);
$keys = explode(' ', $this->config->getAppValue('user_saml', $prefix . $name, ''));
$settings = $this->settings->get($this->settings->getProviderId());
$keys = explode(' ', $settings[$name] ?? $this->config->getAppValue('user_saml', $name, ''));
if (count($keys) === 1 && $keys[0] === '') {
throw new \InvalidArgumentException('Attribute is not configured');

View file

@ -57,9 +57,8 @@ class UserData {
public function hasUidMappingAttribute(): bool {
$this->assertIsInitialized();
$prefix = $this->samlSettings->getPrefix();
$uidMapping = $this->config->getAppValue('user_saml', $prefix . 'general-uid_mapping');
return isset($this->attributes[$uidMapping]);
$attribute = $this->getUidMappingAttribute();
return $attribute !== null && isset($this->attributes[$attribute]);
}
public function getOriginalUid(): string {
@ -84,9 +83,8 @@ class UserData {
}
protected function extractSamlUserId(): string {
$prefix = $this->samlSettings->getPrefix();
$uidMapping = $this->config->getAppValue('user_saml', $prefix . 'general-uid_mapping');
if (isset($this->attributes[$uidMapping])) {
$uidMapping = $this->getUidMappingAttribute();
if ($uidMapping !== null && isset($this->attributes[$uidMapping])) {
if (is_array($this->attributes[$uidMapping])) {
return trim($this->attributes[$uidMapping][0]);
} else {
@ -148,4 +146,12 @@ class UserData {
throw new \LogicException('UserData have to be initialized with setAttributes first');
}
}
protected function getProviderSettings(): array {
return $this->samlSettings->get($this->samlSettings->getProviderId());
}
protected function getUidMappingAttribute(): ?string {
return $this->getProviderSettings()['general-uid_mapping'] ?? null;
}
}

View file

@ -55,12 +55,12 @@ style('user_saml', 'admin');
<?php foreach ($_['general'] as $key => $attribute): ?>
<?php if ($attribute['type'] === 'checkbox' && $attribute['global']): ?>
<p>
<input type="checkbox" data-key="<?php p($key)?>" id="user-saml-general-<?php p($key)?>" name="<?php p($key)?>" value="<?php p(\OC::$server->getConfig()->getAppValue('user_saml', 'general-'.$key, '0')) ?>">
<input type="checkbox" data-key="<?php p($key)?>" id="user-saml-general-<?php p($key)?>" name="<?php p($key)?>" value="<?php p($_['config']['general-'.$key] ?? '0') ?>">
<label for="user-saml-general-<?php p($key)?>"><?php p($attribute['text']) ?></label><br/>
</p>
<?php elseif ($attribute['type'] === 'line' && isset($attribute['global'])): ?>
<p>
<input data-key="<?php p($key)?>" name="<?php p($key) ?>" value="<?php p(\OC::$server->getConfig()->getAppValue('user_saml', 'general-'.$key, '')) ?>" type="text" <?php if (isset($attribute['required']) && $attribute['required'] === true): ?>class="required"<?php endif;?> placeholder="<?php p($attribute['text']) ?>"/>
<input data-key="<?php p($key)?>" name="<?php p($key) ?>" value="<?php p($_['config']['general-'.$key] ?? '') ?>" type="text" <?php if (isset($attribute['required']) && $attribute['required'] === true): ?>class="required"<?php endif;?> placeholder="<?php p($attribute['text']) ?>"/>
</p>
<?php endif; ?>
<?php endforeach; ?>
@ -85,12 +85,12 @@ style('user_saml', 'admin');
<?php foreach ($_['general'] as $key => $attribute): ?>
<?php if ($attribute['type'] === 'checkbox' && !$attribute['global']): ?>
<p>
<input type="checkbox" data-key="<?php p($key)?>" id="user-saml-general-<?php p($key)?>" name="<?php p($key)?>" value="<?php p(\OC::$server->getConfig()->getAppValue('user_saml', 'general-'.$key, '0')) ?>">
<input type="checkbox" data-key="<?php p($key)?>" id="user-saml-general-<?php p($key)?>" name="<?php p($key)?>" value="<?php p($_['config']['general-'.$key] ?? '0') ?>">
<label for="user-saml-general-<?php p($key)?>"><?php p($attribute['text']) ?></label><br/>
</p>
<?php elseif ($attribute['type'] === 'line' && !isset($attribute['global'])): ?>
<p>
<input data-key="<?php p($key)?>" name="<?php p($key) ?>" value="<?php p(\OC::$server->getConfig()->getAppValue('user_saml', 'general-'.$key, '')) ?>" type="text" <?php if (isset($attribute['required']) && $attribute['required'] === true): ?>class="required"<?php endif;?> placeholder="<?php p($attribute['text']) ?>"/>
<input data-key="<?php p($key)?>" name="<?php p($key) ?>" value="<?php p($_['config']['general-'.$key] ?? '') ?>" type="text" <?php if (isset($attribute['required']) && $attribute['required'] === true): ?>class="required"<?php endif;?> placeholder="<?php p($attribute['text']) ?>"/>
</p>
<?php endif; ?>
<?php endforeach; ?>
@ -118,7 +118,7 @@ style('user_saml', 'admin');
</select>
<?php foreach ($_['sp'] as $key => $text): ?>
<p>
<textarea name="<?php p($key) ?>" placeholder="<?php p($text) ?>"><?php p(\OC::$server->getConfig()->getAppValue('user_saml', 'sp-'.$key, '')) ?></textarea>
<textarea name="<?php p($key) ?>" placeholder="<?php p($text) ?>"><?php p($_['config']['sp-'.$key] ?? '') ?></textarea>
</p>
<?php endforeach; ?>
</div>
@ -129,13 +129,13 @@ style('user_saml', 'admin');
<?php print_unescaped($l->t('Configure your IdP settings here.')) ?>
</p>
<p><input data-key="idp-entityId" name="entityId" value="<?php p(\OC::$server->getConfig()->getAppValue('user_saml', 'idp-entityId', '')) ?>" type="text" class="required" placeholder="<?php p($l->t('Identifier of the IdP entity (must be a URI)')) ?>"/></p>
<p><input name="singleSignOnService.url" value="<?php p(\OC::$server->getConfig()->getAppValue('user_saml', 'idp-singleSignOnService.url', '')) ?>" type="text" class="required" placeholder="<?php p($l->t('URL Target of the IdP where the SP will send the Authentication Request Message')) ?>"/></p>
<p><input data-key="idp-entityId" name="entityId" value="<?php p($_['config']['idp-entityId'] ?? '') ?>" type="text" class="required" placeholder="<?php p($l->t('Identifier of the IdP entity (must be a URI)')) ?>"/></p>
<p><input name="singleSignOnService.url" value="<?php p($_['config']['idp-singleSignOnService.url'] ?? '') ?>" type="text" class="required" placeholder="<?php p($l->t('URL Target of the IdP where the SP will send the Authentication Request Message')) ?>"/></p>
<p><span class="toggle"><?php p($l->t('Show optional Identity Provider settings…')) ?></span></p>
<div class="hidden">
<p><input name="singleLogoutService.url" value="<?php p(\OC::$server->getConfig()->getAppValue('user_saml', 'idp-singleLogoutService.url', '')) ?>" type="text" placeholder="<?php p($l->t('URL Location of the IdP where the SP will send the SLO Request')) ?>"/></p>
<p><input name="singleLogoutService.responseUrl" value="<?php p(\OC::$server->getConfig()->getAppValue('user_saml', 'idp-singleLogoutService.responseUrl', '')) ?>" type="text" placeholder="<?php p($l->t('URL Location of the IDP\'s SLO Response')) ?>"/></p>
<p><textarea name="x509cert" placeholder="<?php p($l->t('Public X.509 certificate of the IdP')) ?>"><?php p(\OC::$server->getConfig()->getAppValue('user_saml', 'idp-x509cert', '')) ?></textarea></p>
<p><input name="singleLogoutService.url" value="<?php p($_['config']['idp-singleLogoutService.url'] ?? '') ?>" type="text" placeholder="<?php p($l->t('URL Location of the IdP where the SP will send the SLO Request')) ?>"/></p>
<p><input name="singleLogoutService.responseUrl" value="<?php p($_['config']['idp-singleLogoutService.responseUrl'] ?? '') ?>" type="text" placeholder="<?php p($l->t('URL Location of the IDP\'s SLO Response')) ?>"/></p>
<p><textarea name="x509cert" placeholder="<?php p($l->t('Public X.509 certificate of the IdP')) ?>"><?php p($_['config']['idp-x509cert'] ?? '') ?></textarea></p>
</div>
</div>
@ -151,7 +151,7 @@ style('user_saml', 'admin');
<?php
if ($attribute['type'] === 'line'): ?>
<p>
<input name="<?php p($key) ?>" value="<?php p(\OC::$server->getConfig()->getAppValue('user_saml', 'saml-attribute-mapping-'.$key, '')) ?>" type="text" <?php if (isset($attribute['required']) && $attribute['required'] === true): ?>class="required"<?php endif;?> placeholder="<?php p($attribute['text']) ?>"/>
<input name="<?php p($key) ?>" value="<?php p($_['config']['saml-attribute-mapping-'.$key] ?? '') ?>" type="text" <?php if (isset($attribute['required']) && $attribute['required'] === true): ?>class="required"<?php endif;?> placeholder="<?php p($attribute['text']) ?>"/>
</p>
<?php endif; ?>
<?php endforeach; ?>
@ -168,14 +168,14 @@ style('user_saml', 'admin');
<h4><?php p($l->t('Signatures and encryption offered')) ?></h4>
<?php foreach ($_['security-offer'] as $key => $text): ?>
<p>
<input type="checkbox" id="user-saml-<?php p($key)?>" name="<?php p($key)?>" value="<?php p(\OC::$server->getConfig()->getAppValue('user_saml', 'security-'.$key, '0')) ?>" class="checkbox">
<input type="checkbox" id="user-saml-<?php p($key)?>" name="<?php p($key)?>" value="<?php p($_['config']['security-'.$key] ?? '0') ?>" class="checkbox">
<label for="user-saml-<?php p($key)?>"><?php p($text) ?></label><br/>
</p>
<?php endforeach; ?>
<h4><?php p($l->t('Signatures and encryption required')) ?></h4>
<?php foreach ($_['security-required'] as $key => $text): ?>
<p>
<input type="checkbox" id="user-saml-<?php p($key)?>" name="<?php p($key)?>" value="<?php p(\OC::$server->getConfig()->getAppValue('user_saml', 'security-'.$key, '0')) ?>" class="checkbox">
<input type="checkbox" id="user-saml-<?php p($key)?>" name="<?php p($key)?>" value="<?php p($_['config']['security-'.$key] ?? '0') ?>" class="checkbox">
<label for="user-saml-<?php p($key)?>"><?php p($text) ?></label>
</p>
<?php endforeach; ?>
@ -185,12 +185,12 @@ style('user_saml', 'admin');
<?php $text = $attribute['text'] ?>
<p>
<label><?php p($attribute['text']) ?></label><br />
<input data-key="<?php p($key)?>" name="<?php p($key) ?>" value="<?php p(\OC::$server->getConfig()->getAppValue('user_saml', 'security-'.$key, '')) ?>" type="text" <?php if (isset($attribute['required']) && $attribute['required'] === true): ?>class="required"<?php endif;?> placeholder="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<input data-key="<?php p($key)?>" name="<?php p($key) ?>" value="<?php p($_['config']['security-'.$key] ?? '') ?>" type="text" <?php if (isset($attribute['required']) && $attribute['required'] === true): ?>class="required"<?php endif;?> placeholder="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
</p>
<?php } else { ?>
<?php $text = $attribute ?>
<p>
<input type="checkbox" id="user-saml-<?php p($key)?>" name="<?php p($key)?>" value="<?php p(\OC::$server->getConfig()->getAppValue('user_saml', 'security-'.$key, '0')) ?>" class="checkbox">
<input type="checkbox" id="user-saml-<?php p($key)?>" name="<?php p($key)?>" value="<?php p($_['config']['security-'.$key] ?? '0') ?>" class="checkbox">
<label for="user-saml-<?php p($key)?>"><?php p($text) ?></label><br/>
</p>
<?php } ?>

View file

@ -74,6 +74,15 @@ class FeatureContext implements Context {
)
);
}
shell_exec(
sprintf(
'sudo -u apache %s %s saml:config:delete 1',
PHP_BINARY,
__DIR__ . '/../../../../../../occ',
)
);
$this->changedSettings = [];
}
@ -85,14 +94,33 @@ class FeatureContext implements Context {
*/
public function theSettingIsSetTo($settingName,
$value) {
$this->changedSettings[] = $settingName;
if (in_array($settingName, [
'type',
'general-require_provisioned_account',
'general-allow_multiple_user_back_ends',
'general-use_saml_auth_for_desktop'
])) {
$this->changedSettings[] = $settingName;
shell_exec(
sprintf(
'sudo -u apache %s %s config:app:set --value="%s" user_saml %s',
PHP_BINARY,
__DIR__ . '/../../../../../../occ',
$value,
$settingName
)
);
return;
}
shell_exec(
sprintf(
'sudo -u apache %s %s config:app:set --value="%s" user_saml %s',
'sudo -u apache %s %s saml:config:set --"%s"="%s" %d',
PHP_BINARY,
__DIR__ . '/../../../../../../occ',
$settingName,
$value,
$settingName
1
)
);
}

View file

@ -1,104 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\User_SAML\Tests\AppInfo;
use Test\TestCase;
class Test extends TestCase {
public function testFile() {
$dir = __DIR__;
$routes = require_once __DIR__ . '/../../../appinfo/routes.php';
$expected = [
'routes' => [
[
'name' => 'SAML#login',
'url' => '/saml/login',
'verb' => 'GET',
],
[
'name' => 'SAML#base',
'url' => '/saml',
'verb' => 'GET',
],
[
'name' => 'SAML#getMetadata',
'url' => '/saml/metadata',
'verb' => 'GET',
],
[
'name' => 'SAML#assertionConsumerService',
'url' => '/saml/acs',
'verb' => 'POST',
],
[
'name' => 'SAML#singleLogoutService',
'url' => '/saml/sls',
'verb' => 'GET',
],
[
'name' => 'SAML#singleLogoutService',
'url' => '/saml/sls',
'verb' => 'POST',
'postfix' => 'slspost',
],
[
'name' => 'SAML#notProvisioned',
'url' => '/saml/notProvisioned',
'verb' => 'GET',
],
[
'name' => 'SAML#genericError',
'url' => '/saml/error',
'verb' => 'GET',
],
[
'name' => 'SAML#selectUserBackEnd',
'url' => '/saml/selectUserBackEnd',
'verb' => 'GET',
],
[
'name' => 'Settings#getSamlProviderSettings',
'url' => '/settings/providerSettings/{providerId}',
'verb' => 'GET',
'defaults' => [
'providerId' => '1'
]
],
[
'name' => 'Settings#deleteSamlProviderSettings',
'url' => '/settings/providerSettings/{providerId}',
'verb' => 'DELETE',
'defaults' => [
'providerId' => '1'
]
],
[
'name' => 'Timezone#setTimezone',
'url' => '/config/timezone',
'verb' => 'POST',
],
],
];
$this->assertSame($expected, $routes);
}
}

View file

@ -23,78 +23,46 @@ namespace OCA\User_SAML\Tests\Command;
use OCA\User_SAML\Command\GetMetadata;
use OCA\User_SAML\SAMLSettings;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
class GetMetadataTest extends \Test\TestCase {
/** @var GetMetadata|\PHPUnit_Framework_MockObject_MockObject*/
/** @var GetMetadata|MockObject*/
protected $GetMetadata;
/** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */
private $request;
/** @var ISession|\PHPUnit_Framework_MockObject_MockObject */
private $session;
/** @var SAMLSettings|\PHPUnit_Framework_MockObject_MockObject*/
/** @var SAMLSettings|MockObject*/
private $samlSettings;
/** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */
private $config;
/** @var IURLGenerator|\PHPUnit_Framework_MockObject_MockObject */
private $urlGenerator;
protected function setUp(): void {
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->config = $this->createMock(IConfig::class);
$this->request = $this->createMock(IRequest::class);
$this->session = $this->createMock(ISession::class);
$this->samlSettings = new SAMLSettings($this->urlGenerator,
$this->config,
$this->request,
$this->session);
$this->samlSettings = $this->createMock(SAMLSettings::class);
$this->GetMetadata = new GetMetadata($this->samlSettings);
parent::setUp();
}
public function testGetMetadata() {
$inputInterface = $this->createMock(InputInterface::class);
$outputInterface = $this->createMock(OutputInterface::class);
$this->urlGenerator
->expects($this->at(0))
->method('linkToRouteAbsolute')
->with('user_saml.SAML.base')
->willReturn('https://nextcloud.com/base/');
$this->urlGenerator
->expects($this->at(1))
->method('linkToRouteAbsolute')
->with('user_saml.SAML.getMetadata')
->willReturn('https://nextcloud.com/metadata/');
$this->urlGenerator
->expects($this->at(2))
->method('linkToRouteAbsolute')
->with('user_saml.SAML.assertionConsumerService')
->willReturn('https://nextcloud.com/acs/');
$this->config->expects($this->any())->method('getAppValue')
->willReturnCallback(function ($app, $key, $default) {
if ($key == 'idp-entityId') {
return "dummy";
}
if ($key == 'idp-singleSignOnService.url') {
return "https://example.com/sso";
}
if ($key == 'idp-x509cert') {
return "DUMMY CERTIFICATE";
}
return $default;
});
$this->samlSettings->expects($this->any())
->method('getOneLoginSettingsArray')
->willReturn([
'baseurl' => 'https://nextcloud.com/base/',
'idp' => [
'entityId' => 'dummy',
'singleSignOnService' => ['url' => 'https://example.com/sso'],
'x509cert' => 'DUMMY CERTIFICATE',
],
'sp' => [
'entityId' => 'https://nextcloud.com/metadata/',
'assertionConsumerService' => [
'url' => 'https://nextcloud.com/acs/',
],
]
]);
$outputInterface->expects($this->once())->method('writeln')
->with($this->stringContains('md:EntityDescriptor'));
->with($this->stringContains('md:EntityDescriptor'));
$this->invokePrivate($this->GetMetadata, 'execute', [$inputInterface, $outputInterface]);
}

View file

@ -21,14 +21,19 @@
namespace OCA\User_SAML\Tests\Settings;
use OCA\User_SAML\SAMLSettings;
use OCA\User_SAML\Settings\Admin;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\Defaults;
use OCP\IConfig;
use OCP\IL10N;
use OneLogin\Saml2\Constants;
use PHPUnit\Framework\MockObject\MockObject;
class AdminTest extends \Test\TestCase {
/** @var \OCA\User_SAML\Settings\Admin */
/** @var SAMLSettings|MockObject */
private $settings;
/** @var Admin */
private $admin;
/** @var IL10N|\PHPUnit_Framework_MockObject_MockObject */
private $l10n;
@ -41,11 +46,13 @@ class AdminTest extends \Test\TestCase {
$this->l10n = $this->createMock(IL10N::class);
$this->defaults = $this->createMock(Defaults::class);
$this->config = $this->createMock(IConfig::class);
$this->settings = $this->createMock(SAMLSettings::class);
$this->admin = new \OCA\User_SAML\Settings\Admin(
$this->admin = new Admin(
$this->l10n,
$this->defaults,
$this->config
$this->config,
$this->settings
);
parent::setUp();
@ -193,32 +200,21 @@ class AdminTest extends \Test\TestCase {
['id' => 2, 'name' => 'Provider 2']
],
'name-id-formats' => $nameIdFormats,
'config' => [],
];
return $params;
}
public function testGetFormWithoutType() {
$this->settings->expects($this->once())
->method('getListOfIdps')
->willReturn([
1 => 'Provider 1',
2 => 'Provider 2',
]);
$this->config
->expects($this->at(0))
->method('getAppValue')
->with('user_saml', 'providerIds')
->willReturn('1,2');
$this->config
->expects($this->at(1))
->method('getAppValue')
->willReturn('Provider 1');
$this->config
->expects($this->at(2))
->method('getAppValue')
->willReturn('Provider 2');
$this->config
->expects($this->at(3))
->method('getAppValue')
->with('user_saml', 'sp-name-id-format')
->will($this->returnArgument(2));
$this->config
->expects($this->at(4))
->expects($this->once())
->method('getAppValue')
->with('user_saml', 'type')
->willReturn('');
@ -234,26 +230,14 @@ class AdminTest extends \Test\TestCase {
}
public function testGetFormWithSaml() {
$this->settings->expects($this->once())
->method('getListOfIdps')
->willReturn([
1 => 'Provider 1',
2 => 'Provider 2',
]);
$this->config
->expects($this->at(0))
->method('getAppValue')
->with('user_saml', 'providerIds')
->willReturn('1,2');
$this->config
->expects($this->at(1))
->method('getAppValue')
->willReturn('Provider 1');
$this->config
->expects($this->at(2))
->method('getAppValue')
->willReturn('Provider 2');
$this->config
->expects($this->at(3))
->method('getAppValue')
->with('user_saml', 'sp-name-id-format')
->will($this->returnArgument(2));
$this->config
->expects($this->at(4))
->expects($this->once())
->method('getAppValue')
->with('user_saml', 'type')
->willReturn('saml');

View file

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<phpunit bootstrap="bootstrap.php"
strict="true"
verbose="true"
timeoutForSmallTests="900"
timeoutForMediumTests="900"
@ -16,8 +15,4 @@
<directory suffix=".php">../../../user_saml/lib</directory>
</whitelist>
</filter>
<logging>
<!-- and this is where your report will be written -->
<log type="coverage-clover" target="./clover.xml"/>
</logging>
</phpunit>