mirror of
https://github.com/penpot/penpot.git
synced 2026-08-26 14:48:43 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
a83a5e2eff
@ -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]
|
||||||
|
|||||||
@ -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)))]
|
||||||
|
|||||||
@ -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)))))))
|
||||||
|
|||||||
@ -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)))))
|
||||||
|
|||||||
@ -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])))]]))
|
||||||
|
|||||||
@ -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")]]]])]]]]))
|
||||||
|
|||||||
@ -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]))))
|
||||||
|
|||||||
@ -10355,6 +10355,3 @@ 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"
|
|
||||||
|
|||||||
@ -10000,6 +10000,3 @@ 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"
|
|
||||||
Loading…
x
Reference in New Issue
Block a user