mirror of
https://github.com/penpot/penpot.git
synced 2026-08-07 13:29:07 +00:00
🐛 Prevent admins from granting owner role in team invitations
Add role-ceiling check to create-team-invitations and update-team-invitation-role methods. These RPC methods allowed team admins to grant or elevate invitations to :owner role, bypassing the protection that exists in update-team-member-role. The fix replicates the existing check from update-team-member-role: reject promotion to :owner when the caller is not an owner. Closes #11098 AI-assisted-by: qwen3.7-plus
This commit is contained in:
parent
0ac711aa68
commit
ba8af3a1db
@ -482,6 +482,13 @@
|
|||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :insufficient-permissions))
|
:code :insufficient-permissions))
|
||||||
|
|
||||||
|
;; Don't allow promote to owner to admin users.
|
||||||
|
(when (and (not (:is-owner perms))
|
||||||
|
(or (= role :owner)
|
||||||
|
(some #(= :owner (:role %)) (:invitations params))))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :cant-promote-to-owner))
|
||||||
|
|
||||||
(when (> invitation-count max-invitations-by-request-threshold)
|
(when (> invitation-count max-invitations-by-request-threshold)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :max-invitations-by-request
|
:code :max-invitations-by-request
|
||||||
@ -625,6 +632,11 @@
|
|||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :insufficient-permissions))
|
:code :insufficient-permissions))
|
||||||
|
|
||||||
|
;; Don't allow promote to owner to admin users.
|
||||||
|
(when (and (not (:is-owner perms)) (= role :owner))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :cant-promote-to-owner))
|
||||||
|
|
||||||
(db/update! conn :team-invitation
|
(db/update! conn :team-invitation
|
||||||
{:role (name role) :updated-at (ct/now)}
|
{:role (name role) :updated-at (ct/now)}
|
||||||
{:team-id team-id :email-to (profile/clean-email email)})
|
{:team-id team-id :email-to (profile/clean-email email)})
|
||||||
|
|||||||
@ -1096,3 +1096,102 @@
|
|||||||
:name "My Valid Team"}
|
:name "My Valid Team"}
|
||||||
out (th/command! data)]
|
out (th/command! data)]
|
||||||
(t/is (th/success? out)))))
|
(t/is (th/success? out)))))
|
||||||
|
|
||||||
|
;; --- T7-F-01: Role ceiling in team invitations ---
|
||||||
|
|
||||||
|
(t/deftest admin-cannot-create-invitation-with-owner-role
|
||||||
|
(with-mocks [mock {:target 'app.email/send! :return nil}]
|
||||||
|
(let [owner (th/create-profile* 1 {:is-active true})
|
||||||
|
admin (th/create-profile* 2 {:is-active true})
|
||||||
|
team (th/create-team* 1 {:profile-id (:id owner)})]
|
||||||
|
|
||||||
|
;; Add admin as team member with :admin role
|
||||||
|
(th/create-team-role* {:team-id (:id team)
|
||||||
|
:profile-id (:id admin)
|
||||||
|
:role :admin})
|
||||||
|
|
||||||
|
;; Admin tries to create invitation with :owner role (emails+role format)
|
||||||
|
;; This should FAIL with :cant-promote-to-owner
|
||||||
|
(let [data {::th/type :create-team-invitations
|
||||||
|
::rpc/profile-id (:id admin)
|
||||||
|
:team-id (:id team)
|
||||||
|
:role :owner
|
||||||
|
:emails ["invitee@example.com"]}
|
||||||
|
out (th/command! data)]
|
||||||
|
(t/is (not (th/success? out)))
|
||||||
|
(t/is (th/ex-of-type? (:error out) :validation))
|
||||||
|
(t/is (th/ex-of-code? (:error out) :cant-promote-to-owner))
|
||||||
|
(t/is (= 0 (:call-count @mock)))))))
|
||||||
|
|
||||||
|
(t/deftest admin-cannot-create-invitation-with-owner-role-invitations-format
|
||||||
|
(with-mocks [mock {:target 'app.email/send! :return nil}]
|
||||||
|
(let [owner (th/create-profile* 1 {:is-active true})
|
||||||
|
admin (th/create-profile* 2 {:is-active true})
|
||||||
|
team (th/create-team* 1 {:profile-id (:id owner)})]
|
||||||
|
|
||||||
|
;; Add admin as team member with :admin role
|
||||||
|
(th/create-team-role* {:team-id (:id team)
|
||||||
|
:profile-id (:id admin)
|
||||||
|
:role :admin})
|
||||||
|
|
||||||
|
;; Admin tries to create invitation with :owner role (invitations format)
|
||||||
|
;; This should FAIL with :cant-promote-to-owner
|
||||||
|
(let [data {::th/type :create-team-invitations
|
||||||
|
::rpc/profile-id (:id admin)
|
||||||
|
:team-id (:id team)
|
||||||
|
:invitations [{:email "invitee@example.com" :role :owner}]}
|
||||||
|
out (th/command! data)]
|
||||||
|
(t/is (not (th/success? out)))
|
||||||
|
(t/is (th/ex-of-type? (:error out) :validation))
|
||||||
|
(t/is (th/ex-of-code? (:error out) :cant-promote-to-owner))
|
||||||
|
(t/is (= 0 (:call-count @mock)))))))
|
||||||
|
|
||||||
|
(t/deftest admin-cannot-update-invitation-role-to-owner
|
||||||
|
(with-mocks [mock {:target 'app.email/send! :return nil}]
|
||||||
|
(let [owner (th/create-profile* 1 {:is-active true})
|
||||||
|
admin (th/create-profile* 2 {:is-active true})
|
||||||
|
team (th/create-team* 1 {:profile-id (:id owner)})]
|
||||||
|
|
||||||
|
;; Add admin as team member with :admin role
|
||||||
|
(th/create-team-role* {:team-id (:id team)
|
||||||
|
:profile-id (:id admin)
|
||||||
|
:role :admin})
|
||||||
|
|
||||||
|
;; Owner creates an invitation with :editor role
|
||||||
|
(let [data {::th/type :create-team-invitations
|
||||||
|
::rpc/profile-id (:id owner)
|
||||||
|
:team-id (:id team)
|
||||||
|
:role :editor
|
||||||
|
:emails ["invitee@example.com"]}
|
||||||
|
out (th/command! data)]
|
||||||
|
(t/is (th/success? out)))
|
||||||
|
|
||||||
|
(th/reset-mock! mock)
|
||||||
|
|
||||||
|
;; Admin tries to update invitation role to :owner
|
||||||
|
;; This should FAIL with :cant-promote-to-owner
|
||||||
|
(let [data {::th/type :update-team-invitation-role
|
||||||
|
::rpc/profile-id (:id admin)
|
||||||
|
:team-id (:id team)
|
||||||
|
:email "invitee@example.com"
|
||||||
|
:role :owner}
|
||||||
|
out (th/command! data)]
|
||||||
|
(t/is (not (th/success? out)))
|
||||||
|
(t/is (th/ex-of-type? (:error out) :validation))
|
||||||
|
(t/is (th/ex-of-code? (:error out) :cant-promote-to-owner))))))
|
||||||
|
|
||||||
|
(t/deftest owner-can-create-invitation-with-owner-role
|
||||||
|
(with-mocks [mock {:target 'app.email/send! :return nil}]
|
||||||
|
(let [owner (th/create-profile* 1 {:is-active true})
|
||||||
|
team (th/create-team* 1 {:profile-id (:id owner)})]
|
||||||
|
|
||||||
|
;; Owner creates invitation with :owner role
|
||||||
|
;; This should SUCCEED (owner has full privileges)
|
||||||
|
(let [data {::th/type :create-team-invitations
|
||||||
|
::rpc/profile-id (:id owner)
|
||||||
|
:team-id (:id team)
|
||||||
|
:role :owner
|
||||||
|
:emails ["invitee@example.com"]}
|
||||||
|
out (th/command! data)]
|
||||||
|
(t/is (th/success? out))
|
||||||
|
(t/is (= 1 (:call-count @mock)))))))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user