MDL Shield

Supervisors and teams

tool_murelation

Print Report
Plugin Information

Supervisors and teams plugin (tool_murelation) for Moodle LMS. Provides a framework system for managing supervisor-subordinate relationships and team structures. Supports two UI modes: Supervisors mode (1:1 relationships) and Teams mode (team-based groupings with optional team cohorts). Features framework management, team management, member management, role assignments, cohort synchronization, multi-tenancy support, user profile integration, Privacy API, and Report Builder integration.

Version:2026032950
Release:v5.0.6.06
Reviewed for:5.1
Privacy API
Unit Tests
Behat Tests
Reviewed:2026-04-15
102 files·19,325 lines
Grade Justification

The plugin demonstrates high code quality and strong security practices throughout. All pages properly enforce require_login() and require_capability() checks. All state-changing operations are guarded by moodleform sesskey validation (via the tool_mulib\local\ajax_form base class). Database queries consistently use Moodle's $DB API with parameterized placeholders and no SQL injection vectors. Output is correctly escaped using format_string(), s(), format_text(), and Moodle's html_writer. External web service endpoints validate context and check capabilities before returning data.

The Privacy API implementation is comprehensive, covering metadata declaration, context listing, user listing, data export, and data deletion for both individual users and bulk operations.

Only three low-severity code quality findings were identified: an incorrect parameter order in a role_get_name() call, a mismatched form field name in validation code, and a captype mismatch in a capability definition. None of these represent security vulnerabilities or affect the plugin's security posture.

AI Summary

Overview

tool_murelation is a well-engineered Moodle admin tool plugin that manages supervisor-subordinate relationships and team structures through configurable "frameworks." It supports two modes:

  • Supervisors mode: 1:1 supervisor-subordinate pairings with optional role assignments in user contexts
  • Teams mode: Named teams with a supervisor, multiple members, optional team cohorts, and configurable member limits

Security Analysis

The plugin follows Moodle security best practices consistently:

  • Authentication: Every page calls require_login() before any data access
  • Authorization: Capability checks (require_capability() or has_capability()) gate all operations, using four custom capabilities at system, tenant, and user context levels
  • CSRF protection: All state-changing forms extend tool_mulib\local\ajax_form (which extends moodleform), providing automatic sesskey validation. AJAX pages define AJAX_SCRIPT before including config.php
  • SQL injection: All queries use Moodle's $DB API with parameterized placeholders. The tool_mulib\local\sql helper class is used for complex dynamic query construction with safe parameter binding
  • XSS prevention: Output consistently uses format_string(), s(), format_text(), and html_writer for safe rendering
  • Input validation: All parameters use required_param() / optional_param() with appropriate PARAM_* types. Form fields declare proper types. Web service endpoints validate all parameters

Architecture

The plugin is cleanly separated into:

  • Management pages (management/) for framework and team CRUD operations
  • Profile pages (profile/) for viewing user subordinates and supervised teams
  • Form classes (classes/local/form/) for moodleform definitions
  • External services (classes/external/) for AJAX autocomplete endpoints
  • Business logic (classes/local/) with framework, supervisor, subordinate, uimode_supervisors, uimode_teams helpers
  • Report Builder entities and system reports for data listing
  • Privacy API provider with full GDPR compliance

The codebase is mature, has comprehensive test coverage (PHPUnit and Behat), and handles edge cases like user deletion, tenant changes, and orphaned data via scheduled cron cleanup.

Findings

code qualityLow
Incorrect role_get_name() parameter order in framework entity

The role_get_name() function is called with ROLENAME_ORIGINAL as the second argument, but the function signature is role_get_name(stdClass $role, $context = null, $rolenamedisplay = ROLENAME_ALIAS). This means ROLENAME_ORIGINAL (value 0) is passed as the $context parameter, not the intended $rolenamedisplay parameter.

The third parameter $rolenamedisplay defaults to ROLENAME_ALIAS, so the function returns the alias name instead of the original name as intended.

Compare with the correct call in classes/output/management/renderer.php:117 which correctly passes three arguments: role_get_name($role, null, ROLENAME_ORIGINAL).

Risk Assessment

Low risk. This is a code quality bug with no security implications. The visible impact is cosmetic — in some configurations the role name may be displayed differently than intended. No data is exposed, no access control is bypassed.

Context

This code is in the Report Builder framework entity's column callback for the supervisor role column. It attempts to display the original (untranslated) role name for roles assigned to supervisors. Since ROLENAME_ORIGINAL = 0, PHP treats it as falsy and the function effectively uses null for context, then defaults the display type to ROLENAME_ALIAS. In practice, outside a course context, the alias lookup returns null and the function falls back to the localised role name, so the visible behavior may coincidentally be acceptable.

