Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh 2026-08-25 09:41:31 +02:00
commit a83a5e2eff
9 changed files with 139 additions and 35 deletions

View File

@ -183,6 +183,7 @@
(sv/defmethod ::get-enabled-flags (sv/defmethod ::get-enabled-flags
{::audit/skip true {::audit/skip true
::rpc/auth false
::doc/skip true ::doc/skip true
::doc/added "1.20"} ::doc/added "1.20"}
[_cfg _params] [_cfg _params]

View File

@ -245,6 +245,10 @@
[cfg {:keys [::rpc/profile-id file-id fragment-id share-id]}] [cfg {:keys [::rpc/profile-id file-id fragment-id share-id]}]
(db/run! cfg (fn [cfg] (db/run! cfg (fn [cfg]
(let [perms (perms/get-file-read-permissions cfg profile-id file-id share-id)] (let [perms (perms/get-file-read-permissions cfg profile-id file-id share-id)]
(when (= :share-link (:type perms))
(ex/raise :type :not-found
:code :object-not-found
:hint "object not found"))
(check-read-permissions! perms) (check-read-permissions! perms)
(-> (get-file-fragment cfg file-id fragment-id) (-> (get-file-fragment cfg file-id fragment-id)
(rph/with-http-cache long-cache-duration)))))) (rph/with-http-cache long-cache-duration))))))
@ -392,6 +396,14 @@
(let [perms (perms/get-file-read-permissions cfg profile-id file-id share-id) (let [perms (perms/get-file-read-permissions cfg profile-id file-id share-id)
file (bfc/get-file cfg file-id :read-only? true) file (bfc/get-file cfg file-id :read-only? true)
resolved-page-id (or page-id (-> file :data :pages first))
_ (when (and (= :share-link (:type perms))
(not (contains? (:pages perms) resolved-page-id)))
(ex/raise :type :not-found
:code :object-not-found
:hint "object not found"))
proj (db/get conn :project {:id (:project-id file)}) proj (db/get conn :project {:id (:project-id file)})
team (-> (db/get conn :team {:id (:team-id proj)}) team (-> (db/get conn :team {:id (:team-id proj)})
@ -402,8 +414,7 @@
(cfeat/check-file-features! (:features file))) (cfeat/check-file-features! (:features file)))
page (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)] page (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
(let [page-id (or page-id (-> file :data :pages first)) (let [page (dm/get-in file [:data :pages-index resolved-page-id])]
page (dm/get-in file [:data :pages-index page-id])]
(if (pmap/pointer-map? page) (if (pmap/pointer-map? page)
(deref page) (deref page)
page)))] page)))]

View File

@ -2532,6 +2532,74 @@
share-links (:share-links (:result check))] share-links (:share-links (:result check))]
(t/is (some #(= slink2-id (:id %)) share-links))))))) (t/is (some #(= slink2-id (:id %)) share-links)))))))
(t/deftest share-link-page-scope-enforcement
(let [owner (th/create-profile* 1 {:is-active true})
viewer (th/create-profile* 2 {:is-active true})
proj-id (:default-project-id owner)
file (th/create-file* 1 {:profile-id (:id owner)
:project-id proj-id
:is-shared false})
page-a (get-in file [:data :pages 0])
page-b (uuid/random)
;; Add a second page to the file
_ (th/command! {::th/type :update-file
::rpc/profile-id (:id owner)
:id (:id file)
:session-id (uuid/random)
:revn 0
:vern 0
:changes [{:type :add-page
:id page-b
:page {:id page-b
:name "Page B"
:options {}
:objects {}}}]})
;; Create share-link scoped to page A only
share (th/command! {::th/type :create-share-link
::rpc/profile-id (:id owner)
:file-id (:id file)
:pages #{page-a}
:who-comment "team"
:who-inspect "all"})
share-id (get-in share [:result :id])]
(t/testing "share-link holder can access authorized page"
(let [out (th/command! {::th/type :get-page
::rpc/profile-id (:id viewer)
:file-id (:id file)
:page-id page-a
:share-id share-id})]
(t/is (nil? (:error out)))
(t/is (some? (:result out)))))
(t/testing "share-link holder cannot access out-of-scope page"
(let [out (th/command! {::th/type :get-page
::rpc/profile-id (:id viewer)
:file-id (:id file)
:page-id page-b
:share-id share-id})
err (:error out)
edata (ex-data err)]
(t/is (th/ex-info? err))
(t/is (= :not-found (:type edata)))
(t/is (= :object-not-found (:code edata)))))
(t/testing "team member can access all pages"
(let [out-a (th/command! {::th/type :get-page
::rpc/profile-id (:id owner)
:file-id (:id file)
:page-id page-a})
out-b (th/command! {::th/type :get-page
::rpc/profile-id (:id owner)
:file-id (:id file)
:page-id page-b})]
(t/is (nil? (:error out-a)))
(t/is (nil? (:error out-b)))))))
(t/deftest share-link-deletion-escape-hatches (t/deftest share-link-deletion-escape-hatches
(let [owner (th/create-profile* 1 {:is-active true}) (let [owner (th/create-profile* 1 {:is-active true})
editor (th/create-profile* 2 {:is-active true}) editor (th/create-profile* 2 {:is-active true})
@ -2594,3 +2662,35 @@
::rpc/profile-id (:id owner) ::rpc/profile-id (:id owner)
:id slink-id})] :id slink-id})]
(t/is (nil? (:error out))))))) (t/is (nil? (:error out)))))))
(t/deftest share-link-fragment-access-denied
(let [owner (th/create-profile* 1 {:is-active true})
viewer (th/create-profile* 2 {:is-active true})
proj-id (:default-project-id owner)
file (th/create-file* 1 {:profile-id (:id owner)
:project-id proj-id
:is-shared false})
page-a (get-in file [:data :pages 0])
;; Create share-link
share (th/command! {::th/type :create-share-link
::rpc/profile-id (:id owner)
:file-id (:id file)
:pages #{page-a}
:who-comment "team"
:who-inspect "all"})
share-id (get-in share [:result :id])]
(t/testing "share-link holder cannot access file fragments"
(let [out (th/command! {::th/type :get-file-fragment
::rpc/profile-id (:id viewer)
:file-id (:id file)
:fragment-id (uuid/random)
:share-id share-id})
err (:error out)
edata (ex-data err)]
(t/is (th/ex-info? err))
(t/is (= :not-found (:type edata)))
(t/is (= :object-not-found (:code edata)))))))

View File

@ -61,13 +61,18 @@
;; Delete old teams from state ;; Delete old teams from state
state (update state :teams #(select-keys % team-ids))] state (update state :teams #(select-keys % team-ids))]
(reduce (fn [state {:keys [id organization-id] :as team}] (reduce (fn [state {:keys [id organization-id] :as team}]
(let [team-updated (cond-> (merge (dm/get-in state [:teams id]) team) (let [team-merged (merge (dm/get-in state [:teams id]) team)
(not organization-id) (dissoc :organization-id has-org? (or (some? organization-id) (some? (:organization team)))
:organization-name team-updated (if has-org?
:organization-slug team-merged
:organization-owner-id (dissoc team-merged
:organization-avatar-bg-url :organization
:organization-permissions))] :organization-id
:organization-name
:organization-slug
:organization-owner-id
:organization-avatar-bg-url
:organization-permissions))]
(update state :teams assoc id team-updated))) (update state :teams assoc id team-updated)))
state state
teams))))) teams)))))

View File

