Merge branch '83399-admin-loginas' of https://github.com/jaypha/moodle
This commit is contained in:
+6
-36
@@ -36,47 +36,17 @@ require_sesskey();
|
||||
$course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST);
|
||||
|
||||
// User must be logged in.
|
||||
|
||||
$systemcontext = context_system::instance();
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
|
||||
require_login();
|
||||
|
||||
if (has_capability('moodle/user:loginas', $systemcontext)) {
|
||||
if (is_siteadmin($userid)) {
|
||||
throw new \moodle_exception('nologinas');
|
||||
}
|
||||
$context = $systemcontext;
|
||||
$PAGE->set_context($context);
|
||||
} else {
|
||||
require_login($course);
|
||||
require_capability('moodle/user:loginas', $coursecontext);
|
||||
if (is_siteadmin($userid)) {
|
||||
throw new \moodle_exception('nologinas');
|
||||
}
|
||||
if (!is_enrolled($coursecontext, $userid)) {
|
||||
throw new \moodle_exception('usernotincourse');
|
||||
}
|
||||
$context = $coursecontext;
|
||||
$user = $DB->get_record('user', ['id' => $userid]);
|
||||
|
||||
// Check if course has SEPARATEGROUPS and user is part of that group.
|
||||
if (groups_get_course_groupmode($course) == SEPARATEGROUPS &&
|
||||
!has_capability('moodle/site:accessallgroups', $context)) {
|
||||
$samegroup = false;
|
||||
if ($groups = groups_get_all_groups($course->id, $USER->id)) {
|
||||
foreach ($groups as $group) {
|
||||
if (groups_is_member($group->id, $userid)) {
|
||||
$samegroup = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$samegroup) {
|
||||
throw new \moodle_exception('nologinas');
|
||||
}
|
||||
}
|
||||
$context = \core\session\loginas_helper::get_context_user_can_login_as($USER, $user, $course);
|
||||
if (empty($context)) {
|
||||
throw new moodle_exception('nologinas');
|
||||
}
|
||||
|
||||
$PAGE->set_context($context);
|
||||
|
||||
// Login as this user and return to course home page.
|
||||
\core\session\manager::loginas($userid, $context);
|
||||
// Add a notification to let the logged in as user know that all content will be force cleaned
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<?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\session;
|
||||
|
||||
use context;
|
||||
use stdClass;
|
||||
use context_course;
|
||||
use context_system;
|
||||
use core\session\manager as sessionmanager;
|
||||
|
||||
/**
|
||||
* Helper functions for the 'login as' feature.
|
||||
*
|
||||
* @package core
|
||||
* @author Jason den Dulk <[email protected]>
|
||||
* @copyright 2025 Catalyst IT
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class loginas_helper {
|
||||
|
||||
/**
|
||||
* Determine which context a user can login as another user for, given the requested target user and course. If the
|
||||
* person cannot login at all, then null will be returned.
|
||||
*
|
||||
* @param stdClass $currentuser The current user. Defaults to $USER.
|
||||
* @param stdClass $loginasuser The user to be logged in as.
|
||||
* @param stdClass|null $course The course currently being looked at. Applies to those who can only login within a
|
||||
* course context. If null, then only system context will be considered.
|
||||
* @return context|null Returns the context that the user is able to login as, or null if no context can be found.
|
||||
*/
|
||||
public static function get_context_user_can_login_as(
|
||||
stdClass $currentuser,
|
||||
stdClass $loginasuser,
|
||||
?stdClass $course = null
|
||||
): ?context {
|
||||
|
||||
$systemcontext = context_system::instance();
|
||||
|
||||
if (
|
||||
$loginasuser->deleted || // Can't login as a user that has been removed.
|
||||
$currentuser->id == $loginasuser->id || // Pointless for a user to login as himself.
|
||||
sessionmanager::is_loggedinas() // Already logged in as someone.
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Site admins can always login as someone else.
|
||||
if (is_siteadmin($currentuser)) {
|
||||
return $systemcontext;
|
||||
}
|
||||
|
||||
// Non admins can never login as an admin.
|
||||
if (is_siteadmin($loginasuser)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Now, we accept anyone who can loginas at the site level, and they can do so at the system level.
|
||||
if (has_capability('moodle/user:loginas', $systemcontext, $currentuser)) {
|
||||
return $systemcontext;
|
||||
}
|
||||
|
||||
if (!empty($course)) {
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
|
||||
if (
|
||||
// Reject all that do not have loginas capability.
|
||||
!has_capability('moodle/user:loginas', $coursecontext, $currentuser) ||
|
||||
// Reject if user is trying to login as someone with site level loginas powers.
|
||||
has_capability('moodle/user:loginas', $systemcontext, $loginasuser) ||
|
||||
// Reject if other is not enrolled.
|
||||
!is_enrolled($coursecontext, $loginasuser->id)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if the users are in the same group.
|
||||
if (groups_get_course_groupmode($course) == SEPARATEGROUPS &&
|
||||
!has_capability('moodle/site:accessallgroups', $coursecontext, $currentuser)) {
|
||||
$samegroup = false;
|
||||
if ($groups = groups_get_all_groups($course->id, $currentuser->id)) {
|
||||
foreach ($groups as $group) {
|
||||
if (groups_is_member($group->id, $loginasuser->id)) {
|
||||
$samegroup = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$samegroup) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Passed all checks.
|
||||
return $coursecontext;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -108,9 +108,7 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user,
|
||||
}
|
||||
|
||||
// Login as ...
|
||||
if (!$user->deleted && !$iscurrentuser &&
|
||||
!\core\session\manager::is_loggedinas() && has_capability('moodle/user:loginas',
|
||||
$courseorsystemcontext) && !is_siteadmin($user->id)) {
|
||||
if (!empty(\core\session\loginas_helper::get_context_user_can_login_as($USER, $user, $course))) {
|
||||
$url = new moodle_url('/course/loginas.php',
|
||||
array('id' => $courseid, 'user' => $user->id, 'sesskey' => sesskey()));
|
||||
$node = new core_user\output\myprofile\node('administration', 'loginas', get_string('loginas'), null, $url);
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
<?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\session;
|
||||
|
||||
use context_course;
|
||||
use context_system;
|
||||
|
||||
/**
|
||||
* Unit tests for loginas_helper class.
|
||||
*
|
||||
* @package core
|
||||
* @author Jason den Dulk <[email protected]>
|
||||
* @covers \core\session\loginas_helper
|
||||
* @copyright 2025 Catalyst IT
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
final class loginas_helper_test extends \advanced_testcase {
|
||||
|
||||
/**
|
||||
* Tests various users wanting to login as other users of the same role.
|
||||
*/
|
||||
public function test_loginas_same_role(): void {
|
||||
global $CFG, $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$managerrole = $DB->get_field('role', 'id', ['shortname' => 'manager']);
|
||||
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
|
||||
// By default, users cannot login as other users.
|
||||
$this->assertNull(loginas_helper::get_context_user_can_login_as($user1, $user2));
|
||||
$this->assertNull(loginas_helper::get_context_user_can_login_as($user2, $user1));
|
||||
|
||||
// Admins can login as other admins.
|
||||
$originalsiteadmins = $CFG->siteadmins;
|
||||
$CFG->siteadmins .= ',' . $user1->id . ',' . $user2->id;
|
||||
$systemcontext = context_system::instance();
|
||||
$this->assertEquals($systemcontext, loginas_helper::get_context_user_can_login_as($user1, $user2));
|
||||
$this->assertEquals($systemcontext, loginas_helper::get_context_user_can_login_as($user2, $user1));
|
||||
|
||||
// Managers can login as other managers.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
role_assign($managerrole, $user1->id, $systemcontext->id);
|
||||
role_assign($managerrole, $user2->id, $systemcontext->id);
|
||||
|
||||
$this->assertEquals($systemcontext, loginas_helper::get_context_user_can_login_as($user1, $user2));
|
||||
$this->assertEquals($systemcontext, loginas_helper::get_context_user_can_login_as($user2, $user1));
|
||||
|
||||
// Course managers can login as other course managers, but only in the course context.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
$this->getDataGenerator()->enrol_user($user1->id, $course->id, 'manager');
|
||||
$this->getDataGenerator()->enrol_user($user2->id, $course->id, 'manager');
|
||||
|
||||
$this->assertNull(loginas_helper::get_context_user_can_login_as($user1, $user2));
|
||||
$this->assertNull(loginas_helper::get_context_user_can_login_as($user2, $user1));
|
||||
|
||||
$this->assertEquals($coursecontext, loginas_helper::get_context_user_can_login_as($user1, $user2, $course));
|
||||
$this->assertEquals($coursecontext, loginas_helper::get_context_user_can_login_as($user2, $user1, $course));
|
||||
|
||||
// Students cannot login as another student.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student');
|
||||
$this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student');
|
||||
|
||||
$this->assertNull(loginas_helper::get_context_user_can_login_as($user1, $user2));
|
||||
$this->assertNull(loginas_helper::get_context_user_can_login_as($user2, $user1));
|
||||
|
||||
$this->assertNull(loginas_helper::get_context_user_can_login_as($user1, $user2, $course));
|
||||
$this->assertNull(loginas_helper::get_context_user_can_login_as($user2, $user1, $course));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests trying to login as a deleted user.
|
||||
*/
|
||||
public function test_loginas_deleted_user(): void {
|
||||
global $USER;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Sanity check that ordinary login as works.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$this->assertEquals(context_system::instance(), loginas_helper::get_context_user_can_login_as($USER, $user));
|
||||
|
||||
// Cannot login as a user that has been deleted.
|
||||
$user = $this->getDataGenerator()->create_user(['deleted' => true]);
|
||||
$this->assertNull(loginas_helper::get_context_user_can_login_as($USER, $user));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests various users wanting to login as other users of differing roles.
|
||||
*/
|
||||
public function test_loginas_different_roles(): void {
|
||||
global $CFG, $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$systemcontext = context_system::instance();
|
||||
$managerrole = $DB->get_field('role', 'id', ['shortname' => 'manager']);
|
||||
$originalsiteadmins = $CFG->siteadmins;
|
||||
|
||||
$users = [];
|
||||
for ($i = 0; $i < 11; ++$i) {
|
||||
$users[] = $this->getDataGenerator()->create_user();
|
||||
}
|
||||
|
||||
$courses = [
|
||||
$this->getDataGenerator()->create_course(),
|
||||
$this->getDataGenerator()->create_course(),
|
||||
];
|
||||
$coursecontexts = [
|
||||
context_course::instance($courses[0]->id),
|
||||
context_course::instance($courses[1]->id),
|
||||
];
|
||||
|
||||
// User 0 is an admin.
|
||||
$CFG->siteadmins .= ',' . $users[0]->id;
|
||||
|
||||
// User 1 is a manager.
|
||||
role_assign($managerrole, $users[1]->id, $systemcontext->id);
|
||||
|
||||
// User 2 is a manager and an admin.
|
||||
$CFG->siteadmins .= ',' . $users[2]->id;
|
||||
role_assign($managerrole, $users[2]->id, $systemcontext->id);
|
||||
|
||||
// User 3 is a course manager for course 0.
|
||||
$this->getDataGenerator()->enrol_user($users[3]->id, $courses[0]->id, 'manager');
|
||||
|
||||
// User 4 is a course manager for course 0 and a site manager.
|
||||
$this->getDataGenerator()->enrol_user($users[4]->id, $courses[0]->id, 'manager');
|
||||
role_assign($managerrole, $users[4]->id, $systemcontext->id);
|
||||
|
||||
// User 5 is a course manager for course 0 and an admin.
|
||||
$this->getDataGenerator()->enrol_user($users[5]->id, $courses[0]->id, 'manager');
|
||||
$CFG->siteadmins .= ',' . $users[5]->id;
|
||||
|
||||
// User 6 is a student for course 0.
|
||||
$this->getDataGenerator()->enrol_user($users[6]->id, $courses[0]->id, 'student');
|
||||
|
||||
// User 7 is a student for course 0 and an admin.
|
||||
$this->getDataGenerator()->enrol_user($users[7]->id, $courses[0]->id, 'student');
|
||||
$CFG->siteadmins .= ',' . $users[7]->id;
|
||||
|
||||
// User 8 is a course manager for course 1.
|
||||
$this->getDataGenerator()->enrol_user($users[8]->id, $courses[1]->id, 'manager');
|
||||
|
||||
// User 9 is a student for course 1.
|
||||
$this->getDataGenerator()->enrol_user($users[9]->id, $courses[1]->id, 'student');
|
||||
|
||||
// User 10 is a user without courses or roles.
|
||||
|
||||
// This matrix defines loginas expectations. 'S' = system context. # = course context. 'X' = nothing.
|
||||
$matrix = [
|
||||
// ... 0 1 2 3 4 5 6 7 8 9 10.
|
||||
0 => ['X', 'S', 'S', 'S', 'S', 'S', 'S', 'S', 'S', 'S', 'S'],
|
||||
1 => ['X', 'X', 'X', 'S', 'S', 'X', 'S', 'X', 'S', 'S', 'S'],
|
||||
2 => ['S', 'S', 'X', 'S', 'S', 'S', 'S', 'S', 'S', 'S', 'S'],
|
||||
3 => ['X', 'X', 'X', 'X', 'X', 'X', '0', 'X', 'X', 'X', 'X'],
|
||||
4 => ['X', 'S', 'X', 'S', 'X', 'X', 'S', 'X', 'S', 'S', 'S'],
|
||||
5 => ['S', 'S', 'S', 'S', 'S', 'X', 'S', 'S', 'S', 'S', 'S'],
|
||||
6 => ['X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X'],
|
||||
7 => ['S', 'S', 'S', 'S', 'S', 'S', 'S', 'X', 'S', 'S', 'S'],
|
||||
8 => ['X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', '1', 'X'],
|
||||
9 => ['X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X'],
|
||||
10 => ['X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X'],
|
||||
];
|
||||
|
||||
// Now test each user against each other user, and compare the results to the expectation matrix.
|
||||
foreach ($users as $uid => $user) {
|
||||
foreach ($users as $oid => $other) {
|
||||
$resultsnocourse = loginas_helper::get_context_user_can_login_as($user, $other);
|
||||
$resultscourse0 = loginas_helper::get_context_user_can_login_as($user, $other, $courses[0]);
|
||||
$resultscourse1 = loginas_helper::get_context_user_can_login_as($user, $other, $courses[1]);
|
||||
|
||||
switch ($matrix[$uid][$oid]) {
|
||||
case 'X':
|
||||
// No loginas is possible.
|
||||
$this->assertNull($resultsnocourse);
|
||||
$this->assertNull($resultscourse0);
|
||||
$this->assertNull($resultscourse1);
|
||||
break;
|
||||
case 'S':
|
||||
// Loginas can happen at the site level.
|
||||
$this->assertEquals($systemcontext, $resultsnocourse);
|
||||
$this->assertEquals($systemcontext, $resultscourse0);
|
||||
$this->assertEquals($systemcontext, $resultscourse1);
|
||||
break;
|
||||
case '0':
|
||||
// Loginas can only happen within the context of course 0.
|
||||
$this->assertNull($resultsnocourse);
|
||||
$this->assertEquals($coursecontexts[0], $resultscourse0);
|
||||
$this->assertNull($resultscourse1);
|
||||
break;
|
||||
case '1':
|
||||
// Loginas can only happen within the context of course 1.
|
||||
$this->assertNull($resultsnocourse);
|
||||
$this->assertNull($resultscourse0);
|
||||
$this->assertEquals($coursecontexts[1], $resultscourse1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Providor function for test_loginas_groups().
|
||||
*
|
||||
* @return array[]
|
||||
*/
|
||||
public static function loginas_groups_providor(): array {
|
||||
return [
|
||||
'Separate groups' => [
|
||||
'groupmode' => SEPARATEGROUPS,
|
||||
'accessallgroups' => true,
|
||||
'canloginassamegroup' => true,
|
||||
'canloginasdifferentgroup' => true,
|
||||
],
|
||||
'Separate groups, no access' => [
|
||||
'groupmode' => SEPARATEGROUPS,
|
||||
'accessallgroups' => false,
|
||||
'canloginassamegroup' => true,
|
||||
'canloginasdifferentgroup' => false,
|
||||
],
|
||||
'Visible groups' => [
|
||||
'groupmode' => VISIBLEGROUPS,
|
||||
'accessallgroups' => true,
|
||||
'canloginassamegroup' => true,
|
||||
'canloginasdifferentgroup' => true,
|
||||
],
|
||||
'Visible groups, no access' => [
|
||||
'groupmode' => VISIBLEGROUPS,
|
||||
'accessallgroups' => false,
|
||||
'canloginassamegroup' => true,
|
||||
'canloginasdifferentgroup' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests users wanting to login as other users of different groups.
|
||||
*
|
||||
* @param int $groupmode
|
||||
* @param bool $accessallgroups
|
||||
* @param bool $canloginassamegroup
|
||||
* @param bool $canloginasdifferentgroup
|
||||
*
|
||||
* @dataProvider loginas_groups_providor
|
||||
*/
|
||||
public function test_loginas_groups(
|
||||
int $groupmode,
|
||||
bool $accessallgroups,
|
||||
bool $canloginassamegroup,
|
||||
bool $canloginasdifferentgroup
|
||||
): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Set up manager and students.
|
||||
$manager = $this->getDataGenerator()->create_user();
|
||||
$student1 = $this->getDataGenerator()->create_user();
|
||||
$student2 = $this->getDataGenerator()->create_user();
|
||||
|
||||
$course = $this->getDataGenerator()->create_course(['groupmode' => $groupmode]);
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
|
||||
// Add or remove accessallgroups permission.
|
||||
$managerroleid = $DB->get_field('role', 'id', ['shortname' => 'manager'], MUST_EXIST);
|
||||
$permission = $accessallgroups ? CAP_ALLOW : CAP_PREVENT;
|
||||
assign_capability('moodle/site:accessallgroups', $permission, $managerroleid, $coursecontext, true);
|
||||
|
||||
$this->getDataGenerator()->enrol_user($manager->id, $course->id, 'manager');
|
||||
$this->getDataGenerator()->enrol_user($student1->id, $course->id, 'student');
|
||||
$this->getDataGenerator()->enrol_user($student2->id, $course->id, 'student');
|
||||
|
||||
// Manager and student 1 are in the same group. Student 2 is in a different group.
|
||||
$group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
|
||||
$group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
|
||||
|
||||
$this->getDataGenerator()->create_group_member(['groupid' => $group1->id, 'userid' => $manager->id]);
|
||||
$this->getDataGenerator()->create_group_member(['groupid' => $group1->id, 'userid' => $student1->id]);
|
||||
$this->getDataGenerator()->create_group_member(['groupid' => $group2->id, 'userid' => $student2->id]);
|
||||
|
||||
// Manager wants to login as a student in the same group.
|
||||
$this->assertEquals(
|
||||
$canloginassamegroup,
|
||||
(bool) loginas_helper::get_context_user_can_login_as($manager, $student1, $course)
|
||||
);
|
||||
// Manager wants to login as a student in a different group.
|
||||
$this->assertEquals(
|
||||
$canloginasdifferentgroup,
|
||||
(bool) loginas_helper::get_context_user_can_login_as($manager, $student2, $course)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user