Uploaded image for project: 'Moodle'
  1. Moodle
  2. MDL-69251

LTI enrol method wrongly unenrols users due to bad task internal state

    XMLWordPrintable

Details

    • MOODLE_311_STABLE, MOODLE_38_STABLE, MOODLE_39_STABLE, MOODLE_400_STABLE
    • MOODLE_311_STABLE, MOODLE_400_STABLE
    • MDL-69251-311
    • MDL-69251-400
    • MDL-69251-master
    • Hide
      1. Set up two Moodle sites - one called provider and one called consumer.
      2. Login to the provide site as an admin user
      3. Enable LTI Enrolment and LTI authentication plugins via site admin
      4. Create a course and create an assignment in the course
      5. Publish the assignment via "Published as LTI tools" in the course menu. Important: for master and 4.0: Publish this using the "Legacy LTI (1.1/2.0)" tab.
      6. Keep this tab open for future reference: We'll copy the Launch URL and Secret from here in a little while.
      7. In a new tab, log in to the consumer site as an admin
      8. Go to Site admin > Plugins > Activities > External tool > Manage tools
      9. Click "Configure a tool manually" then make the following form changes:
        • Enter "1.1 Tool - Consumer 1" in "Tool name"
        • Copy the Launch URL from the provider site and paste it in the "Tool URL" field.
        • Copy the Secret from the provider and paste it in the "Shared secret" field
        • Enter "CONSUMER_1" in the "Consumer key" field
        • Expand the "Services" fieldset and set the "IMS LTI Names and Role Provisioning" service to "Use this service to..."
        • Expand the "Privacy" and enable the launcher's name so the name will displayed in provider's course participants
      10. Save the pre-configured tool
      11. Click "Configure a tool manually" again and then make the following form changes:
        • Enter "1.1 Tool - Consumer 2" in "Tool name"
        • Copy the Launch URL from the provider site and paste it in the "Tool URL" field.
        • Copy the Secret from the provider and paste it in the "Shared secret" field
        • Enter "CONSUMER_2" in the "Consumer key" field
        • Expand the "Services" fieldset and set the "IMS LTI Names and Role Provisioning" service to "Do not use this service"
        • Expand the "Privacy" and enable the launcher's name so the name will displayed in provider's course participants
      12. Save the pre-configured tool
      13. In the consumer, create 2 courses as follows:
        • Course 1
        • Course 2
      14. Enrol s1 as as student in Course 1
      15. Unenrol admin from Course 1
      16. Enrol s2 as as student in Course 2
      17. Unenrol admin from Course 2
      18. In Course 1, add an external tool instance and select the "1.1 tool - Consumer 1" tool from the pre-configured tools list
      19. In Course 2, add an external tool instance and select the "1.1 tool - Consumer 2" tool from the pre-configured tools list
      20. Log out
      21. Log in to the consumer site as s1 and launch the tool in Course 1
      22. Log out
      23. Log in to the consumer site as s2 and launch the tool in Course 2
      24. Now back on the provider site tab, go to the course participants page.
      25. Verify you 2 enrolled users in the provider course now: s1 and s2 (on master and 4.0 admin may also be enrolled as a teacher, but ignore that here)
      26. Now, run the sync_members task in the provider site's web root:

        php admin/cli/scheduled_task.php --execute="enrol_lti\task\sync_members"

      27. Now, on the provider site's tab, refresh the participants page
      28. Verify: BOTH users s1 and s2 are still present
      Show
      Set up two Moodle sites - one called provider and one called consumer. Login to the provide site as an admin user Enable LTI Enrolment and LTI authentication plugins via site admin Create a course and create an assignment in the course Publish the assignment via "Published as LTI tools" in the course menu. Important : for master and 4.0: Publish this using the "Legacy LTI (1.1/2.0)" tab. Keep this tab open for future reference: We'll copy the Launch URL and Secret from here in a little while. In a new tab, log in to the consumer site as an admin Go to Site admin > Plugins > Activities > External tool > Manage tools Click "Configure a tool manually" then make the following form changes: Enter "1.1 Tool - Consumer 1" in "Tool name" Copy the Launch URL from the provider site and paste it in the "Tool URL" field. Copy the Secret from the provider and paste it in the "Shared secret" field Enter "CONSUMER_1" in the "Consumer key" field Expand the "Services" fieldset and set the "IMS LTI Names and Role Provisioning" service to "Use this service to..." Expand the "Privacy" and enable the launcher's name so the name will displayed in provider's course participants Save the pre-configured tool Click "Configure a tool manually" again and then make the following form changes: Enter "1.1 Tool - Consumer 2" in "Tool name" Copy the Launch URL from the provider site and paste it in the "Tool URL" field. Copy the Secret from the provider and paste it in the "Shared secret" field Enter "CONSUMER_2" in the "Consumer key" field Expand the "Services" fieldset and set the "IMS LTI Names and Role Provisioning" service to "Do not use this service" Expand the "Privacy" and enable the launcher's name so the name will displayed in provider's course participants Save the pre-configured tool In the consumer, create 2 courses as follows: Course 1 Course 2 Enrol s1 as as student in Course 1 Unenrol admin from Course 1 Enrol s2 as as student in Course 2 Unenrol admin from Course 2 In Course 1, add an external tool instance and select the "1.1 tool - Consumer 1" tool from the pre-configured tools list In Course 2, add an external tool instance and select the "1.1 tool - Consumer 2" tool from the pre-configured tools list Log out Log in to the consumer site as s1 and launch the tool in Course 1 Log out Log in to the consumer site as s2 and launch the tool in Course 2 Now back on the provider site tab, go to the course participants page. Verify you 2 enrolled users in the provider course now: s1 and s2 (on master and 4.0 admin may also be enrolled as a teacher, but ignore that here) Now, run the sync_members task in the provider site's web root: php admin/cli/scheduled_task.php --execute="enrol_lti\task\sync_members" Now, on the provider site's tab, refresh the participants page Verify: BOTH users s1 and s2 are still present
    • 1
    • Team Hedgehog 4.1 sprint 0.1

    Description

      Short explanation

      The enrol_lti\task\sync_members task currently relies on an internal array (namely currentusers) to keep record of users that were retrieved from the LTI membership service.

      It is used as sanity check before both sync_member_information (enrol) and sync_unenrol (unenrol) methods.

      In short, the problem happens because the currentusers class variable never gets reset after each LTI Consumer iteration. What then happens is that the sanity check in the sync_unenrol fails for all the subsequente iterations, and it will unenrol users from a LTI Consumer even when that consumer doesn't provide a membership service.

      Complete explanation

      We'be been experiencing an issue with the wrong unenrolment of users when you have the following setup:

       

      Two LTI consumers for the same LTI Tool:

      • Provider - any Moodle platform configured to publish a course as a LTI Tool
      • Consumer A - registered with Provider with a membership URL
      • Consumer B - registered with Provider without a membership URL

       

      On the Provider website, we need to have the following processing order on the LTI enrol_lti\task\sync_members task:

      1. Consumer A (with membership url)
      2. Consumer B (without the membership url)

       

      The following scenarios WORKS:

      • Consumer A
        • An "User A" coming from the consumer platform launches the LTI Tool and is enrolled into the Provider
      • Consumer B
        • An "User B" coming from the consumer platform launches the LTI Tool and is enrolled into the Provider
      • Provider (running the sync_members task)
        • Start processing Consumer A
          • Checks membership URL - returns 0 members
          • Skip new enrolments (since it returned 0 members)
          • Skip unenrolments (since task's internal currentusers variable is empty)
        • Start processing Consumer B
          • Checks membership URL - fails because it doesn't have one
          • Skip new enrolments (since it failed at fetching members)
          • Skip unenrolments (since task's internal currentusers variable is empty)

       

      The following scenarios BREAKS:

      • Consumer A
        • An "User A" coming from the consumer platform launches the LTI Tool and is enrolled into the Provider
      • Consumer B
        • An "User B" coming from the consumer platform launches the LTI Tool and is enrolled into the Provider
      • Provider (running the sync_members task)
        • Start processing Consumer A
          • Checks membership URL - returns 1 member (that "User A")
          • Updates "User A" enrolment with its membership information and adds him into the currentusers internal task class variable.
          • Processes unenrolments but unenrols no one, since "User A" is in the currentusers array.
        • Start processing Consumer B
          • Checks membership URL - fails because it doesn't have one
          • Skip new enrolments (since it failed at fetching members)
          • Wrongly processes unenrolments, since currentusers is currently not empty (as it was filled with "User A" by the previous Consumer A processing). Since "User B" is not in the currentusers array, the task unenrols him.

       

      The interesting bits of code are all in the enrol/lti/classes/task/sync_members.php file:

       

      line 54 (task class definition):

      protected $currentusers = [];

       

      lines 240 to 270 (enrol/update routine):

      $dbuser = core_user::get_user_by_username($user->username, 'id');
      if ($dbuser) {
          // If email is empty remove it, so we don't update the user with an empty email.
          if (empty($user->email)) {
              unset($user->email);
          }
       
          $user->id = $dbuser->id;
          user_update_user($user);
       
          // Add the information to the necessary arrays.
          if (!in_array($user->id, $this->currentusers)) {
              $this->currentusers[] = $user->id;
          }
          $this->userphotos[$user->id] = $member->image;
      } else {
          if ($this->should_sync_enrol($tool->membersyncmode)) {
              // If the email was stripped/not set then fill it with a default one. This
              // stops the user from being redirected to edit their profile page.
              if (empty($user->email)) {
                  $user->email = $user->username .  "@example.com";
              }
       
              $user->auth = 'lti';
              $user->id = user_create_user($user);
       
              // Add the information to the necessary arrays.
              $this->currentusers[] = $user->id;
              $this->userphotos[$user->id] = $member->image;
          }
      }

       

      line 311 to 330 (un-enrol routine):

      if (empty($this->currentusers)) {
          return 0;
      }
       
      $unenrolcount = 0;
       
      $ltiusersrs = $DB->get_recordset('enrol_lti_users', array('toolid' => $tool->id), 'lastaccess DESC', 'userid');
      // Go through the users and check if any were never listed, if so, remove them.
      foreach ($ltiusersrs as $ltiuser) {
          if (!in_array($ltiuser->userid, $this->currentusers)) {
              $instance = new stdClass();
              $instance->id = $tool->enrolid;
              $instance->courseid = $tool->courseid;
              $instance->enrol = 'lti';
              $ltiplugin->unenrol_user($instance, $ltiuser->userid);
              // Increment unenrol count.
              $unenrolcount++;
          }
      }
      $ltiusersrs->close();

       

      Here is a very dirty but quick fix for this issue, for testing purposes:

       

      line 88 to 90:

      foreach ($tools as $tool) {
      +   $this->currentusers = [];
          mtrace("Starting - Member sync for published tool '$tool->id' for course '$tool->courseid'.");
      

      For posterity, the original patch attached to the issue (thanks to Denis):
      Repo:
      https://github.com/denisjuergen/moodle

      Branch:
      MDL-69251-MOODLE_39_STABLE

      Diff:
      https://github.com/moodle/moodle/compare/MOODLE_39_STABLE...denisjuergen:MDL-69251-MOODLE_39_STABLE

      Attachments

        1. enrol_lti-sync_members.patch
          8 kB
          Denis Brat
        2. MDL-69251_master_1.png
          104 kB
          Angelia Dela Cruz
        3. MDL-69251_master_2.png
          183 kB
          Angelia Dela Cruz
        4. MDL-69251_v311_1.png
          96 kB
          Angelia Dela Cruz
        5. MDL-69251_v311_2.png
          186 kB
          Angelia Dela Cruz
        6. MDL-69251_v400_1.png
          104 kB
          Angelia Dela Cruz
        7. MDL-69251_v400_2.png
          184 kB
          Angelia Dela Cruz

        Activity

          People

            jaked Jake Dallimore
            denisbrat Denis Brat
            Stevani Andolo Stevani Andolo
            Jun Pataleta Jun Pataleta
            Angelia Dela Cruz Angelia Dela Cruz
            Adrian Greeve, Jake Dallimore, Mathew May, Mihail Geshoski
            Votes:
            4 Vote for this issue
            Watchers:
            13 Start watching this issue

            Dates

              Created:
              Updated:
              Resolved:
              12/Sep/22

              Time Tracking

                Estimated:
                Original Estimate - 0 minutes
                0m
                Remaining:
                Remaining Estimate - 0 minutes
                0m
                Logged:
                Time Spent - 1 day, 2 hours, 35 minutes
                1d 2h 35m