diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
index 52682ef9dc9833eeb9d79665900137e4314ed4d6..b8ed2c159a75da5bb42974e1677ab307977c07aa 100644
--- a/app/controllers/concerns/membership_actions.rb
+++ b/app/controllers/concerns/membership_actions.rb
@@ -1,6 +1,5 @@
 module MembershipActions
   extend ActiveSupport::Concern
-  include MembersHelper
 
   def request_access
     membershipable.request_access(current_user)
@@ -10,11 +9,7 @@ module MembershipActions
   end
 
   def approve_access_request
-    @member = membershipable.requesters.find(params[:id])
-
-    return render_403 unless can?(current_user, action_member_permission(:update, @member), @member)
-
-    @member.accept_request
+    Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute
 
     redirect_to polymorphic_url([membershipable, :members])
   end
diff --git a/app/services/members/approve_access_request_service.rb b/app/services/members/approve_access_request_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..416aee2ab51d94e5a3575ff6b5934cb8ed8e5f0e
--- /dev/null
+++ b/app/services/members/approve_access_request_service.rb
@@ -0,0 +1,31 @@
+module Members
+  class ApproveAccessRequestService < BaseService
+    include MembersHelper
+
+    attr_accessor :source
+
+    def initialize(source, current_user, params = {})
+      @source = source
+      @current_user = current_user
+      @params = params
+    end
+
+    def execute
+      condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] }
+      access_requester = source.requesters.find_by!(condition)
+
+      raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester)
+
+      access_requester.access_level = params[:access_level] if params[:access_level]
+      access_requester.accept_request
+
+      access_requester
+    end
+
+    private
+
+    def can_update_access_requester?(access_requester)
+      access_requester && can?(current_user, action_member_permission(:update, access_requester), access_requester)
+    end
+  end
+end
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
index 29a97ccbd75ccdf0bd80db437db09dd9283f8a77..9d1d9058996ccf6eebbb06c48b7fe9ef11df8b8f 100644
--- a/lib/api/access_requests.rb
+++ b/lib/api/access_requests.rb
@@ -55,13 +55,8 @@ module API
         put ':id/access_requests/:user_id/approve' do
           required_attributes! [:user_id]
           source = find_source(source_type, params[:id])
-          authorize_admin_source!(source_type, source)
 
-          member = source.requesters.find_by!(user_id: params[:user_id])
-          if params[:access_level]
-            member.update(access_level: params[:access_level])
-          end
-          member.accept_request
+          member = ::Members::ApproveAccessRequestService.new(source, current_user, params).execute
 
           status :created
           present member.user, with: Entities::Member, member: member
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index c34475976c6367addda843969dbf21edb8086c79..92b97bf3d0cc1962d8ac582f5e5ae37864d20b30 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -2,9 +2,10 @@ require 'spec_helper'
 
 describe Groups::GroupMembersController do
   let(:user)  { create(:user) }
-  let(:group) { create(:group) }
 
   describe '#index' do
+    let(:group) { create(:group) }
+
     before do
       group.add_owner(user)
       stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6fca80b561338e349c1e5703c58ef28a0e210186
--- /dev/null
+++ b/spec/services/members/approve_access_request_service_spec.rb
@@ -0,0 +1,96 @@
+require 'spec_helper'
+
+describe Members::ApproveAccessRequestService, services: true do
+  let(:user) { create(:user) }
+  let(:access_requester) { create(:user) }
+  let(:project) { create(:project, :public) }
+  let(:group) { create(:group, :public) }
+
+  shared_examples 'a service raising ActiveRecord::RecordNotFound' do
+    it 'raises ActiveRecord::RecordNotFound' do
+      expect { described_class.new(source, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound)
+    end
+  end
+
+  shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
+    it 'raises Gitlab::Access::AccessDeniedError' do
+      expect { described_class.new(source, user, params).execute }.to raise_error(Gitlab::Access::AccessDeniedError)
+    end
+  end
+
+  shared_examples 'a service approving an access request' do
+    it 'succeeds' do
+      expect { described_class.new(source, user, params).execute }.to change { source.requesters.count }.by(-1)
+    end
+
+    it 'returns a <Source>Member' do
+      member = described_class.new(source, user, params).execute
+
+      expect(member).to be_a "#{source.class.to_s}Member".constantize
+      expect(member.requested_at).to be_nil
+    end
+
+    context 'with a custom access level' do
+      let(:params) { { user_id: access_requester.id, access_level: Gitlab::Access::MASTER } }
+
+      it 'returns a ProjectMember with the custom access level' do
+        member = described_class.new(source, user, params).execute
+
+        expect(member.access_level).to eq Gitlab::Access::MASTER
+      end
+    end
+  end
+
+  context 'when no access requester are found' do
+    let(:params) { { user_id: 42 } }
+
+    it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
+      let(:source) { project }
+    end
+
+    it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
+      let(:source) { group }
+    end
+  end
+
+  context 'when an access requester is found' do
+    before do
+      project.request_access(access_requester)
+      group.request_access(access_requester)
+    end
+    let(:params) { { user_id: access_requester.id } }
+
+    context 'when current user cannot approve access request to the project' do
+      it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+        let(:source) { project }
+      end
+
+      it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+        let(:source) { group }
+      end
+    end
+
+    context 'when current user can approve access request to the project' do
+      before do
+        project.team << [user, :master]
+        group.add_owner(user)
+      end
+
+      it_behaves_like 'a service approving an access request' do
+        let(:source) { project }
+      end
+
+      it_behaves_like 'a service approving an access request' do
+        let(:source) { group }
+      end
+
+      context 'when given a :id' do
+        let(:params) { { id: project.requesters.find_by!(user_id: access_requester.id).id } }
+
+        it_behaves_like 'a service approving an access request' do
+          let(:source) { project }
+        end
+      end
+    end
+  end
+end