diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md
index 332d93ef207d87179f45a36ab5cb3e6f4adc3ae6..810bc1582b58a0c26db94ddf4f2d3d711765e8c7 100644
--- a/doc/raketasks/user_management.md
+++ b/doc/raketasks/user_management.md
@@ -178,6 +178,41 @@ sudo /etc/init.d/gitlab start
 
 ```
 
+## Bulk assign users to GitLab Duo Pro
+
+To perform bulk user assignment for GitLab Duo Pro, you can use the following Rake task:
+
+```shell
+bundle exec rake duo_pro:bulk_user_assignment DUO_PRO_BULK_USER_FILE_PATH=path/to/your/file.csv
+```
+
+If you prefer to use square brackets in the file path, you can escape them or use double quotes:
+
+```shell
+bundle exec rake duo_pro:bulk_user_assignment\['path/to/your/file.csv'\]
+# or
+bundle exec rake "duo_pro:bulk_user_assignment['path/to/your/file.csv']"
+```
+
+The CSV file should have the following format:
+
+```csv
+username
+user1
+user2
+user3
+user4
+etc..
+```
+
+Ensure that the file contains a header named `username`, and each subsequent row represents a username for user assignment.
+
+The task might raise the following error messages:
+
+- `User is not found`: The specified user was not found.
+- `ERROR_NO_SEATS_AVAILABLE`: No more seats are available for user assignment.
+- `ERROR_INVALID_USER_MEMBERSHIP`: The user is not eligible for assignment due to being inactive, a bot, or a ghost.
+
 ## Related topics
 
 - [Reset a user's password](../security/reset_user_password.md#use-a-rake-task)
diff --git a/ee/lib/duo_pro/bulk_user_assignment.rb b/ee/lib/duo_pro/bulk_user_assignment.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a34b8ef9c64e24362501629adc16ac98f4c11986
--- /dev/null
+++ b/ee/lib/duo_pro/bulk_user_assignment.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+# Duo Pro Bulk User Assignment
+# 1. Set the `add_on_purchase` variable to point to your AddOnPurchase record
+# add_on_purchase = GitlabSubscriptions::AddOnPurchase.find_by(add_on: GitlabSubscriptions::AddOn.code_suggestions.last)
+# 2. Set the `usernames` variable to point to an array of usernames:
+#    usernames = ["user1", "user2", "user3", "user4", "user5"]
+#    If reading from a CSV file
+#    usernames =  CSV.read(FILE_PATH, headers: true).pluck('username')
+# 3. Execute the bulk assignment:
+#    DuoPro::BulkUserAssignment.new(usernames, add_on_purchase).execute
+
+# Error Messages:
+# - `User is not found`
+# - `ERROR_NO_SEATS_AVAILABLE`: No more seats are available.
+# - `ERROR_INVALID_USER_MEMBERSHIP`: User is not eligible for assignment due to being inactive, a bot, or a ghost.
+
+module DuoPro
+  class BulkUserAssignment
+    attr_reader :usernames, :add_on_purchase, :successful_assignments, :failed_assignments
+
+    def initialize(usernames, add_on_purchase)
+      @usernames = usernames
+      @add_on_purchase = add_on_purchase
+      @successful_assignments = []
+      @failed_assignments = []
+    end
+
+    def execute
+      return 'AddOn not purchased' unless add_on_purchase
+
+      process_users(usernames)
+
+      { successful_assignments: successful_assignments, failed_assignments: failed_assignments }
+    end
+
+    private
+
+    def process_users(usernames)
+      usernames.each do |username|
+        user_to_be_assigned = User.find_by_username(username)
+
+        unless user_to_be_assigned
+          log_failed_assignment("User is not found: #{username}")
+          next
+        end
+
+        result = assign(user_to_be_assigned)
+
+        if result.errors.include?("NO_SEATS_AVAILABLE")
+          log_no_seats_available(result, username)
+          break
+        end
+
+        log_result(result, username)
+      end
+    end
+
+    def assign(user)
+      ::GitlabSubscriptions::UserAddOnAssignments::SelfManaged::CreateService.new(
+        add_on_purchase: add_on_purchase,
+        user: user
+      ).execute
+    end
+
+    def log_no_seats_available(result, username)
+      log_failed_assignment("Failed to assign seat to user: #{username}, Errors: #{result.errors}")
+      log_failed_assignment("##No seats are left; users starting from @#{username} onwards were not assigned.##")
+    end
+
+    def log_successful_assignment(username)
+      successful_assignments << "User assigned: #{username}"
+    end
+
+    def log_failed_assignment(message)
+      failed_assignments << message
+    end
+
+    def log_result(result, username)
+      if result.errors.empty?
+        log_successful_assignment(username)
+      else
+        log_failed_assignment("Failed to assign seat to user: #{username}, Errors: #{result.errors}")
+      end
+    end
+  end
+end
diff --git a/ee/lib/tasks/duo_pro/bulk_user_assignment.rake b/ee/lib/tasks/duo_pro/bulk_user_assignment.rake
new file mode 100644
index 0000000000000000000000000000000000000000..ddabee710a43e66c1a417fb8807d96fe2b442aaf
--- /dev/null
+++ b/ee/lib/tasks/duo_pro/bulk_user_assignment.rake
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+namespace :duo_pro do
+  desc 'Bulk user assignment for Code Suggestions'
+  task :bulk_user_assignment, [:duo_pro_bulk_user_file_path] => :environment do |_t, args|
+    file_path = args[:duo_pro_bulk_user_file_path] || ENV['DUO_PRO_BULK_USER_FILE_PATH']
+
+    unless file_path
+      raise <<~ERROR_MESSAGE.strip
+      ================================================================================
+      ## ERROR ##
+      File path is not provided.#{' '}
+      Please set the DUO_PRO_BULK_USER_FILE_PATH environment variable#{' '}
+      or provide it as an argument.
+      ================================================================================
+      ERROR_MESSAGE
+    end
+
+    user_names = read_usernames_from_file(file_path)
+    add_on_purchase = find_add_on_purchase
+
+    unless add_on_purchase
+      raise <<~ERROR_MESSAGE.strip
+      ================================================================================
+      ## ERROR ##
+      Unable to find Duo Pro AddOn purchase.#{' '}
+      Please ensure the necessary AddOn is already purchased and exists.
+      ================================================================================
+      ERROR_MESSAGE
+    end
+
+    result = DuoPro::BulkUserAssignment.new(user_names, add_on_purchase).execute
+    display_results(result)
+  end
+
+  private
+
+  def read_usernames_from_file(file_path)
+    CSV.read(file_path, headers: true).pluck('username')
+  end
+
+  def find_add_on_purchase
+    GitlabSubscriptions::AddOnPurchase.for_code_suggestions.active.first
+  end
+
+  def display_results(result)
+    puts "\nSuccessful Assignments:"
+    puts result[:successful_assignments].join("\n")
+
+    puts "\nFailed Assignments:"
+    puts result[:failed_assignments].join("\n")
+  end
+end
diff --git a/ee/spec/lib/duo_pro/bulk_user_assignment_spec.rb b/ee/spec/lib/duo_pro/bulk_user_assignment_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..08467ad082e6acd62bf4a251d835be67121a142d
--- /dev/null
+++ b/ee/spec/lib/duo_pro/bulk_user_assignment_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe DuoPro::BulkUserAssignment, feature_category: :purchase do
+  describe '#initialize' do
+    subject(:bulk_assignment) { described_class.new([], nil) }
+
+    it 'initializes with the correct attributes' do
+      expect(bulk_assignment.usernames).to eq([])
+      expect(bulk_assignment.add_on_purchase).to be_nil
+      expect(bulk_assignment.successful_assignments).to eq([])
+      expect(bulk_assignment.failed_assignments).to eq([])
+    end
+  end
+
+  describe '#execute' do
+    let(:add_on) { create(:gitlab_subscription_add_on) }
+    let(:usernames) { User.pluck(:username) + ['code_suggestions_not_found_username'] }
+
+    before do
+      create(:user, username: 'code_suggestions_active_user1')
+      create(:user, username: 'code_suggestions_active_user2')
+      create(:user, username: 'code_suggestions_active_user3')
+      create(:user, username: 'code_suggestions_extra_user1')
+      create(:user, username: 'code_suggestions_extra_user2')
+      create(:user, :blocked, username: 'code_suggestions_blocked_user')
+      create(:user, :banned, username: 'code_suggestions_banned_user')
+      create(:user, :bot, username: 'code_suggestions_bot_user')
+      create(:user, :ghost, username: 'code_suggestions_ghost_user')
+    end
+
+    context 'when the AddOn is not purchased' do
+      it 'returns a message indicating AddOn not purchased' do
+        results = described_class.new([], nil).execute
+        expect(results).to eq('AddOn not purchased')
+      end
+    end
+
+    context 'when the AddOn is purchased' do
+      let(:add_on_purchase) do
+        create(:gitlab_subscription_add_on_purchase, :self_managed, quantity: 10, add_on: add_on)
+      end
+
+      subject(:bulk_assignment) { described_class.new(usernames, add_on_purchase) }
+
+      context 'with enough seats' do
+        it 'returns success and failed assignments' do
+          results = bulk_assignment.execute
+
+          expect(results[:successful_assignments]).to eq([
+            "User assigned: code_suggestions_active_user1",
+            "User assigned: code_suggestions_active_user2",
+            "User assigned: code_suggestions_active_user3",
+            "User assigned: code_suggestions_extra_user1",
+            "User assigned: code_suggestions_extra_user2"
+          ])
+
+          expect(results[:failed_assignments]).to eq([
+            "Failed to assign seat to user: code_suggestions_blocked_user, Errors: [\"INVALID_USER_MEMBERSHIP\"]",
+            "Failed to assign seat to user: code_suggestions_banned_user, Errors: [\"INVALID_USER_MEMBERSHIP\"]",
+            "Failed to assign seat to user: code_suggestions_bot_user, Errors: [\"INVALID_USER_MEMBERSHIP\"]",
+            "Failed to assign seat to user: code_suggestions_ghost_user, Errors: [\"INVALID_USER_MEMBERSHIP\"]",
+            "User is not found: code_suggestions_not_found_username"
+          ])
+        end
+      end
+
+      context 'with not enough seats' do
+        let(:add_on_purchase) do
+          create(:gitlab_subscription_add_on_purchase, :self_managed, quantity: 3, add_on: add_on)
+        end
+
+        it 'returns success and failed assignments and stops execution' do
+          results = bulk_assignment.execute
+
+          expect(results[:successful_assignments]).to eq(
+            ["User assigned: code_suggestions_active_user1",
+              "User assigned: code_suggestions_active_user2",
+              "User assigned: code_suggestions_active_user3"])
+
+          expect(results[:failed_assignments]).to eq(
+            ["Failed to assign seat to user: code_suggestions_extra_user1, Errors: [\"NO_SEATS_AVAILABLE\"]",
+              "##No seats are left; users starting from @code_suggestions_extra_user1 onwards were not assigned.##"])
+        end
+      end
+    end
+  end
+end
diff --git a/ee/spec/tasks/duo_pro/bulk_user_assignment_spec.rb b/ee/spec/tasks/duo_pro/bulk_user_assignment_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..dd87c6e0b52e8c6d70f573b9131efd34a4cf14c0
--- /dev/null
+++ b/ee/spec/tasks/duo_pro/bulk_user_assignment_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'duo_pro:bulk_user_assignment', feature_category: :purchase do
+  let(:csv_file_path) { 'spec/fixtures/duo_pro/bulk_user_assignment.csv' }
+
+  before do
+    Rake.application.rake_require('tasks/duo_pro/bulk_user_assignment')
+  end
+
+  describe 'duo_pro:bulk_user_assignment task' do
+    context 'when file_path/DUO_PRO_BULK_USER_FILE_PATH is not provided' do
+      it 'raises an error' do
+        expect do
+          run_rake_task('duo_pro:bulk_user_assignment')
+        end.to raise_error(RuntimeError, /File path is not provided/)
+      end
+    end
+
+    context 'when Duo Pro AddOn purchase is not found' do
+      it 'raises an error for missing Duo Pro AddOn purchase' do
+        expected_error_message = "Unable to find Duo Pro AddOn purchase."
+
+        expect { run_rake_task('duo_pro:bulk_user_assignment', csv_file_path) }
+          .to raise_error(RuntimeError)
+          .with_message(a_string_including(expected_error_message))
+      end
+    end
+
+    context 'when Duo Pro AddOn purchase is found' do
+      let(:add_on) { create(:gitlab_subscription_add_on) }
+
+      before do
+        add_on_purchase = create(:gitlab_subscription_add_on_purchase, :self_managed, quantity: 10, add_on: add_on)
+        allow_next_instance_of(DuoPro::BulkUserAssignment, %w[user1 user2 user3], add_on_purchase) do |instance|
+          response = { successful_assignments: ['success'], failed_assignments: ['Failed'] }
+          allow(instance).to receive(:execute).and_return(response)
+        end
+      end
+
+      it 'outputs success and failed assignments' do
+        expected_output = "\nSuccessful Assignments:\nsuccess\n" \
+                          "\nFailed Assignments:\nFailed\n"
+
+        expect { run_rake_task('duo_pro:bulk_user_assignment', csv_file_path) }
+          .to output(a_string_including(expected_output)).to_stdout
+      end
+
+      context 'with an env variable' do
+        it 'outputs success and failed assignments' do
+          stub_env('DUO_PRO_BULK_USER_FILE_PATH' => csv_file_path)
+          expected_output = "\nSuccessful Assignments:\nsuccess\n" \
+                            "\nFailed Assignments:\nFailed\n"
+
+          expect { run_rake_task('duo_pro:bulk_user_assignment') }
+            .to output(a_string_including(expected_output)).to_stdout
+        end
+      end
+    end
+  end
+end
diff --git a/spec/fixtures/duo_pro/bulk_user_assignment.csv b/spec/fixtures/duo_pro/bulk_user_assignment.csv
new file mode 100644
index 0000000000000000000000000000000000000000..8d2a4346aa29a845fe2d2e90e71fa8b39a38938b
--- /dev/null
+++ b/spec/fixtures/duo_pro/bulk_user_assignment.csv
@@ -0,0 +1,4 @@
+username
+user1
+user2
+user3
\ No newline at end of file