Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
snake committed Nov 13, 2024
1 parent 9926fef commit c09257d
Show file tree
Hide file tree
Showing 20 changed files with 1,564 additions and 2 deletions.
65 changes: 65 additions & 0 deletions lib/db/upgrade.php
Original file line number Diff line number Diff line change
Expand Up @@ -1536,5 +1536,70 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2024100700.03);
}

// if ($oldversion < 2024100700.04) {
// // If mod_lti is present, migrate the relevant link data to the replacement table in core_ltix.
// // This needs to be done as a core step, so that this information is present when installing new services, some of which
// // require re-linking to this data (e.g. ltixservice_gradebookservices).
// if (file_exists($CFG->dirroot . '/mod/lti/version.php')) {
// $table = new xmldb_table('lti_resource_link');
//
// // Drop the UUID index, allowing field modification below.
// $index = new xmldb_index('uuid_index', XMLDB_INDEX_UNIQUE, ['uuid']);
//
// // Conditionally launch drop index uuid_index.
// if ($dbman->index_exists($table, $index)) {
// $dbman->drop_index($table, $index);
// }
//
// // Modify the field, permitting nulls, allowing migrating data without a UUID initially.
// $field = new xmldb_field(
// name: 'uuid',
// type: XMLDB_TYPE_CHAR,
// precision: '36',
// notnull: false,
// );
// $dbman->change_field_notnull($table, $field);
//
// $sql = "INSERT INTO {lti_resource_link} (typeid, component, itemtype, itemid, contextid, legacyid, url, title, text,
// textformat, gradable, launchcontainer, customparams, icon, servicesalt)
// SELECT lti.typeid, :component, :itemtype, lti.id, ctx.id, lti.id, lti.toolurl, lti.name, lti.intro,
// lti.introformat, :gradable, lti.launchcontainer, lti.instructorcustomparameters, lti.icon,
// lti.servicesalt
// FROM {lti} lti
// JOIN {context} ctx ON (ctx.instanceid = lti.course)
// WHERE ctx.contextlevel = :contextlevel";
// $DB->execute($sql, [
// 'component' => 'mod_lti',
// 'itemtype' => 'activity',
// 'gradable' => true,
// 'contextlevel' => CONTEXT_COURSE,
// ]);
//
// $ltirs = $DB->get_recordset('lti', null, '', 'id');
// foreach ($ltirs as $ltirecord) {
// $DB->set_field('lti_resource_link', 'uuid', \core\uuid::generate(), ['legacyid' => $ltirecord->id]);
// }
// $ltirs->close();
//
// // Restore the notnull for UUID.
// $field = new xmldb_field(
// name: 'uuid',
// type: XMLDB_TYPE_CHAR,
// precision: '36',
// notnull: XMLDB_NOTNULL,
// default: null
// );
// $dbman->change_field_notnull($table, $field);
//
// // Conditionally launch add index uuid_index.
// if (!$dbman->index_exists($table, $index)) {
// $dbman->add_index($table, $index);
// }
// }
//
// // Main savepoint reached.
// upgrade_main_savepoint(true, 2024100700.04);
// }

return true;
}
142 changes: 142 additions & 0 deletions ltix/classes/helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -2097,6 +2097,148 @@ public static function get_instance_type(object $instance) : ?object {
return self::get_type($instance->typeid);
}

/**
* Get those claims used in all lti messages and which are generally required.
*
* Must be claims that are generic and used in all lti messages. Note: this includes things like version, deployment_id and
* others which, despite not being listed as applicable to all message types in the core spec, in fact, behave as such.
*
* @param \stdClass $toolregistration
* @param string $messagetype
* @return array
*/
public static function get_lti_message_standard_claims(\stdClass $toolregistration, string $messagetype,
string $issuer): array {

// Note: roles are omitted here because these cannot be calculated in a generic way.

$prefix = LTI_JWT_CLAIM_PREFIX;
return [
'tool_registration_id' => $toolregistration->id, // Moodle-specific. // TODO: Can probably drop this if not used.
'iss' => $issuer,
'aud' => $toolregistration->lti_clientid,
"$prefix/claim/message_type" => $messagetype, // https://www.imsglobal.org/spec/lti/v1p3#message-type-and-schemas.
"$prefix/claim/deployment_id" => $toolregistration->id, // Used in every message.
"$prefix/claim/version" => $toolregistration->lti_ltiversion, // Used in every message.
"nonce" => bin2hex(random_string(10)), // Uniqueness of the message hint payload from request to request.
];
}

