Skip to content

Commit 489ee77

Browse files
committed
feat: add ACLs for client & backend
References #36
1 parent 6280e64 commit 489ee77

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+774
-269
lines changed

_build/gpm.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ menus:
77
- action: home
88
text: modai.admin.menu.home
99
description: modai.admin.menu.home_desc
10+
permission: modai_admin
1011

1112
plugins:
1213
- name: modAI
@@ -180,6 +181,7 @@ systemSettings:
180181
build:
181182
scriptsBefore:
182183
- custom_events.gpm.php
184+
- acls.gpm.php
183185
scriptsAfter:
184186
- lit.gpm.php
185187
- seed.gpm.php

_build/js/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ import { executor } from './executor';
33
import { globalState } from './globalState';
44
import { history } from './history';
55
import { lng } from './lng';
6+
import { checkPermissions } from './permissions';
67
import { initOnResource } from './resource';
78
import { ui } from './ui';
89

10+
import type { Permissions } from './permissions';
11+
912
export type AvailableAgent = {
1013
id: string;
1114
name: string;
@@ -19,6 +22,7 @@ export type Config = {
1922
cssURL: string;
2023
translateFn?: (key: string, params?: Record<string, string>) => string;
2124
availableAgents: Record<string, AvailableAgent>;
25+
permissions: Record<Permissions, 1 | 0>;
2226
};
2327

2428
export const init = (config: Config) => {
@@ -31,5 +35,6 @@ export const init = (config: Config) => {
3135
ui,
3236
lng,
3337
initOnResource,
38+
checkPermissions,
3439
};
3540
};

_build/js/src/permissions.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { globalState } from './globalState';
2+
3+
export type Permissions =
4+
| 'modai_client'
5+
| 'modai_client_chat_text'
6+
| 'modai_client_chat_image'
7+
| 'modai_client_text'
8+
| 'modai_client_vision';
9+
10+
export const checkPermissions = (permissions: Permissions[]) => {
11+
return permissions.every((permission) => {
12+
return globalState.config.permissions[permission];
13+
});
14+
};

_build/js/src/resource.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ const attachImagePlus = (imgPlusPanel: Element, fieldName: string) => {
3939
},
4040
});
4141

42-
altTextWand.style.marginTop = '6px';
42+
if (altTextWand) {
43+
altTextWand.style.marginTop = '6px';
44+
}
4345

4446
imagePlus.altTextField.el.dom.style.display = 'flex';
4547
imagePlus.altTextField.el.dom.style.justifyItems = 'center';

_build/js/src/ui/generateButton/index.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { executor } from '../../executor';
22
import { history } from '../../history';
33
import { lng } from '../../lng';
4+
import { checkPermissions } from '../../permissions';
45
import { confirmDialog } from '../cofirmDialog';
56
import { button } from '../dom/button';
67
import { icon } from '../dom/icon';
@@ -114,8 +115,12 @@ type Target = {
114115
};
115116

