# Pull Request Template ## Description Locks the agent quota check to the account row while creating account users. This fixes a race where concurrent agent-create requests could all observe the same remaining seat before any `account_users` row was inserted. The API continues to return the existing `402 Account limit exceeded. Please purchase more licenses` response when the limit is reached. Bulk create now preflights the requested email count while holding the account lock, then creates each agent through the same locked builder path. The Enterprise custom-role hook now no-ops when create did not produce an agent. Fixes: [CW-7039](https://linear.app/chatwoot/issue/CW-7039/race-condition-in-agent-creation-bypasses-plan-agent-seat-limit) ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? - `POSTGRES_DATABASE=chatwoot_test_c20f_agent_quota REDIS_DB=9 bundle exec rspec spec/builders/agent_builder_spec.rb spec/enterprise/builders/agent_builder_spec.rb spec/controllers/api/v1/accounts/agents_controller_spec.rb spec/enterprise/controllers/api/v1/accounts/agents_controller_spec.rb spec/enterprise/controllers/enterprise/api/v1/accounts/agents_controller_spec.rb` - `bundle exec rubocop app/builders/agent_builder.rb app/controllers/api/v1/accounts/agents_controller.rb enterprise/app/controllers/enterprise/api/v1/accounts/agents_controller.rb spec/builders/agent_builder_spec.rb spec/enterprise/controllers/api/v1/accounts/agents_controller_spec.rb` - `git diff --check` - One-off threaded Rails validation with 8 concurrent `AgentBuilder` calls against an account with one remaining seat: `created: 1`, `limited: 7`, final `count=2`, `limit=2`. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
74 lines
2.5 KiB
Ruby
74 lines
2.5 KiB
Ruby
# The AgentBuilder class is responsible for creating a new agent.
|
|
# It initializes with necessary attributes and provides a perform method
|
|
# to create a user and account user in a transaction.
|
|
class AgentBuilder
|
|
LIMIT_EXCEEDED_MESSAGE = 'Account limit exceeded. Please purchase more licenses'.freeze
|
|
|
|
class LimitExceededError < StandardError
|
|
def initialize
|
|
super(AgentBuilder::LIMIT_EXCEEDED_MESSAGE)
|
|
end
|
|
end
|
|
|
|
# Initializes an AgentBuilder with necessary attributes.
|
|
# @param email [String] the email of the user.
|
|
# @param name [String] the name of the user.
|
|
# @param role [String] the role of the user, defaults to 'agent' if not provided.
|
|
# @param inviter [User] the user who is inviting the agent (Current.user in most cases).
|
|
# @param availability [String] the availability status of the user, defaults to 'offline' if not provided.
|
|
# @param auto_offline [Boolean] the auto offline status of the user.
|
|
pattr_initialize [:email, { name: '' }, :inviter, :account, { role: :agent }, { availability: :offline }, { auto_offline: false }]
|
|
|
|
# Creates a user and account user in a transaction.
|
|
# @return [User] the created user.
|
|
def perform
|
|
account.with_lock do
|
|
raise LimitExceededError unless can_add_agent?
|
|
|
|
ActiveRecord::Base.transaction do
|
|
@user = find_or_create_user
|
|
create_account_user
|
|
end
|
|
end
|
|
@user
|
|
end
|
|
|
|
private
|
|
|
|
def can_add_agent?
|
|
account.usage_limits[:agents] > account.account_users.count
|
|
end
|
|
|
|
# Finds a user by email or creates a new one with a temporary password.
|
|
# @return [User] the found or created user.
|
|
def find_or_create_user
|
|
user = User.from_email(email)
|
|
return user if user
|
|
|
|
@name = email.split('@').first if @name.blank?
|
|
temp_password = "1!aA#{SecureRandom.alphanumeric(12)}"
|
|
User.create!(email: email, name: @name, password: temp_password, password_confirmation: temp_password)
|
|
end
|
|
|
|
# Checks if the user needs confirmation.
|
|
# @return [Boolean] true if the user is persisted and not confirmed, false otherwise.
|
|
def user_needs_confirmation?
|
|
@user.persisted? && !@user.confirmed?
|
|
end
|
|
|
|
# Creates an account user linking the user to the current account.
|
|
def create_account_user
|
|
AccountUser.create!({
|
|
account_id: account.id,
|
|
user_id: @user.id,
|
|
inviter_id: inviter.id
|
|
}.merge({
|
|
role: role,
|
|
availability: availability,
|
|
auto_offline: auto_offline
|
|
}.compact))
|
|
end
|
|
end
|
|
|
|
AgentBuilder.prepend_mod_with('AgentBuilder')
|