🐛 Add content:write permission checks to flow and flex layout plugin API

Add permission checks to prototype flow and flex layout operations
that were missing them, allowing plugins to modify flows and layout
structure without explicit user permission.

Changes:
- page.cljs: Add content:write checks to flow-proxy (name,
  startingBoard setters, remove) and page-proxy (createFlow,
  removeFlow)
- flex.cljs: Add content:write checks to flex-layout-proxy
  (remove, appendChild)

Follows the established pattern from tokens.cljs, shape.cljs,
and library.cljs.

Relates to #11137

AI-assisted-by: qwen3.7-plus
This commit is contained in:
Andrey Antukh 2026-08-18 11:06:09 +00:00
parent 26b786df72
commit 796a76ff32
5 changed files with 156 additions and 2 deletions

View File

@ -325,7 +325,12 @@
:remove :remove
(fn [] (fn []
(st/emit! (dwsl/remove-layout #{id}))) (cond
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :remove "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/remove-layout #{id}))))
:appendChild :appendChild
(fn [child] (fn [child]
@ -350,6 +355,9 @@
(u/changes-component-copy-structure? objects shape child-shape) (u/changes-component-copy-structure? objects shape child-shape)
(u/not-valid plugin-id :appendChild "Cannot change the structure of a component copy") (u/not-valid plugin-id :appendChild "Cannot change the structure of a component copy")
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :appendChild "Plugin doesn't have 'content:write' permission")
:else :else
(st/emit! (st/emit!
(dwsh/relocate-shapes #{child-id} id index) (dwsh/relocate-shapes #{child-id} id index)

View File

@ -64,6 +64,9 @@
(or (not (string? value)) (empty? value)) (or (not (string? value)) (empty? value))
(u/not-valid plugin-id :name value) (u/not-valid plugin-id :name value)
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission")
:else :else
(st/emit! (dwi/update-flow page-id id #(assoc % :name value)))))} (st/emit! (dwi/update-flow page-id id #(assoc % :name value)))))}
@ -79,12 +82,20 @@
(not (shape/shape-proxy? value)) (not (shape/shape-proxy? value))
(u/not-valid plugin-id :startingBoard value) (u/not-valid plugin-id :startingBoard value)
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :startingBoard "Plugin doesn't have 'content:write' permission")
:else :else
(st/emit! (dwi/update-flow page-id id #(assoc % :starting-frame (obj/get value "$id"))))))} (st/emit! (dwi/update-flow page-id id #(assoc % :starting-frame (obj/get value "$id"))))))}
:remove :remove
(fn [] (fn []
(st/emit! (dwi/remove-flow page-id id))))) (cond
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :remove "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwi/remove-flow page-id id))))))
(defn page-proxy? [proxy] (defn page-proxy? [proxy]
(obj/type-of? proxy "PageProxy")) (obj/type-of? proxy "PageProxy"))
@ -315,6 +326,9 @@
(not (shape/shape-proxy? frame)) (not (shape/shape-proxy? frame))
(u/not-valid plugin-id :createFlow-frame frame) (u/not-valid plugin-id :createFlow-frame frame)
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :createFlow "Plugin doesn't have 'content:write' permission")
:else :else
(let [flow-id (uuid/next)] (let [flow-id (uuid/next)]
(st/emit! (st/emit!
@ -328,6 +342,9 @@
(not (flow-proxy? flow)) (not (flow-proxy? flow))
(u/not-valid plugin-id :removeFlow-flow flow) (u/not-valid plugin-id :removeFlow-flow flow)
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :removeFlow "Plugin doesn't have 'content:write' permission")
:else :else
(st/emit! (st/emit!
(dwi/remove-flow id (obj/get flow "$id")) (dwi/remove-flow id (obj/get flow "$id"))

View File

@ -0,0 +1,40 @@
;; 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 frontend-tests.plugins.flex-test
(:require
[app.common.types.shape.layout :as ctl]
[app.common.uuid :as uuid]
[app.main.store :as st]
[app.plugins.flex :as flex]
[app.plugins.register :as r]
[app.plugins.shape :as shape]
[app.plugins.utils :as u]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.mock :as mock]))
;; ---------------------------------------------------------------------------
;; Permission checks (T9-F-05)
;; ---------------------------------------------------------------------------
(t/deftest flex-remove-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
page-id (uuid/next)
id (uuid/next)
errors (atom [])]
(with-redefs [r/check-permission (constantly false)
u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
st/emit! mock/noop]
(let [proxy (flex/flex-layout-proxy plugin-id file-id page-id id)]
(.remove proxy)
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :remove "Plugin doesn't have 'content:write' permission"]
(first @errors)))))))
;; TODO: flex-append-child-checks-permission test requires more complex mocking
;; of u/locate-objects, u/locate-shape, ctl/reverse?, etc. The permission check
;; is in place at flex.cljs line 358.

View File

@ -9,12 +9,17 @@
[app.common.test-helpers.files :as cthf] [app.common.test-helpers.files :as cthf]
[app.common.test-helpers.ids-map :as thi] [app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as cths] [app.common.test-helpers.shapes :as cths]
[app.common.uuid :as uuid]
[app.main.data.workspace.pages :as dwpg] [app.main.data.workspace.pages :as dwpg]
[app.main.store :as st] [app.main.store :as st]
[app.plugins.api :as api] [app.plugins.api :as api]
[app.plugins.page :as page]
[app.plugins.register :as r]
[app.plugins.shape :as shape] [app.plugins.shape :as shape]
[app.plugins.utils :as u]
[app.util.object :as obj] [app.util.object :as obj]
[cljs.test :as t :include-macros true] [cljs.test :as t :include-macros true]
[frontend-tests.helpers.mock :as mock]
[frontend-tests.helpers.state :as ths] [frontend-tests.helpers.state :as ths]
[frontend-tests.helpers.wasm :as thw] [frontend-tests.helpers.wasm :as thw]
[potok.v2.core :as ptk])) [potok.v2.core :as ptk]))
@ -151,3 +156,85 @@
(done)))) (done))))
(mock-page-initialized store page2-id)) (mock-page-initialized store page2-id))
0)))) 0))))
;; ---------------------------------------------------------------------------
;; Permission checks (T9-F-03)
;; ---------------------------------------------------------------------------
(t/deftest flow-name-setter-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
page-id (uuid/next)
flow-id (uuid/next)
errors (atom [])]
(with-redefs [r/check-permission (constantly false)
u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
st/emit! mock/noop]
(let [proxy (page/flow-proxy plugin-id file-id page-id flow-id)]
(set! (.-name proxy) "new-name")
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :name "Plugin doesn't have 'content:write' permission"]
(first @errors)))))))
(t/deftest flow-starting-board-setter-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
page-id (uuid/next)
flow-id (uuid/next)
errors (atom [])]
(with-redefs [r/check-permission (constantly false)
u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
st/emit! mock/noop
shape/shape-proxy? (constantly true)]
(let [proxy (page/flow-proxy plugin-id file-id page-id flow-id)]
(set! (.-startingBoard proxy) #js {})
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :startingBoard "Plugin doesn't have 'content:write' permission"]
(first @errors)))))))
(t/deftest flow-remove-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
page-id (uuid/next)
flow-id (uuid/next)
errors (atom [])]
(with-redefs [r/check-permission (constantly false)
u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
st/emit! mock/noop]
(let [proxy (page/flow-proxy plugin-id file-id page-id flow-id)]
(.remove proxy)
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :remove "Plugin doesn't have 'content:write' permission"]
(first @errors)))))))
(t/deftest create-flow-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
page-id (uuid/next)
errors (atom [])]
(with-redefs [r/check-permission (constantly false)
u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
st/emit! mock/noop
shape/shape-proxy? (constantly true)]
(let [proxy (page/page-proxy plugin-id file-id page-id)
frame #js {"$id" (uuid/next)}]
(.createFlow proxy "flow-name" frame)
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :createFlow "Plugin doesn't have 'content:write' permission"]
(first @errors)))))))
(t/deftest remove-flow-checks-permission
(let [plugin-id "test-plugin"
file-id (uuid/next)
page-id (uuid/next)
flow-id (uuid/next)
errors (atom [])]
(with-redefs [r/check-permission (constantly false)
u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg])))
st/emit! mock/noop
page/flow-proxy? (constantly true)]
(let [proxy (page/page-proxy plugin-id file-id page-id)]
(.removeFlow proxy (page/flow-proxy plugin-id file-id page-id flow-id))
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :removeFlow "Plugin doesn't have 'content:write' permission"]
(first @errors)))))))

View File

@ -41,6 +41,7 @@
[frontend-tests.plugins.comments-test] [frontend-tests.plugins.comments-test]
[frontend-tests.plugins.context-shapes-test] [frontend-tests.plugins.context-shapes-test]
[frontend-tests.plugins.file-test] [frontend-tests.plugins.file-test]
[frontend-tests.plugins.flex-test]
[frontend-tests.plugins.format-test] [frontend-tests.plugins.format-test]
[frontend-tests.plugins.grid-test] [frontend-tests.plugins.grid-test]
[frontend-tests.plugins.interactions-test] [frontend-tests.plugins.interactions-test]
@ -137,6 +138,7 @@
'frontend-tests.plugins.comments-test 'frontend-tests.plugins.comments-test
'frontend-tests.plugins.context-shapes-test 'frontend-tests.plugins.context-shapes-test
'frontend-tests.plugins.file-test 'frontend-tests.plugins.file-test
'frontend-tests.plugins.flex-test
'frontend-tests.plugins.format-test 'frontend-tests.plugins.format-test
'frontend-tests.plugins.grid-test 'frontend-tests.plugins.grid-test
'frontend-tests.plugins.interactions-test 'frontend-tests.plugins.interactions-test