Commit dac155c4 authored by Jan Provaznik's avatar Jan Provaznik

Merge branch 'update_highest_role_with_user_callback' into 'master'

Add User callback to update highest role

See merge request gitlab-org/gitlab!28087
parents 7c701bb5 0d7a2ec0
...@@ -470,7 +470,6 @@ class Member < ApplicationRecord ...@@ -470,7 +470,6 @@ class Member < ApplicationRecord
# for a Member to be commited before attempting to update the highest role. # for a Member to be commited before attempting to update the highest role.
# rubocop: disable CodeReuse/ServiceClass # rubocop: disable CodeReuse/ServiceClass
def update_highest_role def update_highest_role
return unless Feature.enabled?(:highest_role_callback)
return unless user_id.present? return unless user_id.present?
return unless previous_changes[:access_level].present? return unless previous_changes[:access_level].present?
......
...@@ -237,6 +237,7 @@ class User < ApplicationRecord ...@@ -237,6 +237,7 @@ class User < ApplicationRecord
end end
end end
end end
after_commit :update_highest_role, on: [:create, :update]
after_initialize :set_projects_limit after_initialize :set_projects_limit
...@@ -1844,6 +1845,21 @@ class User < ApplicationRecord ...@@ -1844,6 +1845,21 @@ class User < ApplicationRecord
def no_recent_activity? def no_recent_activity?
last_active_at.to_i <= MINIMUM_INACTIVE_DAYS.days.ago.to_i last_active_at.to_i <= MINIMUM_INACTIVE_DAYS.days.ago.to_i
end end
# Triggers the service to schedule a Sidekiq job to update the highest role
# for a User
#
# The job will be called outside of a transaction in order to ensure the changes
# for a Member to be commited before attempting to update the highest role.
# rubocop: disable CodeReuse/ServiceClass
def update_highest_role
return unless (previous_changes.keys & %w(state user_type ghost)).any?
run_after_commit_or_now do
Members::UpdateHighestRoleService.new(id).execute
end
end
# rubocop: enable CodeReuse/ServiceClass
end end
User.prepend_if_ee('EE::User') User.prepend_if_ee('EE::User')
...@@ -4,7 +4,8 @@ module Members ...@@ -4,7 +4,8 @@ module Members
class UpdateHighestRoleService < ::BaseService class UpdateHighestRoleService < ::BaseService
include ExclusiveLeaseGuard include ExclusiveLeaseGuard
LEASE_TIMEOUT = 30.minutes.to_i LEASE_TIMEOUT = 10.minutes.to_i
DELAY = 10.minutes
attr_reader :user_id attr_reader :user_id
...@@ -14,7 +15,7 @@ module Members ...@@ -14,7 +15,7 @@ module Members
def execute def execute
try_obtain_lease do try_obtain_lease do
UpdateHighestRoleWorker.perform_async(user_id) UpdateHighestRoleWorker.perform_in(DELAY, user_id)
end end
end end
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
module Users module Users
class UpdateHighestMemberRoleService < BaseService class UpdateHighestMemberRoleService < BaseService
attr_reader :user, :identity_params attr_reader :user
def initialize(user) def initialize(user)
@user = user @user = user
......
...@@ -11,9 +11,15 @@ class UpdateHighestRoleWorker ...@@ -11,9 +11,15 @@ class UpdateHighestRoleWorker
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def perform(user_id) def perform(user_id)
user = User.active.find_by(id: user_id) user = User.find_by(id: user_id)
Users::UpdateHighestMemberRoleService.new(user).execute if user.present? return unless user.present?
if user.active? && user.user_type.nil? && !user.internal?
Users::UpdateHighestMemberRoleService.new(user).execute
else
UserHighestRole.where(user_id: user_id).delete_all
end
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
end end
---
title: Update user's highest role to keep the users statistics up to date
merge_request: 28087
author:
type: added
...@@ -28,7 +28,8 @@ describe Geo::DesignRepositorySyncService do ...@@ -28,7 +28,8 @@ describe Geo::DesignRepositorySyncService do
let(:url_to_repo) { "#{primary.url}#{project.full_path}.design.git" } let(:url_to_repo) { "#{primary.url}#{project.full_path}.design.git" }
before do before do
allow_any_instance_of(Member).to receive(:update_highest_role) # avoid stubbing exclusive lease for this method # update_highest_role uses exclusive key too:
allow(Gitlab::ExclusiveLease).to receive(:new).and_call_original
stub_exclusive_lease(lease_key, lease_uuid) stub_exclusive_lease(lease_key, lease_uuid)
stub_exclusive_lease("geo_project_housekeeping:#{project.id}") stub_exclusive_lease("geo_project_housekeeping:#{project.id}")
......
...@@ -9,14 +9,16 @@ describe Gitlab::ApplicationContext do ...@@ -9,14 +9,16 @@ describe Gitlab::ApplicationContext do
end end
it 'passes the expected context on to labkit' do it 'passes the expected context on to labkit' do
user = build(:user)
project = build(:project)
fake_proc = duck_type(:call) fake_proc = duck_type(:call)
expected_context = hash_including(user: fake_proc, project: fake_proc, root_namespace: fake_proc) expected_context = hash_including(user: fake_proc, project: fake_proc, root_namespace: fake_proc)
expect(Labkit::Context).to receive(:with_context).with(expected_context) expect(Labkit::Context).to receive(:with_context).with(expected_context)
described_class.with_context( described_class.with_context(
user: build(:user), user: user,
project: build(:project), project: project,
namespace: build(:namespace)) {} namespace: build(:namespace)) {}
end end
......
...@@ -165,10 +165,10 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do ...@@ -165,10 +165,10 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
ActiveSession.set(user, request) ActiveSession.set(user, request)
Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis|
expect(redis.scan_each.to_a).to match_array [ expect(redis.scan_each.to_a).to include(
"session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", "session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d",
"session:lookup:user:gitlab:#{user.id}" "session:lookup:user:gitlab:#{user.id}"
] )
end end
end end
......
...@@ -597,97 +597,49 @@ describe Member do ...@@ -597,97 +597,49 @@ describe Member do
end end
context 'when after_commit :update_highest_role' do context 'when after_commit :update_highest_role' do
context 'with feature flag enabled' do where(:member_type, :source_type) do
where(:member_type, :source_type) do :project_member | :project
:project_member | :project :group_member | :group
:group_member | :group end
end
with_them do with_them do
describe 'create member' do describe 'create member' do
it 'initializes a new Members::UpdateHighestRoleService object' do it 'initializes a new Members::UpdateHighestRoleService object' do
source = create(source_type) # source owner initializes a new service object too source = create(source_type) # source owner initializes a new service object too
user = create(:user) user = create(:user)
expect(Members::UpdateHighestRoleService).to receive(:new).with(user.id).and_call_original expect(Members::UpdateHighestRoleService).to receive(:new).with(user.id).and_call_original
create(member_type, :guest, user: user, source_type => source) create(member_type, :guest, user: user, source_type => source)
end
end end
end
context 'when member exists' do context 'when member exists' do
let!(:member) { create(member_type) } let!(:member) { create(member_type) }
describe 'update member' do
context 'when access level was changed' do
it 'initializes a new Members::UpdateHighestRoleService object' do
expect(Members::UpdateHighestRoleService).to receive(:new).with(member.user_id).and_call_original
member.update(access_level: Gitlab::Access::GUEST)
end
end
context 'when access level was not changed' do
it 'does not initialize a new Members::UpdateHighestRoleService object' do
expect(Members::UpdateHighestRoleService).not_to receive(:new).with(member.user_id)
member.update(notification_level: NotificationSetting.levels[:disabled])
end
end
end
describe 'destroy member' do describe 'update member' do
context 'when access level was changed' do
it 'initializes a new Members::UpdateHighestRoleService object' do it 'initializes a new Members::UpdateHighestRoleService object' do
expect(Members::UpdateHighestRoleService).to receive(:new).with(member.user_id).and_call_original expect(Members::UpdateHighestRoleService).to receive(:new).with(member.user_id).and_call_original
member.destroy member.update(access_level: Gitlab::Access::GUEST)
end end
end end
end
end
end
context 'with feature flag disabled' do
before do
stub_feature_flags(highest_role_callback: false)
end
where(:member_type, :source_type) do
:project_member | :project
:group_member | :group
end
with_them do
describe 'create member' do
it 'does not initialize a new Members::UpdateHighestRoleService object' do
source = create(source_type)
user = create(:user)
expect(Members::UpdateHighestRoleService).not_to receive(:new).with(user.id)
create(member_type, :guest, user: user, source_type => source)
end
end
context 'when member exists' do context 'when access level was not changed' do
let!(:member) { create(member_type) } it 'does not initialize a new Members::UpdateHighestRoleService object' do
expect(Members::UpdateHighestRoleService).not_to receive(:new).with(member.user_id)
describe 'update member' do
context 'when access level was changed' do
it 'does not initialize a new Members::UpdateHighestRoleService object' do
expect(Members::UpdateHighestRoleService).not_to receive(:new).with(member.user_id)
member.update(access_level: Gitlab::Access::GUEST) member.update(notification_level: NotificationSetting.levels[:disabled])
end
end end
end end
end
describe 'destroy member' do describe 'destroy member' do
it 'does not initialize a new Members::UpdateHighestRoleService object' do it 'initializes a new Members::UpdateHighestRoleService object' do
expect(Members::UpdateHighestRoleService).not_to receive(:new).with(member.user_id) expect(Members::UpdateHighestRoleService).to receive(:new).with(member.user_id).and_call_original
member.destroy member.destroy
end
end end
end end
end end
......
...@@ -1221,6 +1221,10 @@ describe User, :do_not_mock_admin_mode do ...@@ -1221,6 +1221,10 @@ describe User, :do_not_mock_admin_mode do
end end
it 'uses SecureRandom to generate the incoming email token' do it 'uses SecureRandom to generate the incoming email token' do
allow_next_instance_of(User) do |user|
allow(user).to receive(:update_highest_role)
end
expect(SecureRandom).to receive(:hex).and_return('3b8ca303') expect(SecureRandom).to receive(:hex).and_return('3b8ca303')
user = create(:user) user = create(:user)
...@@ -4441,4 +4445,52 @@ describe User, :do_not_mock_admin_mode do ...@@ -4441,4 +4445,52 @@ describe User, :do_not_mock_admin_mode do
end end
end end
end end
context 'when after_commit :update_highest_role' do
describe 'create user' do
it 'initializes a new Members::UpdateHighestRoleService object' do
expect_next_instance_of(Members::UpdateHighestRoleService) do |service|
expect(service).to receive(:execute)
end
create(:user)
end
end
context 'when user already exists' do
let!(:user) { create(:user) }
describe 'update user' do
using RSpec::Parameterized::TableSyntax
where(:attributes) do
[
{ state: 'blocked' },
{ ghost: true },
{ user_type: :alert_bot }
]
end
with_them do
context 'when state was changed' do
it 'initializes a new Members::UpdateHighestRoleService object' do
expect_next_instance_of(Members::UpdateHighestRoleService) do |service|
expect(service).to receive(:execute)
end
user.update(attributes)
end
end
end
context 'when state was not changed' do
it 'does not initialize a new Members::UpdateHighestRoleService object' do
expect(Members::UpdateHighestRoleService).not_to receive(:new)
user.update(email: 'newmail@example.com')
end
end
end
end
end
end end
...@@ -22,7 +22,9 @@ describe Members::UpdateHighestRoleService, :clean_gitlab_redis_shared_state do ...@@ -22,7 +22,9 @@ describe Members::UpdateHighestRoleService, :clean_gitlab_redis_shared_state do
expect(service.exclusive_lease.exists?).to be_truthy expect(service.exclusive_lease.exists?).to be_truthy
end end
it 'schedules a job' do it 'schedules a job in the future', :aggregate_failures do
expect(UpdateHighestRoleWorker).to receive(:perform_in).with(described_class::DELAY, user.id).and_call_original
Sidekiq::Testing.fake! do Sidekiq::Testing.fake! do
expect { subject }.to change(UpdateHighestRoleWorker.jobs, :size).by(1) expect { subject }.to change(UpdateHighestRoleWorker.jobs, :size).by(1)
end end
......
...@@ -30,7 +30,7 @@ describe Users::ActivityService do ...@@ -30,7 +30,7 @@ describe Users::ActivityService do
end end
it 'tries to obtain ExclusiveLease' do it 'tries to obtain ExclusiveLease' do
expect(Gitlab::ExclusiveLease).to receive(:new).and_call_original expect(Gitlab::ExclusiveLease).to receive(:new).with("activity_service:#{user.id}", anything).and_call_original
subject.execute subject.execute
end end
...@@ -56,7 +56,7 @@ describe Users::ActivityService do ...@@ -56,7 +56,7 @@ describe Users::ActivityService do
end end
it 'does not try to obtain ExclusiveLease' do it 'does not try to obtain ExclusiveLease' do
expect(Gitlab::ExclusiveLease).not_to receive(:new) expect(Gitlab::ExclusiveLease).not_to receive(:new).with("activity_service:#{user.id}", anything)
subject.execute subject.execute
end end
......
...@@ -52,6 +52,8 @@ describe ClusterUpdateAppWorker do ...@@ -52,6 +52,8 @@ describe ClusterUpdateAppWorker do
let(:lease_key) { "#{described_class.name.underscore}-#{application.id}" } let(:lease_key) { "#{described_class.name.underscore}-#{application.id}" }
before do before do
# update_highest_role uses exclusive key too:
allow(Gitlab::ExclusiveLease).to receive(:new).and_call_original
stub_exclusive_lease_taken(lease_key) stub_exclusive_lease_taken(lease_key)
end end
...@@ -62,8 +64,6 @@ describe ClusterUpdateAppWorker do ...@@ -62,8 +64,6 @@ describe ClusterUpdateAppWorker do
end end
it 'does not allow same app to be updated concurrently by different project', :aggregate_failures do it 'does not allow same app to be updated concurrently by different project', :aggregate_failures do
stub_exclusive_lease("refresh_authorized_projects:#{user.id}")
stub_exclusive_lease("update_highest_role:#{user.id}")
project1 = create(:project, namespace: create(:namespace, owner: user)) project1 = create(:project, namespace: create(:namespace, owner: user))
expect(Clusters::Applications::PrometheusUpdateService).not_to receive(:new) expect(Clusters::Applications::PrometheusUpdateService).not_to receive(:new)
...@@ -87,8 +87,6 @@ describe ClusterUpdateAppWorker do ...@@ -87,8 +87,6 @@ describe ClusterUpdateAppWorker do
application2 = create(:clusters_applications_prometheus, :installed) application2 = create(:clusters_applications_prometheus, :installed)
lease_key2 = "#{described_class.name.underscore}-#{application2.id}" lease_key2 = "#{described_class.name.underscore}-#{application2.id}"
stub_exclusive_lease("refresh_authorized_projects:#{user.id}")
stub_exclusive_lease("update_highest_role:#{user.id}")
project2 = create(:project, namespace: create(:namespace, owner: user)) project2 = create(:project, namespace: create(:namespace, owner: user))
stub_exclusive_lease(lease_key2) stub_exclusive_lease(lease_key2)
......
...@@ -30,9 +30,11 @@ describe NewIssueWorker do ...@@ -30,9 +30,11 @@ describe NewIssueWorker do
end end
it 'logs an error' do it 'logs an error' do
issue = create(:issue)
expect(Rails.logger).to receive(:error).with('NewIssueWorker: couldn\'t find User with ID=99, skipping job') expect(Rails.logger).to receive(:error).with('NewIssueWorker: couldn\'t find User with ID=99, skipping job')
worker.perform(create(:issue).id, 99) worker.perform(issue.id, 99)
end end
end end
......
...@@ -15,9 +15,11 @@ describe NewMergeRequestWorker do ...@@ -15,9 +15,11 @@ describe NewMergeRequestWorker do
end end
it 'logs an error' do it 'logs an error' do
user = create(:user)
expect(Rails.logger).to receive(:error).with('NewMergeRequestWorker: couldn\'t find MergeRequest with ID=99, skipping job') expect(Rails.logger).to receive(:error).with('NewMergeRequestWorker: couldn\'t find MergeRequest with ID=99, skipping job')
worker.perform(99, create(:user).id) worker.perform(99, user.id)
end end
end end
...@@ -30,9 +32,11 @@ describe NewMergeRequestWorker do ...@@ -30,9 +32,11 @@ describe NewMergeRequestWorker do
end end
it 'logs an error' do it 'logs an error' do
merge_request = create(:merge_request)
expect(Rails.logger).to receive(:error).with('NewMergeRequestWorker: couldn\'t find User with ID=99, skipping job') expect(Rails.logger).to receive(:error).with('NewMergeRequestWorker: couldn\'t find User with ID=99, skipping job')
worker.perform(create(:merge_request).id, 99) worker.perform(merge_request.id, 99)
end end
end end
......
...@@ -8,56 +8,73 @@ describe UpdateHighestRoleWorker, :clean_gitlab_redis_shared_state do ...@@ -8,56 +8,73 @@ describe UpdateHighestRoleWorker, :clean_gitlab_redis_shared_state do
let(:worker) { described_class.new } let(:worker) { described_class.new }
describe '#perform' do describe '#perform' do
let(:active_scope_attributes) do context 'when user is not found' do
{ it 'does not update or deletes any highest role', :aggregate_failures do
state: 'active', expect { worker.perform(-1) }.not_to change(UserHighestRole, :count)
ghost: false, end
user_type: nil
}
end end
let(:user) { create(:user, attributes) }
subject { worker.perform(user.id) }
context 'when user is found' do context 'when user is found' do
let(:attributes) { active_scope_attributes } let(:active_attributes) do
{
state: 'active',
ghost: false,
user_type: nil
}
end
let(:user) { create(:user, active_attributes) }
it 'updates the highest role for the user' do subject { worker.perform(user.id) }
user_highest_role = create(:user_highest_role, user: user)
create(:group_member, :developer, user: user)
expect { subject } context 'when user is active and not internal' do
.to change { user_highest_role.reload.highest_access_level } context 'when user highest role exists' do
.from(nil) it 'updates the highest role for the user' do
.to(Gitlab::Access::DEVELOPER) user_highest_role = create(:user_highest_role, user: user)
end create(:group_member, :developer, user: user)
end
context 'when user is not found' do expect { subject }
shared_examples 'no update' do .to change { user_highest_role.reload.highest_access_level }
it 'does not update any highest role' do .from(nil)
expect(Users::UpdateHighestMemberRoleService).not_to receive(:new) .to(Gitlab::Access::DEVELOPER)
end
end
context 'when user highest role does not exist' do
it 'creates the highest role for the user' do
create(:group_member, :developer, user: user)
worker.perform(user.id) expect { subject }.to change { UserHighestRole.count }.by(1)
end
end end
end end
context 'when user is blocked' do context 'when user is either inactive or internal' do
let(:attributes) { active_scope_attributes.merge(state: 'blocked') } using RSpec::Parameterized::TableSyntax
it_behaves_like 'no update' where(:additional_attributes) do
end [
{ state: 'blocked' },
{ ghost: true },
{ user_type: :alert_bot }
]
end
context 'when user is a ghost' do with_them do
let(:attributes) { active_scope_attributes.merge(ghost: true) } it 'deletes highest role' do
user = create(:user, active_attributes.merge(additional_attributes))
create(:user_highest_role, user: user)
it_behaves_like 'no update' expect { worker.perform(user.id) }.to change { UserHighestRole.count }.from(1).to(0)
end end
end
context 'when user has a user type' do context 'when user highest role does not exist' do
let(:attributes) { active_scope_attributes.merge(user_type: :alert_bot) } it 'does not delete a highest role' do
user = create(:user, state: 'blocked')
it_behaves_like 'no update' expect { worker.perform(user.id) }.not_to change(UserHighestRole, :count)
end
end
end end
end end
end end
......
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