Commit e658da7a authored by Matt Clarkson's avatar Matt Clarkson
Browse files

Bug 1668472: Add LTI SSO

* Extends the existing webservice auth to support SSO via LTI
* Adds an LTI module to support SSO and future LTI features
* Adds per-oauth token config to enable/disable on-the-fly user creation

behatnotneeded

Change-Id: Id6488930f37bdfd8200b4e9261f5292f2b72fbc7
parent 6957a254
......@@ -195,5 +195,17 @@
<INDEX NAME="timelogged" UNIQUE="false" FIELDS="timelogged"/>
</INDEXES>
</TABLE>
<TABLE NAME="oauth_server_config" COMMENT="Table to store settings related to an oauth server registry">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true"/>
<FIELD NAME="oauthserverregistryid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false"/>
<FIELD NAME="field" TYPE="char" LENGTH="255" NOTNULL="true"/>
<FIELD NAME="value" TYPE="text" LENGTH="big" NOTNULL="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="oauthserverregistryidfk" TYPE="foreign" FIELDS="oauthserverregistryid" REFTABLE="oauth_server_registry" REFFIELDS="id"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
......@@ -578,6 +578,18 @@ function xmldb_auth_webservice_upgrade($oldversion=0) {
change_field_notnull($table, $field, false);
}
if ($oldversion < 2017030600) {
$table = new XMLDBTable('oauth_server_config');
$table->addFieldInfo('id', XMLDB_TYPE_INTEGER, 10, XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null, null, null);
$table->addFieldInfo('oauthserverregistryid', XMLDB_TYPE_INTEGER, 10, XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null);
$table->addFieldInfo('field', XMLDB_TYPE_CHAR, 255, null, XMLDB_NOTNULL);
$table->addFieldInfo('value', XMLDB_TYPE_TEXT, null, null, null);
$table->addKeyInfo('primary', XMLDB_KEY_PRIMARY, array('id'));
$table->addKeyInfo('oauthserverregistryidfk', XMLDB_KEY_FOREIGN, array('oauthserverregistryid'), 'oauth_server_registry', array('id'));
create_table($table);
}
// sweep for webservice updates everytime
$status = external_reload_webservices();
......
......@@ -64,6 +64,21 @@ class AuthWebservice extends AuthInternal {
$validate = parent::validate_password($theysent, $wehave, $salt);
return (!empty($validate)) ? true : false;
}
/**
* Logout user and redirect to referring site
*/
public function logout() {
global $USER, $SESSION;
if ($SESSION->get('logouturl')) {
$logouturl = $SESSION->get('logouturl');
$USER->logout();
redirect($logouturl);
}
}
}
/**
......
......@@ -12,7 +12,7 @@
defined('INTERNAL') || die();
$config = new stdClass();
$config->version = 2016101100;
$config->release = '2.0.1';
$config->version = 2017030600;
$config->release = '2.0.2';
$config->requires_config = 0;
$config->requires_parent = 0;
<?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('PUBLIC', 1);
require(dirname(dirname(dirname(__FILE__))) . '/init.php');
header('Content-type: text/xml; charset=utf-8');
$smarty = smarty();
$smarty->assign('sitename', get_config('sitename'));
$smarty->assign('description', get_string('facebookdescription'));
$smarty->assign('launchurl', get_config('wwwroot').'webservice/rest/server.php');
$smarty->display('module:lti:xmlmetadata.tpl');
<?php
/**
*
* @package mahara
* @subpackage module-lti
* @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.
*
*/
defined('INTERNAL') || die();
$string['autoconfiguredesc'] = 'Automatically enable settings and configurations needed for LTI.';
$string['autoconfiguretitle'] = 'Auto-configure LTI?';
$string['autocreateusers'] = 'Auto-create users?';
$string['autocreationnotenabled'] = 'Auto-creation of user accounts not enabled';
$string['configstep'] = 'Conguration item';
$string['configstepstatus'] = 'Status';
$string['ltiserviceexists'] = 'LTI service group is registered';
$string['noticeenabled'] = 'The LTI API is currently enabled.';
$string['noticenotenabled'] = 'The LTI API is <b>not</b> currently enabled.';
$string['oauthprotocolenabled'] = 'OAuth protocol enabled';
$string['restprotocolenabled'] = 'REST protocol enabled';
$string['usernameexists'] = 'Username already exists "%s"';
$string['webserviceauthdisabled'] = 'Webservice auth is not enabled for this institution';
$string['webserviceproviderenabled'] = 'Incoming web service requests allowed';
$string['institutiondenied'] = 'Access to \'%s\' denied. Please contact your institution admin.';
<?php
/**
*
* @package mahara
* @subpackage module.lti
* @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.
*
*/
defined('INTERNAL') || die();
/**
* This plugin supports the webservices provided by the LTI module
*/
class PluginModuleLti extends PluginModule {
private static $default_config = array(
'autocreateusers' => false,
);
public static function postinst($fromversion) {
require_once(get_config('docroot') . 'webservice/lib.php');
external_reload_component('module/lti', false);
}
public static function has_config() {
return true;
}
public static function has_oauth_service_config() {
return true;
}
/**
* Check the status of each configuration element needed for the LTI API
*
* @param boolean $clearcache Whether to clear the cached results of the check
* @return array Information about the status of each config step needed.
*/
public static function check_service_status($clearcache = false) {
static $statuslist = null;
if (!$clearcache && $statuslist !== null) {
return $statuslist;
}
require_once(get_config('docroot') . 'webservice/lib.php');
// Check all the configs needed for the LTI API to work.
$statuslist = array();
$statuslist[] = array(
'name' => get_string('webserviceproviderenabled', 'module.lti'),
'status' => (bool) get_config('webservice_provider_enabled')
);
$statuslist[] = array(
'name' => get_string('oauthprotocolenabled', 'module.lti'),
'status' => webservice_protocol_is_enabled('oauth')
);
$statuslist[] = array(
'name' => get_string('restprotocolenabled', 'module.lti'),
'status' => webservice_protocol_is_enabled('rest')
);
$servicerec = get_record('external_services', 'shortname', 'maharalti', 'component', 'module/lti', null, null, 'enabled, restrictedusers, tokenusers');
$statuslist[] = array(
'name' => get_string('ltiserviceexists', 'module.lti'),
'status' => (bool) $servicerec
);
return $statuslist;
}
/**
* Determine whether the LTI webservice, as a whole, is fully configured
* @param boolean $clearcache Whether to clear cached results from a previous check
* @return boolean
*/
public static function is_service_ready($clearcache = false) {
return array_reduce(
static::check_service_status($clearcache),
function($carry, $item) {
return $carry && $item['status'];
},
true
);
}
public static function get_config_options() {
$statuslist = static::check_service_status(true);
$ready = static::is_service_ready();
$smarty = smarty_core();
$smarty->assign('statuslist', $statuslist);
if ($ready) {
$smarty->assign('notice', get_string('noticeenabled', 'module.lti'));
}
else {
$smarty->assign('notice', get_string('noticenotenabled', 'module.lti'));
}
$statushtml = $smarty->fetch('module:lti:statustable.tpl');
unset($smarty);
$elements = array();
$elements['statustable'] = array(
'type' => 'html',
'value' => $statushtml
);
if (!$ready) {
$elements['activate'] = array(
'type' => 'switchbox',
'title' => get_string('autoconfiguretitle', 'module.lti'),
'description' => get_string('autoconfiguredesc', 'module.lti'),
'switchtext' => 'yesno',
);
}
$form = array('elements' => $elements);
if (!$ready) {
// HACK: Reload the page after form submission, so that the status
// table gets updated.
$form['jssuccesscallback'] = 'module_lti_reload_page';
}
return $form;
}
public static function save_config_options(Pieform $form, $values) {
if (!empty($values['activate'])) {
set_config('webservice_provider_enabled', true);
set_config('webservice_provider_oauth_enabled', true);
set_config('webservice_provider_rest_enabled', true);
require_once(get_config('docroot') . 'webservice/lib.php');
external_reload_component('module/lti', false);
set_field('external_services', 'enabled', 1, 'shortname', 'lti', 'component', 'module/lti');
}
return true;
}
public static function get_oauth_service_config_options($serverid) {
$dbconfig = get_records_assoc('oauth_server_config', 'oauthserverregistryid', $serverid, '', 'field, value');
$elements = array(
'autocreateusers' => array(
'type' => 'switchbox',
'title' => get_string('autocreateusers', 'module.lti'),
'defaultvalue' => isset($dbconfig['autocreateusers']->value) ? $dbconfig['autocreateusers']->value : self::$default_config['autocreateusers'],
),
);
return $elements;
}
public static function save_oauth_service_config_options($serverid, $values) {
return update_oauth_server_config($serverid, 'autocreateusers', (int)$values['autocreateusers']);
}
}
<?php
/**
*
* @package mahara
* @subpackage module-lti
* @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.
*
*/
defined('INTERNAL') || die();
$config = new stdClass();
$config->version = 2017030600;
$config->release = '1.0.0';
<?php
/**
*
* @package mahara
* @subpackage module-lti
* @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.
*/
if (!defined('INTERNAL')) {
die();
}
require_once(get_config('docroot') . 'webservice/lib.php');
/**
* Functions needed to launch Mahara as an LTI provider
*/
class module_lti_launch extends external_api {
/**
* parameter definition for input of launch method
*
* Returns description of method parameters
* @return external_function_parameters
*/
public static function launch_parameters() {
return new external_function_parameters(
array(
// Required Params
'launch_presentation_return_url' => new external_value(PARAM_URL, 'LTI launch_presentation_return_url', VALUE_REQUIRED),
'lis_person_contact_email_primary' => new external_value(PARAM_EMAIL, 'LTI lis_person_contact_email_primary', VALUE_REQUIRED),
'lis_person_name_family' => new external_value(PARAM_TEXT, 'LTI lis_person_name_family', VALUE_REQUIRED),
'lis_person_name_given' => new external_value(PARAM_TEXT, 'LTI lis_person_name_given', VALUE_REQUIRED),
// Optional Params
'context_id' => new external_value(PARAM_TEXT, 'LTI context_id', VALUE_OPTIONAL),
'context_label' => new external_value(PARAM_TEXT, 'LTI context_label', VALUE_OPTIONAL),
'context_title' => new external_value(PARAM_TEXT, 'LTI context_title', VALUE_OPTIONAL),
'context_type' => new external_value(PARAM_TEXT, 'LTI context_type', VALUE_OPTIONAL),
'ext_lms' => new external_value(PARAM_TEXT, 'LTI ext_lms', VALUE_OPTIONAL),
'ext_roles' => new external_value(PARAM_TEXT, 'LTI ext_roles', VALUE_OPTIONAL),
'ext_user_username' => new external_value(PARAM_TEXT, 'LTI ext_user_username', VALUE_OPTIONAL),
'launch_presentation_document_target' => new external_value(PARAM_TEXT, 'LTI launch_presentation_document_target', VALUE_OPTIONAL),
'launch_presentation_height' => new external_value(PARAM_NUMBER, 'LTI launch_presentation_height', VALUE_OPTIONAL),
'launch_presentation_locale' => new external_value(PARAM_TEXT, 'LTI launch_presentation_locale', VALUE_OPTIONAL),
'launch_presentation_width' => new external_value(PARAM_NUMBER, 'LTI launch_presentation_width', VALUE_OPTIONAL),
'lis_course_section_sourcedid' => new external_value(PARAM_TEXT, 'LTI lis_course_section_sourcedid', VALUE_OPTIONAL),
'lis_outcome_service_url' => new external_value(PARAM_TEXT, 'LTI lis_outcome_service_url', VALUE_OPTIONAL),
'lis_person_name_full' => new external_value(PARAM_TEXT, 'LTI lis_person_name_full', VALUE_OPTIONAL),
'lis_person_sourcedid' => new external_value(PARAM_TEXT, 'LTI lis_person_sourcedid', VALUE_OPTIONAL),
'lis_result_sourcedid' => new external_value(PARAM_TEXT, 'LTI lis_result_sourcedid', VALUE_OPTIONAL),
'lti_message_type' => new external_value(PARAM_TEXT, 'LTI lti_message_type', VALUE_OPTIONAL),
'lti_version' => new external_value(PARAM_TEXT, 'LTI lti_version', VALUE_OPTIONAL),
'resource_link_description' => new external_value(PARAM_TEXT, 'LTI resource_link_description', VALUE_OPTIONAL),
'resource_link_id' => new external_value(PARAM_TEXT, 'LTI resource_link_id', VALUE_OPTIONAL),
'resource_link_title' => new external_value(PARAM_TEXT, 'LTI resource_link_title', VALUE_OPTIONAL),
'roles' => new external_value(PARAM_TEXT, 'LTI roles', VALUE_OPTIONAL),
'tool_consumer_info_product_family_code' => new external_value(PARAM_TEXT, 'LTI tool_consumer_info_product_family_code', VALUE_OPTIONAL),
'tool_consumer_info_version' => new external_value(PARAM_TEXT, 'LTI tool_consumer_info_version', VALUE_OPTIONAL),
'tool_consumer_instance_contact_email' => new external_value(PARAM_TEXT, 'LTI tool_consumer_instance_contact_email', VALUE_OPTIONAL),
'tool_consumer_instance_description' => new external_value(PARAM_TEXT, 'LTI tool_consumer_instance_description', VALUE_OPTIONAL),
'tool_consumer_instance_guid' => new external_value(PARAM_TEXT, 'LTI tool_consumer_instance_guid', VALUE_OPTIONAL),
'tool_consumer_instance_name' => new external_value(PARAM_TEXT, 'LTI tool_consumer_instance_name', VALUE_OPTIONAL),
'user_id' => new external_value(PARAM_TEXT, 'LTI user_id', VALUE_OPTIONAL),
'user_image' => new external_value(PARAM_URL, 'LTI user_image', VALUE_OPTIONAL),
// Canvas specific LTI params
'custom_canvas_api_domain' => new external_value(PARAM_TEXT, 'LTI custom_canvas_api_domain', VALUE_OPTIONAL),
'custom_canvas_api_domain' => new external_value(PARAM_TEXT, 'LTI custom_canvas_api_domain', VALUE_OPTIONAL),
'custom_canvas_course_id' => new external_value(PARAM_TEXT, 'LTI custom_canvas_course_id', VALUE_OPTIONAL),
'custom_canvas_enrollment_state' => new external_value(PARAM_TEXT, 'LTI custom_canvas_enrollment_state', VALUE_OPTIONAL),
'custom_canvas_user_id' => new external_value(PARAM_TEXT, 'LTI custom_canvas_user_id', VALUE_OPTIONAL),
'custom_canvas_user_login_id' => new external_value(PARAM_TEXT, 'LTI custom_canvas_user_login_id', VALUE_OPTIONAL),
'custom_canvas_workflow_state' => new external_value(PARAM_TEXT, 'LTI custom_canvas_workflow_state', VALUE_OPTIONAL),
)
);
}
/**
* parameter definition for output of autologin_redirect method
*/
public static function launch_returns() {
return null;
}
public static function launch($params) {
global $USER, $SESSION, $WEBSERVICE_INSTITUTION, $WEBSERVICE_OAUTH_SERVERID;
$keys = array_keys(self::launch_parameters()->keys);
$params = array_combine($keys, func_get_args());
// Get auth instance for institution that issued OAuth key
$authinstanceid = get_field('auth_instance', 'id', 'instancename', 'webservice', 'institution', $WEBSERVICE_INSTITUTION);
if (!$authinstanceid) {
$USER->logout();
throw new AccessDeniedException(get_string('webserviceauthdisabled', 'module.lti'));
}
// Check for userid in auth_remote_user
$userid = get_field('auth_remote_user', 'localusr', 'authinstance', $authinstanceid, 'remoteusername', $params['user_id']);
$updateremote = false;
$updateuser = true;
// User not found - try to match on email
if (!$userid && isset($params['lis_person_contact_email_primary'])) {
log_debug('User not found in auth_remote_user with user_id:'.$params['user_id']);
$userid = get_field('artefact_internal_profile_email', 'owner', 'email', $params['lis_person_contact_email_primary'], 'verified', 1);
$updateremote = true;
}
// Check user belongs to institution specified by OAuth key
if ($userid) {
$is_site_admin = false;
foreach (get_site_admins() as $site_admin) {
if ($site_admin->id == $userid) {
$is_site_admin = true;
break;
}
}
if (!$is_site_admin) {
// check user is member of configured OAuth institution
$institutions = array_keys(load_user_institutions($userid));
if (!in_array($WEBSERVICE_INSTITUTION, $institutions)) {
$USER->logout();
die_info(get_string('institutiondenied', 'module.lti', institution_display_name($WEBSERVICE_INSTITUTION)));
}
}
}
// Auto create user if auth allowed
$canautocreate = get_field('oauth_server_config', 'value', 'oauthserverregistryid', $WEBSERVICE_OAUTH_SERVERID, 'field', 'autocreateusers');
if (!$userid) {
if ($canautocreate) {
$user = new stdClass;
$user->email = $params['lis_person_contact_email_primary'];
$user->password = sha1(uniqid('', true));
$user->firstname = $params['lis_person_name_given'];
$user->lastname = $params['lis_person_name_family'];
$user->authinstance = $authinstanceid;
// Make sure that the username doesn't already exist
if (get_record('usr', 'username', $user->email)) {
$USER->logout();
throw new WebserviceInvalidParameterException(get_string('usernameexists', 'module.lti', $user->email));
}
$user->username = $user->email;
$userid = create_user($user, array(), $WEBSERVICE_INSTITUTION, true, $params['user_id']);
$updateremote = false;
$updateuser = false;
}
else {
$USER->logout();
throw new AccessDeniedException(get_string('autocreationnotenabled', 'module.lti'));
}
}
$user = get_record('usr', 'id', $userid, 'deleted', 0);
if ($updateuser) {
$user->email = $params['lis_person_contact_email_primary'];
$user->firstname = $params['lis_person_name_given'];
$user->lastname = $params['lis_person_name_family'];
unset($user->password);
update_user($user);
}
log_debug('found userid: '.$user->id);
if ($updateremote) {
$authremoteuser = new StdClass;
$authremoteuser->authinstance = $authinstanceid;
$authremoteuser->remoteusername = $params['user_id'];
$authremoteuser->localusr = $user->id;
insert_record('auth_remote_user', $authremoteuser);
}
log_debug('reanimating: '.var_export($user->username, true));
$USER->reanimate($user->id, $authinstanceid);
if (isset($params['launch_presentation_return_url'])) {
$SESSION->set('logouturl', $params['launch_presentation_return_url']);
}
redirect(get_config('wwwroot'));
}
}
<?php
/**
* Core external functions and service definitions.
*
* @package mahara
* @subpackage module-lti
* @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.
*/
$services = array(
'Mahara LTI integration' => array(
'shortname' => 'maharalti',
'functions' => [
'module_lti_launch',
],
'enabled' => 1,
'restrictedusers' => 0,
'tokenusers' => 0,
// Increment this whenever you make a change to the profile or
// behavior of this service and its exposed functions.
'apiversion' => 1,
),
);
$functions = array(
'module_lti_launch' => array(
'classname' => 'module_lti_launch',
'methodname' => 'launch',
'description' => "Launch and LTI activity",
'type' => 'write',
),
);
<div class="alert alert-default">
{if $header}<h3>{$header}</h3>{/if}
<p>{$notice|safe}</p>
</div>
<table class="table fullwidth table-padded">
<tbody>
<thead>
<tr>
<th>{str tag="configstep" section="module.lti"}</th>
<th>{str tag="configstepstatus" section="module.lti"}</th>
</tr>
</thead>
{foreach from=$statuslist item=item}
<tr>
<td>
<h3 class="title">
{$item.name}
</h3>
</td>
<td>
{if $item.status}
<span class="icon icon-check text-success" title="{str tag="readylabel" section="module.lti"}" role="presentation" aria-hidden="true"></span>
{else}
<span class="icon icon-exclamation-triangle" title="{str tag="notreadylabel" section="module.lti"}" role="presentation" aria-hidden="true"></span>
{/if}
</td>
</tr>
{/foreach}
</tbody>
</table>
<script type="text/javascript">
if (typeof module_lti_reload_page === "undefined") {
function module_lti_reload_page() {
window.location.reload(true);
}
}
</script>
<cartridge_basiclti_link xmlns="http://www.imsglobal.org/xsd/imslticc_v1p0" xmlns:blti="http://www.imsglobal.org/xsd/imsbasiclti_v1p0" xmlns:lticm="http://www.imsglobal.org/xsd/imslticm_v1p0" xmlns:lticp="http://www.imsglobal.org/xsd/imslticp_v1p0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/imslticc_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticc_v1p0.xsd http://www.imsglobal.org/xsd/imsbasiclti_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imsbasiclti_v1p0p1.xsd http://www.imsglobal.org/xsd/imslticm_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticm_v1p0.xsd http://www.imsglobal.org/xsd/imslticp_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticp_v1p0.xsd">
<blti:title>{$sitename}</blti:title>
<blti:description>
{$description}
</blti:description>
<blti:launch_url>{$launchurl}</blti:launch_url>
<blti:extensions platform="canvas.instructure.com">
<lticm:property name="icon_url">{$sitelogo}</lticm:property>
<lticm:property name="link_text">{$sitename}</lticm:property>