# 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>
113 lines
2.9 KiB
Ruby
113 lines
2.9 KiB
Ruby
class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
|
before_action :fetch_agent, except: [:create, :index, :bulk_create]
|
|
before_action :check_authorization
|
|
|
|
def index
|
|
@agents = agents
|
|
end
|
|
|
|
def create
|
|
builder = AgentBuilder.new(
|
|
email: new_agent_params['email'],
|
|
name: new_agent_params['name'],
|
|
role: new_agent_params['role'],
|
|
availability: new_agent_params['availability'],
|
|
auto_offline: new_agent_params['auto_offline'],
|
|
inviter: current_user,
|
|
account: Current.account
|
|
)
|
|
|
|
@agent = builder.perform
|
|
rescue AgentBuilder::LimitExceededError => e
|
|
render_payment_required(e.message)
|
|
end
|
|
|
|
def update
|
|
@agent.update!(agent_params.slice(:name).compact)
|
|
@agent.current_account_user.update!(agent_params.slice(*account_user_attributes).compact)
|
|
end
|
|
|
|
def destroy
|
|
@agent.current_account_user.destroy!
|
|
delete_user_record(@agent)
|
|
head :ok
|
|
end
|
|
|
|
def bulk_create
|
|
emails = params[:emails]
|
|
|
|
bulk_create_agents(emails)
|
|
# This endpoint is used to bulk create agents during onboarding
|
|
# onboarding_step key in present in Current account custom attributes, since this is a one time operation
|
|
clear_onboarding_step
|
|
head :ok
|
|
rescue AgentBuilder::LimitExceededError => e
|
|
render_payment_required(e.message)
|
|
end
|
|
|
|
private
|
|
|
|
def check_authorization
|
|
super(User)
|
|
end
|
|
|
|
def fetch_agent
|
|
@agent = agents.find(params[:id])
|
|
end
|
|
|
|
def account_user_attributes
|
|
[:role, :availability, :auto_offline]
|
|
end
|
|
|
|
def allowed_agent_params
|
|
[:name, :email, :role, :availability, :auto_offline]
|
|
end
|
|
|
|
def agent_params
|
|
params.require(:agent).permit(allowed_agent_params)
|
|
end
|
|
|
|
def new_agent_params
|
|
params.require(:agent).permit(:email, :name, :role, :availability, :auto_offline)
|
|
end
|
|
|
|
def agents
|
|
@agents ||= Current.account.users.order_by_full_name.includes(:account_users, { avatar_attachment: [:blob] })
|
|
end
|
|
|
|
def bulk_create_agents(emails)
|
|
Current.account.with_lock do
|
|
raise AgentBuilder::LimitExceededError if emails.count > available_agent_count
|
|
|
|
emails.each { |email| create_agent_from_email(email) }
|
|
end
|
|
end
|
|
|
|
def create_agent_from_email(email)
|
|
builder = AgentBuilder.new(
|
|
email: email,
|
|
name: email.split('@').first,
|
|
inviter: current_user,
|
|
account: Current.account
|
|
)
|
|
builder.perform
|
|
rescue ActiveRecord::RecordInvalid => e
|
|
Rails.logger.info "[Agent#bulk_create] ignoring email #{email}, errors: #{e.record.errors}"
|
|
end
|
|
|
|
def clear_onboarding_step
|
|
Current.account.custom_attributes.delete('onboarding_step')
|
|
Current.account.save!
|
|
end
|
|
|
|
def available_agent_count
|
|
Current.account.usage_limits[:agents] - Current.account.account_users.count
|
|
end
|
|
|
|
def delete_user_record(agent)
|
|
DeleteObjectJob.perform_later(agent) if agent.reload.account_users.blank?
|
|
end
|
|
end
|
|
|
|
Api::V1::Accounts::AgentsController.prepend_mod_with('Api::V1::Accounts::AgentsController')
|