Commit 8665b925 authored by Robert Lyon's avatar Robert Lyon
Browse files

Bug 1650995: Auth saml idp metadata fix



This patch allows the dataroot/metadata/*.xml file to be named after
the idp rather than the Mahara institution.

Also added
- A select dropdown so that institution can pick existing auth to be
paired to
- Upgrade to rename the dataroot/metadata/*.xml file
- Check to stop being able to add blank metadata field
- An alert for user when updating metadata if other institutions are also being effected
- Delete the metadata if deleted institution is only one using it

behatnotneeded

Change-Id: Ie3f5cdc523404b1081352ede67aab591e79b6dbb
Signed-off-by: Robert Lyon's avatarRobert Lyon <robertl@catalyst.net.nz>
parent f8ba3cb6
......@@ -141,6 +141,22 @@ if ($institution || $add) {
}
foreach ($authinstanceids as $id) {
// Check if authinstance is SAML and this is the only institution using the related idp metadata
if ($idps = get_records_sql_array("SELECT aic.value FROM {auth_instance} ai
JOIN {auth_instance_config} aic ON aic.instance = ai.id
WHERE aic.field = 'institutionidpentityid'
AND ai.authname = 'saml' AND ai.id = ?", array($id))) {
foreach ($idps as $idp) {
if (!count_records_sql("SELECT COUNT(*) FROM {auth_instance_config} aic
WHERE value = ? AND instance != ?", array($idp->value, $id))) {
safe_require('auth', 'saml');
$idpfile = AuthSaml::prepare_metadata_path($idp->value);
if (file_exists($idpfile)) {
unlink($idpfile);
}
}
}
}
delete_records('auth_instance_config', 'instance', $id);
delete_records('auth_remote_user', 'authinstance', $id);
}
......
<?php
/**
*
* @package mahara
* @subpackage core
* @author Catalyst IT Ltd
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
* @copyright For copyright information on Mahara, please see the README file distributed with this software.
*
*/
define('INTERNAL', 1);
define('JSON', 1);
require(dirname(dirname(dirname(__FILE__))) . '/init.php');
safe_require('auth', 'saml');
$idp = param_variable('idp', null);
$data = new StdClass();
if (file_exists(AuthSaml::prepare_metadata_path($idp))) {
$rawxml = file_get_contents(AuthSaml::prepare_metadata_path($idp));
$data->metadata = $rawxml;
$data->error = false;
}
else {
$data->error = 'unable to find metadata';
}
json_reply(false, array('data' => $data));
......@@ -32,17 +32,24 @@ $string['errornomemcache'] = 'A memcache server is needed for auth/saml. Either
$string['errornomemcache7php'] = 'A memcache server is needed for auth/saml. Either list the paths to your memcache servers in the $cfg->memcacheservers config variable or install memcache locally.<br>To install the PHP library "memcache" locally:<br>sudo apt-get install php-memcache<br>sudo phpenmod memcache<br>Then restart you web server.';
$string['errorbadconfig'] = 'The SimpleSAMLPHP config directory %s is incorrect.';
$string['errorbadmetadata'] = 'Badly formed SAML metadata. Ensure XML contains one valid Identity Provider.';
$string['errorduplicateidp1'] = 'The Identity Provider "%s" is already in use by institution "%s". Ensure the XML contains one valid and unique Identity Provider.';
$string['errorbadinstitutioncombo'] = 'There is already an existing authentication instance with this institution attribute and institution value combination.';
$string['errormissingmetadata'] = 'You have chosen to add new Identity Provider metadata but none is supplied.';
$string['errormissinguserattributes1'] = 'You seem to be authenticated, but we did not receive the required user attributes. Please check that your Identity Provider releases the first name, surname, and email fields for SSO to %s or inform the administrator.';
$string['errorregistrationenabledwithautocreate1'] = 'An institution has registration enabled. For security reasons this excludes user auto-creation, unless you are using remote usernames.';
$string['errorremoteuser1'] = 'Matching on "remoteuser" is mandatory if "usersuniquebyusername" is turned off.';
$string['IdPSelection'] = 'Identity Provider selection';
$string['noidpsfound'] = 'No Identity Providers found';
$string['idpentityid'] = 'Identity Provider entity';
$string['idpentityadded'] = "Added the Identity Provider metadata for this SAML instance.";
$string['idpentityupdated'] = "Updated the Identity Provider metadata for this SAML instance.";
$string['idpentityupdatedduplicates'] = array(
0 => "Updated the Identity Provider metadata for this and 1 other SAML instance.",
1 => "Updated the Identity Provider metadata for this and %s other SAML instances."
);
$string['idpprovider'] = 'Provider';
$string['institutionattribute'] = 'Institution attribute (contains "%s")';
$string['institutionidp'] = 'Institution Identity Provider SAML metadata';
$string['institutionidpentity'] = 'Available Identity Providers';
$string['institutionvalue'] = 'Institution value to check against attribute';
$string['libchecks'] = 'Checking for correct libraries installed: %s';
$string['link'] = 'Link accounts';
......@@ -53,6 +60,7 @@ $string['logintolinkdesc'] = '<p><b>You are currently connected with remote user
$string['logo'] = 'Logo';
$string['institutionregex'] = 'Do partial string match with institution shortname';
$string['login'] = 'SSO';
$string['newidpentity'] = 'Add new Identity Provider';
$string['notusable'] = 'Please install the SimpleSAMLPHP Service Provider libraries';
$string['obsoletesamlplugin'] = 'The auth/saml plugin needs to be reconfigured. Please update the plugin via the <a href="%s">plugin configuration</a> form.';
$string['obsoletesamlinstance'] = 'The SAML authentication instance <a href="%s">%s</a> for institution "%s" needs updating.';
......
<!-- @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later -->
<!-- @copyright For copyright information on Mahara, please see the README file distributed with this software. -->
<h3>SAML metadata for the Identity Provider (IdP) to connect to</h3>
<p>This element requires the XML formatted metadata for the IdP that you want to
connect to. If the same IdP has already been configured for another institution,
then leave this blank.</p>
<p>This element requires the XML formatted metadata for the IdP that you want to connect to.</p>
<p>If you are adding a new IdP please select "Add new Identity Provider" from "Available Identity Providers" and add in the relevant metadata to "Institution Identity Provider SAML metadata" field.</p>
<p>Otherwise select an existing IdP from "Available Identity Providers". If you make any changes to the metadata, it will be updated for the other institutions using that metadata. Therefore, be careful what you change.</p>
......@@ -22,6 +22,11 @@ class AuthSaml extends Auth {
return get_config('dataroot') . 'metadata/';
}
public static function prepare_metadata_path($idp) {
$path = self::get_metadata_path() . preg_replace('/[\/:\.]/', '_', $idp) . '.xml';
return $path;
}
public static function get_certificate_path() {
check_dir_exists(get_config('dataroot') . 'certificate/');
return get_config('dataroot') . 'certificate/';
......@@ -44,6 +49,7 @@ class AuthSaml extends Auth {
$this->config['remoteuser'] = true;
$this->config['loginlink'] = false;
$this->config['institutionidp'] = '';
$this->config['institutionidpentityid'] = '';
$this->instanceid = $id;
if (!empty($id)) {
......@@ -283,21 +289,22 @@ class AuthSaml extends Auth {
class PluginAuthSaml extends PluginAuth {
private static $default_config = array(
'user_attribute' => '',
'weautocreateusers' => 0,
'firstnamefield' => '',
'surnamefield' => '',
'emailfield' => '',
'studentidfield' => '',
'updateuserinfoonlogin' => 1,
'institution' => '',
'institutionattribute' => '',
'institutionvalue' => '',
'institutionregex' => 0,
'remoteuser' => 1,
'loginlink' => 0,
'active' => 1
);
'user_attribute' => '',
'weautocreateusers' => 0,
'firstnamefield' => '',
'surnamefield' => '',
'emailfield' => '',
'studentidfield' => '',
'updateuserinfoonlogin' => 1,
'institution' => '',
'institutionattribute' => '',
'institutionvalue' => '',
'institutionregex' => 0,
'remoteuser' => 1,
'loginlink' => 0,
'institutionidpentityid' => '',
'active' => 1
);
public static function can_be_disabled() {
return true;
......@@ -509,6 +516,19 @@ class PluginAuthSaml extends PluginAuth {
return true;
}
public static function get_idps($xml) {
$xml = new SimpleXMLElement($xml);
$xml->registerXPathNamespace('md', 'urn:oasis:names:tc:SAML:2.0:metadata');
$xml->registerXPathNamespace('mdui', 'urn:oasis:names:tc:SAML:metadata:ui');
// Find all IDPSSODescriptor elements and then work back up to the entityID.
$idps = $xml->xpath('//md:EntityDescriptor[//md:IDPSSODescriptor]');
$entityid = null;
if ($idps && isset($idps[0])) {
$entityid = (string)$idps[0]->attributes('', true)->entityID[0];
}
return array($entityid, $idps);
}
public static function get_instance_config_options($institution, $instance = 0) {
if (!class_exists('SimpleSAML_XHTML_IdPDisco')) {
global $SESSION;
......@@ -545,25 +565,23 @@ class PluginAuthSaml extends PluginAuth {
// lookup the institution metadata
$entityid = "";
self::$default_config['institutionidp'] = "";
if (file_exists(AuthSaml::get_metadata_path() . $institution . '.xml')) {
$rawxml = file_get_contents(AuthSaml::get_metadata_path() . $institution . '.xml');
if (empty($rawxml)) {
// bad metadata - get rid of it
unlink(AuthSaml::get_metadata_path() . $institution . '.xml');
}
else {
$xml = new SimpleXMLElement($rawxml);
$xml->registerXPathNamespace('md', 'urn:oasis:names:tc:SAML:2.0:metadata');
$xml->registerXPathNamespace('mdui', 'urn:oasis:names:tc:SAML:metadata:ui');
// Find all IDPSSODescriptor elements and then work back up to the entityID.
$idps = $xml->xpath('//md:EntityDescriptor[//md:IDPSSODescriptor]');
if ($idps && isset($idps[0])) {
$entityid = (string)$idps[0]->attributes('', true)->entityID[0];
self::$default_config['institutionidp'] = $rawxml;
if (!empty(self::$default_config['institutionidpentityid'])) {
$idpfile = AuthSaml::prepare_metadata_path(self::$default_config['institutionidpentityid']);
if (file_exists($idpfile)) {
$rawxml = file_get_contents($idpfile);
if (empty($rawxml)) {
// bad metadata - get rid of it
unlink($idpfile);
}
else {
// bad metadata - get rid of it
unlink(AuthSaml::get_metadata_path() . $institution . '.xml');
list ($entityid, $idps) = self::get_idps($rawxml);
if ($entityid) {
self::$default_config['institutionidp'] = $rawxml;
}
else {
// bad metadata - get rid of it
unlink($idpfile);
}
}
}
}
......@@ -572,6 +590,74 @@ class PluginAuthSaml extends PluginAuth {
if ($entityid) {
$idp_title .= " (" . $entityid . ")";
}
$entityidps = array();
$entityidp_hiddenlabel = true;
// Fetch the idp info via disco
require_once(get_config('docroot') . 'auth/saml/extlib/simplesamlphp/vendor/autoload.php');
require_once(get_config('docroot') . 'auth/saml/extlib/_autoload.php');
SimpleSAML_Configuration::init(get_config('docroot') . 'auth/saml/config');
$discoHandler = new PluginAuthSaml_IdPDisco(array('saml20-idp-remote', 'shib13-idp-remote'), 'saml');
$disco = $discoHandler->getTheIdPs();
if (count($disco['list']) > 0) {
$lang = current_language();
$lang = explode('.', $lang);
$lang = strtolower(array_shift($lang));
$entityidp_hiddenlabel = false;
foreach($disco['list'] as $idp) {
$idpname = (isset($idp['name'][$lang])) ? $idp['name'][$lang] : $idp['entityid'];
$entityidps[$idp['entityid']] = $idpname;
}
}
asort($entityidps);
// add the 'New' option to the top of the list
$entityidps = array('new' => get_string('newidpentity', 'auth.saml')) + $entityidps;
$idpselectjs = <<< EOF
<script type="application/javascript">
jQuery('document').ready(function($) {
function update_idp_label(idp) {
var idplabel = $('label[for="auth_config_institutionidp"]').html();
// remove the idp entity from string
if (idplabel.lastIndexOf('(') != -1) {
idplabel = idplabel.substring(0, idplabel.lastIndexOf('('));
}
// add in new one
if (idp) {
idplabel = idplabel.trim() + ' (' + idp + ')';
}
$('label[for="auth_config_institutionidp"]').html(idplabel);
}
function update_idp_info(idp) {
if (idp == 'new') {
// clear the metadata box
$('#auth_config_institutionidp').val('');
update_idp_label(false);
}
else {
// fetch the metadata info and update the textarea
idpsafe = idp.replace(/[\/:\.]/g, '_'); // change dots to underscores as that is how we save file
sendjsonrequest(config.wwwroot + 'auth/saml/idpmetadata.json.php', {'idp': idpsafe}, 'POST', function (data) {
if (!data.error) {
$('#auth_config_institutionidp').val(data.data.metadata);
}
});
update_idp_label(idp);
}
}
// On change
$('#auth_config_institutionidpentityid').on('change', function() {
update_idp_info($(this).val());
});
// On load
update_idp_info($('#auth_config_institutionidpentityid').val());
});
</script>
EOF;
$elements = array(
'instance' => array(
'type' => 'hidden',
......@@ -594,6 +680,13 @@ class PluginAuthSaml extends PluginAuth {
'title' => get_string('active', 'auth'),
'defaultvalue' => (int) self::$default_config['active'],
),
'institutionidpentityid' => array(
'type' => 'select',
'title' => get_string('institutionidpentity', 'auth.saml'),
'options' => $entityidps,
'defaultvalue' => ($entityid ? $entityid : 'new'),
'hiddenlabel' => $entityidp_hiddenlabel,
),
'institutionidp' => array(
'type' => 'textarea',
'title' => $idp_title,
......@@ -603,6 +696,10 @@ class PluginAuthSaml extends PluginAuth {
'help' => true,
'class' => 'under-label',
),
'idpselectjs' => array(
'type' => 'html',
'value' => $idpselectjs,
),
'institutionattribute' => array(
'type' => 'text',
'title' => get_string('institutionattribute', 'auth.saml', $institution),
......@@ -702,40 +799,18 @@ class PluginAuthSaml extends PluginAuth {
if (!empty($values['institutionidp'])) {
try {
$xml = new SimpleXMLElement($values['institutionidp']);
$xml->registerXPathNamespace('md', 'urn:oasis:names:tc:SAML:2.0:metadata');
$xml->registerXPathNamespace('mdui', 'urn:oasis:names:tc:SAML:metadata:ui');
// Find all IDPSSODescriptor elements and then work back up to the entityID.
$idps = $xml->xpath('//md:EntityDescriptor[//md:IDPSSODescriptor]');
if ($idps && isset($idps[0])) {
$entityid = (string)$idps[0]->attributes('', true)->entityID[0];
}
else {
list ($entityid, $idps) = self::get_idps($values['institutionidp']);
if (!$entityid) {
throw new Exception("Could not find entityId", 1);
}
// has this IdP already been configured?
$institutions = get_records_sql_array(
'SELECT aic.value AS idpentityid,
ai.institution AS institution
FROM {auth_instance_config} as aic
JOIN {auth_instance} AS ai
ON aic.instance = ai.id
WHERE field = \'institutionidpentityid\' AND value = ? AND
ai.institution <> ?
ORDER BY instance',
array($entityid, $values['institution']));
$i = 'Unknown';
if (is_array($institutions)) {
$i = $institutions[0]->institution;
$form->set_error('institutionidp', get_string('errorduplicateidp1', 'auth.saml', $entityid, $i));
}
}
catch (Exception $e) {
$form->set_error('institutionidp', get_string('errorbadmetadata', 'auth.saml'));
}
}
else {
$form->set_error('institutionidpentityid', get_string('errormissingmetadata', 'auth.saml'));
}
// If we're using Mahara usernames (usr.username) instead of remote usernames
// (auth_remote_user.remoteusername), then autocreation cannot be enabled if any
......@@ -768,6 +843,7 @@ class PluginAuthSaml extends PluginAuth {
}
public static function save_instance_config_options($values, Pieform $form) {
global $SESSION;
$authinstance = new stdClass();
......@@ -804,22 +880,43 @@ class PluginAuthSaml extends PluginAuth {
$current = array();
}
// grab the entityId
if (!empty($values['institutionidp'])) {
$xml = new SimpleXMLElement($values['institutionidp']);
$xml->registerXPathNamespace('md', 'urn:oasis:names:tc:SAML:2.0:metadata');
$xml->registerXPathNamespace('mdui', 'urn:oasis:names:tc:SAML:metadata:ui');
// Find all IDPSSODescriptor elements and then work back up to the entityID.
$idps = $xml->xpath('//md:EntityDescriptor[//md:IDPSSODescriptor]');
$entityid = (string)$idps[0]->attributes('', true)->entityID[0];
// grab the entityId from the metadata
list ($entityid, $idps) = self::get_idps($values['institutionidp']);
$changedxml = false;
if ($values['institutionidpentityid'] != 'new') {
$existingidpfile = AuthSaml::prepare_metadata_path($values['institutionidpentityid']);
if (file_exists($existingidpfile)) {
$rawxml = file_get_contents($existingidpfile);
if ($rawxml != $values['institutionidp']) {
$changedxml = true;
// find out which institutions are using it
$duplicates = get_records_sql_array("
SELECT COUNT(aic.instance) AS instances
FROM {auth_instance_config} aic
JOIN {auth_instance} ai ON (ai.authname = 'saml' AND ai.id = aic.instance)
WHERE aic.field = 'institutionidpentityid' AND aic.value = ? AND aic.instance != ?",
array($values['institutionidpentityid'], $values['instance']));
if ($duplicates[0]->instances > 0) {
$SESSION->add_ok_msg(get_string('idpentityupdatedduplicates', 'auth.saml', $duplicates[0]->instances));
}
else {
$SESSION->add_ok_msg(get_string('idpentityupdated', 'auth.saml'));
}
}
else {
$SESSION->add_ok_msg(get_string('idpentityadded', 'auth.saml'));
}
}
else {
// existing idpfile not found so just save it
$changedxml = true;
}
}
else {
// cleanup old one if exists
$entityid = "";
if (file_exists(AuthSaml::get_metadata_path() . $values['institution'] . '.xml')) {
// bad metadata - get rid of it
unlink(AuthSaml::get_metadata_path() . $values['institution'] . '.xml');
}
$values['institutionidpentityid'] = $entityid;
$changedxml = true;
$SESSION->add_ok_msg(get_string('idpentityadded', 'auth.saml'));
}
self::$default_config = array(
......@@ -853,8 +950,9 @@ class PluginAuthSaml extends PluginAuth {
}
// save the institution config
if (!empty($values['institutionidp'])) {
file_put_contents(AuthSaml::get_metadata_path() . $values['institution'] . '.xml', $values['institutionidp']);
if ($changedxml) {
$idpfile = AuthSaml::prepare_metadata_path($values['institutionidpentityid']);
file_put_contents($idpfile, $values['institutionidp']);
}
return $values;
......
......@@ -4895,5 +4895,26 @@ function xmldb_core_upgrade($oldversion=0) {
}
}
if ($oldversion < 2017021400) {
if ($results = get_records_sql_array("SELECT ai.institution, aic.value AS idp
FROM {auth_instance_config} aic
JOIN {auth_instance} ai ON ai.id = aic.instance
WHERE aic.field = 'institutionidpentityid' AND aic.value != ''", array())) {
log_debug('Change SAML metadata naming convention');
foreach ($results as $result) {
safe_require('auth', 'saml');
$metadata_current_file = AuthSaml::get_metadata_path() . $result->institution . '.xml';
$idp = str_replace('.', '_', $result->idp);
$metadata_new_file = AuthSaml::prepare_metadata_path($idp);
if (file_exists($metadata_current_file)) {
// rename metadata from institution to idp
if (copy($metadata_current_file, $metadata_new_file)) {
unlink($metadata_current_file);
}
}
}
}
}
return $status;
}
......@@ -1515,7 +1515,7 @@ function site_warnings() {
WHERE ai.id NOT IN (
SELECT instance FROM {auth_instance_config} aic
WHERE aic.field = ?
) AND ai.authname = ?", array('institutionidpentityid', 'saml'))) {
) AND ai.authname = ? ORDER BY i.displayname", array('institutionidpentityid', 'saml'))) {
foreach ($samls as $saml) {
$warnings[] = get_string('obsoletesamlinstance', 'auth.saml', get_config('wwwroot') . 'admin/users/addauthority.php?id=' . $saml->id . '&edit=1&i=' . $saml->name . '&p=saml', $saml->instancename, $saml->displayname);
}
......
......@@ -16,7 +16,7 @@ $config = new stdClass();
// See https://wiki.mahara.org/wiki/Developer_Area/Version_Numbering_Policy
// For upgrades on stable branches, increment the version by one. On master, use the date.
$config->version = 2017012700;
$config->version = 2017021400;
$config->series = '17.04';
$config->release = '17.04dev';
$config->minupgradefrom = 2012080604;
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment