mirror of
https://github.com/penpot/penpot.git
synced 2026-08-07 05:18:36 +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
|
||||
: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)
|
||||
(ex/raise :type :validation
|
||||
:code :max-invitations-by-request
|
||||
@ -625,6 +632,11 @@
|
||||
(ex/raise :type :validation
|
||||
: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
|
||||
{:role (name role) :updated-at (ct/now)}
|
||||
{:team-id team-id :email-to (profile/clean-email email)})
|
||||
|
||||
@ -1096,3 +1096,102 @@
|
||||
:name "My Valid Team"}
|
||||
out (th/command! data)]
|
||||
(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