Commit b8ddb24b authored by Richard Mansfield's avatar Richard Mansfield Committed by Gerrit Code Review
Browse files

Merge changes Ib93680e2,I5321c027,Ia8a79939

* changes:
  Redevelop auth/saml - self-linking accounts, dual login
  Enable multiple auth_remote_user connections
  Add SAML based SSO Login link
parents ec13abf0 93358ed3
......@@ -244,15 +244,42 @@ function edituser_site_validate(Pieform $form, $values) {
}
}
// Check that the external username isn't already in use
if (isset($values['remoteusername']) &&
$usedby = get_record_select('auth_remote_user',
'authinstance = ? AND remoteusername = ? AND localusr != ?',
array($values['authinstance'], $values['remoteusername'], $values['id']))
) {
$usedbyuser = get_field('usr', 'username', 'id', $usedby->localusr);
$SESSION->add_error_msg(get_string('duplicateremoteusername', 'auth', $usedbyuser));
$form->set_error('remoteusername', get_string('duplicateremoteusernameformerror', 'auth'));
// Check that the external username isn't already in use by someone else
if (isset($values['authinstance']) && isset($values['remoteusername'])) {
// there are 4 cases for changes on the page
// 1) ai and remoteuser have changed
// 2) just ai has changed
// 3) just remoteuser has changed
// 4) the ai changes and the remoteuser is wiped - this is a delete of the old ai-remoteuser
// determine the current remoteuser
$current_remotename = get_field('auth_remote_user', 'remoteusername',
'authinstance', $user->authinstance, 'localusr', $user->id);
if (!$current_remotename) {
$current_remotename = $user->username;
}
// what should the new remoteuser be
$new_remoteuser = get_field('auth_remote_user', 'remoteusername',
'authinstance', $values['authinstance'], 'localusr', $user->id);
if (!$new_remoteuser) {
$new_remoteuser = $user->username;
}
if (strlen(trim($values['remoteusername'])) > 0) {
// value changed on page - use it
if ($values['remoteusername'] != $current_remotename) {
$new_remoteuser = $values['remoteusername'];
}
}
// what really counts is who owns the target remoteuser slot
$target_owner = get_field('auth_remote_user', 'localusr',
'authinstance', $values['authinstance'], 'remoteusername', $new_remoteuser);
// target remoteuser is owned by someone else
if ($target_owner && $target_owner != $user->id) {
$usedbyuser = get_field('usr', 'username', 'id', $target_owner);
$SESSION->add_error_msg(get_string('duplicateremoteusername', 'auth', $usedbyuser));
$form->set_error('remoteusername', get_string('duplicateremoteusernameformerror', 'auth'));
}
}
}
......@@ -292,34 +319,55 @@ function edituser_site_submit(Pieform $form, $values) {
}
set_account_preference($user->id, 'maildisabled', $values['maildisabled']);
// Authinstance can be changed by institutional admins if both the
// old and new authinstances belong to the admin's institutions
$remotename = get_field('auth_remote_user', 'remoteusername', 'authinstance', $user->authinstance, 'localusr', $user->id);
if (!$remotename) {
$remotename = $user->username;
}
if (isset($values['authinstance'])
&& ($values['authinstance'] != $user->authinstance
|| (isset($values['remoteusername']) && $values['remoteusername'] != $remotename))) {
$authinst = get_records_select_assoc('auth_instance', 'id = ? OR id = ?',
// process the change of the authinstance and or the remoteuser
if (isset($values['authinstance']) && isset($values['remoteusername'])) {
// Authinstance can be changed by institutional admins if both the
// old and new authinstances belong to the admin's institutions
$authinst = get_records_select_assoc('auth_instance', 'id = ? OR id = ?',
array($values['authinstance'], $user->authinstance));
if ($USER->get('admin') ||
($USER->is_institutional_admin($authinst[$values['authinstance']]->institution) &&
$USER->is_institutional_admin($authinst[$user->authinstance]->institution))) {
delete_records('auth_remote_user', 'localusr', $user->id);
// determine the current remoteuser
$current_remotename = get_field('auth_remote_user', 'remoteusername',
'authinstance', $user->authinstance, 'localusr', $user->id);
if (!$current_remotename) {
$current_remotename = $user->username;
}
// if the remoteuser is empty and the ai has changed - delete the old remoteuser record
if (strlen(trim($values['remoteusername'])) == 0 &&
$values['authinstance'] != $user->authinstance) {
delete_records('auth_remote_user', 'authinstance', $user->authinstance, 'localusr', $user->id);
}
// we do not create a remoteuser record for internal ai's
if ($authinst[$values['authinstance']]->authname != 'internal') {
if (isset($values['remoteusername']) && strlen($values['remoteusername']) > 0) {
$un = $values['remoteusername'];
// what should the new remoteuser be
$new_remoteuser = get_field('auth_remote_user', 'remoteusername',
'authinstance', $values['authinstance'], 'localusr', $user->id);
// save the remotename for the target existence check
$target_remotename = $new_remoteuser;
if (!$new_remoteuser) {
$new_remoteuser = $user->username;
}
if (strlen(trim($values['remoteusername'])) > 0) {
// value changed on page - use it
if ($values['remoteusername'] != $current_remotename) {
$new_remoteuser = $values['remoteusername'];
}
}
else {
$un = $remotename;
// only update remote name if the input actually changed on the page or it doesn't yet exist
if ($current_remotename != $new_remoteuser || !$target_remotename) {
// only remove the ones related to this traget authinstance as we now allow multiple
// for dual login mechanisms
delete_records('auth_remote_user', 'authinstance', $values['authinstance'], 'localusr', $user->id);
insert_record('auth_remote_user', (object) array(
'authinstance' => $values['authinstance'],
'remoteusername' => $new_remoteuser,
'localusr' => $user->id,
));
}
insert_record('auth_remote_user', (object) array(
'authinstance' => $values['authinstance'],
'remoteusername' => $un,
'localusr' => $user->id,
));
}
// update the ai on the user master
$user->authinstance = $values['authinstance'];
// update the global $authobj to match the new authinstance
......@@ -331,7 +379,7 @@ function edituser_site_submit(Pieform $form, $values) {
// Only change the pw if the new auth instance allows for it
if (method_exists($authobj, 'change_password')) {
$user->passwordchange = (int) ($values['passwordchange'] == 'on');
$user->passwordchange = (int) (isset($values['passwordchange']) && $values['passwordchange'] == 'on' ? 1 : 0);
if (isset($values['password']) && $values['password'] !== '') {
$userobj = new User();
......
......@@ -522,7 +522,7 @@ function uploadcsv_submit(Pieform $form, $values) {
log_debug('added user ' . $user->username);
}
else if (isset($UPDATES[$user->username])) {
$updated = update_user($user, $profilefields, $remoteuser, $values);
$updated = update_user($user, $profilefields, $remoteuser, $values, true);
if (empty($updated)) {
// Nothing changed for this user
......
......@@ -1016,6 +1016,9 @@ function auth_get_login_form() {
'register' => array(
'value' => '<div id="login-helplinks">' . '<a href="' . get_config('wwwroot') . 'forgotpass.php" tabindex="2">' . get_string('lostusernamepassword') . '</a></div>'
),
'loginsaml' => array(
'value' => ((count_records('auth_instance', 'authname', 'saml') == 0) ? '' : '<div id="login-helplinks"><a href="' . get_config('wwwroot') . 'auth/saml/" tabindex="2">' . get_string('login', 'auth.saml') . '</a></div>')
),
'login_submitted' => array(
'type' => 'hidden',
'value' => 1
......@@ -1568,7 +1571,10 @@ function auth_generate_login_form() {
'register' => array(
'value' => '<div id="login-helplinks">' . $registerlink
. '<a href="' . get_config('wwwroot') . 'forgotpass.php" tabindex="2">' . get_string('lostusernamepassword') . '</a></div>'
)
),
'loginsaml' => array(
'value' => ((count_records('auth_instance', 'authname', 'saml') == 0) ? '' : '<a href="' . get_config('wwwroot') . 'auth/saml/" tabindex="2">' . get_string('login', 'auth.saml') . '</a>')
),
)
)));
......
<?php
<?php
/**
* Mahara: Electronic portfolio, weblog, resume builder and social networking
* Copyright (C) 2006-2009 Catalyst IT Ltd (http://www.catalyst.net.nz)
......@@ -20,7 +20,7 @@
* @subpackage auth-saml
* @author Piers Harding <piers@catalyst.net.nz>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL
* @copyright (C) 2006-2009 Catalyst IT Ltd http://catalyst.net.nz
* @copyright (C) 2006-2011 Catalyst IT Ltd http://catalyst.net.nz
*
* This file incorporates work covered by the following copyright and
* permission notice:
......@@ -45,20 +45,20 @@
define('INTERNAL', 1);
define('PUBLIC', 1);
define('SAML_RETRIES', 5);
global $CFG, $USER, $SESSION;
require(dirname(dirname(dirname(__FILE__))) . '/init.php');
require_once(get_config('docroot') .'auth/saml/lib.php');
require_once(get_config('libroot') .'institution.php');
// do our own partial initialisation so that we can get at the config
// this version of init.php has the user session initiation stuff ripped out
// this is because SimpleSAMLPHP does all kinds of things with the PHP session
// handling including changing the cookie names etc.
require(dirname(__FILE__) . '/init.php');
// check that the plugin is active
if (get_field('auth_installed', 'active', 'name', 'saml') != 1) {
redirect();
}
// get the config pointing to the SAML library - and load it
$samllib = get_config_plugin('auth', 'saml', 'simplesamlphplib');
if (null === $samllib) {
exit(0);
if (!file_exists($samllib.'/lib/_autoload.php')) {
throw new AuthInstanceException(get_string('errorbadssphplib','auth.saml'));
}
require_once($samllib.'/lib/_autoload.php');
......@@ -71,7 +71,7 @@ SimpleSAML_Configuration::init($samlconfig);
$saml_session = SimpleSAML_Session::getInstance();
// do we have a logout request?
if(param_variable("logout", false)) {
if (param_variable("logout", false)) {
// logout the saml session
$sp = $saml_session->getAuthority();
if (! $sp) {
......@@ -85,35 +85,44 @@ if (! in_array($sp, SimpleSAML_Auth_Source::getSources())) {
$sp = 'default-sp';
}
$as = new SimpleSAML_Auth_Simple($sp);
// Check the SimpleSAMLphp config is compatible
$saml_config = SimpleSAML_Configuration::getInstance();
$session_handler = $saml_config->getString('session.handler', false);
if (!$session_handler || $session_handler == 'phpsession') {
throw new AuthInstanceException(get_string('errorbadssphp','auth.saml'));
}
// what is the session like?
$valid_saml_session = $saml_session->isValid($sp);
// do we have a returnto URL ?
// figure out what the returnto URL should be
$wantsurl = param_variable("wantsurl", false);
if($wantsurl) {
$_SESSION['wantsurl'] = $wantsurl;
}
else if (isset($_SESSION['wantsurl'])) {
$wantsurl = $_SESSION['wantsurl'];
}
else if (! $saml_session->getIdP()){
$_SESSION['wantsurl'] = array_key_exists('HTTP_REFERER',$_SERVER) ? $_SERVER['HTTP_REFERER'] : $CFG->wwwroot;
$wantsurl = $_SESSION['wantsurl'];
}
else {
$wantsurl = $CFG->wwwroot;
if (!$wantsurl) {
if (isset($_SESSION['wantsurl'])) {
$wantsurl = $_SESSION['wantsurl'];
}
else if (! $saml_session->getIdP()) {
$wantsurl = array_key_exists('HTTP_REFERER',$_SERVER) ? $_SERVER['HTTP_REFERER'] : $CFG->wwwroot;
}
else {
$wantsurl = $CFG->wwwroot;
}
}
// taken from Moodle clean_param
// taken from Moodle clean_param - make sure the wantsurl is correctly formed
include_once('validateurlsyntax.php');
if (!validateUrlSyntax($wantsurl, 's?H?S?F?E?u-P-a?I?p?f?q?r?')) {
$wantsurl = $CFG->wwwroot;
}
// trim off any reference to login and stash
$_SESSION['wantsurl'] = preg_replace('/\&login$/', '', $wantsurl);
// now - are we logged in?
$as->requireAuth();
// ensure that $_SESSION is cleared
// ensure that $_SESSION is cleared for simplesamlphp
if (isset($_SESSION['wantsurl'])) {
unset($_SESSION['wantsurl']);
}
......@@ -136,156 +145,212 @@ require_once(dirname(dirname(dirname(__FILE__))) . '/auth/lib.php');
$SESSION = Session::singleton();
$USER = new LiveUser();
$THEME = new Theme($USER);
// The installer does its own auth_setup checking, because some upgrades may
// break logging in and so need to allow no logins.
if (!defined('INSTALLER')) {
auth_setup();
// ***********************************************************************
// END of copied stuff from original init.php
// ***********************************************************************
// restart the session for Mahara
@session_start();
if (!$SESSION->get('wantsurl')) {
$SESSION->set('wantsurl', preg_replace('/\&login$/', '', $wantsurl));
}
if (get_config('siteclosed')) {
if ($USER->admin) {
if (get_config('disablelogin')) {
$USER->logout();
}
else if (!defined('INSTALLER')) {
redirect('/admin/upgrade.php');
}
}
if (!$USER->admin) {
if ($USER->is_logged_in()) {
$USER->logout();
}
if (!defined('HOME') && !defined('INSTALLER')) {
redirect();
}
}
// now start the hunt for the associated authinstance for the organisation attached to the saml_attributes
global $instance;
$instance = auth_saml_find_authinstance($saml_attributes);
// if we don't have an auth instance then this is a serious failure
if (!$instance) {
throw new UserNotFoundException(get_string('errorbadinstitution','auth.saml'));
}
// check to see if we're installed...
if (!get_config('installed')) {
ensure_install_sanity();
// stash the existing logged in user - if we have one
$current_user = $USER;
$is_loggedin = $USER->is_logged_in();
$scriptfilename = str_replace('\\', '/', $_SERVER['SCRIPT_FILENAME']);
if (false === strpos($scriptfilename, 'admin/index.php')
&& false === strpos($scriptfilename, 'admin/upgrade.php')
&& false === strpos($scriptfilename, 'admin/upgrade.json.php')) {
redirect('/admin/');
}
// check the instance and do a test login
$can_login = false;
try {
$auth = new AuthSaml($instance->id);
$can_login = $auth->request_user_authorise($saml_attributes);
}
catch (AccessDeniedException $e) {
throw new UserNotFoundException(get_string('errnosamluser','auth.saml'));
}
catch (XmlrpcClientException $e) {
throw new AccessDeniedException($e->getMessage());
}
catch (AuthInstanceException $e) {
throw new AccessDeniedException(get_string('errormissinguserattributes', 'auth.saml'));
}
if (defined('JSON') && !defined('NOSESSKEY')) {
$sesskey = param_variable('sesskey', null);
global $USER;
if ($sesskey === null || $USER->get('sesskey') != $sesskey) {
$USER->logout();
json_reply('global', get_string('invalidsesskey'), 1);
// if we can login with SAML - then let them go
if ($can_login) {
// they are logged in, so they dont need to be here
if ($SESSION->get('wantsurl')) {
$wantsurl = $SESSION->get('wantsurl');
$SESSION->set('wantsurl', null);
}
session_write_close();
redirect($wantsurl);
}
// ***********************************************************************
// END of copied stuff from original init.php
// ***********************************************************************
// are we configured to allow testing of local login and linking?
$loginlink = get_field('auth_instance_config', 'value', 'field', 'loginlink', 'instance', $instance->id);
if (empty($loginlink)) {
throw new UserNotFoundException(get_string('errnosamluser','auth.saml'));
}
// restart the session for Mahara
@session_start();
// used in the submit callback for auth_saml_loginlink_screen()
global $remoteuser;
$user_attribute = get_field('auth_instance_config', 'value', 'field', 'user_attribute', 'instance', $instance->id);
$remoteuser = $saml_attributes[$user_attribute][0];
require_once(get_config('docroot') .'auth/saml/lib.php');
require_once(get_config('libroot') .'institution.php');
// is the local account already logged in or can the SAML auth succeed - if not try to get
// them to log in local/manual
if (!$is_loggedin) {
// cannot match user account - so offer them the login-link/register page
// if we can't login locally, and cant login via SAML then we should offer to register - but this should probably appear on the local login page anyway
auth_saml_login_screen($remoteuser);
}
else {
// if we can login locally, but can't login with SAML then we offer to link the accounts SAML -> local one
auth_saml_loginlink_screen($remoteuser, $current_user->username);
}
exit(0);
// if the user is not logged in, then lets start it going
if(!$USER->is_logged_in()) {
simplesaml_init($saml_config, $valid_saml_session, $saml_attributes, $as);
/**
* callback for linking local account with remote SAML account
*
* @param Pieform $form
* @param array $values
*/
function auth_saml_loginlink_submit(Pieform $form, $values) {
global $USER, $instance, $remoteuser;
// create the new account linking
db_begin();
delete_records('auth_remote_user', 'authinstance', $instance->id, 'localusr', $USER->id);
insert_record('auth_remote_user', (object) array(
'authinstance' => $instance->id,
'remoteusername' => $remoteuser,
'localusr' => $USER->id,
));
db_commit();
session_write_close();
redirect('/auth/saml/');
}
// they are logged in, so they dont need to be here
// header('Location: '.$CFG->wwwroot);
redirect($wantsurl);
/**
* check the validity of the users current SAML 2.0 session
* if its bad, force log them out of Mahara, and redirect them to the IdP
* if it's good, find an applicable saml auth instance, and try logging them in with it
* passing in the attributes found from the IdP
* Find the connected authinstance for the organisation attached to this SAML account
*
* @param object $saml_config saml configuration object
* @param boolean $valid_saml_session is there a valid saml2 session
* @param array $saml_attributes saml attributes passed in by the IdP
* @param object $as new saml user object
* @return nothing
* @param array $saml_attributes
*
* @return object authinstance record
*/
function simplesaml_init($saml_config, $valid_saml_session, $saml_attributes, $as) {
global $CFG, $USER, $SESSION;
// $idp = get_config_plugin('auth', 'saml', 'idpidentity');
$retry = $SESSION->get('retry');
if ($retry > SAML_RETRIES) {
throw new AccessTotallyDeniedException(get_string('errorretryexceeded','auth.saml', $retry));
}
else if (!$valid_saml_session) { #
if ($USER->is_logged_in()) {
$USER->logout();
}
$SESSION->set('messages', array());
$SESSION->set('retry', $retry + 1);
// not valid session. Ship user off to the Identity Provider
$as->requireAuth();
} else {
// find all the possible institutions/auth instances
$instances = recordset_to_array(get_recordset_sql("SELECT * FROM {auth_instance_config} aic, {auth_instance} ai WHERE ai.id = aic.instance AND ai.authname = 'saml' AND aic.field = 'institutionattribute'"));
// find the one (it should be only one) that has the right field, and the right field value for institution
$instance = false;
$institutions = array();
foreach ($instances as $row) {
$institutions[]= $row->instance.':'.$row->institution.':'.$row->value;
if (isset($saml_attributes[$row->value])) {
// does this institution use a regex match against the institution check value?
if ($configvalue = get_record('auth_instance_config', 'instance', $row->instance, 'field', 'institutionregex')) {
$is_regex = (boolean) $configvalue->value;
}
else {
$is_regex = false;
}
if ($configvalue = get_record('auth_instance_config', 'instance', $row->instance, 'field', 'institutionvalue')) {
$institution_value = $configvalue->value;
}
else {
$institution_value = $row->institution;
}
function auth_saml_find_authinstance($saml_attributes) {
// find the one (it should be only one) that has the right field, and the right field value for institution
$instance = false;
$institutions = array();
if ($is_regex) {
foreach ($saml_attributes[$row->value] as $attr) {
if (preg_match('/'.trim($institution_value).'/', $attr)) {
$instance = $row;
break;
}
}
}
else {
foreach ($saml_attributes[$row->value] as $attr) {
if ($attr == $institution_value) {
$instance = $row;
break;
}
// find all the possible institutions/auth instances of type saml
$instances = recordset_to_array(get_recordset_sql("SELECT * FROM {auth_instance_config} aic, {auth_instance} ai WHERE ai.id = aic.instance AND ai.authname = 'saml' AND aic.field = 'institutionattribute'"));
foreach ($instances as $row) {
$institutions[]= $row->instance.':'.$row->institution.':'.$row->value;
if (isset($saml_attributes[$row->value])) {
// does this institution use a regex match against the institution check value?
if ($configvalue = get_record('auth_instance_config', 'instance', $row->instance, 'field', 'institutionregex')) {
$is_regex = (boolean) $configvalue->value;
}
else {
$is_regex = false;
}
if ($configvalue = get_record('auth_instance_config', 'instance', $row->instance, 'field', 'institutionvalue')) {
$institution_value = $configvalue->value;
}
else {
$institution_value = $row->institution;
}
if ($is_regex) {
foreach ($saml_attributes[$row->value] as $attr) {
if (preg_match('/'.trim($institution_value).'/', $attr)) {
$instance = $row;
break;
}
}
}
}
if (!$instance) {
log_warn("auth/saml: could not find an authinstance from: " . join(", ", $institutions));
log_warn("auth/saml: could not find the saml institutionattribute for user: ".var_export($saml_attributes, true));
throw new UserNotFoundException(get_string('errorbadinstitution','auth.saml'));
}
try {
$auth = new AuthSaml($instance->id);
if ($auth->request_user_authorise($saml_attributes)) {
session_write_close();
}
else {
throw new UserNotFoundException(get_string('errnosamluser','auth.saml'));
foreach ($saml_attributes[$row->value] as $attr) {
if ($attr == $institution_value) {
$instance = $row;
break;
}
}
}
} catch (AccessDeniedException $e) {
throw new UserNotFoundException(get_string('errnosamluser','auth.saml'));
}
}
return $instance;
}
/**
* present the login-link screen where users are asked if they want to link
* the current loggedin local account to the remote saml one
*
* @param string $remoteuser
* @param string $currentuser
*/
function auth_saml_loginlink_screen($remoteuser, $currentuser) {
require_once('pieforms/pieform.php');
$form = array(
'name' => 'loginlink',
'renderer' => 'div',
'successcallback' => 'auth_saml_loginlink_submit',
'method' => 'post',
'plugintype' => 'auth',
'pluginname' => 'saml',
'elements' => array(
'linklogins' => array(
'value' => '<div><b>' . get_string('linkaccounts', 'auth.saml', $remoteuser, $currentuser) . '</b></div><br/>'
),
'submit' => array(
'type' => 'submitcancel',
'value' => array(get_string('link','auth.saml'), get_string('cancel')),
'goto' => get_config('wwwroot'),
),
'link_submitted' => array(
'type' => 'hidden',
'value' => 1
),
),
'dieaftersubmit' => false,
'iscancellable' => true
);
$form = new Pieform($form);
$smarty = smarty(array(), array(), array(), array('pagehelp' => false, 'sidebars' => false));
$smarty->assign('form', $form->build());
$smarty->assign('PAGEHEADING', get_string('link', 'auth.saml'));
$smarty->display('form.tpl');
exit;