@ -140,23 +140,16 @@
[:a {:on-click on-nav-settings} (tr "labels.settings")]]]] [:a {:on-click on-nav-settings} (tr "labels.settings")]]]]
[:div {:class (stl/css :dashboard-buttons)} [:div {:class (stl/css :dashboard-buttons)}
(when (and (or invitations-section? members-section?) (not-empty invitations)) (when (and (or invitations-section? members-section?) (not-empty invitations))
(let [organization (:organization team) (let [invite-button (mf/html
owners-only-invites? (and (contains? cfg/flags :admin-console) [:> button* {:class (stl/css :invite-button)
organization :variant "secondary"
(= (get-in organization [:permissions :send-invitations]) "owners")) :on-click on-invite-member
title-text (if owners-only-invites? :disabled (not can-invite?)
(tr "dashboard.invite-profile-disabled.owners-only" (:name organization)) :data-testid "invite-member"}
(tr "dashboard.invite-profile-disabled")) (tr "dashboard.invite-profile")])]
invite-button (mf/html
[:> button* {:class (stl/css :invite-button)
:variant "secondary"
:on-click on-invite-member
:disabled (not can-invite?)
:data-testid "invite-member"}
(tr "dashboard.invite-profile")])]
(if can-invite? (if can-invite?
invite-button invite-button
[:> tooltip* {:content title-text [:> tooltip* {:content (tr "dashboard.invite-profile-disabled")
:id "invite-member-disabled-tooltip" :id "invite-member-disabled-tooltip"
:tab-index 0} :tab-index 0}
invite-button])))]])) invite-button])))]]))

View File

@ -121,9 +121,7 @@
[:a {:class (stl/css :link) :href "mailto:sales@penpot.app"} [:a {:class (stl/css :link) :href "mailto:sales@penpot.app"}
"sales@penpot.app"]] "sales@penpot.app"]]
[:div {:class (stl/css :activation-code)} [:div {:class (stl/css :activation-code)}
[:p {:class (stl/css :modal-text-large)}
(tr "nitrate.form.subscribe-with-code")]
[:p {:class (stl/css :modal-text-large)} [:p {:class (stl/css :modal-text-large)}
[:a {:class (stl/css :link) [:a {:class (stl/css :link)
:on-click on-activate-click} :on-click on-activate-click}
(tr "nitrate.form.enter-code")]]]])]]]])) (tr "nitrate.form.subscribe-with-code")]]]])]]]]))

View File

@ -35,6 +35,7 @@
[:> toast* [:> toast*
{:level (or (:level notification) :info) {:level (or (:level notification) :info)
:type (:type notification) :type (:type notification)
:is-html (:is-html notification)
:detail (:detail notification) :detail (:detail notification)
:on-close on-close} :on-close on-close}
content] content]
@ -57,5 +58,6 @@
[:> toast* [:> toast*
{:level (or (:level notification) :info) {:level (or (:level notification) :info)
:type (:type notification) :type (:type notification)
:is-html (:is-html notification)
:detail (:detail notification) :detail (:detail notification)
:on-close on-close} content])))) :on-close on-close} content]))))

View File

@ -10354,7 +10354,4 @@ msgid "labels.sso-error.retry"
msgstr "Try again" msgstr "Try again"
msgid "dashboard.invite-profile-disabled" msgid "dashboard.invite-profile-disabled"
msgstr "You don't have permission to invite people to this team" msgstr "You don't have permission to invite people to this team"
msgid "dashboard.invite-profile-disabled.owners-only"
msgstr "Only team owners can invite within %s"

View File

@ -9999,7 +9999,4 @@ msgid "labels.sso-error.retry"
msgstr "Intentar de nuevo" msgstr "Intentar de nuevo"
msgid "dashboard.invite-profile-disabled" msgid "dashboard.invite-profile-disabled"
msgstr "No tienes permiso para invitar a personas a este equipo" msgstr "No tienes permiso para invitar a personas a este equipo"
msgid "dashboard.invite-profile-disabled.owners-only"
msgstr "Solo los propietarios del equipo pueden invitar dentro de %s"