// TODO may not be needed.
public static function get_lti_message_optional_claims(\stdClass $toolregistration, string $messagetype,
string $issuer, \stdClass $user): array {

$prefix = LTI_JWT_CLAIM_PREFIX;

return [
"$prefix/claim/context" => new stdClass(),
"$prefix/claim/resource_link" => new stdClass(), // Specific to this message.
"$prefix/claim/tool_platform" => new stdClass(),
"$prefix/claim/target_link_uri" => '',
"$prefix/claim/launch_presentation" => new stdClass(),
"$prefix/claim/lis" => new stdClass(),
];
}

/**
* A context-based IMS role assignment helper for LTI 1.3 ONLY. This is not compatible with legacy roles.
*
* The logic here is based on the legacy role calc code in {@see self::get_ims_role()} and {@see oauth_helper::sign_jwt()}.
*
* @param int $userid
* @param \core\context $context
* @return array
* @throws coding_exception
*/
public static function get_lti_message_roles(int $userid, \core\context $context): array {
$roles = [];

// moodle/ltix:manage is granted at mod level.
// TODO: create a tracker to address the capability migration stuff.
// Do we want launch to be handled at mod-level as the 'usual' context? Probably not. Probably better as course.

// Previously, cmid would be present in resource link launch.
// Cmid would be omitted if doing a deep linking launch, where no cmid existed yet.
if (has_capability('moodle/ltix:manage', $context, $userid)) {
array_push($roles, 'Instructor');
} else {
array_push($roles, 'Learner');
}

if (has_capability('moodle/ltix:admin', $context, $userid)) {
// Explicitly defined admins: drop the learner role and always granted IMS admin role.
$roles = array_diff($roles, array('Learner'));
array_push($roles, 'urn:lti:sysrole:ims/lis/Administrator', 'urn:lti:instrole:ims/lis/Administrator');
} else if (is_siteadmin($userid)) {
// De-facto admins (is an admin in Moodle): drop the learner role and conditionally grant IMS admin role.
// Here, an additional check is required in course-specific contexts to support the 'loginas' feature.
// If not in a course-related context, the IMS roles is always added.
$roles = array_diff($roles, array('Learner'));
$coursecontext = $context->get_course_context(false); // Will return false if course context not applicable.
if (($coursecontext && !is_role_switched($coursecontext->instanceid)) || !$coursecontext) {
array_push($roles, 'urn:lti:sysrole:ims/lis/Administrator', 'urn:lti:instrole:ims/lis/Administrator');
}
}

// Convert shortnames to correct full name + fix legacy role names.
$finalroles = [];
foreach ($roles as $role) {
if (strpos($role, 'urn:lti:role:ims/lis/') === 0) {
$role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#' . substr($role, 21);
} else if (strpos($role, 'urn:lti:instrole:ims/lis/') === 0) {
$role = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#' . substr($role, 25);
} else if (strpos($role, 'urn:lti:sysrole:ims/lis/') === 0) {
$role = 'http://purl.imsglobal.org/vocab/lis/v2/system/person#' . substr($role, 24);
} else if ((strpos($role, '://') === false) && (strpos($role, 'urn:') !== 0)) {
$role = "http://purl.imsglobal.org/vocab/lis/v2/membership#{$role}";
}
$finalroles[] = $role;
}

return $finalroles;
}

// TODO: do we need this method?
public static function get_token_claims_for_launch(string $messagetype, string $nonce): array {
$prefix = LTI_JWT_CLAIM_PREFIX;

if ($messagetype === 'LtiResourceLinkRequest') {
$claims = [
'iss' => '', // $CFG->wwwroot, to be added here.
'sub' => '', // Only applicable after authentication.
'aud' => '', // tool config's client id, to be added here.
'exp' => 1000,
'iat' => 1000,
'azp' => '',
'nonce' => '', // Only applicable after authentication because it's sent in the auth request.
'name' => '', // Only applicable after authentication.
'given_name' => '', // Only applicable after authentication.
'family_name' => '', // Only applicable after authentication.
'middle_name' => '', // Only applicable after authentication.
'picture' => '', // Only applicable after authentication.
'email' => '', // Only applicable after authentication.
'locale' => '', // Only applicable after authentication.
"$prefix/claim/deployment_id" => '',
"$prefix/claim/message_type" => 'LtiResourceLinkRequest',
"$prefix/claim/version" => '',
"$prefix/claim/roles" => [],
"$prefix/claim/role_scope_mentor" => [],
"$prefix/claim/context" => new stdClass(),
"$prefix/claim/resource_link" => new stdClass(),
"$prefix/claim/tool_platform" => new stdClass(),
"$prefix/claim/target_link_uri" => '',
"$prefix/claim/launch_presentation" => new stdClass(),
"$prefix/claim/lis" => new stdClass(),
];

// TODO Add custom claims, including substitution params for the given tool and given resource link (tool takes priority if named the same).

} else {
// Other message types
}
return $claims;
}

