Commit 9ee36ff7 authored by Doug Stull's avatar Doug Stull Committed by Imre Farkas

Add more helpful error when not authorized to update

- currently it defaults to model validations, which isn't correct

Changelog: changed
parent 03de48c2
...@@ -650,7 +650,6 @@ Layout/LineLength: ...@@ -650,7 +650,6 @@ Layout/LineLength:
- 'app/services/loose_foreign_keys/batch_cleaner_service.rb' - 'app/services/loose_foreign_keys/batch_cleaner_service.rb'
- 'app/services/loose_foreign_keys/cleaner_service.rb' - 'app/services/loose_foreign_keys/cleaner_service.rb'
- 'app/services/members/approve_access_request_service.rb' - 'app/services/members/approve_access_request_service.rb'
- 'app/services/members/create_service.rb'
- 'app/services/members/destroy_service.rb' - 'app/services/members/destroy_service.rb'
- 'app/services/members/invitation_reminder_email_service.rb' - 'app/services/members/invitation_reminder_email_service.rb'
- 'app/services/members/mailgun/process_webhook_service.rb' - 'app/services/members/mailgun/process_webhook_service.rb'
......
...@@ -16,6 +16,7 @@ module Members ...@@ -16,6 +16,7 @@ module Members
@errors = [] @errors = []
@invites = invites_from_params @invites = invites_from_params
@source = params[:source] @source = params[:source]
@tasks_to_be_done_members = []
end end
def execute def execute
...@@ -25,6 +26,7 @@ module Members ...@@ -25,6 +26,7 @@ module Members
validate_invitable! validate_invitable!
add_members add_members
create_tasks_to_be_done
enqueue_onboarding_progress_action enqueue_onboarding_progress_action
publish_event! publish_event!
...@@ -40,7 +42,8 @@ module Members ...@@ -40,7 +42,8 @@ module Members
private private
attr_reader :source, :errors, :invites, :member_created_namespace_id, :members attr_reader :source, :errors, :invites, :member_created_namespace_id, :members,
:tasks_to_be_done_members, :member_created_member_task_id
def invites_from_params def invites_from_params
return params[:user_ids] if params[:user_ids].is_a?(Array) return params[:user_ids] if params[:user_ids].is_a?(Array)
...@@ -76,13 +79,15 @@ module Members ...@@ -76,13 +79,15 @@ module Members
) )
members.each { |member| process_result(member) } members.each { |member| process_result(member) }
create_tasks_to_be_done
end end
def process_result(member) def process_result(member)
if member.invalid? existing_errors = member.errors.full_messages
add_error_for_member(member)
# calling invalid? clears any errors that were added outside of the
# rails validation process
if member.invalid? || existing_errors.present?
add_error_for_member(member, existing_errors)
else else
after_execute(member: member) after_execute(member: member)
@member_created_namespace_id ||= member.namespace_id @member_created_namespace_id ||= member.namespace_id
...@@ -90,20 +95,29 @@ module Members ...@@ -90,20 +95,29 @@ module Members
end end
# overridden # overridden
def add_error_for_member(member) def add_error_for_member(member, existing_errors)
prefix = "#{member.user.username}: " if member.user.present? prefix = "#{member.user.username}: " if member.user.present?
errors << "#{prefix}#{member.errors.full_messages.to_sentence}" errors << "#{prefix}#{all_member_errors(member, existing_errors).to_sentence}"
end
def all_member_errors(member, existing_errors)
existing_errors.concat(member.errors.full_messages).uniq
end end
def after_execute(member:) def after_execute(member:)
super super
build_tasks_to_be_done_members(member)
track_invite_source(member) track_invite_source(member)
end end
def track_invite_source(member) def track_invite_source(member)
Gitlab::Tracking.event(self.class.name, 'create_member', label: invite_source, property: tracking_property(member), user: current_user) Gitlab::Tracking.event(self.class.name,
'create_member',
label: invite_source,
property: tracking_property(member),
user: current_user)
end end
def invite_source def invite_source
...@@ -117,17 +131,28 @@ module Members ...@@ -117,17 +131,28 @@ module Members
member.invite? ? 'net_new_user' : 'existing_user' member.invite? ? 'net_new_user' : 'existing_user'
end end
def create_tasks_to_be_done def build_tasks_to_be_done_members(member)
return if params[:tasks_to_be_done].blank? || params[:tasks_project_id].blank? return unless tasks_to_be_done?(member)
# Only create task issues for existing users. Tasks for new users are created when they signup.
valid_members = members.select { |member| member.valid? && member.member_task.valid? && member.user.present? }
return unless valid_members.present?
@tasks_to_be_done_members << member
# We can take the first `member_task` here, since all tasks will have the same attributes needed # We can take the first `member_task` here, since all tasks will have the same attributes needed
# for the `TasksToBeDone::CreateWorker`, ie. `project` and `tasks_to_be_done`. # for the `TasksToBeDone::CreateWorker`, ie. `project` and `tasks_to_be_done`.
member_task = valid_members[0].member_task @member_created_member_task_id ||= member.member_task.id
TasksToBeDone::CreateWorker.perform_async(member_task.id, current_user.id, valid_members.map(&:user_id)) end
def tasks_to_be_done?(member)
return false if params[:tasks_to_be_done].blank? || params[:tasks_project_id].blank?
# Only create task issues for existing users. Tasks for new users are created when they signup.
member.member_task&.valid? && member.user.present?
end
def create_tasks_to_be_done
return unless member_created_member_task_id # signal if there is any work to be done here
TasksToBeDone::CreateWorker.perform_async(member_created_member_task_id,
current_user.id,
tasks_to_be_done_members.map(&:user_id))
end end
def user_limit def user_limit
......
...@@ -4,15 +4,13 @@ module Members ...@@ -4,15 +4,13 @@ module Members
# This class serves as more of an app-wide way we add/create members # This class serves as more of an app-wide way we add/create members
# All roads to add members should take this path. # All roads to add members should take this path.
class CreatorService class CreatorService
include Gitlab::Experiment::Dsl
class << self class << self
def parsed_access_level(access_level) def parsed_access_level(access_level)
access_levels.fetch(access_level) { access_level.to_i } access_levels.fetch(access_level) { access_level.to_i }
end end
def access_levels def access_levels
raise NotImplementedError Gitlab::Access.sym_options_with_owner
end end
end end
...@@ -25,7 +23,7 @@ module Members ...@@ -25,7 +23,7 @@ module Members
def execute def execute
find_or_build_member find_or_build_member
update_member commit_member
create_member_task create_member_task
member member
...@@ -33,23 +31,39 @@ module Members ...@@ -33,23 +31,39 @@ module Members
private private
delegate :new_record?, to: :member
attr_reader :source, :user, :access_level, :member, :args attr_reader :source, :user, :access_level, :member, :args
def update_member def assign_member_attributes
return unless can_update_member?
member.attributes = member_attributes member.attributes = member_attributes
end
if member.request? def commit_member
approve_request if can_commit_member?
assign_member_attributes
commit_changes
else else
member.save add_commit_error
end end
end end
def can_update_member? def can_commit_member?
# There is no current user for bulk actions, in which case anything is allowed # There is no current user for bulk actions, in which case anything is allowed
!current_user # inheriting classes will add more logic return true if skip_authorization?
if new_record?
can_create_new_member?
else
can_update_existing_member?
end
end
def can_create_new_member?
raise NotImplementedError
end
def can_update_existing_member?
raise NotImplementedError
end end
# Populates the attributes of a member. # Populates the attributes of a member.
...@@ -64,6 +78,14 @@ module Members ...@@ -64,6 +78,14 @@ module Members
} }
end end
def commit_changes
if member.request?
approve_request
else
member.save
end
end
def create_member_task def create_member_task
return unless member.persisted? return unless member.persisted?
return if member_task_attributes.value?(nil) return if member_task_attributes.value?(nil)
...@@ -93,6 +115,20 @@ module Members ...@@ -93,6 +115,20 @@ module Members
args[:current_user] args[:current_user]
end end
def skip_authorization?
!current_user
end
def add_commit_error
msg = if new_record?
_('not authorized to create member')
else
_('not authorized to update member')
end
member.errors.add(:base, msg)
end
def find_or_build_member def find_or_build_member
@user = parse_user_param @user = parse_user_param
......
...@@ -3,14 +3,14 @@ ...@@ -3,14 +3,14 @@
module Members module Members
module Groups module Groups
class CreatorService < Members::CreatorService class CreatorService < Members::CreatorService
def self.access_levels
Gitlab::Access.sym_options_with_owner
end
private private
def can_update_member? def can_create_new_member?
super || current_user.can?(:update_group_member, member) current_user.can?(:admin_group_member, member.group)
end
def can_update_existing_member?
current_user.can?(:update_group_member, member)
end end
end end
end end
......
...@@ -51,8 +51,8 @@ module Members ...@@ -51,8 +51,8 @@ module Members
end end
override :add_error_for_member override :add_error_for_member
def add_error_for_member(member) def add_error_for_member(member, existing_errors)
errors[invited_object(member)] = member.errors.full_messages.to_sentence errors[invited_object(member)] = all_member_errors(member, existing_errors).to_sentence
end end
def invited_object(member) def invited_object(member)
......
...@@ -3,19 +3,28 @@ ...@@ -3,19 +3,28 @@
module Members module Members
module Projects module Projects
class CreatorService < Members::CreatorService class CreatorService < Members::CreatorService
def self.access_levels
Gitlab::Access.sym_options_with_owner
end
private private
def can_update_member? def can_create_new_member?
super || current_user.can?(:update_project_member, member) || adding_the_creator_as_owner_in_a_personal_project? # order is important here!
# The `admin_project_member` check has side-effects that causes projects not be created if this area is hit
# during project creation.
# Call that triggers is current_user.can?(:admin_project_member, member.project)
# I tracked back to base_policy.rb admin check and specifically in
# Gitlab::Auth::CurrentUserMode.new(@user).admin_mode? call.
# This calls user.admin? and that specific call causes issues with project creation in
# spec/requests/api/projects_spec.rb specs and others, mostly around project creation.
# https://gitlab.com/gitlab-org/gitlab/-/issues/358931 for investigation
adding_the_creator_as_owner_in_a_personal_project? || current_user.can?(:admin_project_member, member.project)
end
def can_update_existing_member?
current_user.can?(:update_project_member, member)
end end
def adding_the_creator_as_owner_in_a_personal_project? def adding_the_creator_as_owner_in_a_personal_project?
# this condition is reached during testing setup a lot due to use of `.add_user` # this condition is reached during testing setup a lot due to use of `.add_user`
member.project.personal_namespace_holder?(member.user) && member.new_record? member.project.personal_namespace_holder?(member.user)
end end
end end
end end
......
...@@ -45317,6 +45317,12 @@ msgstr "" ...@@ -45317,6 +45317,12 @@ msgstr ""
msgid "none" msgid "none"
msgstr "" msgstr ""
msgid "not authorized to create member"
msgstr ""
msgid "not authorized to update member"
msgstr ""
msgid "not found" msgid "not found"
msgstr "" msgstr ""
......
...@@ -143,6 +143,32 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ ...@@ -143,6 +143,32 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
end end
end end
context 'when adding a project_bot' do
let_it_be(:project_bot) { create(:user, :project_bot) }
let(:user_ids) { project_bot.id }
context 'when project_bot is already a member' do
before do
source.add_developer(project_bot)
end
it 'does not update the member' do
expect(execute_service[:status]).to eq(:error)
expect(execute_service[:message]).to eq("#{project_bot.username}: not authorized to update member")
expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(false)
end
end
context 'when project_bot is not already a member' do
it 'adds the member' do
expect(execute_service[:status]).to eq(:success)
expect(source.users).to include project_bot
expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(true)
end
end
end
context 'when tracking the invite source', :snowplow do context 'when tracking the invite source', :snowplow do
context 'when invite_source is not passed' do context 'when invite_source is not passed' do
let(:additional_params) { {} } let(:additional_params) { {} }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Members::CreatorService do
let_it_be(:source, reload: true) { create(:group, :public) }
let_it_be(:member_type) { GroupMember }
let_it_be(:user) { create(:user) }
let_it_be(:current_user) { create(:user) }
describe '#execute' do
it 'raises error for new member on authorization check implementation' do
expect do
described_class.new(source, user, :maintainer, current_user: current_user).execute
end.to raise_error(NotImplementedError)
end
it 'raises error for an existing member on authorization check implementation' do
source.add_developer(user)
expect do
described_class.new(source, user, :maintainer, current_user: current_user).execute
end.to raise_error(NotImplementedError)
end
end
end
...@@ -88,19 +88,55 @@ RSpec.shared_examples_for "member creation" do ...@@ -88,19 +88,55 @@ RSpec.shared_examples_for "member creation" do
expect(member).to be_persisted expect(member).to be_persisted
end end
context 'when admin mode is enabled', :enable_admin_mode do context 'when adding a project_bot' do
let_it_be(:project_bot) { create(:user, :project_bot) }
before_all do
source.add_owner(user)
end
context 'when project_bot is already a member' do
before do
source.add_developer(project_bot)
end
it 'does not update the member' do
member = described_class.new(source, project_bot, :maintainer, current_user: user).execute
expect(source.users.reload).to include(project_bot)
expect(member).to be_persisted
expect(member.access_level).to eq(Gitlab::Access::DEVELOPER)
expect(member.errors.full_messages).to include(/not authorized to update member/)
end
end
context 'when project_bot is not already a member' do
it 'adds the member' do
member = described_class.new(source, project_bot, :maintainer, current_user: user).execute
expect(source.users.reload).to include(project_bot)
expect(member).to be_persisted
end
end
end
context 'when admin mode is enabled', :enable_admin_mode, :aggregate_failures do
it 'sets members.created_by to the given admin current_user' do it 'sets members.created_by to the given admin current_user' do
member = described_class.new(source, user, :maintainer, current_user: admin).execute member = described_class.new(source, user, :maintainer, current_user: admin).execute
expect(member).to be_persisted
expect(source.users.reload).to include(user)
expect(member.created_by).to eq(admin) expect(member.created_by).to eq(admin)
end end
end end
context 'when admin mode is disabled' do context 'when admin mode is disabled' do
it 'rejects setting members.created_by to the given admin current_user' do it 'rejects setting members.created_by to the given admin current_user', :aggregate_failures do
member = described_class.new(source, user, :maintainer, current_user: admin).execute member = described_class.new(source, user, :maintainer, current_user: admin).execute
expect(member.created_by).to be_nil expect(member).not_to be_persisted
expect(source.users.reload).not_to include(user)
expect(member.errors.full_messages).to include(/not authorized to create member/)
end end
end end
...@@ -142,7 +178,7 @@ RSpec.shared_examples_for "member creation" do ...@@ -142,7 +178,7 @@ RSpec.shared_examples_for "member creation" do
end end
context 'when called with an unknown user id' do context 'when called with an unknown user id' do
it 'adds the user as a member' do it 'does not add the user as a member' do
expect(source.users).not_to include(user) expect(source.users).not_to include(user)
described_class.new(source, non_existing_record_id, :maintainer).execute described_class.new(source, non_existing_record_id, :maintainer).execute
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment