Skip to content

Commit

Permalink
MBS-9809: Implement restriction to course contexts
Browse files Browse the repository at this point in the history
  • Loading branch information
PhMemmel committed Dec 25, 2024
1 parent f82ae4e commit f556c14
Show file tree
Hide file tree
Showing 11 changed files with 228 additions and 16 deletions.
12 changes: 12 additions & 0 deletions classes/form/rights_config_form.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ class rights_config_form extends \moodleform {
/** @var string Constant for defining the action option "confirm" for the action {@see self::ACTION_CHANGE_CONFIRM_STATUS}. */
const ACTIONOPTION_CHANGE_CONFIRM_STATE_UNCONFIRM = 'unconfirm';

/** @var string Constant for defining the action "change usage scope". */
const ACTION_CHANGE_SCOPE = 'changescope';

/**
* Form definition.
*/
Expand All @@ -70,6 +73,7 @@ public function definition() {
self::ACTION_ASSIGN_ROLE => get_string('assignrole', 'local_ai_manager'),
self::ACTION_CHANGE_LOCK_STATE => get_string('changelockstate', 'local_ai_manager'),
self::ACTION_CHANGE_CONFIRM_STATE => get_string('changeconfirmstate', 'local_ai_manager'),
self::ACTION_CHANGE_SCOPE => get_string('changescope', 'local_ai_manager'),
]);