/**
* Return the launch data required for opening the external tool.
*
Expand Down
6 changes: 6 additions & 0 deletions ltix/classes/local/lticore/exception/lti_exception.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php

namespace core_ltix\local\lticore\exception;

class lti_exception extends \moodle_exception {
}
104 changes: 104 additions & 0 deletions ltix/classes/local/lticore/lti_launch_request_builder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

namespace core_ltix\local\lticore;

use core_ltix\helper;
use core_ltix\local\lticore\message\lti_message;
use core_ltix\local\lticore\message\lti_message_base;
use core_ltix\local\lticore\token\lti_token;
use core_ltix\local\ltiopenid\jwks_helper;

/**
* Class encapsulating the creation of the launch request.
*
* @package core_ltix
* @copyright 2024 Jake Dallimore <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class lti_launch_request_builder {

/**
* Build a launch request, the first request to send to the tool's initiate login endpoint.
*
* Note: For the value of $tool MUST pass in the result of \core_ltix\helper::get_type_type_config
*
* @param \stdClass $toolconfig
* @param string $messagetype
* @param string $issuer
* @param string $targetlinkuri
* @param string $loginhint
* @param array $roles
* @param array $extraclaims
* @return lti_message_base
*/
public function build_launch_request(
\stdClass $toolconfig,
string $messagetype,
string $issuer,
string $targetlinkuri,
string $loginhint,
array $roles = [],
array $extraclaims = []
): lti_message_base {

// TODO Ideally, another object should build the token.
// A rough breakdown of the types of data included in a token and where that data comes from:
// 1. message types, as defined in the various spec docs, can require claims that apply to that message type only
// 2. services can include claims based on message types (there is existing code for this)
// - service::get_launch_parameters()
// - A service may wish to include a claim for ANY message to the tool, and that's valid. e.g. AGS claim.
// 3. custom claims may be supported, depending on message type. Substitution needs to take place here too.
// - build_custom_parameters() etc.
// - services are also allowed to take part in substitution.
// - some message types require custom claims from other message types. E.g. subreview requires resourcelink custom params.
// Of the above, none make sense to implement here.
// 1 is claims that only apply to one specific message type.
// 2 is claims that may apply to certain message types (several, all, only 1), but may require message-type-specific data,
// not available here (e.g. details of a resource link might be required to add the claim to a particular message type).
// In this case, we'd have those details when building an LtiResourceLinkRequest message, but not when deep linking prior.
// 3 is claims that may or may not be present for the given message type, so needs to be handled for the types that support
// it.
// 1, 2 and 3 should be handled in the subclasses.

// Create the partially complete launch token. This will be finalised with user claims during auth.
$claims = array_merge(
helper::get_lti_message_standard_claims($toolconfig, $messagetype, $issuer),
$extraclaims,
);
$ltitoken = new lti_token($claims);

// Roles could differ depending on the placement, so must be left to the individual message type/passed in.
// TODO: autoload the consts via class consts....
global $CFG;
require_once($CFG->dirroot . '/ltix/constants.php');
$ltitoken->add_claim(LTI_JWT_CLAIM_PREFIX.'/claim/roles', $roles);

// Note: Single deployment model means the $tool->id IS the deployment id.
$params = [
'iss' => $issuer,
'target_link_uri' => $targetlinkuri,
'login_hint' => $loginhint,
'lti_message_hint' => $ltitoken->to_jwt(
privatekey: jwks_helper::get_private_key()['key'],
kid: jwks_helper::get_private_key()['kid']
),
'client_id' => $toolconfig->lti_clientid,
'lti_deployment_id' => $toolconfig->id,
];
return new lti_message($toolconfig->lti_initiatelogin, $params);
}
}
29 changes: 29 additions & 0 deletions ltix/classes/local/lticore/lti_launch_service_claims_builder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

namespace core_ltix\local\lticore;

/**
* Describes types responsible for loading service-specific launch request data.
*
* @package core_ltix
* @copyright 2024 Jake Dallimore <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class lti_launch_service_claims_builder {
abstract public function get_target_link_uri(): string;
abstract public function get_launch_parameters(): array;
}
Loading

0 comments on commit c09257d

Please sign in to comment.