Skip to content

Commit

Permalink
MBS-9811: Add hook for restricting access
Browse files Browse the repository at this point in the history
  • Loading branch information
PhMemmel committed Dec 29, 2024
1 parent cf58c95 commit 15d15e2
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 8 deletions.
153 changes: 153 additions & 0 deletions classes/hook/additional_user_restriction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?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 local_ai_manager\hook;

use context;
use local_ai_manager\base_purpose;
use local_ai_manager\local\userinfo;

/**
* Hook for allowing other plugins to further restrict the access to use the AI tools through the local_ai_manager.
*
* This hook will be dispatched whenever a user tries to send a request to the AI tool via local_ai_manager.
*
* @package local_ai_manager
* @copyright 2024 ISB Bayern
* @author Philipp Memmel
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\core\attribute\label('Allows plugins to further restrict the use of AI tools through local_ai_manager.')]
#[\core\attribute\tags('local_ai_manager')]
class additional_user_restriction {

/** @var bool If access to the AI tool should be granted or not */
private bool $allowed = true;
/** @var int The corresponding HTTP status code, 200 if access is being granted */
private int $code = 200;
/** @var string A localized error message that is being shown to the user in case of an error */
private string $message = '';
/** @var string Optional debug info */
private string $debuginfo = '';

/**
* Constructor for the hook.
*/
public function __construct(
/** @var userinfo The userinfo object of the user that tries to access an AI tool */
private readonly userinfo $userinfo,
/** @var ?context The context or null if no context has been specified */
private readonly ?context $context,
/** @var base_purpose The purpose which is being tried to use */
private readonly base_purpose $purpose,
) {
}

/**
* Getter for the userinfo object.
*
* @return userinfo The userinfo object of the user trying to access the AI tool
*/
public function get_userinfo(): userinfo {
return $this->userinfo;
}

/**
* Getter for the current context.
*
* @return ?context the context from which the AI tool is being tried to access
*/
public function get_context(): ?context {
return $this->context;
}

/**
* Getter for the currently used purpose.
*
* @return base_purpose The purpose being used
*/
public function get_purpose(): base_purpose {
return $this->purpose;
}

/**
* Set if the access for the current user should be denied or not.
*
* If access is granted, you do not need to do anything, because it is the default.
* If access should be restricted, pass $allowed = false and also provide a code !== 200 as well as an
* already localized error message that is being used as feedback to the user
*
* @param bool $allowed true if access is granted, false otherwise
* @param int $code an HTTP status code that should be returned to the user, will be set to 200 in case of
* $allowed = true
* @param string $message localized message that should be shown to the user in case of restricted access
* @param string $debuginfo optional debug info
*/
public function set_access_allowed(bool $allowed, int $code = 0, string $message = '', string $debuginfo = ''): void {
if ($allowed) {
$this->allowed = true;
$this->code = 200;
return;
}

$this->allowed = false;
if ($code === 200) {
throw new \coding_exception('You have to provide a different code than 200 in case of a denied access.');
}
$this->code = $code;
if (empty($message)) {
throw new \coding_exception('You have to provide a message in case of a denied access.');
}
$this->message = $message;
$this->debuginfo = $debuginfo;
}

/**
* Standard getter for the allowed attribute.
*
* @return bool if access is being granted or not
*/
public function is_allowed(): bool {
return $this->allowed;
}

/**
* Standard getter for the corresponding HTTP status code.
*
* @return int the HTTP status code
*/
public function get_code(): int {
return $this->code;
}

/**
* Standard getter for the message in case of an error.
*
* @return string the error message
*/
public function get_message(): string {
return $this->message;
}

/**
* Standard getter for the optional debug info.
*
* @return string the debug info
*/
public function get_debuginfo(): string {
return $this->debuginfo;
}
}
12 changes: 12 additions & 0 deletions classes/manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
use core_plugin_manager;
use local_ai_manager\event\get_ai_response_failed;
use local_ai_manager\event\get_ai_response_succeeded;
use local_ai_manager\hook\additional_user_restriction;
use local_ai_manager\local\config_manager;
use local_ai_manager\local\connector_factory;
use local_ai_manager\local\prompt_response;
Expand Down Expand Up @@ -154,6 +155,17 @@ public function perform_request(string $prompttext, array $options = []): prompt
}
}

// Provide an additional hook for further limiting access.
$context = empty($options['contextid']) ? null : context::instance_by_id($options['contextid']);
$restrictionhook = new additional_user_restriction($userinfo, $context, $this->purpose);
\core\di::get(\core\hook\manager::class)->dispatch($restrictionhook);
if (!$restrictionhook->is_allowed()) {
return prompt_response::create_from_error(
$restrictionhook->get_code(),
$restrictionhook->get_message(),
$restrictionhook->get_debuginfo());
}

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
14 changes: 7 additions & 7 deletions tests/ai_manager_utils_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public function test_get_next_free_itemid(): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();

$this->assertEquals(1, ai_manager_utils::get_next_free_itemid('block_ai_chat', 12));
$this->assertEquals(1, ai_manager_utils::get_next_free_itemid('block_ai_control', 12));

$record = new stdClass();
$record->userid = $user->id;
Expand All @@ -47,7 +47,7 @@ public function test_get_next_free_itemid(): void {
$record->modelinfo = 'testmodel-3.5';
$record->prompttext = 'some prompt';
$record->promptcompletion = 'some prompt response';
$record->component = 'block_ai_chat';
$record->component = 'block_ai_control';
$record->contextid = 12;
$record->itemid = 5;
$record->timecreated = time();
Expand All @@ -60,13 +60,13 @@ public function test_get_next_free_itemid(): void {
$record->modelinfo = 'anothertestmodel-4.0';
$record->prompttext = 'some other prompt';
$record->promptcompletion = 'some prompt response';
$record->component = 'block_ai_chat';
$record->component = 'block_ai_control';
$record->contextid = 12;
$record->itemid = 7;
$record->timecreated = time();
$DB->insert_record('local_ai_manager_request_log', $record);

$this->assertEquals(8, ai_manager_utils::get_next_free_itemid('block_ai_chat', 12));
$this->assertEquals(8, ai_manager_utils::get_next_free_itemid('block_ai_control', 12));

$record = new stdClass();
$record->userid = $user->id;
Expand All @@ -75,16 +75,16 @@ public function test_get_next_free_itemid(): void {
$record->modelinfo = 'anothertestmodel-4.0';
$record->prompttext = 'some other prompt';
$record->promptcompletion = 'some prompt response';
$record->component = 'block_ai_chat';
$record->component = 'block_ai_control';
// Other context id, so this record should not be relevant.
$record->contextid = 23;
$record->itemid = 10;
$record->timecreated = time();
$DB->insert_record('local_ai_manager_request_log', $record);

$this->assertEquals(8, ai_manager_utils::get_next_free_itemid('block_ai_chat', 12));
$this->assertEquals(8, ai_manager_utils::get_next_free_itemid('block_ai_control', 12));
$this->assertEquals(1, ai_manager_utils::get_next_free_itemid('mod_ai', 23));
$this->assertEquals(11, ai_manager_utils::get_next_free_itemid('block_ai_chat', 23));
$this->assertEquals(11, ai_manager_utils::get_next_free_itemid('block_ai_control', 23));
}

/**
Expand Down
5 changes: 5 additions & 0 deletions tests/manager_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ public function test_perform_request(array $configuration, int $expectedcode, st
\core\di::set(config_manager::class, $configmanager);
\core\di::set(connector_factory::class, $connectorfactory);

// We disable the hook here so we have a defined setup for this unit test.
// The hook callbacks should be tested whereever the callback is being implemented.
$this->redirectHook(\local_ai_manager\hook\additional_user_restriction::class, function() {});

$manager = new manager('chat');

// Now we finally finished our setup. Call the perform_request method and check the result.
Expand All @@ -156,6 +160,7 @@ public function test_perform_request(array $configuration, int $expectedcode, st
} else {
$this->assertEquals($result->get_errormessage(), $message);
}
$this->stopHookRedirections();
}

/**
Expand Down
2 changes: 1 addition & 1 deletion version.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
*/
defined('MOODLE_INTERNAL') || die();

$plugin->version = 2024122300;
$plugin->version = 2024122900;
$plugin->requires = 2024042200;
$plugin->release = '0.0.3';
$plugin->component = 'local_ai_manager';
Expand Down

0 comments on commit 15d15e2

Please sign in to comment.