Andrey Antukh a322a5352a 🐛 Add dedicated RPC methods for plugin registry operations
Add `add-profile-plugin` and `remove-profile-plugin` RPC methods for
atomic plugin registry operations, preventing manipulation via the
broader `update-profile-props` endpoint.

- Close the `:plugins` field in `update-profile-props` schema to
  eliminate the mass assignment attack vector for plugin data.
- Define `valid-permissions` and a closed `schema:permissions` enum to
  restrict plugin permissions to known values.
- Migrate the frontend to use the new granular RPC methods with
  optimistic updates and rollback on failure.
- Add comprehensive backend tests covering valid/invalid permissions,
  updates, removal, and rejection via old endpoint.

AI-assisted-by: qwen3.7-plus
2026-08-07 18:42:26 +02:00

76 lines
2.8 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.rpc.commands.plugins
(:require
[app.common.exceptions :as ex]
[app.common.schema :as sm]
[app.common.types.plugins :as ctp]
[app.db :as db]
[app.rpc :as-alias rpc]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]))
(defn- validate-plugin-permissions!
"Validates that all permissions in the plugin are within the valid set."
[plugin]
(let [permissions (:permissions plugin)
invalid (remove ctp/valid-permissions permissions)]
(when (seq invalid)
(ex/raise :type :validation
:code :invalid-plugin-permissions
:hint (str "Invalid permissions: " (pr-str (set invalid)))
:invalid-permissions (set invalid)))))
(def ^:private
schema:add-profile-plugin
[:map {:title "add-profile-plugin"}
[:plugin ctp/schema:registry-entry]])
(sv/defmethod ::add-profile-plugin
{::doc/added "2.18"
::sm/params schema:add-profile-plugin
::sm/result ctp/schema:registry-entry
::db/transaction true}
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id plugin]}]
(validate-plugin-permissions! plugin)
(let [profile (profile/get-profile conn profile-id ::db/for-update true)
plugins (get-in profile [:props :plugins] {:ids [] :data {}})
plugin-id (:plugin-id plugin)
plugins (-> plugins
(update :ids #(vec (distinct (conj % plugin-id))))
(assoc-in [:data plugin-id] plugin))]
(db/update! conn :profile
{:props (db/tjson (assoc (:props profile) :plugins plugins))}
{:id profile-id}
{::db/return-keys false})
plugin))
(def ^:private
schema:remove-profile-plugin
[:map {:title "remove-profile-plugin"}
[:plugin-id ::sm/uuid]])
(sv/defmethod ::remove-profile-plugin
{::doc/added "2.18"
::sm/params schema:remove-profile-plugin
::sm/result :nil
::db/transaction true}
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id plugin-id]}]
(let [profile (profile/get-profile conn profile-id ::db/for-update true)
plugins (get-in profile [:props :plugins] {:ids [] :data {}})
plugin-id-str (str plugin-id)
plugins (-> plugins
(update :ids #(vec (remove (partial = plugin-id-str) %)))
(update :data dissoc plugin-id-str))]
(db/update! conn :profile
{:props (db/tjson (assoc (:props profile) :plugins plugins))}
{:id profile-id}
{::db/return-keys false})
nil))