🐛 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:
Andrey Antukh 2026-08-05 20:00:24 +00:00
parent 0ac711aa68
commit ba8af3a1db
2 changed files with 111 additions and 0 deletions

View File

@ -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)})

View File

@ -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)))))))