diff --git a/plugins/computers/api/policy_routes.py b/plugins/computers/api/policy_routes.py index a515acc8..65592e4e 100644 --- a/plugins/computers/api/policy_routes.py +++ b/plugins/computers/api/policy_routes.py @@ -40,6 +40,23 @@ def _known_actions(): return set(COMMAND_ALLOWLIST.keys()) +def _is_valid_action(name): + """True when an action name is either an allowlisted command or a monitor + attachment (monitor:) pointing at an existing monitor definition. + + Monitor actions are intentionally kept out of the command allowlist so they + are never enqueued as commands; they are only valid as policy attachments. + """ + if name in _known_actions(): + return True + from plugins.endpoints.commands import monitor_id_from_action + from plugins.endpoints.models import EndpointMonitor + monitorid = monitor_id_from_action(name) + if monitorid is None: + return False + return db.session.get(EndpointMonitor, monitorid) is not None + + def _configpolicy_dict(policy): data = policy.to_dict() data['actions'] = [a.to_dict() for a in policy.actions] @@ -89,11 +106,15 @@ def _apply_actions(policy, actions): Returns an error_response on failure, else None. """ - known = _known_actions() policy.actions.clear() + # Flush the deletes before re-appending. The (configurationpolicyid, action) + # unique constraint collides if SQLAlchemy orders a re-added action's INSERT + # before the old row's DELETE in the same flush, which surfaces as a generic + # "value already in use" IntegrityError on an otherwise valid edit. + db.session.flush() for row in actions or []: name = (row.get('action') or '').strip() - if name not in known: + if not _is_valid_action(name): return error_response(ErrorCodes.VALIDATION_ERROR, f'Unknown action: {name!r}') mode = (row.get('mode') or ACTION_MODE_GATE).strip() @@ -259,6 +280,10 @@ def get_business_policy(policy_id: int): def _apply_items(policy, items): """Replace a business policy's configuration-policy items, validating each.""" policy.items.clear() + # Flush the deletes before re-appending so the (businesspolicyid, + # configurationpolicyid) unique constraint does not collide with a re-added + # item whose INSERT would otherwise race ahead of the old row's DELETE. + db.session.flush() seen = set() for order, row in enumerate(items or []): cpid = row.get('configurationpolicyid') @@ -375,14 +400,19 @@ def delete_business_policy(policy_id: int): @computers_bp.route('/policies/actions', methods=['GET']) @require_permission('computers.view') def list_policy_actions(): - """The allowlisted action names + labels a policy may reference.""" + """The allowlisted action names + labels a policy may reference. + + Returns command actions tagged kind='command'. Monitor attachments are not + listed here: the UI builds that branch from the endpoints monitor catalog + (endpointsApi.monitors) so newly created monitors appear without a reload. + """ gate = _feature_gate() if gate: return gate from plugins.endpoints.commands import COMMAND_ALLOWLIST return success_response([ {'action': name, 'label': meta.get('label'), - 'description': meta.get('description')} + 'description': meta.get('description'), 'kind': 'command'} for name, meta in COMMAND_ALLOWLIST.items() ]) diff --git a/plugins/computers/frontend/routes.js b/plugins/computers/frontend/routes.js index 3d7d0895..e1464b56 100644 --- a/plugins/computers/frontend/routes.js +++ b/plugins/computers/frontend/routes.js @@ -70,8 +70,8 @@ export default [ meta: { requiresAuth: true, requiresAdmin: true, plugin: 'computers' } }, { - path: 'settings/business-policies', - name: 'business-policies', + path: 'settings/site-policies', + name: 'site-policies', component: () => import('./views/BusinessPoliciesList.vue'), meta: { requiresAuth: true, requiresAdmin: true, plugin: 'computers' } } diff --git a/plugins/computers/frontend/views/ActionPicker.vue b/plugins/computers/frontend/views/ActionPicker.vue new file mode 100644 index 00000000..7111d195 --- /dev/null +++ b/plugins/computers/frontend/views/ActionPicker.vue @@ -0,0 +1,265 @@ + + + + + diff --git a/plugins/computers/frontend/views/BusinessPoliciesList.vue b/plugins/computers/frontend/views/BusinessPoliciesList.vue index 4a6b20ae..e2f3b971 100644 --- a/plugins/computers/frontend/views/BusinessPoliciesList.vue +++ b/plugins/computers/frontend/views/BusinessPoliciesList.vue @@ -1,8 +1,8 @@