Identified Code
return role_get_name($role, ROLENAME_ORIGINAL);
Suggested Fix

Pass null as the context (second argument) and ROLENAME_ORIGINAL as the third argument:

return role_get_name($role, null, ROLENAME_ORIGINAL);
code qualityLow
Capability captype is 'write' for a read-only capability

The tool/murelation:viewpositions capability is described as a view capability ("View all supervisors and subordinates"), but its captype is set to 'write' instead of 'read'.

In Moodle, captype is metadata used for categorization in the capability overview UI. Setting a view capability as 'write' causes it to appear in the wrong category when administrators review capabilities, and may confuse the risk assessment model.

Risk Assessment

Low risk. The captype field does not affect capability checking behavior — has_capability() and require_capability() work identically regardless of this value. The impact is limited to the admin capability overview showing this read capability in the wrong category. No security implications.

Context

Moodle's capability system uses captype ('read' or 'write') for categorization in the admin capability overview. The viewpositions capability is used throughout the plugin to guard read-only operations like viewing team pages, profile subordinate lists, and report listings. The companion managepositions capability (correctly typed as 'write') is used for state-changing operations.

Identified Code
'tool/murelation:viewpositions' => [
    'captype' => 'write',
    'riskbitmask' => RISK_PERSONAL,
    'contextlevel' => CONTEXT_USER,
    'archetypes' => [
        'manager' => CAP_ALLOW,
    ],
],
Suggested Fix

Change captype to 'read' since this capability only grants view access:

'tool/murelation:viewpositions' => [
    'captype' => 'read',
    'riskbitmask' => RISK_PERSONAL,
    'contextlevel' => CONTEXT_USER,
    'archetypes' => [
        'manager' => CAP_ALLOW,
    ],
],
code qualityLow
Form validation references wrong field name in subordinates_create_select

The subordinates_create_select form defines a field named supuserid but the validation() method references $data['subuserid'] (with sub prefix instead of sup). This means:

  1. $data['subuserid'] is undefined in the form data, evaluating to null
  2. The else branch always triggers, adding an error to the non-existent subuserid element
  3. The actual supuserid field value is never validated server-side by this form

The parent page (management/subordinates_create.php lines 74-82) separately validates the supuserid value before use, so this does not create a security vulnerability.

Risk Assessment

Low risk. The validation bug means the form's server-side validation of the supervisor selection is effectively bypassed. However, the parent page independently validates the value, and the autocomplete web service also validates candidates. There is no realistic exploit path — an attacker would need tool/murelation:managepositions capability at system/tenant level, and even then the page-level validation would catch invalid values.

Context

This form is used as the first step in the bulk subordinate creation flow. It allows a manager to select a supervisor user before adding subordinates. The form is rendered via management/subordinates_create.php, which independently validates the supuserid parameter (lines 74-82) via subordinates_create_select_supuserid::validate_value() and also validates the user exists as a confirmed, non-deleted user (line 101). These redundant checks in the page script prevent the form validation bug from being exploitable.

Identified Code
if ($data['subuserid']) {
    $error = subordinates_create_select_supuserid::validate_value($data['subuserid'], $this->wsarguments, $context);
    if ($error !== null) {
        $errors['subuserid'] = $error;
    }
} else {
    $errors['subuserid'] = get_string('required');
}
Suggested Fix

Replace all occurrences of subuserid with supuserid to match the form element name:

if ($data['supuserid']) {
    $error = subordinates_create_select_supuserid::validate_value($data['supuserid'], $this->wsarguments, $context);
    if ($error !== null) {
        $errors['supuserid'] = $error;
    }
} else {
    $errors['supuserid'] = get_string('required');
}
Additional AI Notes

The plugin has a clean dependency on tool_mulib (version 2026032950+), which provides shared utilities including AJAX form handling (ajax_form base class), SQL query builder (sql class), user/cohort autocomplete base classes, dropdown UI components, and multi-tenancy helpers. The tool_mulib plugin is not bundled — it is declared as a dependency in version.php.

The plugin includes comprehensive test coverage with both PHPUnit tests (covering generators, external services, local classes, privacy provider, and cron tasks) and Behat acceptance tests (covering framework CRUD, supervisor management, and team management workflows).

All web service endpoints are declared as 'ajax' => true with 'loginrequired' => true, and each endpoint independently validates context and capabilities before returning data. This follows the Moodle external API pattern correctly.

The cron task performs important data integrity maintenance: cleaning up deleted user references, synchronizing role assignments, synchronizing team cohort memberships, and resolving tenant allocation mismatches. This defensive cleanup approach means the plugin gracefully handles missed events.

This review was generated by an AI system and may contain inaccuracies. Findings should be verified by a human reviewer before acting on them.