$actionselectsgroup[] = $mform->createElement('select', 'role', '', [
Expand All @@ -96,6 +100,14 @@ public function definition() {
);
$mform->hideif('confirmstate', 'action', 'neq', self::ACTION_CHANGE_CONFIRM_STATE);

$actionselectsgroup[] = $mform->createElement('select', 'scope', '',
[
userinfo::SCOPE_COURSES_ONLY => get_string('scope_courses', 'local_ai_manager'),
userinfo::SCOPE_EVERYWHERE => get_string('scope_everywhere', 'local_ai_manager'),
]
);
$mform->hideif('scope', 'action', 'neq', self::ACTION_CHANGE_SCOPE);

$mform->addGroup($actionselectsgroup, 'actiongroup', get_string('executebulkuseractions', 'local_ai_manager') . ':', [' '],
false);

Expand Down
32 changes: 28 additions & 4 deletions classes/local/rights_config_table.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public function __construct(
$this->set_attribute('id', $uniqid);
$this->define_baseurl($baseurl);
// Define the list of columns to show.
$columns = ['checkbox', 'lastname', 'firstname', 'role', 'locked', 'confirmed'];
$columns = ['checkbox', 'lastname', 'firstname', 'role', 'locked', 'confirmed', 'scope'];
$checkboxheader = html_writer::div('', 'rights-table-selection_info', ['id' => 'rights-table-selection_info']);
$checkboxheader .= html_writer::empty_tag('input', ['type' => 'checkbox', 'id' => 'rights-table-selectall_checkbox']);
$headers = [
Expand All @@ -67,6 +67,7 @@ public function __construct(
get_string('role', 'local_ai_manager'),
get_string('locked', 'local_ai_manager'),
get_string('confirmed', 'local_ai_manager'),
get_string('scope', 'local_ai_manager'),
];

$tenantfield = get_config('local_ai_manager', 'tenantcolumn');
Expand Down Expand Up @@ -100,7 +101,7 @@ public function __construct(
}
}

$fields = 'u.id as id, lastname, firstname, role, locked, ui.confirmed';
$fields = 'u.id as id, lastname, firstname, role, locked, ui.confirmed, ui.scope';
$from =
'{user} u LEFT JOIN {local_ai_manager_userinfo} ui ON u.id = ui.userid';
$where = 'u.deleted != 1 AND u.suspended != 1 AND ' . $tenantfield . ' = :tenant' . $rolewhere;
Expand All @@ -117,16 +118,17 @@ public function __construct(
$this->no_sorting('role');
$this->no_sorting('locked');
$this->no_sorting('confirmed');
$this->no_sorting('scope');
$this->collapsible(false);
$this->sortable(true, 'lastname');

$this->set_sql($usertableextend->get_fields(), $usertableextend->get_from(),
$usertableextend->get_where() . ' GROUP BY u.id, role, locked, ui.confirmed',
$usertableextend->get_where() . ' GROUP BY u.id, role, locked, ui.confirmed, ui.scope',
$usertableextend->get_params());
// We need to use this because we are using "GROUP BY" which is not being expected by the sql table.
$this->set_count_sql("SELECT COUNT(*) FROM (SELECT " . $usertableextend->get_fields() . " FROM "
. $usertableextend->get_from() . " WHERE " . $usertableextend->get_where() .
" GROUP BY u.id, role, locked, ui.confirmed) AS subquery",
" GROUP BY u.id, role, locked, ui.confirmed, ui.scope) AS subquery",
$usertableextend->get_params());
parent::setup();
}
Expand Down Expand Up @@ -174,6 +176,28 @@ public function col_confirmed($value) {
}
}

/**
* Get the icon representing the user scope.
*
* @param stdClass $value the object containing the information of the current row
* @return string the resulting string for the confirmed column
*/
public function col_scope($value) {
$userinfo = new userinfo($value->id);
$scope = empty($value->scope) ? $userinfo->get_default_scope() : intval($value->scope);
switch ($scope) {
case userinfo::SCOPE_EVERYWHERE:
return '<i class="fa fa-globe local_ai_manager-green" title="' .
get_string('scope_everywhere', 'local_ai_manager') . '"></i>';
case userinfo::SCOPE_COURSES_ONLY:
return '<i class="fa fa-graduation-cap local_ai_manager-red" title="' .
get_string('scope_courses', 'local_ai_manager') . '"></i>';
default:
// Should not happen.
return 'No scope';
}
}

#[\Override]
public function other_cols($column, $row) {
if ($column === 'checkbox') {
Expand Down
45 changes: 44 additions & 1 deletion classes/local/userinfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ class userinfo {
/** @var int This is not really a role, but is being used to signal that the default role for a user should be assigned. */
public const ROLE_DEFAULT = -1;

/** @var int Constant identifying that the scope for using AI tools of this user is not limited. */
public const SCOPE_EVERYWHERE = 1;

/** @var int Constant identifying that the scope for using AI tools of this user restricted to courses. */
public const SCOPE_COURSES_ONLY = 2;

/** @var false|stdClass The database record or false if there is none (yet) */
private false|stdClass $record;

Expand All @@ -53,13 +59,16 @@ class userinfo {
/** @var bool The confirmed state of the user */
private bool $confirmed;

/** @var int The scope of the user, one of {@see self::SCOPE_EVERYWHERE} or {@see self::SCOPE_COURSES_ONLY}. */
private int $scope;

/**
* Create a userinfo object.
*
* @param int $userid The userid to create the userinfo object for
*/
public function __construct(
/** @var int $userid The userid to create the userinfo object for */
/** @var int $userid The userid to create the userinfo object for */
private readonly int $userid
) {
$this->load();
Expand All @@ -74,6 +83,7 @@ public function load(): void {
$this->role = !empty($this->record->role) ? $this->record->role : $this->get_default_role();
$this->locked = !empty($this->record->locked);
$this->confirmed = !empty($this->record->confirmed);
$this->scope = !empty($this->record->scope) ? $this->record->scope : $this->get_default_scope();
}

/**
Expand Down Expand Up @@ -132,6 +142,7 @@ public function store() {
$newrecord->role = $this->role;
$newrecord->locked = $this->locked ? 1 : 0;
$newrecord->confirmed = $this->confirmed ? 1 : 0;
$newrecord->scope = $this->scope;
$newrecord->timemodified = time();
if ($this->record) {
$newrecord->id = $this->record->id;
Expand Down Expand Up @@ -207,6 +218,38 @@ public function is_confirmed(): bool {
return $this->confirmed;
}

/**
* Sets the scope of the user.
*
* @param int $scope The scope the user should have, has to be one of {@see self::SCOPE_EVERYWHERE} or
* {@see self::SCOPE_COURSES_ONLY}
* @throws \coding_exception if a wrong scope has been passed
*/
public function set_scope(int $scope): void {
if (!in_array($scope, [self::SCOPE_EVERYWHERE, self::SCOPE_COURSES_ONLY])) {
throw new \coding_exception('Wrong scope specified, use one of SCOPE_EVERYWHERE or SCOPE_COURSES_ONLY');
}
$this->scope = $scope;
}

/**
* Returns the default scope of a user.
*
* @return int the default scope of a user that has not been assigned a scope yet
*/
public function get_default_scope() {
return $this->get_default_role() === self::ROLE_BASIC ? self::SCOPE_COURSES_ONLY : self::SCOPE_EVERYWHERE;
}

/**
* Getter for the scope.
*
* @return int one of {@see self::SCOPE_EVERYWHERE} or {@see self::SCOPE_COURSES_ONLY}
*/
public function get_scope(): int {
return $this->scope;
}

/**
* Helper function to get the tenant for a user.
*
Expand Down
24 changes: 24 additions & 0 deletions classes/manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,30 @@ public function perform_request(string $prompttext, array $options = []): prompt
return prompt_response::create_from_error(403, get_string('error_http403notconfirmed', 'local_ai_manager'), '');
}

if ($userinfo->get_scope() === userinfo::SCOPE_COURSES_ONLY) {
if (empty($options['contextid'])) {
throw new \coding_exception('The "contextid" is missing in the passed options, '
. 'but is required if the user\'s scope is set to "courses only"');
}
$contextid = $options['contextid'];
$context = context::instance_by_id($contextid);
if ($context->contextlevel < CONTEXT_COURSE) {
// In case of system context, course category context, user context we throw a permission denied error.
return prompt_response::create_from_error(403, get_string('error_http403coursesonly', 'local_ai_manager'), '');
} else if ($context->contextlevel === CONTEXT_BLOCK) {
// Block context is a bit special though...
if ($context->get_parent_context()->contextlevel !== CONTEXT_COURSE) {
return prompt_response::create_from_error(403, get_string('error_http403coursesonly', 'local_ai_manager'), '');
} else {
$courseid = $context->get_parent_context()->instanceid;
if ($courseid === intval(SITEID)) {
return prompt_response::create_from_error(403, get_string('error_http403coursesonly', 'local_ai_manager'),
'');
}
}
}
}

if (intval($this->configmanager->get_max_requests($this->purpose, $userinfo->get_role())) === 0) {
return prompt_response::create_from_error(403, get_string('error_http403usertype', 'local_ai_manager'), '');
}
Expand Down
3 changes: 2 additions & 1 deletion db/install.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="local/ai_manager/db" VERSION="20241105" COMMENT="XMLDB file for Moodle local/ai_manager"
<XMLDB PATH="local/ai_manager/db" VERSION="20241223" COMMENT="XMLDB file for Moodle local/ai_manager"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
Expand Down Expand Up @@ -76,6 +76,7 @@
<FIELD NAME="role" TYPE="int" LENGTH="2" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="locked" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="confirmed" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="scope" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Defines if the user is allowed to use AI tools outside courses"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
</FIELDS>
<KEYS>
Expand Down
24 changes: 24 additions & 0 deletions db/upgrade.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

use local_ai_manager\local\userinfo;

/**
* Define upgrade steps to be performed to upgrade the plugin from the old version to the current one.
*
Expand Down Expand Up @@ -178,5 +180,27 @@ function xmldb_local_ai_manager_upgrade($oldversion) {
upgrade_plugin_savepoint(true, 2024120200, 'local', 'ai_manager');
}

if ($oldversion < 2024122300) {

// Define field scope to be added to local_ai_manager_userinfo.
$table = new xmldb_table('local_ai_manager_userinfo');
$field = new xmldb_field('scope', XMLDB_TYPE_INTEGER, '1', null, null, null, null, 'confirmed');

// Conditionally launch add field scope.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}

$userids = $DB->get_fieldset('local_ai_manager_userinfo', 'userid');
foreach ($userids as $userid) {
// This will set the correct default value for the "scope" and update the record afterwards.
$userinfo = new userinfo($userid);
$userinfo->store();
}

// AI manager savepoint reached.
upgrade_plugin_savepoint(true, 2024122300, 'local', 'ai_manager');
}

return true;
}
5 changes: 5 additions & 0 deletions lang/de/local_ai_manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
$string['cachedef_googleauth'] = 'Cache für Google-OAuth2-Access-Token';
$string['changeconfirmstate'] = 'Zustimmung der Nutzungsbedingungen ändern';
$string['changelockstate'] = 'Gesperrt-Zustand verändern';
$string['changescope'] = 'Nutzungsbereich verändern';
$string['configure_instance'] = 'KI-Tool-Instanzen konfigurieren';
$string['configureaitool'] = 'KI-Tool konfigurieren';
$string['configurepurposes'] = 'Einsatzzwecke konfigurieren';
Expand All @@ -70,6 +71,7 @@
$string['endpoint'] = 'API-Endpunkt';
$string['error_http400'] = 'Fehler beim Bereinigen der übergebenen Optionen';
$string['error_http403blocked'] = 'Ihr Tenant-Manager hat den Zugriff auf die KI-Tools für Sie blockiert';
$string['error_http403coursesonly'] = 'Sie haben keine Berechtigung, KI-Tools außerhalb von Kursen zu nutzen.';
$string['error_http403disabled'] = 'Ihr Tenant-Manager hat die KI-Tools für Ihren Tenant nicht aktiviert';
$string['error_http403nocapability'] = 'Sie haben nicht die Berechtigung, um den AI-Manager zu nutzen ("local/ai_manager:use")';
$string['error_http403notconfirmed'] = 'Sie haben die Nutzungsbedingungen noch nicht akzeptiert';
Expand Down Expand Up @@ -173,6 +175,9 @@
$string['role_basic'] = 'Standardrolle';
$string['role_extended'] = 'Erweiterte Rolle';
$string['role_unlimited'] = 'Unbeschränkte Rolle';
$string['scope'] = 'Nutzungsbereich';
$string['scope_courses'] = 'Nur in Kursen';
$string['scope_everywhere'] = 'Überall';
$string['select_tool_for_purpose'] = 'Einsatzzweck "{$a}"';
$string['selecteduserscount'] = '{$a} ausgewählt';
$string['serviceaccountjson'] = 'Inhalt der JSON-Datei des Google-Serviceaccounts';
Expand Down
13 changes: 9 additions & 4 deletions lang/en/local_ai_manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
$string['cachedef_googleauth'] = 'Cache for Google OAuth2 access token';
$string['changeconfirmstate'] = 'Change confirmation of terms of use';
$string['changelockstate'] = 'Change lock status';
$string['changescope'] = 'Change usage scope';
$string['configure_instance'] = 'Configure AI Tool Instances';
$string['configureaitool'] = 'Configure AI tool';
$string['configurepurposes'] = 'Configure the purposes';
Expand All @@ -69,11 +70,12 @@
$string['enable_ai_integration'] = 'Enable AI integration';
$string['endpoint'] = 'API endpoint';
$string['error_http400'] = 'Error sanitizing passed options';
$string['error_http403blocked'] = 'Your ByCS admin has blocked access to the AI tools for you';
$string['error_http403disabled'] = 'Your ByCS admin has not enabled the AI tools feature';
$string['error_http403blocked'] = 'Your tenant manager has blocked access to the AI tools for you';
$string['error_http403coursesonly'] = 'You do not have the permission to use AI tool outside courses.';
$string['error_http403disabled'] = 'Your tenant manager has not enabled the AI tools feature';
$string['error_http403nocapability'] = 'You do not have the capability to use the AI manager ("local/ai_manager:use")';
$string['error_http403notconfirmed'] = 'You have not yet confirmed the terms of use';
$string['error_http403usertype'] = 'Your ByCS admin has disabled this purpose for your user type';
$string['error_http403usertype'] = 'Your tenant manager has disabled this purpose for your user type';
$string['error_http409'] = 'The itemid {$a} is already taken';
$string['error_http429'] = 'You have reached the maximum amount of requests. You are only allowed to send {$a->count} requests in a period of {$a->period}';
$string['error_limitreached'] = 'You have reached the maximum amount of requests for this purpose. Please wait until the counter has been reset.';
Expand Down Expand Up @@ -150,7 +152,7 @@
$string['portrait'] = 'portrait';
$string['preconfiguredmodel'] = 'Preconfigured model';
$string['privacy:metadata'] = 'The local ai_manager plugin does not store any personal data.';
$string['privacy_table_description'] = 'In the table below, you can see an overview of the AI tools configured by your school. Your ByCS admin may have provided additional notes on the terms of use and privacy notices of the respective AI tools in the "Info link" column.';
$string['privacy_table_description'] = 'In the table below, you can see an overview of the AI tools configured by your school. Your tenant manager may have provided additional notes on the terms of use and privacy notices of the respective AI tools in the "Info link" column.';
$string['privacy_terms_description'] = 'Following are the notes about data privacy and terms of use in the exact same form like you confirmed or still have to confirm to use the AI functionalities.';
$string['privacy_terms_heading'] = 'Privacy and Terms of Use';
$string['privacy_terms_missing'] = 'No terms of use have been specified.';
Expand All @@ -173,6 +175,9 @@
$string['role_basic'] = 'base role';
$string['role_extended'] = 'extended role';
$string['role_unlimited'] = 'unlimited role';
$string['scope'] = 'usage scope';
$string['scope_courses'] = 'Only in courses';
$string['scope_everywhere'] = 'Everywhere';
$string['select_tool_for_purpose'] = 'Purpose {$a}';
$string['selecteduserscount'] = '{$a} selected';
$string['serviceaccountjson'] = 'Content of the JSON file of the Google service account';
Expand Down
3 changes: 3 additions & 0 deletions rights_config.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@
case rights_config_form::ACTION_CHANGE_CONFIRM_STATE:
$userinfo->set_confirmed($data->confirmstate === rights_config_form::ACTIONOPTION_CHANGE_CONFIRM_STATE_CONFIRM);
break;
case rights_config_form::ACTION_CHANGE_SCOPE:
$userinfo->set_scope(intval($data->scope));
break;
default:
throw new \coding_exception('Unknown action: ' . $data->action);
}
Expand Down
Loading

0 comments on commit f556c14

Please sign in to comment.