116117
const createLocalChat = (config: LocalChatConfig & Target) => {
118+
if (!ui.localChat.verifyPermissions(config)) {
119+
return;
120+
}
121+
117122
const { shadow } = createWandEl(() => {
118-
ui.localChat(config);
123+
ui.localChat.createModal(config);
119124
});
120125

121126
config.targetEl.appendChild(shadow);
@@ -138,6 +143,10 @@ const createForcedTextPrompt = ({
138143
field,
139144
...rest
140145
}: ForcedTextConfig) => {
146+
if (!checkPermissions(['modai_client', 'modai_client_text'])) {
147+
return;
148+
}
149+
141150
const { shadow, generate } = createWandEl<HistoryElement>(async () => {
142151
const done = createLoadingOverlay(input);
143152

@@ -232,6 +241,10 @@ type VisionConfig = {
232241
};
233242

234243
const createVisionPrompt = (config: VisionConfig & Target) => {
244+
if (!checkPermissions(['modai_client', 'modai_client_vision'])) {
245+
return;
246+
}
247+
235248
const { shadow } = createWandEl(async () => {
236249
const canvas = document.createElement('canvas');
237250
const ctx = canvas.getContext('2d');

_build/js/src/ui/index.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
import { createGenerateButton } from './generateButton';
2-
import { createModal } from './localChat';
2+
import { createModal, verifyPermissions } from './localChat';
3+
import { LocalChatConfig } from './localChat/types';
34
import { createLoadingOverlay } from './overlay';
45

6+
type LocalChat = {
7+
/**
8+
* @deprecated use the ui.localChat.createModal instead
9+
*/
10+
(config: LocalChatConfig): ReturnType<typeof createModal>;
11+
createModal: typeof createModal;
12+
verifyPermissions: typeof verifyPermissions;
13+
};
14+
515
export const ui = {
616
createLoadingOverlay,
7-
localChat: createModal,
17+
localChat: Object.assign({}, createModal, {
18+
createModal: createModal,
19+
verifyPermissions,
20+
}) as LocalChat,
821
generateButton: createGenerateButton,
922
};

_build/js/src/ui/localChat/index.ts

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { closeModal, sendMessage } from './modalActions';
22
import { buildModal } from './modalBuilder';
33
import { globalState } from '../../globalState';
44
import { lng } from '../../lng';
5+
import { checkPermissions } from '../../permissions';
56

67
import type { LocalChatConfig } from './types';
78

@@ -33,17 +34,13 @@ export const createModal = (config: LocalChatConfig) => {
3334
}
3435

3536
if (!config.type) {
36-
config.type = 'text';
37-
}
38-
39-
if (!config.availableTypes) {
40-
config.availableTypes = [config.type];
37+
alert(lng('modai.error.type_required'));
38+
return;
4139
}
4240

43-
config.availableTypes = config.availableTypes.filter((type) => modalTypes.includes(type));
44-
45-
if (config.availableTypes.length > 0 && !config.availableTypes.includes(config.type)) {
46-
config.availableTypes.unshift(config.type);
41+
const hasPermissions = verifyPermissions(config);
42+
if (!hasPermissions) {
43+
return;
4744
}
4845

4946
const modal = buildModal(config);
@@ -65,3 +62,36 @@ export const createModal = (config: LocalChatConfig) => {
6562

6663
return modal;
6764
};
65+
66+
export const verifyPermissions = (config: LocalChatConfig) => {
67+
if (!checkPermissions(['modai_client'])) {
68+
return false;
69+
}
70+
71+
if (
72+
!checkPermissions(['modai_client_chat_image']) &&
73+
!checkPermissions(['modai_client_chat_text'])
74+
) {
75+
return false;
76+
}
77+
78+
if (!config.availableTypes) {
79+
config.availableTypes = [config.type];
80+
}
81+
82+
config.availableTypes = config.availableTypes
83+
.filter((type) => modalTypes.includes(type))
84+
.filter((type) =>
85+
checkPermissions([type === 'text' ? 'modai_client_chat_text' : 'modai_client_chat_image']),
86+
);
87+
88+
if (config.availableTypes.length > 0 && !config.availableTypes.includes(config.type)) {
89+
config.type = config.availableTypes[0];
90+
}
91+
92+
if (config.availableTypes.length === 0 || !config.availableTypes.includes(config.type)) {
93+
return false;
94+
}
95+
96+
return true;
97+
};

_build/js/src/ui/localChat/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export type Modal = HTMLDivElement & {
4343

4444
export type LocalChatConfig = {
4545
key: string;
46-
type?: ModalType;
46+
type: ModalType;
4747
availableTypes?: ModalType[];
4848
namespace?: string;
4949
/**

_build/scripts/acls.gpm.php

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<?php
2+
3+
use MODX\Revolution\modAccessContext;
4+
use MODX\Revolution\modAccessPermission;
5+
use MODX\Revolution\modAccessPolicy;
6+
use MODX\Revolution\modAccessPolicyTemplate;
7+
use MODX\Revolution\modAccessPolicyTemplateGroup;
8+
use MODX\Revolution\modContext;
9+
use MODX\Revolution\modUserGroup;
10+
11+
return new class() {
12+
/**
13+
* @var \MODX\Revolution\modX
14+
*/
15+
private $modx;
16+
17+
/**
18+
* @var int
19+
*/
20+
private $action;
21+
22+
/**
23+
* @param \MODX\Revolution\modX $modx
24+
* @param int $action
25+
* @return bool
26+
*/
27+
public function __invoke(&$modx, $action)
28+
{
29+
$this->modx =& $modx;
30+
$this->action = $action;
31+
32+
if ($this->action === \xPDO\Transport\xPDOTransport::ACTION_UNINSTALL) {
33+
return true;
34+
}
35+
36+
$group = $modx->getObject(modAccessPolicyTemplateGroup::class, ['name' => 'Administrator']);
37+
if (!$group) {
38+
return;
39+
}
40+
41+
/** @var modAccessPolicyTemplate $template */
42+
$template = $modx->getObject(modAccessPolicyTemplate::class, ['name' => 'modAI', 'template_group' => $group->get('id')]);
43+
if (!$template) {
44+
$template = $modx->newObject(modAccessPolicyTemplate::class);
45+
$template->set('name', 'modAI');
46+
$template->set('template_group', $group->get('id'));
47+
$template->set('description', 'A policy template to for modAI');
48+
$template->set('lexicon', 'modai:permissions');
49+
$template->save();
50+
}
51+
52+
$permissions = [
53+
'modai_admin',
54+
'modai_admin_tools',
55+
'modai_admin_tool_save',
56+
'modai_admin_tool_delete',
57+
'modai_admin_context_providers',
58+
'modai_admin_context_provider_save',
59+
'modai_admin_context_provider_delete',
60+
'modai_admin_agents',
61+
'modai_admin_agent_save',
62+
'modai_admin_agent_delete',
63+
'modai_admin_agent_tool_save',
64+
'modai_admin_agent_tool_delete',
65+
'modai_admin_agent_context_provider_save',
66+
'modai_admin_agent_context_provider_delete',
67+
'modai_admin_related_agent_save',
68+
'modai_admin_related_agent_delete',
69+
70+
'modai_client',
71+
'modai_client_text',
72+
'modai_client_vision',
73+
'modai_client_chat_text',
74+
'modai_client_chat_image',
75+
];
76+
77+
foreach ($permissions as $permission) {
78+
/** @var modAccessPermission $obj */
79+
$obj = $modx->getObject(modAccessPermission::class, [
80+
'template' => $template->get('id'),
81+
'name' => $permission
82+
]);
83+
84+
if (!$obj) {
85+
$obj = $modx->newObject(modAccessPermission::class);
86+
$obj->set('template', $template->get('id'));
87+
$obj->set('name', $permission);
88+
}
89+
90+
$obj->set('description', "modai.permissions.$permission");
91+
$obj->save();
92+
}
93+
94+
/** @var modAccessPolicy $adminPolicy */
95+
$adminPolicy = $modx->getObject(modAccessPolicy::class, ['name' => 'modAI Admin']);
96+
if (!$adminPolicy) {
97+
$adminPolicy = $modx->newObject(modAccessPolicy::class);
98+
$adminPolicy->set('name', 'modAI Admin');
99+
$adminPolicy->set('description', 'Administrator policy for modAI.');
100+
$adminPolicy->set('template', $template->get('id'));
101+
$adminPolicy->set('lexicon', $template->get('lexicon'));
102+
}
103+
104+
$data = [];
105+
106+
foreach ($permissions as $permission) {
107+
$data[$permission] = true;
108+
}
109+
110+
$adminPolicy->set('data', $data);
111+
$adminPolicy->save();
112+
113+
/** @var modUserGroup $adminUserGroup */
114+
$adminUserGroup = $modx->getObject(modUserGroup::class, ['id' => 1]);
115+
if ($adminUserGroup) {
116+
/** @var modContext[] $contexts */
117+
$contexts = $modx->getIterator(modContext::class);
118+
foreach ($contexts as $context) {
119+
$contextAccess = $modx->getObject(modAccessContext::Class, ['target' => $context->get('key'), 'principal_class' => modUserGroup::class, 'principal' => 1, 'policy' => $adminPolicy->get('id')]);
120+
if (!$contextAccess) {
121+
$contextAccess = $modx->newObject(modAccessContext::class);
122+
$contextAccess->set('target', $context->get('key'));
123+
$contextAccess->set('principal_class', modUserGroup::class);
124+
$contextAccess->set('principal', 1);
125+
$contextAccess->set('policy', $adminPolicy->get('id'));
126+
$contextAccess->set('authority', 0);
127+
$contextAccess->save();
128+
}
129+
}
130+
}
131+
132+
return true;
133+
}
134+
};

0 commit comments

Comments
 (0)