The AI module currently has no declared rights ($this->rights stays
empty), so per-user/group attribution of "who is allowed to talk to
the LLM" is not possible. Assistant access is gated only by
isModEnabled('ai'), meaning any authenticated user on an AI-enabled
install can send organisational data to the configured LLM provider
-- which is a GDPR / EU AI Act concern (see issue #38331).
This patch introduces ONE proper Dolibarr right that admins can
attribute per user/group via the standard permission UI
(Users -> Permissions):
ai/assistant/use -> right to use the AI Assistant page
NOT granted by default; admins must
explicitly assign it to authorized users
Per the feedback from @sonikf and @eldy on PR #38312:
- Setup access intentionally stays a hard-coded $user->admin check.
The technical setup of the AI module (API keys, provider URLs) is
an admin task in line with how every other Dolibarr module is
configured -- a dedicated 'ai/setup/write' right would only
duplicate responsibility that admins already hold.
- ai/assistant/use defaults to OFF (not granted) so that, on a
fresh install or after enabling the module, no user can talk to
the LLM until the admin explicitly authorises them. This matches
the EU AI Act expectation that AI usage is an opt-in per-user
decision owned by the GDPR DPO / AI module DPA.
Access checks added:
- htdocs/ai/assistant/index.php
+ if (!$user->hasRight('ai','assistant','use')) accessforbidden();
Added after the existing isModEnabled('ai') / AI_MCP_ENABLED gate.
- htdocs/ai/assistant/parse_intent.php
+ if (!$user->hasRight('ai','assistant','use')) accessforbidden();
Defence in depth: the AJAX endpoint that actually talks to the LLM
cannot be reached by a user who lacks the right, even via direct
call bypassing the Assistant page.
- htdocs/ai/admin/setup.php
Unchanged behaviour: $user->admin check stays (just a comment
clarifying the reason).
The right is declared between the existing MODULEBUILDER PERMISSIONS
placeholders in modAi.class.php so the standard right-installation
flow applies: after applying this change, administrators must
disable/re-enable the AI module once to register the new right in
llx_rights_def (existing Dolibarr mechanism, no migration script
needed).
Tested:
- Fresh non-admin user (no extra right granted) -> Assistant page
shows accessforbidden; parse_intent.php also blocks direct call.
- Same user with 'ai/assistant/use' granted by an admin ->
Assistant accessible, LLM round-trip works.
- Admin user -> Assistant accessible transparently (admins hold
every declared right by definition); Setup accessible via the
unchanged $user->admin check.
- Module disabled -> both still blocked by the existing
isModEnabled gate.
Refs: #38331 (EU AI Act / GDPR for the AI module).
Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
307 lines
9.3 KiB
PHP
307 lines
9.3 KiB
PHP
<?php
|
|
/* Copyright (C) 2004-2017 Laurent Destailleur <eldy@users.sourceforge.net>
|
|
* Copyright (C) 2022 Lamrani Abdel
|
|
* Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
|
|
* Copyright (C) 2024-2025 Frédéric France <frederic.france@free.fr>
|
|
* Coryright (C) 2024 Alexandre Spangaro <alexandre@inovea-conseil.com>
|
|
* Copyright (C) 2026 Nick Fragoulis
|
|
* Copyright (C) 2026 Jose Martinez <jose.martinez@pichinov.com>
|
|
*
|
|
* This program 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.
|
|
*
|
|
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/**
|
|
* \file htdocs/ai/admin/setup.php
|
|
* \ingroup ai
|
|
* \brief Ai setup page.
|
|
*/
|
|
|
|
// Load Dolibarr environment
|
|
require '../../main.inc.php';
|
|
/**
|
|
* @var Conf $conf
|
|
* @var DoliDB $db
|
|
* @var HookManager $hookmanager
|
|
* @var Translate $langs
|
|
* @var User $user
|
|
*/
|
|
require_once DOL_DOCUMENT_ROOT."/core/lib/admin.lib.php";
|
|
require_once DOL_DOCUMENT_ROOT."/core/class/doleditor.class.php";
|
|
require_once DOL_DOCUMENT_ROOT."/ai/lib/ai.lib.php";
|
|
|
|
$langs->loadLangs(array("admin", "website", "other"));
|
|
|
|
|
|
// Parameters
|
|
$action = GETPOST('action', 'aZ09');
|
|
$backtopage = GETPOST('backtopage', 'alpha');
|
|
$modulepart = GETPOST('modulepart', 'aZ09'); // Used by actions_setmoduleoptions.inc.php
|
|
|
|
if (empty($action)) {
|
|
$action = 'edit';
|
|
}
|
|
|
|
$content = GETPOST('content');
|
|
|
|
$error = 0;
|
|
$setupnotempty = 0;
|
|
|
|
|
|
// Set this to 1 to use the factory to manage constants. Warning, the generated module will be compatible with version v15+ only
|
|
$useFormSetup = 1;
|
|
|
|
if (!class_exists('FormSetup')) {
|
|
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formsetup.class.php';
|
|
}
|
|
|
|
$formSetup = new FormSetup($db);
|
|
|
|
// List all available AI
|
|
$arrayofai = getListOfAIServices();
|
|
|
|
// List all available features
|
|
$arrayofaifeatures = getListOfAIFeatures();
|
|
|
|
// Main Service Selection
|
|
$item = $formSetup->newItem('AI_API_SERVICE'); // Name of constant must end with _KEY so it is encrypted when saved into database.
|
|
$item->setAsSelect($arrayofai);
|
|
$item->cssClass = 'minwidth150';
|
|
|
|
// Loop for Provider Configs
|
|
foreach ($arrayofai as $ia => $iarecord) {
|
|
if ($ia == '-1') {
|
|
continue;
|
|
}
|
|
$ialabel = $iarecord['label'];
|
|
// Setup conf AI_PUBLIC_INTERFACE_TOPIC
|
|
/*$item = $formSetup->newItem('AI_API_'.strtoupper($ia).'_ENDPOINT'); // Name of constant must end with _KEY so it is encrypted when saved into database.
|
|
$item->defaultFieldValue = '';
|
|
$item->cssClass = 'minwidth500';*/
|
|
|
|
// API Key
|
|
$item = $formSetup->newItem('AI_API_'.strtoupper($ia).'_KEY')->setAsSecureKey(); // Name of constant must end with _KEY so it is encrypted when saved into database.
|
|
$item->nameText = $langs->trans("AI_API_KEY").' ('.$ialabel.')';
|
|
$item->defaultFieldValue = '';
|
|
$item->fieldParams['hideGenerateButton'] = 1;
|
|
$item->fieldParams['trClass'] = 'iaservice '.$ia;
|
|
$item->cssClass = 'minwidth500 text-security input'.$ia;
|
|
$item->helpText = '<span class="helptoshow">HelpToShow</span>';
|
|
|
|
// API URL
|
|
$item = $formSetup->newItem('AI_API_'.strtoupper($ia).'_URL'); // Name of constant must end with _KEY so it is encrypted when saved into database.
|
|
$item->nameText = $langs->trans("AI_API_URL").' ('.$ialabel.')';
|
|
$item->defaultFieldValue = $iarecord['url'];
|
|
$item->fieldParams['trClass'] = 'iaservice iaurl '.$ia;
|
|
$item->cssClass = 'minwidth500 input'.$ia;
|
|
if ($ia == 'custom') {
|
|
$item->fieldAttr['placeholder'] = 'https://domainofapi.com/v1/';
|
|
}
|
|
}
|
|
|
|
$setupnotempty = + count($formSetup->items);
|
|
|
|
|
|
$dirmodels = array_merge(array('/'), (array) $conf->modules_parts['models']);
|
|
|
|
// Access control
|
|
// Setup intentionally stays admin-only: per @sonikf and @eldy feedback,
|
|
// the AI module's technical setup (API keys, provider configuration) is
|
|
// a task done by the admin user, just like for every other Dolibarr module.
|
|
// Per-user/group AI usage is gated separately by 'ai/assistant/use'.
|
|
if (!$user->admin) {
|
|
accessforbidden();
|
|
}
|
|
if (!isModEnabled('ai')) {
|
|
accessforbidden('Module AI not activated.');
|
|
}
|
|
|
|
|
|
/*
|
|
* Actions
|
|
*/
|
|
|
|
include DOL_DOCUMENT_ROOT.'/core/actions_setmoduleoptions.inc.php';
|
|
|
|
$action = 'edit';
|
|
|
|
|
|
/*
|
|
* View
|
|
*/
|
|
|
|
$help_url = '';
|
|
$title = "AiSetup";
|
|
|
|
llxHeader('', $langs->trans($title), $help_url, '', 0, 0, '', '', '', 'mod-ai page-admin');
|
|
|
|
// Subheader
|
|
$linkback = '<a href="'.($backtopage ? $backtopage : dolBuildUrl(DOL_URL_ROOT.'/admin/modules.php', ['restore_lastsearch_values' => 1])).'">'.img_picto($langs->trans("BackToModuleList"), 'back', 'class="pictofixedwidth"').'<span class="hideonsmartphone">'.$langs->trans("BackToModuleList").'</span></a>';
|
|
|
|
print load_fiche_titre($langs->trans($title), $linkback, 'title_setup');
|
|
|
|
// Configuration header
|
|
$head = aiAdminPrepareHead();
|
|
print dol_get_fiche_head($head, 'settings', $langs->trans($title), -1, "ai");
|
|
|
|
|
|
if ($action == 'edit') {
|
|
print $formSetup->generateOutput(true);
|
|
} elseif (!empty($formSetup->items)) {
|
|
print $formSetup->generateOutput();
|
|
print '<div class="tabsAction">';
|
|
print '<a class="butAction" href="'.$_SERVER["PHP_SELF"].'?action=edit&token='.newToken().'">'.$langs->trans("Modify").'</a>';
|
|
print '</div>';
|
|
} else {
|
|
print '<br>'.$langs->trans("NothingToSetup");
|
|
}
|
|
|
|
|
|
if (empty($setupnotempty)) {
|
|
print '<br>'.$langs->trans("NothingToSetup");
|
|
}
|
|
|
|
print '<script type="text/javascript">
|
|
jQuery(document).ready(function() {
|
|
function showHideAIService(aiservice) {
|
|
console.log("showHideAIService: We select the AI service "+aiservice);
|
|
jQuery(".iaservice").hide();
|
|
|
|
if (aiservice != "-1") {
|
|
jQuery(".iaservice."+aiservice).show();
|
|
const arrayofia = {';
|
|
$i = 0;
|
|
foreach ($arrayofai as $key => $airecord) {
|
|
if ($key == -1) {
|
|
continue;
|
|
}
|
|
if ($i) {
|
|
print ', ';
|
|
}
|
|
$i++;
|
|
print dol_escape_js($key).': \''.dol_escape_js($airecord['url']).'\'';
|
|
}
|
|
print '};
|
|
const arrayofextlink = {';
|
|
$i = 0;
|
|
foreach ($arrayofai as $key => $airecord) {
|
|
if ($key == -1) {
|
|
continue;
|
|
}
|
|
if ($i) {
|
|
print ', ';
|
|
}
|
|
$i++;
|
|
print dol_escape_js($key).': \''.dol_escape_js($airecord['setup']).'\'';
|
|
}
|
|
print '};
|
|
console.log("Check URL for .iaurl."+aiservice+" .input"+aiservice);
|
|
if (jQuery(".iaurl."+aiservice+" .input"+aiservice).val() == \'\') {
|
|
console.log("URL is empty, we fill with default value of IA selected");
|
|
jQuery(".iaurl."+aiservice+" .input"+aiservice).val(arrayofia[aiservice]);
|
|
}
|
|
jQuery(".helptoshow").text(arrayofextlink[aiservice]);
|
|
}
|
|
}
|
|
|
|
jQuery("#AI_API_SERVICE").change(function() {
|
|
var aiservice = $(this).val();
|
|
|
|
showHideAIService(aiservice);
|
|
|
|
jQuery(".sectiontest").hide(); /* Hide test section, will appear after the save */
|
|
});
|
|
|
|
showHideAIService("'.getDolGlobalString("AI_API_SERVICE").'");
|
|
});
|
|
</script>';
|
|
|
|
// Page end
|
|
print dol_get_fiche_end();
|
|
|
|
|
|
// The section for test
|
|
|
|
if (getDolGlobalString("AI_API_SERVICE")) {
|
|
print '<br><div class="sectiontest">';
|
|
|
|
// Section to test
|
|
print '<form action="'.$_SERVER["PHP_SELF"].'" method="POST">';
|
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
|
print '<input type="hidden" name="action" value="add">';
|
|
print '<input type="hidden" name="backtopage" value="'.$backtopage.'">';
|
|
|
|
|
|
$key = 'textgenerationother'; // The HTML ID of field to fill
|
|
|
|
//if (GETPOST('functioncode') == 'textgenerationemail') {
|
|
|
|
print '<br>';
|
|
//print '<hr>';
|
|
|
|
include_once DOL_DOCUMENT_ROOT.'/core/class/html.formmail.class.php';
|
|
include_once DOL_DOCUMENT_ROOT."/core/class/html.formai.class.php";
|
|
$formai = new FormAI($db);
|
|
$formmail = new FormMail($db);
|
|
|
|
$showlinktoai = $key; // 'textgeneration', 'imagegeneration', ...
|
|
$showlinktoailabel = $langs->trans("AITestText");
|
|
$showlinktolayout = 0;
|
|
$htmlname = $key;
|
|
$formmail->withaiprompt = '';
|
|
|
|
// Fill $out
|
|
|
|
$out = $langs->trans("Test").': ';
|
|
include DOL_DOCUMENT_ROOT.'/core/tpl/formlayoutai.tpl.php';
|
|
print $out;
|
|
|
|
print ' ';
|
|
if (getDolGlobalString("AI_DEBUG")) {
|
|
print ' <span class="small opacitymedium">'.$langs->trans("DebugOnInFile", 'dolibarr_ai.log').'</span>';
|
|
} else {
|
|
print ' <span class="small opacitymedium">'.$langs->trans("DebugOff", 'dolibarr_ai.log').'</span>';
|
|
}
|
|
|
|
print '<br><textarea id="'.$htmlname.'" placeholder="Click on picto to enter a prompt or enter a message and click picto to make text transformation..." class="quatrevingtpercent" rows="4"></textarea>'; // The div
|
|
|
|
print '<br><br>';
|
|
|
|
|
|
$showlinktoai .= 'html';
|
|
$htmlname .= 'html';
|
|
$formmail->withaiprompt = 'html';
|
|
|
|
// Fill $out
|
|
$out = $langs->trans("Test").': ';
|
|
include DOL_DOCUMENT_ROOT.'/core/tpl/formlayoutai.tpl.php';
|
|
print $out;
|
|
|
|
print ' ';
|
|
if (getDolGlobalString("AI_DEBUG")) {
|
|
print ' <span class="small opacitymedium">'.$langs->trans("DebugOnInFile", 'dolibarr_ai.log').'</span>';
|
|
} else {
|
|
print ' <span class="small opacitymedium">'.$langs->trans("DebugOff", 'dolibarr_ai.log').'</span>';
|
|
}
|
|
|
|
print '<br>';
|
|
$doleditor = new DolEditor($htmlname, '', '', 150, 'dolibarr_details');
|
|
print $doleditor->Create(1);
|
|
|
|
print '</form>';
|
|
|
|
print '</div>';
|
|
}
|
|
|
|
llxFooter();
|
|
$db->close();
|