diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index ec6c099df5b0c7bb4e7fa84b07b23298f82e43c0..ac7721cbe1512ad19cfc6d2178794fefe36c2835 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -39,3 +39,20 @@
     }
   }
 }
+
+.groups-cover-block {
+
+  .container-fluid {
+    position: relative;
+  }
+
+  .access-request-button {
+    @include btn-gray;
+    position: absolute;
+    right: 16px;
+    bottom: 32px;
+    padding: 3px 10px;
+    text-transform: none;
+    background-color: $background-color;
+  }
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index bb25090425579f489004e1bbc951a8342401c08f..0e4cefc55c2b8607ec70bc17ce6da98c4e2036aa 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -229,13 +229,20 @@
     right: 16px;
     bottom: 0;
 
-    .btn {
-      padding: 3px 10px;
-      background-color: $background-color;
+    @media (max-width: $screen-lg-min) {
+      top: 0;
     }
 
-    @media (max-width: 1304px) {
-      top: 0;
+    .access-request-button {
+      position: absolute;
+      right: 0;
+      bottom: 61px;
+
+      @media (max-width: $screen-lg-min) {
+        position: relative;
+        bottom: 0;
+        margin-right: 10px;
+      }
     }
   }
 
@@ -286,10 +293,6 @@
   color: #555;
 }
 
-.project_member_row form {
-  margin: 0;
-}
-
 .transfer-project .select2-container {
   min-width: 200px;
 }
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a24273fad0b232c79974ff3bc5343b1a5ebe4fb7
--- /dev/null
+++ b/app/controllers/concerns/membership_actions.rb
@@ -0,0 +1,58 @@
+module MembershipActions
+  extend ActiveSupport::Concern
+  include MembersHelper
+
+  def request_access
+    membershipable.request_access(current_user)
+
+    redirect_to polymorphic_path(membershipable),
+                notice: 'Your request for access has been queued for review.'
+  end
+
+  def approve_access_request
+    @member = membershipable.members.request.find(params[:id])
+
+    return render_403 unless can?(current_user, action_member_permission(:update, @member), @member)
+
+    @member.accept_request
+
+    redirect_to polymorphic_url([membershipable, :members])
+  end
+
+  def leave
+    @member = membershipable.members.find_by(user_id: current_user)
+    return render_403 unless @member
+
+    source_type = @member.real_source_type.humanize(capitalize: false)
+
+    if can?(current_user, action_member_permission(:destroy, @member), @member)
+      notice =
+        if @member.request?
+          "Your access request to the #{source_type} has been withdrawn."
+        else
+          "You left the \"#{@member.source.human_name}\" #{source_type}."
+        end
+      @member.destroy
+
+      redirect_to [:dashboard, @member.real_source_type.tableize], notice: notice
+    else
+      if cannot_leave?
+        alert = "You can not leave the \"#{@member.source.human_name}\" #{source_type}."
+        alert << " Transfer or delete the #{source_type}."
+        redirect_to polymorphic_url(membershipable), alert: alert
+      else
+        render_403
+      end
+    end
+  end
+
+  protected
+
+  def membershipable
+    raise NotImplementedError
+  end
+
+  def cannot_leave?
+    raise NotImplementedError
+  end
+end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 48dbf656e84a9207381c89f6820578ab3bdd2dc7..d0f2e2949f08b461ddc91f71c520d92800868cfa 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -1,11 +1,13 @@
 class Groups::GroupMembersController < Groups::ApplicationController
+  include MembershipActions
+
   # Authorize
-  before_action :authorize_admin_group_member!, except: [:index, :leave]
+  before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
 
   def index
     @project = @group.projects.find(params[:project_id]) if params[:project_id]
     @members = @group.group_members
-    @members = @members.non_invite unless can?(current_user, :admin_group, @group)
+    @members = @members.non_pending unless can?(current_user, :admin_group, @group)
 
     if params[:search].present?
       users = @group.users.search(params[:search]).to_a
@@ -58,25 +60,16 @@ def resend_invite
     end
   end
 
-  def leave
-    @group_member = @group.group_members.find_by(user_id: current_user)
-
-    if can?(current_user, :destroy_group_member, @group_member)
-      @group_member.destroy
-
-      redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.")
-    else
-      if @group.last_owner?(current_user)
-        redirect_to(dashboard_groups_path, alert: "You can not leave #{group.name} group because you're the last owner. Transfer or delete the group.")
-      else
-        return render_403
-      end
-    end
-  end
-
   protected
 
   def member_params
     params.require(:group_member).permit(:access_level, :user_id)
   end
+
+  # MembershipActions concern
+  alias_method :membershipable, :group
+
+  def cannot_leave?
+    @group.last_owner?(current_user)
+  end
 end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index cdea5f0b776258a0e71d6cb2c7656fe715e17573..35d067cd02924c7c2481f0b861b87b9ba576be97 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -1,10 +1,12 @@
 class Projects::ProjectMembersController < Projects::ApplicationController
+  include MembershipActions
+
   # Authorize
-  before_action :authorize_admin_project_member!, except: [:leave, :index]
+  before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
 
   def index
     @project_members = @project.project_members
-    @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
+    @project_members = @project_members.non_pending unless can?(current_user, :admin_project, @project)
 
     if params[:search].present?
       users = @project.users.search(params[:search]).to_a
@@ -14,9 +16,10 @@ def index
     @project_members = @project_members.order('access_level DESC')
 
     @group = @project.group
+
     if @group
       @group_members = @group.group_members
-      @group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group)
+      @group_members = @group_members.non_pending unless can?(current_user, :admin_group, @group)
 
       if params[:search].present?
         users = @group.users.search(params[:search]).to_a
@@ -73,26 +76,6 @@ def resend_invite
     end
   end
 
-  def leave
-    @project_member = @project.project_members.find_by(user_id: current_user)
-
-    if can?(current_user, :destroy_project_member, @project_member)
-      @project_member.destroy
-
-      respond_to do |format|
-        format.html { redirect_to dashboard_projects_path, notice: "You left the project." }
-        format.js { head :ok }
-      end
-    else
-      if current_user == @project.owner
-        message = 'You can not leave your own project. Transfer or delete the project.'
-        redirect_back_or_default(default: { action: 'index' }, options: { alert: message })
-      else
-        render_403
-      end
-    end
-  end
-
   def apply_import
     source_project = Project.find(params[:source_project_id])
 
@@ -112,4 +95,11 @@ def apply_import
   def member_params
     params.require(:project_member).permit(:user_id, :access_level)
   end
+
+  # MembershipActions concern
+  alias_method :membershipable, :project
+
+  def cannot_leave?
+    current_user == @project.owner
+  end
 end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 2ce2d4e694f38cae75a009d7ec2aba16e9c2958b..3a43e936aeeb27d7818207374fd256022e44be51 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -13,10 +13,23 @@
 #   merge_request_path(merge_request)
 #
 module GitlabRoutingHelper
+  # Project
   def project_path(project, *args)
     namespace_project_path(project.namespace, project, *args)
   end
 
+  def project_url(project, *args)
+    namespace_project_url(project.namespace, project, *args)
+  end
+
+  def edit_project_path(project, *args)
+    edit_namespace_project_path(project.namespace, project, *args)
+  end
+
+  def edit_project_url(project, *args)
+    edit_namespace_project_url(project.namespace, project, *args)
+  end
+
   def project_files_path(project, *args)
     namespace_project_tree_path(project.namespace, project, @ref || project.repository.root_ref)
   end
@@ -41,10 +54,6 @@ def activity_project_path(project, *args)
     activity_namespace_project_path(project.namespace, project, *args)
   end
 
-  def edit_project_path(project, *args)
-    edit_namespace_project_path(project.namespace, project, *args)
-  end
-
   def runners_path(project, *args)
     namespace_project_runners_path(project.namespace, project, *args)
   end
@@ -65,14 +74,6 @@ def milestone_path(entity, *args)
     namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args)
   end
 
-  def project_url(project, *args)
-    namespace_project_url(project.namespace, project, *args)
-  end
-
-  def edit_project_url(project, *args)
-    edit_namespace_project_url(project.namespace, project, *args)
-  end
-
   def issue_url(entity, *args)
     namespace_project_issue_url(entity.project.namespace, entity.project, entity, *args)
   end
@@ -92,4 +93,56 @@ def toggle_subscription_path(entity, *args)
       toggle_subscription_namespace_project_merge_request_path(entity.project.namespace, entity.project, entity)
     end
   end
+
+  ## Members
+  def project_members_url(project, *args)
+    namespace_project_project_members_url(project.namespace, project)
+  end
+
+  def project_member_path(project_member, *args)
+    namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
+  end
+
+  def request_access_project_members_path(project, *args)
+    request_access_namespace_project_project_members_path(project.namespace, project)
+  end
+
+  def leave_project_members_path(project, *args)
+    leave_namespace_project_project_members_path(project.namespace, project)
+  end
+
+  def approve_access_request_project_member_path(project_member, *args)
+    approve_access_request_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
+  end
+
+  def resend_invite_project_member_path(project_member, *args)
+    resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
+  end
+
+  # Groups
+
+  ## Members
+  def group_members_url(group, *args)
+    group_group_members_url(group, *args)
+  end
+
+  def group_member_path(group_member, *args)
+    group_group_member_path(group_member.source, group_member)
+  end
+
+  def request_access_group_members_path(group, *args)
+    request_access_group_group_members_path(group)
+  end
+
+  def leave_group_members_path(group, *args)
+    leave_group_group_members_path(group)
+  end
+
+  def approve_access_request_group_member_path(group_member, *args)
+    approve_access_request_group_group_member_path(group_member.source, group_member)
+  end
+
+  def resend_invite_group_member_path(group_member, *args)
+    resend_invite_group_group_member_path(group_member.source, group_member)
+  end
 end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 4cac69c6795272dc5afa715e8bc83306f5bc3ebd..b9211e884733f693e0905a3fbcc2c748564a244f 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -1,24 +1,4 @@
 module GroupsHelper
-  def remove_user_from_group_message(group, member)
-    if member.user
-      "Are you sure you want to remove \"#{member.user.name}\" from \"#{group.name}\"?"
-    else
-      "Are you sure you want to revoke the invitation for \"#{member.invite_email}\" to join \"#{group.name}\"?"
-    end
-  end
-
-  def leave_group_message(group)
-    "Are you sure you want to leave \"#{group}\" group?"
-  end
-
-  def should_user_see_group_roles?(user, group)
-    if user
-      user.is_admin? || group.members.exists?(user_id: user.id)
-    else
-      false
-    end
-  end
-
   def can_change_group_visibility_level?(group)
     can?(current_user, :change_visibility_level, group)
   end
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a53828ef4e7d056f6e63178c8a206d9a0de65c73
--- /dev/null
+++ b/app/helpers/members_helper.rb
@@ -0,0 +1,45 @@
+module MembersHelper
+  # Returns a `<action>_<source>_member` association, e.g.:
+  # - admin_project_member, update_project_member, destroy_project_member
+  # - admin_group_member, update_group_member, destroy_group_member
+  def action_member_permission(action, member)
+    "#{action}_#{member.type.underscore}".to_sym
+  end
+
+  def can_see_member_roles?(source:, user: nil)
+    return false unless user
+
+    user.is_admin? || source.members.exists?(user_id: user.id)
+  end
+
+  def remove_member_message(member, user: nil)
+    user = current_user if defined?(current_user)
+
+    text = 'Are you sure you want to '
+    action =
+      if member.request?
+        if member.user == user
+          'withdraw your access request for'
+        else
+          "deny #{member.user.name}'s request to join"
+        end
+      elsif member.invite?
+        "revoke the invitation for #{member.invite_email} to join"
+      else
+        "remove #{member.user.name} from"
+      end
+
+    text << action << " the #{member.source.human_name} #{member.real_source_type.humanize(capitalize: false)}?"
+  end
+
+  def remove_member_title(member)
+    text = " from #{member.real_source_type.humanize(capitalize: false)}"
+
+    text.prepend(member.request? ? 'Deny access request' : 'Remove user')
+  end
+
+  def leave_confirmation_message(member_source)
+    "Are you sure you want to leave the " \
+    "\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?"
+  end
+end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 5e5d170a9f30ee4ca1001298832a47fa5881f789..d30dd66202bb250c5b7834cf814bbc45f1cabc8f 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -1,12 +1,4 @@
 module ProjectsHelper
-  def remove_from_project_team_message(project, member)
-    if member.user
-      "You are going to remove #{member.user.name} from #{project.name} project team. Are you sure?"
-    else
-      "You are going to revoke the invitation for #{member.invite_email} to join #{project.name} project team. Are you sure?"
-    end
-  end
-
   def link_to_project(project)
     link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
       title = content_tag(:span, project.name, class: 'project-name')
@@ -115,14 +107,6 @@ def can_change_visibility_level?(project, current_user)
     end
   end
 
-  def user_max_access_in_project(user_id, project)
-    level = project.team.max_member_access(user_id)
-
-    if level
-      Gitlab::Access.options_with_owner.key(level)
-    end
-  end
-
   def license_short_name(project)
     return 'LICENSE' if project.repository.license_key.nil?
 
@@ -286,10 +270,6 @@ def project_status_css_class(status)
     end
   end
 
-  def leave_project_message(project)
-    "Are you sure you want to leave \"#{project.name}\" project?"
-  end
-
   def new_readme_path
     ref = @repository.root_ref if @repository
     ref ||= 'master'
diff --git a/app/mailers/emails/groups.rb b/app/mailers/emails/groups.rb
deleted file mode 100644
index 1c43f95dc8c61529b0ea4c18fadf91692f0d3575..0000000000000000000000000000000000000000
--- a/app/mailers/emails/groups.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-module Emails
-  module Groups
-    def group_access_granted_email(group_member_id)
-      @group_member = GroupMember.find(group_member_id)
-      @group = @group_member.group
-
-      @target_url = group_url(@group)
-      @current_user = @group_member.user
-
-      mail(to: @group_member.user.notification_email,
-           subject: subject("Access to group was granted"))
-    end
-
-    def group_member_invited_email(group_member_id, token)
-      @group_member = GroupMember.find group_member_id
-      @group = @group_member.group
-      @token = token
-
-      @target_url = group_url(@group)
-      @current_user = @group_member.user
-
-      mail(to: @group_member.invite_email,
-           subject: "Invitation to join group #{@group.name}")
-    end
-
-    def group_invite_accepted_email(group_member_id)
-      @group_member = GroupMember.find group_member_id
-      return if @group_member.created_by.nil?
-
-      @group = @group_member.group
-
-      @target_url = group_url(@group)
-      @current_user = @group_member.created_by
-
-      mail(to: @group_member.created_by.notification_email,
-           subject: subject("Invitation accepted"))
-    end
-
-    def group_invite_declined_email(group_id, invite_email, access_level, created_by_id)
-      return if created_by_id.nil?
-
-      @group = Group.find(group_id)
-      @current_user = @created_by = User.find(created_by_id)
-      @access_level = access_level
-      @invite_email = invite_email
-      
-      @target_url = group_url(@group)
-      mail(to: @created_by.notification_email,
-           subject: subject("Invitation declined"))
-    end
-  end
-end
diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6dde2e9847db95e5b408e28b8859712334c40ca1
--- /dev/null
+++ b/app/mailers/emails/members.rb
@@ -0,0 +1,81 @@
+module Emails
+  module Members
+    extend ActiveSupport::Concern
+    include MembersHelper
+
+    included do
+      helper_method :member_source, :member
+    end
+
+    def member_access_requested_email(member_source_type, member_id)
+      @member_source_type = member_source_type
+      @member_id = member_id
+
+      admins = member_source.members.owners_and_masters.includes(:user).pluck(:notification_email)
+
+      mail(to: admins,
+           subject: subject("Request to join the #{member_source.human_name} #{member_source.model_name.singular}"))
+    end
+
+    def member_access_granted_email(member_source_type, member_id)
+      @member_source_type = member_source_type
+      @member_id = member_id
+
+      mail(to: member.user.notification_email,
+           subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was granted"))
+    end
+
+    def member_access_denied_email(member_source_type, source_id, user_id)
+      @member_source_type = member_source_type
+      @member_source = member_source_class.find(source_id)
+      requester = User.find(user_id)
+
+      mail(to: requester.notification_email,
+           subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was denied"))
+    end
+
+    def member_invited_email(member_source_type, member_id, token)
+      @member_source_type = member_source_type
+      @member_id = member_id
+      @token = token
+
+      mail(to: member.invite_email,
+           subject: "Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}")
+    end
+
+    def member_invite_accepted_email(member_source_type, member_id)
+      @member_source_type = member_source_type
+      @member_id = member_id
+      return unless member.created_by
+
+      mail(to: member.created_by.notification_email,
+           subject: subject('Invitation accepted'))
+    end
+
+    def member_invite_declined_email(member_source_type, source_id, invite_email, created_by_id)
+      return unless created_by_id
+
+      @member_source_type = member_source_type
+      @member_source = member_source_class.find(source_id)
+      @invite_email = invite_email
+      inviter = User.find(created_by_id)
+
+      mail(to: inviter.notification_email,
+           subject: subject('Invitation declined'))
+    end
+
+    def member
+      @member ||= Member.find(@member_id)
+    end
+
+    def member_source
+      @member_source ||= member.source
+    end
+
+    private
+
+    def member_source_class
+      @member_source_type.classify.constantize
+    end
+  end
+end
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index fdf1e9f5afcdd9b8d181a9a3b04033031f143939..689fb3e0ffb834e97fa28644d21261f3619679a6 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -1,55 +1,5 @@
 module Emails
   module Projects
-    def project_access_granted_email(project_member_id)
-      @project_member = ProjectMember.find project_member_id
-      @project = @project_member.project
-
-      @target_url = namespace_project_url(@project.namespace, @project)
-      @current_user = @project_member.user
-
-      mail(to: @project_member.user.notification_email,
-           subject: subject("Access to project was granted"))
-    end
-
-    def project_member_invited_email(project_member_id, token)
-      @project_member = ProjectMember.find project_member_id
-      @project = @project_member.project
-      @token = token
-
-      @target_url = namespace_project_url(@project.namespace, @project)
-      @current_user = @project_member.user
-
-      mail(to: @project_member.invite_email,
-           subject: "Invitation to join project #{@project.name_with_namespace}")
-    end
-
-    def project_invite_accepted_email(project_member_id)
-      @project_member = ProjectMember.find project_member_id
-      return if @project_member.created_by.nil?
-
-      @project = @project_member.project
-
-      @target_url = namespace_project_url(@project.namespace, @project)
-      @current_user = @project_member.created_by
-
-      mail(to: @project_member.created_by.notification_email,
-           subject: subject("Invitation accepted"))
-    end
-
-    def project_invite_declined_email(project_id, invite_email, access_level, created_by_id)
-      return if created_by_id.nil?
-
-      @project = Project.find(project_id)
-      @current_user = @created_by = User.find(created_by_id)
-      @access_level = access_level
-      @invite_email = invite_email
-
-      @target_url = namespace_project_url(@project.namespace, @project)
-
-      mail(to: @created_by.notification_email,
-           subject: subject("Invitation declined"))
-    end
-
     def project_was_moved_email(project_id, user_id, old_path_with_namespace)
       @current_user = @user = User.find user_id
       @project = Project.find project_id
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 1c663bdd5212754d56235a59052ad3b5a7fc5892..0cc709f68e46c0319f737f3faa7c590f20064671 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -6,13 +6,15 @@ class Notify < BaseMailer
   include Emails::Notes
   include Emails::Projects
   include Emails::Profile
-  include Emails::Groups
   include Emails::Builds
+  include Emails::Members
 
   add_template_helper MergeRequestsHelper
   add_template_helper DiffHelper
   add_template_helper BlobHelper
   add_template_helper EmailsHelper
+  add_template_helper MembersHelper
+  add_template_helper GitlabRoutingHelper
 
   def test_email(recipient_email, subject, body)
     mail(to: recipient_email,
diff --git a/app/models/ability.rb b/app/models/ability.rb
index aea946f9224b35f0243db4ab807925e969ca7a6f..647a73aa1cedca05c25fb3100a114e938f33d618 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -187,6 +187,8 @@ def project_team_rules(team, user)
         project_report_rules
       elsif team.guest?(user)
         project_guest_rules
+      else
+        []
       end
     end
 
diff --git a/app/models/concerns/access_requestable.rb b/app/models/concerns/access_requestable.rb
new file mode 100644
index 0000000000000000000000000000000000000000..eedd32a729fb660a8825cd7d5103ca2589e0ca58
--- /dev/null
+++ b/app/models/concerns/access_requestable.rb
@@ -0,0 +1,16 @@
+# == AccessRequestable concern
+#
+# Contains functionality related to objects that can receive request for access.
+#
+# Used by Project, and Group.
+#
+module AccessRequestable
+  extend ActiveSupport::Concern
+
+  def request_access(user)
+    members.create(
+      access_level: Gitlab::Access::DEVELOPER,
+      user: user,
+      requested_at: Time.now.utc)
+  end
+end
diff --git a/app/models/group.rb b/app/models/group.rb
index aec92e335e63a36a8928de46ab96fefa8fba0130..b8dffe9f5b9bac062ab71799d942d91e9c3c1044 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -3,11 +3,12 @@
 class Group < Namespace
   include Gitlab::ConfigHelper
   include Gitlab::VisibilityLevel
+  include AccessRequestable
   include Referable
 
   has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
   alias_method :members, :group_members
-  has_many :users, through: :group_members
+  has_many :users, -> { where(members: { requested_at: nil }) }, through: :group_members
   has_many :project_group_links, dependent: :destroy
   has_many :shared_projects, through: :project_group_links, source: :project
   has_many :notification_settings, dependent: :destroy, as: :source
@@ -58,6 +59,10 @@ def to_reference(_from_project = nil)
     "#{self.class.reference_prefix}#{name}"
   end
 
+  def web_url
+    Gitlab::Routing.url_helpers.group_url(self)
+  end
+
   def human_name
     name
   end
diff --git a/app/models/member.rb b/app/models/member.rb
index d3060f07fc0fecaa683f491949d353643dd41bb8..cea6d259760ef2ad0edffdce4309fd959acb87ca 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -26,20 +26,28 @@ class Member < ActiveRecord::Base
       allow_nil: true
     }
 
-  scope :invite, -> { where(user_id: nil) }
-  scope :non_invite, -> { where("user_id IS NOT NULL") }
+  scope :invite, -> { where.not(invite_token: nil) }
+  scope :non_invite, -> { where(invite_token: nil) }
+  scope :request, -> { where.not(requested_at: nil) }
+  scope :non_request, -> { where(requested_at: nil) }
+  scope :non_pending, -> { non_request.non_invite }
+
   scope :guests, -> { where(access_level: GUEST) }
   scope :reporters, -> { where(access_level: REPORTER) }
   scope :developers, -> { where(access_level: DEVELOPER) }
   scope :masters,  -> { where(access_level: MASTER) }
   scope :owners,  -> { where(access_level: OWNER) }
+  scope :owners_and_masters,  -> { where(access_level: [OWNER, MASTER]) }
 
   before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
+
   after_create :send_invite, if: :invite?
-  after_create :create_notification_setting, unless: :invite?
-  after_create :post_create_hook, unless: :invite?
-  after_update :post_update_hook, unless: :invite?
-  after_destroy :post_destroy_hook, unless: :invite?
+  after_create :send_request, if: :request?
+  after_create :create_notification_setting, unless: :pending?
+  after_create :post_create_hook, unless: :pending?
+  after_update :post_update_hook, unless: :pending?
+  after_destroy :post_destroy_hook, unless: :pending?
+  after_destroy :post_decline_request, if: :request?
 
   delegate :name, :username, :email, to: :user, prefix: true
 
@@ -96,10 +104,31 @@ def project_creator?(member, access_level)
     end
   end
 
+  def real_source_type
+    source_type
+  end
+
   def invite?
     self.invite_token.present?
   end
 
+  def request?
+    requested_at.present?
+  end
+
+  def pending?
+    invite? || request?
+  end
+
+  def accept_request
+    return false unless request?
+
+    updated = self.update(requested_at: nil)
+    after_accept_request if updated
+
+    updated
+  end
+
   def accept_invite!(new_user)
     return false unless invite?
 
@@ -157,6 +186,10 @@ def send_invite
     # override in subclass
   end
 
+  def send_request
+    # override in subclass
+  end
+
   def post_create_hook
     system_hook_service.execute_hooks_for(self, :create)
   end
@@ -177,6 +210,14 @@ def after_decline_invite
     # override in subclass
   end
 
+  def after_accept_request
+    post_create_hook
+  end
+
+  def post_decline_request
+    # override in subclass
+  end
+
   def system_hook_service
     SystemHooksService.new
   end
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index f63a0debf1a80304d660ff765b6033d92d0aab78..363db8779689bf47d89aad38238232e1b8910da8 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -8,9 +8,6 @@ class GroupMember < Member
   validates_format_of :source_type, with: /\ANamespace\z/
   default_scope { where(source_type: SOURCE_TYPE) }
 
-  scope :with_group, ->(group) { where(source_id: group.id) }
-  scope :with_user, ->(user) { where(user_id: user.id) }
-
   def self.access_level_roles
     Gitlab::Access.options_with_owner
   end
@@ -23,6 +20,11 @@ def access_field
     access_level
   end
 
+  # Because source_type is `Namespace`...
+  def real_source_type
+    'Group'
+  end
+
   private
 
   def send_invite
@@ -31,6 +33,12 @@ def send_invite
     super
   end
 
+  def send_request
+    notification_service.new_group_access_request(self)
+
+    super
+  end
+
   def post_create_hook
     notification_service.new_group_member(self)
 
@@ -56,4 +64,10 @@ def after_decline_invite
 
     super
   end
+
+  def post_decline_request
+    notification_service.decline_group_access_request(self)
+
+    super
+  end
 end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 46955b430f35c266b2ab924c5e68e7e0b3c797c3..250ee04fd1d1f68ac1123ff295b857d1a0b50433 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -11,8 +11,6 @@ class ProjectMember < Member
   default_scope { where(source_type: SOURCE_TYPE) }
 
   scope :in_project, ->(project) { where(source_id: project.id) }
-  scope :in_projects, ->(projects) { where(source_id: projects.pluck(:id)) }
-  scope :with_user, ->(user) { where(user_id: user.id) }
 
   before_destroy :delete_member_todos
 
@@ -84,7 +82,7 @@ def roles_hash
       Gitlab::Access.sym_options
     end
 
-    def access_roles
+    def access_level_roles
       Gitlab::Access.options
     end
   end
@@ -113,6 +111,12 @@ def send_invite
     super
   end
 
+  def send_request
+    notification_service.new_project_access_request(self)
+
+    super
+  end
+
   def post_create_hook
     unless owner?
       event_service.join_project(self.project, self.user)
@@ -148,6 +152,12 @@ def after_decline_invite
     super
   end
 
+  def post_decline_request
+    notification_service.decline_project_access_request(self)
+
+    super
+  end
+
   def event_service
     EventCreateService.new
   end
diff --git a/app/models/project.rb b/app/models/project.rb
index dfa99fe0df273ac1174e902f3ea5c18c236d5363..0d2e612436a840cc5d52edaba178dcf543a60c6b 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -5,6 +5,7 @@ class Project < ActiveRecord::Base
   include Gitlab::ShellAdapter
   include Gitlab::VisibilityLevel
   include Gitlab::CurrentSettings
+  include AccessRequestable
   include Referable
   include Sortable
   include AfterCommitQueue
@@ -102,8 +103,9 @@ def update_forks_visibility_level
   has_many :snippets,           dependent: :destroy, class_name: 'ProjectSnippet'
   has_many :hooks,              dependent: :destroy, class_name: 'ProjectHook'
   has_many :protected_branches, dependent: :destroy
-  has_many :project_members, dependent: :destroy, as: :source, class_name: 'ProjectMember'
-  has_many :users, through: :project_members
+  has_many :project_members,    dependent: :destroy, as: :source, class_name: 'ProjectMember'
+  alias_method :members, :project_members
+  has_many :users, -> { where(members: { requested_at: nil }) }, through: :project_members
   has_many :deploy_keys_projects, dependent: :destroy
   has_many :deploy_keys, through: :deploy_keys_projects
   has_many :users_star_projects, dependent: :destroy
@@ -680,16 +682,6 @@ def owner
     end
   end
 
-  def project_member_by_name_or_email(name = nil, email = nil)
-    user = users.find_by('name like ? or email like ?', name, email)
-    project_members.where(user: user) if user
-  end
-
-  # Get Team Member record by user id
-  def project_member_by_id(user_id)
-    project_members.find_by(user_id: user_id)
-  end
-
   def name_with_namespace
     @name_with_namespace ||= begin
                                if namespace
@@ -699,6 +691,7 @@ def name_with_namespace
                                end
                              end
   end
+  alias_method :human_name, :name_with_namespace
 
   def path_with_namespace
     if namespace
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index e29e854860ae238f238e95af9cb3e5c99afb371c..73e736820af25d81fcc2ba9b0eb65a3be44ea222 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -21,23 +21,13 @@ def <<(args)
     end
   end
 
-  def find(user_id)
-    user = project.users.find_by(id: user_id)
-
-    if group
-      user ||= group.users.find_by(id: user_id)
-    end
-
-    user
-  end
-
   def find_member(user_id)
-    member = project.project_members.find_by(user_id: user_id)
+    member = project.members.non_request.find_by(user_id: user_id)
 
     # If user is not in project members
     # we should check for group membership
     if group && !member
-      member = group.group_members.find_by(user_id: user_id)
+      member = group.members.non_request.find_by(user_id: user_id)
     end
 
     member
@@ -61,13 +51,10 @@ def truncate
     ProjectMember.truncate_team(project)
   end
 
-  def users
-    members
-  end
-
   def members
     @members ||= fetch_members
   end
+  alias_method :users, :members
 
   def guests
     @guests ||= fetch_members(:guests)
@@ -150,7 +137,7 @@ def human_max_access(user_id)
   def max_member_access(user_id)
     access = []
 
-    project.project_members.each do |member|
+    project.members.non_request.each do |member|
       if member.user_id == user_id
         access << member.access_field if member.access_field
         break
@@ -158,7 +145,7 @@ def max_member_access(user_id)
     end
 
     if group
-      group.group_members.each do |member|
+      group.members.non_request.each do |member|
         if member.user_id == user_id
           access << member.access_field if member.access_field
           break
@@ -173,6 +160,7 @@ def max_member_access(user_id)
     access.compact.max
   end
 
+  private
 
   def max_invited_level(user_id)
     project.project_group_links.map do |group_link|
@@ -189,17 +177,15 @@ def max_invited_level(user_id)
     end.compact.max
   end
 
-  private
-
   def fetch_members(level = nil)
-    project_members = project.project_members
-    group_members = group ? group.group_members : []
+    project_members = project.members.non_request
+    group_members = group ? group.members.non_request : []
     invited_members = []
 
     if project.invited_groups.any? && project.allowed_to_share_with_group?
       project.project_group_links.each do |group_link|
         invited_group = group_link.group
-        im = invited_group.group_members
+        im = invited_group.members.non_request
 
         if level
           int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
diff --git a/app/models/user.rb b/app/models/user.rb
index a5b3c8afe51b539e4f635b2056a3103abba15e6f..8d0427da5aba4d2f08a95d72d411e0a61c3bcb42 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -56,8 +56,7 @@ class User < ActiveRecord::Base
 
   # Groups
   has_many :members, dependent: :destroy
-  has_many :project_members, source: 'ProjectMember'
-  has_many :group_members, source: 'GroupMember'
+  has_many :group_members, dependent: :destroy, source: 'GroupMember'
   has_many :groups, through: :group_members
   has_many :owned_groups, -> { where members: { access_level: Gitlab::Access::OWNER } }, through: :group_members, source: :group
   has_many :masters_groups, -> { where members: { access_level: Gitlab::Access::MASTER } }, through: :group_members, source: :group
@@ -65,13 +64,13 @@ class User < ActiveRecord::Base
   # Projects
   has_many :groups_projects,          through: :groups, source: :projects
   has_many :personal_projects,        through: :namespace, source: :projects
+  has_many :project_members,          dependent: :destroy, class_name: 'ProjectMember'
   has_many :projects,                 through: :project_members
   has_many :created_projects,         foreign_key: :creator_id, class_name: 'Project'
   has_many :users_star_projects, dependent: :destroy
   has_many :starred_projects, through: :users_star_projects, source: :project
 
   has_many :snippets,                 dependent: :destroy, foreign_key: :author_id, class_name: "Snippet"
-  has_many :project_members,          dependent: :destroy, class_name: 'ProjectMember'
   has_many :issues,                   dependent: :destroy, foreign_key: :author_id
   has_many :notes,                    dependent: :destroy, foreign_key: :author_id
   has_many :merge_requests,           dependent: :destroy, foreign_key: :author_id
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 875a3f4fab6a60ee1bac1d95965e5a5c7fb3b75a..f804ac171c4915f33acd75225d31d879594fc619 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -173,16 +173,26 @@ def new_note(note)
     end
   end
 
+  # Project access request
+  def new_project_access_request(project_member)
+    mailer.member_access_requested_email(project_member.real_source_type, project_member.id).deliver_later
+  end
+
+  def decline_project_access_request(project_member)
+    mailer.member_access_denied_email(project_member.real_source_type, project_member.project.id, project_member.user.id).deliver_later
+  end
+
   def invite_project_member(project_member, token)
-    mailer.project_member_invited_email(project_member.id, token).deliver_later
+    mailer.member_invited_email(project_member.real_source_type, project_member.id, token).deliver_later
   end
 
   def accept_project_invite(project_member)
-    mailer.project_invite_accepted_email(project_member.id).deliver_later
+    mailer.member_invite_accepted_email(project_member.real_source_type, project_member.id).deliver_later
   end
 
   def decline_project_invite(project_member)
-    mailer.project_invite_declined_email(
+    mailer.member_invite_declined_email(
+      project_member.real_source_type,
       project_member.project.id,
       project_member.invite_email,
       project_member.access_level,
@@ -191,23 +201,33 @@ def decline_project_invite(project_member)
   end
 
   def new_project_member(project_member)
-    mailer.project_access_granted_email(project_member.id).deliver_later
+    mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later
   end
 
   def update_project_member(project_member)
-    mailer.project_access_granted_email(project_member.id).deliver_later
+    mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later
+  end
+
+  # Group access request
+  def new_group_access_request(group_member)
+    mailer.member_access_requested_email(group_member.real_source_type, group_member.id).deliver_later
+  end
+
+  def decline_group_access_request(group_member)
+    mailer.member_access_denied_email(group_member.real_source_type, group_member.group.id, group_member.user.id).deliver_later
   end
 
   def invite_group_member(group_member, token)
-    mailer.group_member_invited_email(group_member.id, token).deliver_later
+    mailer.member_invited_email(group_member.real_source_type, group_member.id, token).deliver_later
   end
 
   def accept_group_invite(group_member)
-    mailer.group_invite_accepted_email(group_member.id).deliver_later
+    mailer.member_invite_accepted_email(group_member.id).deliver_later
   end
 
   def decline_group_invite(group_member)
-    mailer.group_invite_declined_email(
+    mailer.member_invite_declined_email(
+      group_member.real_source_type,
       group_member.group.id,
       group_member.invite_email,
       group_member.access_level,
@@ -216,11 +236,11 @@ def decline_group_invite(group_member)
   end
 
   def new_group_member(group_member)
-    mailer.group_access_granted_email(group_member.id).deliver_later
+    mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later
   end
 
   def update_group_member(group_member)
-    mailer.group_access_granted_email(group_member.id).deliver_later
+    mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later
   end
 
   def project_was_moved(project, old_path_with_namespace)
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index f309e80a39a7d8a2b42530f9b3ed242552169be9..5b8a0262ea076b8827a1dbc799ac7389cc1a0d29 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -109,7 +109,7 @@
             %span.pull-right.light
               = member.human_access
               - if can?(current_user, :destroy_group_member, member)
-                = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
+                = link_to group_group_member_path(@group, member), data: { confirm: remove_member_message(member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
                   %i.fa.fa-minus.fa-inverse
       .panel-footer
         = paginate @members, param_name: 'members_page', theme: 'gitlab'
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 73986d21bcf73c90a1dc435c3d28489fc9167cb0..9e55a562e18032d4cda87d49032792ec72fe6790 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -142,7 +142,7 @@
               %i.fa.fa-pencil-square-o
         %ul.well-list
           - @group_members.each do |member|
-            = render 'groups/group_members/group_member', member: member, show_controls: false
+            = render 'shared/members/member', member: member, show_controls: false
         .panel-footer
           = paginate @group_members, param_name: 'group_members_page', theme: 'gitlab'
 
@@ -172,7 +172,7 @@
                 %span.light Owner
               - else
                 %span.light= project_member.human_access
-                = link_to namespace_project_project_member_path(@project.namespace, @project, project_member), data: { confirm: remove_from_project_team_message(@project, project_member)}, method: :delete, remote: true, class: "btn btn-sm btn-remove" do
+                = link_to namespace_project_project_member_path(@project.namespace, @project, project_member), data: { confirm: remove_member_message(project_member)}, method: :delete, remote: true, class: "btn btn-sm btn-remove" do
                   %i.fa.fa-times
       .panel-footer
         = paginate @project_members, param_name: 'project_members_page', theme: 'gitlab'
diff --git a/app/views/admin/users/groups.html.haml b/app/views/admin/users/groups.html.haml
index dbecb7bbfd641578894ed8a33f6a1927e0c3e7c2..b0a709a568a62ac73c42b56e888becc704922264 100644
--- a/app/views/admin/users/groups.html.haml
+++ b/app/views/admin/users/groups.html.haml
@@ -13,7 +13,7 @@
           .pull-right
             %span.light= group_member.human_access
             - unless group_member.owner?
-              = link_to group_group_member_path(group, group_member), data: { confirm: remove_user_from_group_message(group, group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
+              = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
                 %i.fa.fa-times.fa-inverse
 - else
   .nothing-here-block This user has no groups.
diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml
index b655b2a15f5b123dcec41050ee77926de734172b..84b9ceb23b3ba11686904ebfb65b460c81e7a911 100644
--- a/app/views/admin/users/projects.html.haml
+++ b/app/views/admin/users/projects.html.haml
@@ -38,6 +38,5 @@
                   %span.light= member.human_access
 
                   - if member.respond_to? :project
-                    = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_from_project_team_message(project, member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do
+                    = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do
                       %i.fa.fa-times
-
diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml
deleted file mode 100644
index 6bb542e658dba01b2e4fd9b10d65bb0a3b508b23..0000000000000000000000000000000000000000
--- a/app/views/groups/group_members/_group_member.html.haml
+++ /dev/null
@@ -1,57 +0,0 @@
-- user = member.user
-- return unless user || member.invite?
-- show_roles = local_assigns.fetch(:show_roles, true)
-
-%li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)}
-  %span{class: ("list-item-name" if show_controls)}
-    - if member.user
-      = image_tag avatar_icon(user, 24), class: "avatar s24", alt: ''
-      %strong
-        = link_to user.name, user_path(user)
-      %span.cgray= user.username
-      - if user == current_user
-        %span.label.label-success It's you
-      - if user.blocked?
-        %label.label.label-danger
-          %strong Blocked
-    - else
-      = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: ''
-      %strong
-        = member.invite_email
-      %span.cgray
-        invited
-        - if member.created_by
-          by
-          = link_to member.created_by.name, user_path(member.created_by)
-        = time_ago_with_tooltip(member.created_at)
-
-      - if show_controls && can?(current_user, :admin_group_member, @group)
-        = link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
-          Resend invite
-
-  - if show_roles && should_user_see_group_roles?(current_user, @group)
-    %span.pull-right
-      %strong.member-access-level= member.human_access
-      - if show_controls
-        - if can?(current_user, :update_group_member, member)
-          = button_tag class: "btn-xs btn btn-grouped inline js-toggle-button",
-                       title: 'Edit access level', type: 'button' do
-            = icon('pencil')
-
-        - if can?(current_user, :destroy_group_member, member)
-          &nbsp;
-          - if current_user == user
-            = link_to leave_group_group_members_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
-              = icon("sign-out")
-              Leave
-          - else
-            = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
-              = icon('trash')
-
-    .edit-member.hide.js-toggle-content
-      %br
-      = form_for [@group, member], remote: true do |f|
-        .prepend-top-10
-          = f.select :access_level, options_for_select(GroupMember.access_level_roles, member.access_level), {}, class: 'form-control'
-        .prepend-top-10
-          = f.submit 'Save', class: 'btn btn-save btn-sm'
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 0eb6bbd442015e0843a46163d6e6e372cb031cc2..a36531e095a477f070eba0f6b4769394c938e7a3 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -6,12 +6,13 @@
       .panel-heading
         Add new user to group
       .panel-body
-        - if should_user_see_group_roles?(current_user, @group)
-          %p.light
-            Members of group have access to all group projects.
+        %p.light
+          Members of group have access to all group projects.
         .new-group-member-holder
           = render "new_group_member"
 
+    = render 'shared/members/requests', membership_source: @group, members: @members.request
+
   .panel.panel-default
     .panel-heading
       %strong #{@group.name}
@@ -25,9 +26,8 @@
           = button_tag class: 'btn', title: 'Search' do
             = icon("search")
     %ul.content-list
-      - @members.each do |member|
-        = render 'groups/group_members/group_member', member: member, show_controls: true
-    = paginate @members, theme: 'gitlab'
+      = render partial: 'shared/members/member', collection: @members.non_request, as: :member
+    = paginate @members.non_request, theme: 'gitlab'
 
 :javascript
   $('form.member-search-form').on('submit', function(event) {
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index df726e2b2b9f355528af9b3ba3247c159815847c..b0b3a51ce58629cbf6d25df0d48b6e567424811a 100644
--- a/app/views/groups/group_members/update.js.haml
+++ b/app/views/groups/group_members/update.js.haml
@@ -1,2 +1,2 @@
 :plain
-  $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render(@group_member, member: @group_member, show_controls: true))}');
+  $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render(@group_member, member: @group_member))}');
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 85635bc461618e852642cfdf967d445ad090d6ee..62ebd69485cd80d8da617ed2da982b29bc5a3aed 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -19,6 +19,9 @@
         .cover-desc.description
           = markdown(@group.description, pipeline: :description)
 
+    - if current_user
+      = render 'shared/members/access_request_buttons', source: @group
+
 %div{ class: container_class }
   .top-area
     %ul.nav-links
diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml
index 0b2673f1a8291f6b21140939901a5d474ab5c49e..dac46648b9f5652741e3748c324437cb432a750b 100644
--- a/app/views/layouts/nav/_group_settings.html.haml
+++ b/app/views/layouts/nav/_group_settings.html.haml
@@ -14,7 +14,3 @@
             %li
               = link_to edit_group_path(@group) do
                 Edit Group
-          %li
-            = link_to leave_group_group_members_path(@group),
-              data: { confirm: leave_group_message(@group.name) }, method: :delete, title: 'Leave group' do
-              Leave Group
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 0ac44b084a9bdf3277c1b118f55000b7fc86dffd..718acb424b2bdc0f4408c14aba6cd3e83db8842c 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -1,13 +1,15 @@
 - if current_user
-  - access = user_max_access_in_project(current_user.id, @project)
-  - can_edit = can?(current_user, :admin_project, @project)
   .controls
     .dropdown.project-settings-dropdown
       %a.dropdown-new.btn.btn-default#project-settings-button{href: '#', 'data-toggle' => 'dropdown'}
         = icon('cog')
         = icon('caret-down')
       %ul.dropdown-menu.dropdown-menu-align-right
+        - access = @project.team.max_member_access(current_user.id)
+        - can_edit = can?(current_user, :admin_project, @project)
+
         = render 'layouts/nav/project_settings', access: access, can_edit: can_edit
+
         - if can_edit || access
           %li.divider
           - if can_edit
@@ -16,8 +18,8 @@
                 Edit Project
           - if access
             %li
-              = link_to leave_namespace_project_project_members_path(@project.namespace, @project),
-                data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do
+              = link_to polymorphic_path([:leave, @project, :members]),
+                data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do
                 Leave Project
 
 %div{ class: nav_control_class }
diff --git a/app/views/notify/group_access_granted_email.html.haml b/app/views/notify/group_access_granted_email.html.haml
deleted file mode 100644
index f1916d624b6b35d9fabe1808660022197e8b324d..0000000000000000000000000000000000000000
--- a/app/views/notify/group_access_granted_email.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-%p
-  = "You have been granted #{@group_member.human_access} access to group"
-  = link_to group_url(@group) do
-    = @group.name
diff --git a/app/views/notify/group_access_granted_email.text.erb b/app/views/notify/group_access_granted_email.text.erb
deleted file mode 100644
index ef9617bfc16ad3d47aa75524e5d28a379ca1639b..0000000000000000000000000000000000000000
--- a/app/views/notify/group_access_granted_email.text.erb
+++ /dev/null
@@ -1,4 +0,0 @@
-
-You have been granted <%= @group_member.human_access %> access to group <%= @group.name %>
-
-<%= url_for(group_url(@group)) %>
diff --git a/app/views/notify/group_invite_accepted_email.html.haml b/app/views/notify/group_invite_accepted_email.html.haml
deleted file mode 100644
index 55efad384a79552ba51ee49935e04fcdc9a53c1e..0000000000000000000000000000000000000000
--- a/app/views/notify/group_invite_accepted_email.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-%p
-  #{@group_member.invite_email}, now known as
-  #{link_to @group_member.user.name, user_url(@group_member.user)},
-  has accepted your invitation to join group
-  #{link_to @group.name, group_url(@group)}.
-
diff --git a/app/views/notify/group_invite_accepted_email.text.erb b/app/views/notify/group_invite_accepted_email.text.erb
deleted file mode 100644
index f8b70f7a5a60c8d617cd94e3db88a91da13c8007..0000000000000000000000000000000000000000
--- a/app/views/notify/group_invite_accepted_email.text.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-<%= @group_member.invite_email %>, now known as <%= @group_member.user.name %>, has accepted your invitation to join group <%= @group.name %>.
-
-<%= group_url(@group) %>
diff --git a/app/views/notify/group_invite_declined_email.html.haml b/app/views/notify/group_invite_declined_email.html.haml
deleted file mode 100644
index f9525d84fac655d96f2dbaced015467d4bcad1fa..0000000000000000000000000000000000000000
--- a/app/views/notify/group_invite_declined_email.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-%p
-  #{@invite_email}
-  has declined your invitation to join group
-  #{link_to @group.name, group_url(@group)}.
-
diff --git a/app/views/notify/group_invite_declined_email.text.erb b/app/views/notify/group_invite_declined_email.text.erb
deleted file mode 100644
index 6c19a288d15fb2030a5c814645fbd4484156c276..0000000000000000000000000000000000000000
--- a/app/views/notify/group_invite_declined_email.text.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-<%= @invite_email %> has declined your invitation to join group <%= @group.name %>.
-
-<%= group_url(@group) %>
diff --git a/app/views/notify/group_member_invited_email.html.haml b/app/views/notify/group_member_invited_email.html.haml
deleted file mode 100644
index 163e88bfea3b455a6fd478ed25236f703d681372..0000000000000000000000000000000000000000
--- a/app/views/notify/group_member_invited_email.html.haml
+++ /dev/null
@@ -1,14 +0,0 @@
-%p
-  You have been invited
-  - if inviter = @group_member.created_by
-    by
-    = link_to inviter.name, user_url(inviter)
-  to join group
-  = link_to @group.name, group_url(@group)
-  as #{@group_member.human_access}.
-
-%p
-  = link_to 'Accept invitation', invite_url(@token)
-  or
-  = link_to 'decline', decline_invite_url(@token)
-
diff --git a/app/views/notify/group_member_invited_email.text.erb b/app/views/notify/group_member_invited_email.text.erb
deleted file mode 100644
index 28ce4819b14eedf5132758b9e88db81bb6d10940..0000000000000000000000000000000000000000
--- a/app/views/notify/group_member_invited_email.text.erb
+++ /dev/null
@@ -1,4 +0,0 @@
-You have been invited <%= "by #{@group_member.created_by.name} " if @group_member.created_by %>to join group <%= @group.name %> as <%= @group_member.human_access %>.
-
-Accept invitation: <%= invite_url(@token) %>
-Decline invitation: <%= decline_invite_url(@token) %>
diff --git a/app/views/notify/member_access_denied_email.html.haml b/app/views/notify/member_access_denied_email.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..71c9c50071a0ada253a2a16775e22c623e62c5a3
--- /dev/null
+++ b/app/views/notify/member_access_denied_email.html.haml
@@ -0,0 +1,4 @@
+%p
+  Your request to join the
+  #{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}
+  has been denied.
diff --git a/app/views/notify/member_access_denied_email.text.erb b/app/views/notify/member_access_denied_email.text.erb
new file mode 100644
index 0000000000000000000000000000000000000000..87f2ef817eee25ba18e61fbba454ff2c11a062ec
--- /dev/null
+++ b/app/views/notify/member_access_denied_email.text.erb
@@ -0,0 +1,3 @@
+Your request to join the <%= member_source.human_name %> <%= member_source.model_name.singular %> has been denied.
+
+<%= member_source.web_url %>
diff --git a/app/views/notify/member_access_granted_email.html.haml b/app/views/notify/member_access_granted_email.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..18dec8065393c01d1ddbffbf4eb476eac67f3bcf
--- /dev/null
+++ b/app/views/notify/member_access_granted_email.html.haml
@@ -0,0 +1,3 @@
+%p
+  You have been granted #{member.human_access} access to the
+  #{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}.
diff --git a/app/views/notify/member_access_granted_email.text.erb b/app/views/notify/member_access_granted_email.text.erb
new file mode 100644
index 0000000000000000000000000000000000000000..a9fb3a589a51ad57b05a3a432874fea1a28221e0
--- /dev/null
+++ b/app/views/notify/member_access_granted_email.text.erb
@@ -0,0 +1,3 @@
+You have been granted <%= member.human_access %> access to the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
+
+<%= member_source.web_url %>
diff --git a/app/views/notify/member_access_requested_email.html.haml b/app/views/notify/member_access_requested_email.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..76f1f08a0cbfe9fdbcd2e2b9072ea13e1c0a83f4
--- /dev/null
+++ b/app/views/notify/member_access_requested_email.html.haml
@@ -0,0 +1,3 @@
+%p
+  #{link_to member.user.name, member.user} requested #{member.human_access}
+  access to the #{link_to member_source.human_name, polymorphic_url([member_source, :members])} #{member_source.model_name.singular}.
diff --git a/app/views/notify/member_access_requested_email.text.erb b/app/views/notify/member_access_requested_email.text.erb
new file mode 100644
index 0000000000000000000000000000000000000000..9c5ee0eaf26a4a9f4c05d2e164c680f985b6aa1a
--- /dev/null
+++ b/app/views/notify/member_access_requested_email.text.erb
@@ -0,0 +1,3 @@
+<%= member.user.name %> (<%= user_url(member.user) %>) requested <%= member.human_access %> access to the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
+
+<%= polymorphic_url([member_source, :members]) %>
diff --git a/app/views/notify/member_invite_accepted_email.html.haml b/app/views/notify/member_invite_accepted_email.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..2d1d40881ebc7f98660d7bbfdcf82d783c7ebbb0
--- /dev/null
+++ b/app/views/notify/member_invite_accepted_email.html.haml
@@ -0,0 +1,5 @@
+%p
+  #{member.invite_email}, now known as
+  #{link_to member.user.name, user_url(member.user)},
+  has accepted your invitation to join the
+  #{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}.
diff --git a/app/views/notify/member_invite_accepted_email.text.erb b/app/views/notify/member_invite_accepted_email.text.erb
new file mode 100644
index 0000000000000000000000000000000000000000..cef87101427e3310fc4d43f9cf10ef04325794cc
--- /dev/null
+++ b/app/views/notify/member_invite_accepted_email.text.erb
@@ -0,0 +1,3 @@
+<%= member.invite_email %>, now known as <%= member.user.name %>, has accepted your invitation to join the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
+
+<%= member_source.web_url %>
diff --git a/app/views/notify/member_invite_declined_email.html.haml b/app/views/notify/member_invite_declined_email.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..aa1b373d1a630d5bc97c23c1c2b13c7981cd7d32
--- /dev/null
+++ b/app/views/notify/member_invite_declined_email.html.haml
@@ -0,0 +1,4 @@
+%p
+  #{@invite_email}
+  has declined your invitation to join the
+  #{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}.
diff --git a/app/views/notify/member_invite_declined_email.text.erb b/app/views/notify/member_invite_declined_email.text.erb
new file mode 100644
index 0000000000000000000000000000000000000000..8bc305910c49c1c6f16335b8c34433fa911716cc
--- /dev/null
+++ b/app/views/notify/member_invite_declined_email.text.erb
@@ -0,0 +1,3 @@
+<%= @invite_email %> has declined your invitation to join the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
+
+<%= member_source.web_url %>
diff --git a/app/views/notify/member_invited_email.html.haml b/app/views/notify/member_invited_email.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..b8b75da3f2f02d47d0a77b5a6dc2856788a2797e
--- /dev/null
+++ b/app/views/notify/member_invited_email.html.haml
@@ -0,0 +1,13 @@
+%p
+  You have been invited
+  - if member.created_by
+    by
+    = link_to member.created_by.name, user_url(member.created_by)
+  to join the
+  = link_to member_source.human_name, member_source.web_url
+  #{member_source.model_name.singular} as #{member.human_access}.
+
+%p
+  = link_to 'Accept invitation', invite_url(@token)
+  or
+  = link_to 'decline', decline_invite_url(@token)
diff --git a/app/views/notify/member_invited_email.text.erb b/app/views/notify/member_invited_email.text.erb
new file mode 100644
index 0000000000000000000000000000000000000000..0a6393355be535c32ff49159e51ed7f413291dc1
--- /dev/null
+++ b/app/views/notify/member_invited_email.text.erb
@@ -0,0 +1,4 @@
+You have been invited <%= "by #{member.created_by.name} " if member.created_by %>to join the <%= member_source.human_name %> <%= member_source.model_name.singular %> as <%= member.human_access %>.
+
+Accept invitation: <%= invite_url(@token) %>
+Decline invitation: <%= decline_invite_url(@token) %>
diff --git a/app/views/notify/project_access_granted_email.html.haml b/app/views/notify/project_access_granted_email.html.haml
deleted file mode 100644
index dfc30a2d360cd312cfdb959731994ea6cafa8138..0000000000000000000000000000000000000000
--- a/app/views/notify/project_access_granted_email.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-%p
-  = "You have been granted #{@project_member.human_access} access to project"
-%p
-  = link_to namespace_project_url(@project.namespace, @project) do
-    = @project.name_with_namespace
diff --git a/app/views/notify/project_access_granted_email.text.erb b/app/views/notify/project_access_granted_email.text.erb
deleted file mode 100644
index 68eb1611ba7e6f4e73faa8d09ca8de7d01892ba9..0000000000000000000000000000000000000000
--- a/app/views/notify/project_access_granted_email.text.erb
+++ /dev/null
@@ -1,4 +0,0 @@
-
-You have been granted <%= @project_member.human_access %> access to project <%= @project.name_with_namespace %>
-
-<%= url_for(namespace_project_url(@project.namespace, @project)) %>
diff --git a/app/views/notify/project_invite_accepted_email.html.haml b/app/views/notify/project_invite_accepted_email.html.haml
deleted file mode 100644
index 7e58d30b10a89eaca450b4ee0cae8cd451dd1eff..0000000000000000000000000000000000000000
--- a/app/views/notify/project_invite_accepted_email.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-%p
-  #{@project_member.invite_email}, now known as
-  #{link_to @project_member.user.name, user_url(@project_member.user)},
-  has accepted your invitation to join project
-  #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}.
-
diff --git a/app/views/notify/project_invite_accepted_email.text.erb b/app/views/notify/project_invite_accepted_email.text.erb
deleted file mode 100644
index fcbe752114dace69c69679b299f93cf1e3672751..0000000000000000000000000000000000000000
--- a/app/views/notify/project_invite_accepted_email.text.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-<%= @project_member.invite_email %>, now known as <%= @project_member.user.name %>, has accepted your invitation to join project <%= @project.name_with_namespace %>.
-
-<%= namespace_project_url(@project.namespace, @project) %>
diff --git a/app/views/notify/project_invite_declined_email.html.haml b/app/views/notify/project_invite_declined_email.html.haml
deleted file mode 100644
index c2d7e6f6e3a0aed662a768042970f210c80099e2..0000000000000000000000000000000000000000
--- a/app/views/notify/project_invite_declined_email.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-%p
-  #{@invite_email}
-  has declined your invitation to join project
-  #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}.
-
diff --git a/app/views/notify/project_invite_declined_email.text.erb b/app/views/notify/project_invite_declined_email.text.erb
deleted file mode 100644
index 484687fa51cbeef4ea3527d3b96e62c3e6bc2117..0000000000000000000000000000000000000000
--- a/app/views/notify/project_invite_declined_email.text.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-<%= @invite_email %> has declined your invitation to join project <%= @project.name_with_namespace %>.
-
-<%= namespace_project_url(@project.namespace, @project) %>
diff --git a/app/views/notify/project_member_invited_email.html.haml b/app/views/notify/project_member_invited_email.html.haml
deleted file mode 100644
index 79eb89616de49402bfdde544b88fcfa633248b10..0000000000000000000000000000000000000000
--- a/app/views/notify/project_member_invited_email.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-%p
-  You have been invited 
-  - if inviter = @project_member.created_by
-    by
-    = link_to inviter.name, user_url(inviter)
-  to join project
-  = link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)
-  as #{@project_member.human_access}.
-
-%p
-  = link_to 'Accept invitation', invite_url(@token)
-  or
-  = link_to 'decline', decline_invite_url(@token)
diff --git a/app/views/notify/project_member_invited_email.text.erb b/app/views/notify/project_member_invited_email.text.erb
deleted file mode 100644
index e07062721158bb29e0a0131e03dd912224d708ff..0000000000000000000000000000000000000000
--- a/app/views/notify/project_member_invited_email.text.erb
+++ /dev/null
@@ -1,4 +0,0 @@
-You have been invited <%= "by #{@project_member.created_by.name} " if @project_member.created_by %>to join project <%= @project.name_with_namespace %> as <%= @project_member.human_access %>.
-
-Accept invitation: <%= invite_url(@token) %>
-Decline invitation: <%= decline_invite_url(@token) %>
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index f5bc1b4e409b6a005ad5648ada91cad34742e91d..2b19ee93eea6645bc7070fb80d47ed5329ef11f9 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -29,10 +29,13 @@
           .project-clone-holder
             = render "shared/clone_panel"
 
-      .project-repo-buttons.btn-group.project-right-buttons
-        = render "projects/buttons/download"
-        = render 'projects/buttons/dropdown'
-        = render 'projects/buttons/notifications'
+      .project-repo-buttons.project-right-buttons
+        - if current_user
+          = render 'shared/members/access_request_buttons', source: @project
+        .btn-group
+          = render "projects/buttons/download"
+          = render 'projects/buttons/dropdown'
+          = render 'projects/buttons/notifications'
 
 :javascript
   new Star();
diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml
index 3b97dc9328f34dada6f9a7fce74cf8d797de08f3..a7a97181096932d82026e4ae8feff5274405ad83 100644
--- a/app/views/projects/buttons/_notifications.html.haml
+++ b/app/views/projects/buttons/_notifications.html.haml
@@ -1,7 +1,7 @@
 - if @notification_setting
   = form_for @notification_setting, url: namespace_project_notification_setting_path(@project.namespace.becomes(Namespace), @project), method: :patch, remote: true, html: { class: 'inline', id: 'notification-form' } do |f|
     = f.hidden_field :level
-    .dropdown
+    .dropdown.hidden-sm
       %button.btn.btn-default.notifications-btn#notifications-button{ data: { toggle: "dropdown" }, aria: { haspopup: "true", expanded: "false" } }
         = icon('bell')
         = notification_title(@notification_setting.level)
diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml
index 6671ee2c6d6be9a7cd73f7ba68e4b3643bd3bf32..cb6136c215a9158f1eb1355e998609bc039474fe 100644
--- a/app/views/projects/project_members/_group_members.html.haml
+++ b/app/views/projects/project_members/_group_members.html.haml
@@ -6,11 +6,14 @@
       (#{members.count})
     - if can?(current_user, :admin_group_member, @group)
       .controls
-        = link_to group_group_members_path(@group), class: 'btn' do
-          Manage group members
+        = link_to 'Manage group members',
+                  group_group_members_path(@group),
+                  class: 'btn'
   %ul.content-list
-    - members.limit(20).each do |member|
-      = render 'groups/group_members/group_member', member: member, show_controls: false
-    - if members.count > 20
+    = render partial: 'shared/members/member',
+             collection: members.limit(20),
+             as: :member,
+             locals: { show_controls: false }
+    - if members.size > 20
       %li
         and #{members.count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(@group)}
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index f0f3bb3c177eab2d04f5b1f01ef0135f4b6c4fb2..82892a3335828e877ba7510598c6fe34bca68e4d 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -9,7 +9,7 @@
   .form-group
     = f.label :access_level, "Project Access", class: 'control-label'
     .col-sm-10
-      = select_tag :access_level, options_for_select(ProjectMember.access_roles, @project_member.access_level), class: "project-access-select select2"
+      = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "project-access-select select2"
       .help-block
         Read more about role permissions
         %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml
deleted file mode 100644
index 268f140d7db5b6038076d78ac99c7d23187debb1..0000000000000000000000000000000000000000
--- a/app/views/projects/project_members/_project_member.html.haml
+++ /dev/null
@@ -1,55 +0,0 @@
-- user = member.user
-- return unless user || member.invite?
-
-%li{class: "#{dom_class(member)} js-toggle-container project_member_row access-#{member.human_access.downcase}", id: dom_id(member)}
-  %span.list-item-name
-    - if member.user
-      = image_tag avatar_icon(user, 24), class: "avatar s24", alt: ''
-      %strong
-        = link_to user.name, user_path(user)
-      %span.cgray= user.username
-      - if user == current_user
-        %span.label.label-success It's you
-      - if user.blocked?
-        %label.label.label-danger
-          %strong Blocked
-    - else
-      = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: ''
-      %strong
-        = member.invite_email
-      %span.cgray
-        invited
-        - if member.created_by
-          by
-          = link_to member.created_by.name, user_path(member.created_by)
-        = time_ago_with_tooltip(member.created_at)
-
-      - if can?(current_user, :admin_project_member, @project)
-        = link_to resend_invite_namespace_project_project_member_path(@project.namespace, @project, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
-          Resend invite
-
-  - if can?(current_user, :admin_project_member, @project)
-    .pull-right
-      %strong= member.human_access
-      - if can?(current_user, :update_project_member, member)
-        = button_tag class: "btn-xs btn-grouped inline btn js-toggle-button",
-                     title: 'Edit access level', type: 'button' do
-          = icon('pencil')
-
-      - if can?(current_user, :destroy_project_member, member)
-        &nbsp;
-        - if current_user == user
-          = link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: leave_project_message(@project) }, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do
-            = icon("sign-out")
-            Leave
-        - else
-          = link_to namespace_project_project_member_path(@project.namespace, @project, member), data: { confirm: remove_from_project_team_message(@project, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do
-            = icon('trash')
-
-    .edit-member.hide.js-toggle-content
-      %br
-      = form_for member, as: :project_member, url: namespace_project_project_member_path(@project.namespace, @project, member), remote: true do |f|
-        .prepend-top-10
-          = f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level), {}, class: 'form-control'
-        .prepend-top-10
-          = f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/projects/project_members/_shared_group_members.html.haml b/app/views/projects/project_members/_shared_group_members.html.haml
index ae13f8428f0e1a89a9d1d8165b1b9d21ce528dfd..952844acefc3997e195d30ae05f869199a4f840a 100644
--- a/app/views/projects/project_members/_shared_group_members.html.haml
+++ b/app/views/projects/project_members/_shared_group_members.html.haml
@@ -14,8 +14,10 @@
             %i.fa.fa-pencil-square-o
             Edit group members
     %ul.content-list
-      - shared_group.group_members.order('access_level DESC').limit(20).each do |member|
-        = render 'groups/group_members/group_member', member: member, show_controls: false, show_roles: false
+      = render partial: 'shared/members/member',
+               collection: shared_group.group_members.order(access_level: :desc).limit(20),
+               as: :member,
+               locals: { show_controls: false, show_roles: false }
       - if shared_group_users_count > 20
         %li
           and #{shared_group_users_count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(shared_group)}
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index e8dce30425f13d48bad944618d5abb6046fb626e..03207614258b08c3f914c09cb7ebd5c9f382e168 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -11,8 +11,7 @@
         = button_tag class: 'btn', title: 'Search' do
           = icon("search")
   %ul.content-list
-    - members.each do |project_member|
-      = render 'project_member', member: project_member
+    = render partial: 'shared/members/member', collection: members, as: :member
 
 :javascript
   $('form.member-search-form').on('submit', function (event) {
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 15dc064e7eaac047f865adcf49494891ccae684a..357ccccaf1d73b99c9f1d5b8629b3a2eccaaf434 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -13,7 +13,9 @@
           Users with access to this project are listed below.
         = render "new_project_member"
 
-  = render "team", members: @project_members
+    = render 'shared/members/requests', membership_source: @project, members: @project_members.request
+
+  = render 'team', members: @project_members.non_request
 
   - if @group
     = render "group_members", members: @group_members
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index a25365a94f2b9e1380f92c76e136a2da6fa79195..1ad953510056feb56e98f72dd76124d5d63aa94a 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -9,7 +9,7 @@
         = link_to edit_group_path(group), class: "btn" do
           = icon('cogs')
 
-      = link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn", title: 'Leave this group' do
+      = link_to leave_group_group_members_path(group), data: { confirm: leave_confirmation_message(group) }, method: :delete, class: "btn", title: 'Leave this group' do
         = icon('sign-out')
 
   .stats
diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..ed0a6ebcf84f119390c709bbb4123125651da321
--- /dev/null
+++ b/app/views/shared/members/_access_request_buttons.html.haml
@@ -0,0 +1,12 @@
+- member = source.members.find_by(user_id: current_user.id)
+
+- if member
+  - if member.request?
+    = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
+              method: :delete,
+              data: { confirm: remove_member_message(member) },
+              class: 'btn access-request-button hidden-xs'
+- else
+  = link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
+            method: :post,
+            class: 'btn access-request-button hidden-xs'
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..c69d4cbfbe34914b54d227a0df842989cd58fd87
--- /dev/null
+++ b/app/views/shared/members/_member.html.haml
@@ -0,0 +1,77 @@
+- show_roles = local_assigns.fetch(:show_roles, true)
+- show_controls = local_assigns.fetch(:show_controls, true)
+- user = member.user
+
+%li.js-toggle-container{ class: dom_class(member), id: dom_id(member) }
+  %span{ class: ("list-item-name" if show_controls) }
+    - if user
+      = image_tag avatar_icon(user, 24), class: "avatar s24", alt: ''
+      %strong
+        = link_to user.name, user_path(user)
+      %span.cgray= user.username
+
+      - if user == current_user
+        %span.label.label-success It's you
+
+      - if user.blocked?
+        %label.label.label-danger
+          %strong Blocked
+
+      - if member.request?
+        %span.cgray
+          – Requested
+          = time_ago_with_tooltip(member.requested_at)
+    - else
+      = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: ''
+      %strong= member.invite_email
+      %span.cgray
+        – Invited
+        - if member.created_by
+          by
+          = link_to member.created_by.name, user_path(member.created_by)
+        = time_ago_with_tooltip(member.created_at)
+
+      - if show_controls && can?(current_user, action_member_permission(:admin, member), member.source)
+        = link_to 'Resend invite', polymorphic_path([:resend_invite, member]),
+                  method: :post,
+                  class: 'btn-xs btn'
+
+  - if show_roles && can_see_member_roles?(source: member.source, user: current_user)
+    %span.pull-right
+      %strong= member.human_access
+      - if show_controls
+        - if can?(current_user, action_member_permission(:update, member), member)
+          = button_tag icon('pencil'),
+                       type: 'button',
+                       class: 'btn-xs btn btn-grouped inline js-toggle-button',
+                       title: 'Edit access level'
+
+          - if member.request?
+            &nbsp;
+            = link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]),
+                      method: :post,
+                      class: 'btn-xs btn btn-success',
+                      title: 'Grant access'
+
+        - if can?(current_user, action_member_permission(:destroy, member), member)
+          &nbsp;
+          - if current_user == user
+            = link_to icon('sign-out', text: 'Leave'), polymorphic_path([:leave, member.source, :members]),
+                      method: :delete,
+                      data: { confirm: leave_confirmation_message(member.source) },
+                      class: 'btn-xs btn btn-remove'
+          - else
+            = link_to icon('trash'), member,
+                      remote: true,
+                      method: :delete,
+                      data: { confirm: remove_member_message(member) },
+                      class: 'btn-xs btn btn-remove',
+                      title: remove_member_title(member)
+
+    .edit-member.hide.js-toggle-content
+      %br
+      = form_for member, remote: true do |f|
+        .prepend-top-10
+          = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control'
+        .prepend-top-10
+          = f.submit 'Save', class: 'btn btn-save btn-sm'
diff --git a/app/views/shared/members/_requests.html.haml b/app/views/shared/members/_requests.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..b5963876034533e5ca288be635653559805e830b
--- /dev/null
+++ b/app/views/shared/members/_requests.html.haml
@@ -0,0 +1,8 @@
+- if members.any?
+  .panel.panel-default
+    .panel-heading
+      %strong= membership_source.name
+      access requests
+      %small= "(#{members.size})"
+    %ul.content-list
+      = render partial: 'shared/members/member', collection: members, as: :member
diff --git a/config/routes.rb b/config/routes.rb
index 59724b737f676a01bae64b09c82ba00ce99d61e8..fe48ae95e29655b97c9fbf6146b88481aae6b0af 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -30,6 +30,11 @@
     mount LetterOpenerWeb::Engine, at: '/rails/letter_opener'
   end
 
+  concern :access_requestable do
+    post :request_access, on: :collection
+    post :approve_access_request, on: :member
+  end
+
   namespace :ci do
     # CI API
     Ci::API::API.logger Rails.logger
@@ -409,7 +414,7 @@
     end
 
     scope module: :groups do
-      resources :group_members, only: [:index, :create, :update, :destroy] do
+      resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
         post :resend_invite, on: :member
         delete :leave, on: :collection
       end
@@ -766,7 +771,7 @@
           end
         end
 
-        resources :project_members, except: [:new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ } do
+        resources :project_members, except: [:new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do
           collection do
             delete :leave
 
diff --git a/db/migrate/20160314114439_add_requested_at_to_members.rb b/db/migrate/20160314114439_add_requested_at_to_members.rb
new file mode 100644
index 0000000000000000000000000000000000000000..273819d4cd8069f160d0a360bfa72f1610d16f03
--- /dev/null
+++ b/db/migrate/20160314114439_add_requested_at_to_members.rb
@@ -0,0 +1,5 @@
+class AddRequestedAtToMembers < ActiveRecord::Migration
+  def change
+    add_column :members, :requested_at, :datetime
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c1e447d3cdaa4f74300adc7f095fcebe193d63ee..5fe39c5e59cf55cacb4a82c40f7ffe2baf91c70f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -537,6 +537,7 @@
     t.string   "invite_email"
     t.string   "invite_token"
     t.datetime "invite_accepted_at"
+    t.datetime "requested_at"
   end
 
   add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree
diff --git a/features/steps/dashboard/group.rb b/features/steps/dashboard/group.rb
index 0c6a0ae37251c03f5cd5d838576eddf7b55ddcc6..9b79a3be49b3a2f373265c128c8a351e938f5e22 100644
--- a/features/steps/dashboard/group.rb
+++ b/features/steps/dashboard/group.rb
@@ -62,6 +62,6 @@ class Spinach::Features::DashboardGroup < Spinach::FeatureSteps
   end
 
   step 'I should see the "Can not leave message"' do
-    expect(page).to have_content "You can not leave Owned group because you're the last owner"
+    expect(page).to have_content "You can not leave the \"Owned\" group."
   end
 end
diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb
index 0706df3aec5d709e923f27d79dbd61839beb5095..dfa2fa75def097b4a4fdf0c2b62827a33c8dc810 100644
--- a/features/steps/group/members.rb
+++ b/features/steps/group/members.rb
@@ -53,7 +53,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
   step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
     page.within '.content-list' do
       expect(page).to have_content('sjobs@apple.com')
-      expect(page).to have_content('invited')
+      expect(page).to have_content('Invited')
       expect(page).to have_content('Reporter')
     end
   end
@@ -116,11 +116,9 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
     member = mary_jane_member
 
     page.within "#group_member_#{member.id}" do
-      find(".js-toggle-button").click
-      page.within "#edit_group_member_#{member.id}" do
-        select 'Developer', from: 'group_member_access_level'
-        click_on 'Save'
-      end
+      click_button "Edit access level"
+      select 'Developer', from: 'group_member_access_level'
+      click_on 'Save'
     end
   end
 
@@ -128,9 +126,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
     member = mary_jane_member
 
     page.within "#group_member_#{member.id}" do
-      page.within '.member-access-level' do
-        expect(page).to have_content "Developer"
-      end
+      expect(page).to have_content "Developer"
     end
   end
 
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index c6ced747370c3c75dd15e9ed5ceca9de755a5eaa..f32576d2cb1b6dcbd33acb87adc56eebf0c5ae44 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -26,8 +26,11 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
   end
 
   step 'I should see "Mike" in team list as "Reporter"' do
-    page.within ".access-reporter" do
+    user = User.find_by(name: 'Mike')
+    project_member = project.project_members.find_by(user_id: user.id)
+    page.within "#project_member_#{project_member.id}" do
       expect(page).to have_content('Mike')
+      expect(page).to have_content('Reporter')
     end
   end
 
@@ -40,16 +43,20 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
   end
 
   step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
-    page.within ".access-reporter" do
+    project_member = project.project_members.find_by(invite_email: 'sjobs@apple.com')
+    page.within "#project_member_#{project_member.id}" do
       expect(page).to have_content('sjobs@apple.com')
-      expect(page).to have_content('invited')
+      expect(page).to have_content('Invited')
       expect(page).to have_content('Reporter')
     end
   end
 
   step 'I should see "Dmitriy" in team list as "Developer"' do
-    page.within ".access-developer" do
+    user = User.find_by(name: 'Dmitriy')
+    project_member = project.project_members.find_by(user_id: user.id)
+    page.within "#project_member_#{project_member.id}" do
       expect(page).to have_content('Dmitriy')
+      expect(page).to have_content('Developer')
     end
   end
 
@@ -65,15 +72,14 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
   end
 
   step 'I should see "Dmitriy" in team list as "Reporter"' do
-    page.within ".access-reporter" do
+    user = User.find_by(name: 'Dmitriy')
+    project_member = project.project_members.find_by(user_id: user.id)
+    page.within "#project_member_#{project_member.id}" do
       expect(page).to have_content('Dmitriy')
+      expect(page).to have_content('Reporter')
     end
   end
 
-  step 'I click link "Remove from team"' do
-    click_link "Remove from team"
-  end
-
   step 'I should not see "Dmitriy" in team list' do
     user = User.find_by(name: "Dmitriy")
     expect(page).not_to have_content(user.name)
@@ -120,7 +126,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
     user = User.find_by(name: 'Dmitriy')
     project_member = project.project_members.find_by(user_id: user.id)
     page.within "#project_member_#{project_member.id}" do
-      click_link('Remove user from team')
+      click_link('Remove user from project')
     end
   end
 
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 14370ac218df24857a779cb8bdb3568880480f46..cc29c7ef428b7639875dab104b452ad9f73c814a 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -88,10 +88,7 @@ class ProjectMember < UserBasic
     class Group < Grape::Entity
       expose :id, :name, :path, :description, :visibility_level
       expose :avatar_url
-
-      expose :web_url do |group, options|
-        Gitlab::Routing.url_helpers.group_url(group)
-      end
+      expose :web_url
     end
 
     class GroupDetail < Group
diff --git a/lib/api/project_members.rb b/lib/api/project_members.rb
index 4aefdf319c68283ce3832dc08e0e57c93033645f..b703da0557a095a2a14f0b4338fac8d7e8a9d95d 100644
--- a/lib/api/project_members.rb
+++ b/lib/api/project_members.rb
@@ -46,7 +46,7 @@ class ProjectMembers < Grape::API
         required_attributes! [:user_id, :access_level]
 
         # either the user is already a team member or a new one
-        project_member = user_project.project_member_by_id(params[:user_id])
+        project_member = user_project.project_member(params[:user_id])
         if project_member.nil?
           project_member = user_project.project_members.new(
             user_id: params[:user_id],
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index a59865987151f57d85e0f12194a65c79ced1ab86..89c2c26a367630f87304f59d4d42a9290be5ec89 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -4,17 +4,211 @@
   let(:user)  { create(:user) }
   let(:group) { create(:group) }
 
-  context "index" do
+  describe '#index' do
     before do
       group.add_owner(user)
       stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
     end
 
     it 'renders index with group members' do
-      get :index, group_id: group.path
+      get :index, group_id: group
 
       expect(response.status).to eq(200)
       expect(response).to render_template(:index)
     end
   end
+
+  describe '#destroy' do
+    let(:group) { create(:group, :public) }
+
+    context 'when member is not found' do
+      it 'returns 403' do
+        delete :destroy, group_id: group,
+                         id: 42
+
+        expect(response.status).to eq(403)
+      end
+    end
+
+    context 'when member is found' do
+      let(:user) { create(:user) }
+      let(:group_user) { create(:user) }
+      let(:member) do
+        group.add_developer(group_user)
+        group.members.find_by(user_id: group_user)
+      end
+
+      context 'when user does not have enough rights' do
+        before do
+          group.add_developer(user)
+          sign_in(user)
+        end
+
+        it 'returns 403' do
+          delete :destroy, group_id: group,
+                           id: member
+
+          expect(response.status).to eq(403)
+          expect(group.users).to include group_user
+        end
+      end
+
+      context 'when user has enough rights' do
+        before do
+          group.add_owner(user)
+          sign_in(user)
+        end
+
+        it '[HTML] removes user from members' do
+          delete :destroy, group_id: group,
+                           id: member
+
+          expect(response).to set_flash.to 'User was successfully removed from group.'
+          expect(response).to redirect_to(group_group_members_path(group))
+          expect(group.users).not_to include group_user
+        end
+
+        it '[JS] removes user from members' do
+          xhr :delete, :destroy, group_id: group,
+                                 id: member
+
+          expect(response).to be_success
+          expect(group.users).not_to include group_user
+        end
+      end
+    end
+  end
+
+  describe '#leave' do
+    let(:group) { create(:group, :public) }
+    let(:user) { create(:user) }
+
+    context 'when member is not found' do
+      before { sign_in(user) }
+
+      it 'returns 403' do
+        delete :leave, group_id: group
+
+        expect(response.status).to eq(403)
+      end
+    end
+
+    context 'when member is found' do
+      context 'and is not an owner' do
+        before do
+          group.add_developer(user)
+          sign_in(user)
+        end
+
+        it 'removes user from members' do
+          delete :leave, group_id: group
+
+          expect(response).to set_flash.to "You left the \"#{group.name}\" group."
+          expect(response).to redirect_to(dashboard_groups_path)
+          expect(group.users).not_to include user
+        end
+      end
+
+      context 'and is an owner' do
+        before do
+          group.add_owner(user)
+          sign_in(user)
+        end
+
+        it 'cannot removes himself from the group' do
+          delete :leave, group_id: group
+
+          expect(response).to redirect_to(group_path(group))
+          expect(response).to set_flash[:alert].to "You can not leave the \"#{group.name}\" group. Transfer or delete the group."
+          expect(group.users).to include user
+        end
+      end
+
+      context 'and is a requester' do
+        before do
+          group.request_access(user)
+          sign_in(user)
+        end
+
+        it 'removes user from members' do
+          delete :leave, group_id: group
+
+          expect(response).to set_flash.to 'Your access request to the group has been withdrawn.'
+          expect(response).to redirect_to(dashboard_groups_path)
+          expect(group.members.request).to be_empty
+          expect(group.users).not_to include user
+        end
+      end
+    end
+  end
+
+  describe '#request_access' do
+    let(:group) { create(:group, :public) }
+    let(:user) { create(:user) }
+
+    before do
+      sign_in(user)
+    end
+
+    it 'creates a new GroupMember that is not a team member' do
+      post :request_access, group_id: group
+
+      expect(response).to set_flash.to 'Your request for access has been queued for review.'
+      expect(response).to redirect_to(group_path(group))
+      expect(group.members.request.exists?(user_id: user)).to be_truthy
+      expect(group.users).not_to include user
+    end
+  end
+
+  describe '#approve_access_request' do
+    let(:group) { create(:group, :public) }
+
+    context 'when member is not found' do
+      it 'returns 403' do
+        post :approve_access_request, group_id: group,
+                                      id: 42
+
+        expect(response.status).to eq(403)
+      end
+    end
+
+    context 'when member is found' do
+      let(:user) { create(:user) }
+      let(:group_requester) { create(:user) }
+      let(:member) do
+        group.request_access(group_requester)
+        group.members.request.find_by(user_id: group_requester)
+      end
+
+      context 'when user does not have enough rights' do
+        before do
+          group.add_developer(user)
+          sign_in(user)
+        end
+
+        it 'returns 403' do
+          post :approve_access_request, group_id: group,
+                                        id: member
+
+          expect(response.status).to eq(403)
+          expect(group.users).not_to include group_requester
+        end
+      end
+
+      context 'when user has enough rights' do
+        before do
+          group.add_owner(user)
+          sign_in(user)
+        end
+
+        it 'adds user to members' do
+          post :approve_access_request, group_id: group,
+                                        id: member
+
+          expect(response).to redirect_to(group_group_members_path(group))
+          expect(group.users).to include group_requester
+        end
+      end
+    end
+  end
 end
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index 750fbecdd0710518f1f3bb57f4588a5ec38095a3..fc5f458e79543b3496af06c64b6e6fe380716492 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -1,22 +1,22 @@
 require('spec_helper')
 
 describe Projects::ProjectMembersController do
-  let(:project) { create(:project) }
-  let(:another_project) { create(:project, :private) }
-  let(:user) { create(:user) }
-  let(:member) { create(:user) }
-
-  before do
-    project.team << [user, :master]
-    another_project.team << [member, :guest]
-    sign_in(user)
-  end
-
   describe '#apply_import' do
+    let(:project) { create(:project) }
+    let(:another_project) { create(:project, :private) }
+    let(:user) { create(:user) }
+    let(:member) { create(:user) }
+
+    before do
+      project.team << [user, :master]
+      another_project.team << [member, :guest]
+      sign_in(user)
+    end
+
     shared_context 'import applied' do
       before do
-        post(:apply_import, namespace_id: project.namespace.to_param,
-                            project_id: project.to_param,
+        post(:apply_import, namespace_id: project.namespace,
+                            project_id: project,
                             source_project_id: another_project.id)
       end
     end
@@ -48,18 +48,231 @@
   end
 
   describe '#index' do
-    let(:project) { create(:project, :private) }
-
     context 'when user is member' do
-      let(:member) { create(:user) }
-
       before do
+        project = create(:project, :private)
+        member = create(:user)
         project.team << [member, :guest]
         sign_in(member)
-        get :index, namespace_id: project.namespace.to_param, project_id: project.to_param
+
+        get :index, namespace_id: project.namespace, project_id: project
       end
 
       it { expect(response.status).to eq(200) }
     end
   end
+
+  describe '#destroy' do
+    let(:project) { create(:project, :public) }
+
+    context 'when member is not found' do
+      it 'returns 404' do
+        delete :destroy, namespace_id: project.namespace,
+                         project_id: project,
+                         id: 42
+
+        expect(response.status).to eq(404)
+      end
+    end
+
+    context 'when member is found' do
+      let(:user) { create(:user) }
+      let(:team_user) { create(:user) }
+      let(:member) do
+        project.team << [team_user, :developer]
+        project.members.find_by(user_id: team_user.id)
+      end
+
+      context 'when user does not have enough rights' do
+        before do
+          project.team << [user, :developer]
+          sign_in(user)
+        end
+
+        it 'returns 404' do
+          delete :destroy, namespace_id: project.namespace,
+                           project_id: project,
+                           id: member
+
+          expect(response.status).to eq(404)
+          expect(project.users).to include team_user
+        end
+      end
+
+      context 'when user has enough rights' do
+        before do
+          project.team << [user, :master]
+          sign_in(user)
+        end
+
+        it '[HTML] removes user from members' do
+          delete :destroy, namespace_id: project.namespace,
+                           project_id: project,
+                           id: member
+
+          expect(response).to redirect_to(
+            namespace_project_project_members_path(project.namespace, project)
+          )
+          expect(project.users).not_to include team_user
+        end
+
+        it '[JS] removes user from members' do
+          xhr :delete, :destroy, namespace_id: project.namespace,
+                                 project_id: project,
+                                 id: member
+
+          expect(response).to be_success
+          expect(project.users).not_to include team_user
+        end
+      end
+    end
+  end
+
+  describe '#leave' do
+    let(:project) { create(:project, :public) }
+    let(:user) { create(:user) }
+
+    context 'when member is not found' do
+      before { sign_in(user) }
+
+      it 'returns 403' do
+        delete :leave, namespace_id: project.namespace,
+                       project_id: project
+
+        expect(response.status).to eq(403)
+      end
+    end
+
+    context 'when member is found' do
+      context 'and is not an owner' do
+        before do
+          project.team << [user, :developer]
+          sign_in(user)
+        end
+
+        it 'removes user from members' do
+          delete :leave, namespace_id: project.namespace,
+                         project_id: project
+
+          expect(response).to set_flash.to "You left the \"#{project.human_name}\" project."
+          expect(response).to redirect_to(dashboard_projects_path)
+          expect(project.users).not_to include user
+        end
+      end
+
+      context 'and is an owner' do
+        before do
+          project.update(namespace_id: user.namespace_id)
+          project.team << [user, :master, user]
+          sign_in(user)
+        end
+
+        it 'cannot remove himself from the project' do
+          delete :leave, namespace_id: project.namespace,
+                         project_id: project
+
+          expect(response).to redirect_to(
+            namespace_project_path(project.namespace, project)
+          )
+          expect(response).to set_flash[:alert].to "You can not leave the \"#{project.human_name}\" project. Transfer or delete the project."
+          expect(project.users).to include user
+        end
+      end
+
+      context 'and is a requester' do
+        before do
+          project.request_access(user)
+          sign_in(user)
+        end
+
+        it 'removes user from members' do
+          delete :leave, namespace_id: project.namespace,
+                         project_id: project
+
+          expect(response).to set_flash.to 'Your access request to the project has been withdrawn.'
+          expect(response).to redirect_to(dashboard_projects_path)
+          expect(project.members.request).to be_empty
+          expect(project.users).not_to include user
+        end
+      end
+    end
+  end
+
+  describe '#request_access' do
+    let(:project) { create(:project, :public) }
+    let(:user) { create(:user) }
+
+    before do
+      sign_in(user)
+    end
+
+    it 'creates a new ProjectMember that is not a team member' do
+      post :request_access, namespace_id: project.namespace,
+                            project_id: project
+
+      expect(response).to set_flash.to 'Your request for access has been queued for review.'
+      expect(response).to redirect_to(
+        namespace_project_path(project.namespace, project)
+      )
+      expect(project.members.request.exists?(user_id: user)).to be_truthy
+      expect(project.users).not_to include user
+    end
+  end
+
+  describe '#approve' do
+    let(:project) { create(:project, :public) }
+
+    context 'when member is not found' do
+      it 'returns 404' do
+        post :approve_access_request, namespace_id: project.namespace,
+                                      project_id: project,
+                                      id: 42
+
+        expect(response.status).to eq(404)
+      end
+    end
+
+    context 'when member is found' do
+      let(:user) { create(:user) }
+      let(:team_requester) { create(:user) }
+      let(:member) do
+        project.request_access(team_requester)
+        project.members.request.find_by(user_id: team_requester.id)
+      end
+
+      context 'when user does not have enough rights' do
+        before do
+          project.team << [user, :developer]
+          sign_in(user)
+        end
+
+        it 'returns 404' do
+          post :approve_access_request, namespace_id: project.namespace,
+                                        project_id: project,
+                                        id: member
+
+          expect(response.status).to eq(404)
+          expect(project.users).not_to include team_requester
+        end
+      end
+
+      context 'when user has enough rights' do
+        before do
+          project.team << [user, :master]
+          sign_in(user)
+        end
+
+        it 'adds user to members' do
+          post :approve_access_request, namespace_id: project.namespace,
+                                        project_id: project,
+                                        id: member
+
+          expect(response).to redirect_to(
+            namespace_project_project_members_path(project.namespace, project)
+          )
+          expect(project.users).to include team_requester
+        end
+      end
+    end
+  end
 end
diff --git a/spec/features/groups/members/owner_manages_access_requests_spec.rb b/spec/features/groups/members/owner_manages_access_requests_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..22525ce530b1522dd70ab8b94a857c2b2e593124
--- /dev/null
+++ b/spec/features/groups/members/owner_manages_access_requests_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+feature 'Groups > Members > Owner manages access requests', feature: true do
+  let(:user) { create(:user) }
+  let(:owner) { create(:user) }
+  let(:group) { create(:group, :public) }
+
+  background do
+    group.request_access(user)
+    group.add_owner(owner)
+    login_as(owner)
+  end
+
+  scenario 'owner can see access requests' do
+    visit group_group_members_path(group)
+
+    expect_visible_access_request(group, user)
+  end
+
+  scenario 'master can grant access' do
+    visit group_group_members_path(group)
+
+    expect_visible_access_request(group, user)
+
+    perform_enqueued_jobs { click_on 'Grant access' }
+
+    expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
+    expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{group.name} group was granted"
+  end
+
+  scenario 'master can deny access' do
+    visit group_group_members_path(group)
+
+    expect_visible_access_request(group, user)
+
+    perform_enqueued_jobs { click_on 'Deny access' }
+
+    expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
+    expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{group.name} group was denied"
+  end
+
+
+  def expect_visible_access_request(group, user)
+    expect(group.members.request.exists?(user_id: user)).to be_truthy
+    expect(page).to have_content "#{group.name} access requests (1)"
+    expect(page).to have_content user.name
+  end
+end
diff --git a/spec/features/groups/members/user_requests_access_spec.rb b/spec/features/groups/members/user_requests_access_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a878a96b6ee398e62710d8eddfd1ebd3f1c9c44f
--- /dev/null
+++ b/spec/features/groups/members/user_requests_access_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+feature 'Groups > Members > User requests access', feature: true do
+  let(:user) { create(:user) }
+  let(:owner) { create(:user) }
+  let(:group) { create(:group, :public) }
+
+  background do
+    group.add_owner(owner)
+    login_as(user)
+    visit group_path(group)
+  end
+
+  scenario 'user can request access to a group' do
+    perform_enqueued_jobs { click_link 'Request Access' }
+
+    expect(ActionMailer::Base.deliveries.last.to).to eq [owner.notification_email]
+    expect(ActionMailer::Base.deliveries.last.subject).to match "Request to join the #{group.name} group"
+
+    expect(group.members.request.exists?(user_id: user)).to be_truthy
+    expect(page).to have_content 'Your request for access has been queued for review.'
+
+    expect(page).to have_content 'Withdraw Access Request'
+  end
+
+  scenario 'user is not listed in the group members page' do
+    click_link 'Request Access'
+
+    expect(group.members.request.exists?(user_id: user)).to be_truthy
+
+    click_link 'Members'
+
+    page.within('.content') do
+      expect(page).not_to have_content(user.name)
+    end
+  end
+
+  scenario 'user can withdraw its request for access' do
+    click_link 'Request Access'
+
+    expect(group.members.request.exists?(user_id: user)).to be_truthy
+
+    click_link 'Withdraw Access Request'
+
+    expect(group.members.request.exists?(user_id: user)).to be_falsey
+    expect(page).to have_content 'Your access request to the group has been withdrawn.'
+  end
+end
diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5fe4caa12f07492d1991d041190024debe9578b7
--- /dev/null
+++ b/spec/features/projects/members/master_manages_access_requests_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Master manages access requests', feature: true do
+  let(:user) { create(:user) }
+  let(:master) { create(:user) }
+  let(:project) { create(:project, :public) }
+
+  background do
+    project.request_access(user)
+    project.team << [master, :master]
+    login_as(master)
+  end
+
+  scenario 'master can see access requests' do
+    visit namespace_project_project_members_path(project.namespace, project)
+
+    expect_visible_access_request(project, user)
+  end
+
+  scenario 'master can grant access' do
+    visit namespace_project_project_members_path(project.namespace, project)
+
+    expect_visible_access_request(project, user)
+
+    perform_enqueued_jobs { click_on 'Grant access' }
+
+    expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
+    expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.name_with_namespace} project was granted"
+  end
+
+  scenario 'master can deny access' do
+    visit namespace_project_project_members_path(project.namespace, project)
+
+    expect_visible_access_request(project, user)
+
+    perform_enqueued_jobs { click_on 'Deny access' }
+
+    expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
+    expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.name_with_namespace} project was denied"
+  end
+
+  def expect_visible_access_request(project, user)
+    expect(project.members.request.exists?(user_id: user)).to be_truthy
+    expect(page).to have_content "#{project.name} access requests (1)"
+    expect(page).to have_content user.name
+  end
+end
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fd92a3a2f0cf20eb61d0dcb6bfd8f5acd305e80a
--- /dev/null
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+feature 'Projects > Members > User requests access', feature: true do
+  let(:user) { create(:user) }
+  let(:master) { create(:user) }
+  let(:project) { create(:project, :public) }
+
+  background do
+    project.team << [master, :master]
+    login_as(user)
+    visit namespace_project_path(project.namespace, project)
+  end
+
+  scenario 'user can request access to a project' do
+    perform_enqueued_jobs { click_link 'Request Access' }
+
+    expect(ActionMailer::Base.deliveries.last.to).to eq [master.notification_email]
+    expect(ActionMailer::Base.deliveries.last.subject).to eq "Request to join the #{project.name_with_namespace} project"
+
+    expect(project.members.request.exists?(user_id: user)).to be_truthy
+    expect(page).to have_content 'Your request for access has been queued for review.'
+
+    expect(page).to have_content 'Withdraw Access Request'
+  end
+
+  scenario 'user is not listed in the project members page' do
+    click_link 'Request Access'
+
+    expect(project.members.request.exists?(user_id: user)).to be_truthy
+
+    open_project_settings_menu
+    click_link 'Members'
+
+    visit namespace_project_project_members_path(project.namespace, project)
+    page.within('.content') do
+      expect(page).not_to have_content(user.name)
+    end
+  end
+
+  scenario 'user can withdraw its request for access' do
+    click_link 'Request Access'
+
+    expect(project.members.request.exists?(user_id: user)).to be_truthy
+
+    click_link 'Withdraw Access Request'
+
+    expect(project.members.request.exists?(user_id: user)).to be_falsey
+    expect(page).to have_content 'Your access request to the project has been withdrawn.'
+  end
+
+  def open_project_settings_menu
+    find('#project-settings-button').click
+  end
+end
diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..14847d0a49e584e630cf21e9d6937d2ba41baf6f
--- /dev/null
+++ b/spec/helpers/gitlab_routing_helper_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe GitlabRoutingHelper do
+  describe 'Project URL helpers' do
+    describe '#project_members_url' do
+      let(:project) { build_stubbed(:empty_project) }
+
+      it { expect(project_members_url(project)).to eq namespace_project_project_members_url(project.namespace, project) }
+    end
+
+    describe '#project_member_path' do
+      let(:project_member) { create(:project_member) }
+
+      it { expect(project_member_path(project_member)).to eq namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) }
+    end
+
+    describe '#request_access_project_members_path' do
+      let(:project) { build_stubbed(:empty_project) }
+
+      it { expect(request_access_project_members_path(project)).to eq request_access_namespace_project_project_members_path(project.namespace, project) }
+    end
+
+    describe '#leave_project_members_path' do
+      let(:project) { build_stubbed(:empty_project) }
+
+      it { expect(leave_project_members_path(project)).to eq leave_namespace_project_project_members_path(project.namespace, project) }
+    end
+
+    describe '#approve_access_request_project_member_path' do
+      let(:project_member) { create(:project_member) }
+
+      it { expect(approve_access_request_project_member_path(project_member)).to eq approve_access_request_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) }
+    end
+
+    describe '#resend_invite_project_member_path' do
+      let(:project_member) { create(:project_member) }
+
+      it { expect(resend_invite_project_member_path(project_member)).to eq resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) }
+    end
+  end
+
+  describe 'Group URL helpers' do
+    describe '#group_members_url' do
+      let(:group) { build_stubbed(:group) }
+
+      it { expect(group_members_url(group)).to eq group_group_members_url(group) }
+    end
+
+    describe '#group_member_path' do
+      let(:group_member) { create(:group_member) }
+
+      it { expect(group_member_path(group_member)).to eq group_group_member_path(group_member.source, group_member) }
+    end
+
+    describe '#request_access_group_members_path' do
+      let(:group) { build_stubbed(:group) }
+
+      it { expect(request_access_group_members_path(group)).to eq request_access_group_group_members_path(group) }
+    end
+
+    describe '#leave_group_members_path' do
+      let(:group) { build_stubbed(:group) }
+
+      it { expect(leave_group_members_path(group)).to eq leave_group_group_members_path(group) }
+    end
+
+    describe '#approve_access_request_group_member_path' do
+      let(:group_member) { create(:group_member) }
+
+      it { expect(approve_access_request_group_member_path(group_member)).to eq approve_access_request_group_group_member_path(group_member.source, group_member) }
+    end
+
+    describe '#resend_invite_group_member_path' do
+      let(:group_member) { create(:group_member) }
+
+      it { expect(resend_invite_group_member_path(group_member)).to eq resend_invite_group_group_member_path(group_member.source, group_member) }
+    end
+  end
+end
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0b1a76156e033756c86144a23b8e7748a3a1528a
--- /dev/null
+++ b/spec/helpers/members_helper_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe MembersHelper do
+  describe '#action_member_permission' do
+    let(:project_member) { build(:project_member) }
+    let(:group_member) { build(:group_member) }
+
+    it { expect(action_member_permission(:admin, project_member)).to eq :admin_project_member }
+    it { expect(action_member_permission(:admin, group_member)).to eq :admin_group_member }
+  end
+
+  describe '#can_see_member_roles?' do
+    let(:project) { create(:empty_project) }
+    let(:group) { create(:group) }
+    let(:user) { build(:user) }
+    let(:admin) { build(:user, :admin) }
+    let(:project_member) { create(:project_member, project: project) }
+    let(:group_member) { create(:group_member, group: group) }
+
+    it { expect(can_see_member_roles?(source: project, user: nil)).to be_falsy }
+    it { expect(can_see_member_roles?(source: group, user: nil)).to be_falsy }
+    it { expect(can_see_member_roles?(source: project, user: admin)).to be_truthy }
+    it { expect(can_see_member_roles?(source: group, user: admin)).to be_truthy }
+    it { expect(can_see_member_roles?(source: project, user: project_member.user)).to be_truthy }
+    it { expect(can_see_member_roles?(source: group, user: group_member.user)).to be_truthy }
+  end
+
+  describe '#remove_member_message' do
+    let(:requester) { build(:user) }
+    let(:project) { create(:project) }
+    let(:project_member) { build(:project_member, project: project) }
+    let(:project_member_invite) { build(:project_member, project: project).tap { |m| m.generate_invite_token! } }
+    let(:project_member_request) { project.request_access(requester) }
+    let(:group) { create(:group) }
+    let(:group_member) { build(:group_member, group: group) }
+    let(:group_member_invite) { build(:group_member, group: group).tap { |m| m.generate_invite_token! } }
+    let(:group_member_request) { group.request_access(requester) }
+
+    it { expect(remove_member_message(project_member)).to eq "Are you sure you want to remove #{project_member.user.name} from the #{project.name_with_namespace} project?" }
+    it { expect(remove_member_message(project_member_invite)).to eq "Are you sure you want to revoke the invitation for #{project_member_invite.invite_email} to join the #{project.name_with_namespace} project?" }
+    it { expect(remove_member_message(project_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{project.name_with_namespace} project?" }
+    it { expect(remove_member_message(project_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{project.name_with_namespace} project?" }
+    it { expect(remove_member_message(group_member)).to eq "Are you sure you want to remove #{group_member.user.name} from the #{group.name} group?" }
+    it { expect(remove_member_message(group_member_invite)).to eq "Are you sure you want to revoke the invitation for #{group_member_invite.invite_email} to join the #{group.name} group?" }
+    it { expect(remove_member_message(group_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{group.name} group?" }
+    it { expect(remove_member_message(group_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{group.name} group?" }
+  end
+
+  describe '#remove_member_title' do
+    let(:requester) { build(:user) }
+    let(:project) { create(:project) }
+    let(:project_member) { build(:project_member, project: project) }
+    let(:project_member_request) { project.request_access(requester) }
+    let(:group) { create(:group) }
+    let(:group_member) { build(:group_member, group: group) }
+    let(:group_member_request) { group.request_access(requester) }
+
+    it { expect(remove_member_title(project_member)).to eq 'Remove user from project' }
+    it { expect(remove_member_title(project_member_request)).to eq 'Deny access request from project' }
+    it { expect(remove_member_title(group_member)).to eq 'Remove user from group' }
+    it { expect(remove_member_title(group_member_request)).to eq 'Deny access request from group' }
+  end
+
+  describe '#leave_confirmation_message' do
+    let(:project) { build_stubbed(:project) }
+    let(:group) { build_stubbed(:group) }
+    let(:user) { build_stubbed(:user) }
+
+    it { expect(leave_confirmation_message(project)).to eq "Are you sure you want to leave the \"#{project.name_with_namespace}\" project?" }
+    it { expect(leave_confirmation_message(group)).to eq "Are you sure you want to leave the \"#{group.name}\" group?" }
+  end
+end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index ac5af8740dc142c2b1b8ea3d6a5e567e978209cb..09e0bbfd00b807f702bd336cc980a396975f56bb 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -45,16 +45,6 @@
     end
   end
 
-  describe 'user_max_access_in_project' do
-    let(:project) { create(:project) }
-    let(:user) { create(:user) }
-    before do
-      project.team.add_user(user, Gitlab::Access::MASTER)
-    end
-
-    it { expect(helper.user_max_access_in_project(user.id, project)).to eq('Master') }
-  end
-
   describe "readme_cache_key" do
     let(:project) { create(:project) }
 
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 818825b1477e2452ec593c63f67b1848184b22aa..1e6eb20ab39259239f9d330bf45dc53a41c3e6b3 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -400,26 +400,136 @@
       end
     end
 
+    describe 'project access requested' do
+      let(:project) { create(:project) }
+      let(:user) { create(:user) }
+      let(:project_member) do
+        project.request_access(user)
+        project.members.request.find_by(user_id: user.id)
+      end
+      subject { Notify.member_access_requested_email('project', project_member.id) }
+
+      it_behaves_like 'an email sent from GitLab'
+      it_behaves_like 'it should not have Gmail Actions links'
+      it_behaves_like "a user cannot unsubscribe through footer link"
+
+      it 'contains all the useful information' do
+        is_expected.to have_subject "Request to join the #{project.name_with_namespace} project"
+        is_expected.to have_body_text /#{project.name_with_namespace}/
+        is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/
+        is_expected.to have_body_text /#{project_member.human_access}/
+      end
+    end
+
+    describe 'project access denied' do
+      let(:project) { create(:project) }
+      let(:user) { create(:user) }
+      let(:project_member) do
+        project.request_access(user)
+        project.members.request.find_by(user_id: user.id)
+      end
+      subject { Notify.member_access_denied_email('project', project.id, user.id) }
+
+      it_behaves_like 'an email sent from GitLab'
+      it_behaves_like 'it should not have Gmail Actions links'
+      it_behaves_like "a user cannot unsubscribe through footer link"
+
+      it 'contains all the useful information' do
+        is_expected.to have_subject "Access to the #{project.name_with_namespace} project was denied"
+        is_expected.to have_body_text /#{project.name_with_namespace}/
+        is_expected.to have_body_text /#{project.web_url}/
+      end
+    end
+
     describe 'project access changed' do
       let(:project) { create(:project) }
       let(:user) { create(:user) }
       let(:project_member) { create(:project_member, project: project, user: user) }
-      subject { Notify.project_access_granted_email(project_member.id) }
+      subject { Notify.member_access_granted_email('project', project_member.id) }
 
       it_behaves_like 'an email sent from GitLab'
       it_behaves_like 'it should not have Gmail Actions links'
       it_behaves_like "a user cannot unsubscribe through footer link"
 
-      it 'has the correct subject' do
-        is_expected.to have_subject /Access to project was granted/
+      it 'contains all the useful information' do
+        is_expected.to have_subject "Access to the #{project.name_with_namespace} project was granted"
+        is_expected.to have_body_text /#{project.name_with_namespace}/
+        is_expected.to have_body_text /#{project.web_url}/
+        is_expected.to have_body_text /#{project_member.human_access}/
       end
+    end
 
-      it 'contains name of project' do
-        is_expected.to have_body_text /#{project.name}/
-      end
+    def invite_to_project(project:, email:, inviter:)
+      ProjectMember.add_user(project.project_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
 
-      it 'contains new user role' do
+      project.project_members.invite.last
+    end
+
+    describe 'project invitation' do
+      let(:project) { create(:project) }
+      let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
+      let(:project_member) { invite_to_project(project: project, email: 'toto@example.com', inviter: master) }
+
+      subject { Notify.member_invited_email('project', project_member.id, project_member.invite_token) }
+
+      it_behaves_like 'an email sent from GitLab'
+      it_behaves_like 'it should not have Gmail Actions links'
+      it_behaves_like "a user cannot unsubscribe through footer link"
+
+      it 'contains all the useful information' do
+        is_expected.to have_subject "Invitation to join the #{project.name_with_namespace} project"
+        is_expected.to have_body_text /#{project.name_with_namespace}/
+        is_expected.to have_body_text /#{project.web_url}/
         is_expected.to have_body_text /#{project_member.human_access}/
+        is_expected.to have_body_text /#{project_member.invite_token}/
+      end
+    end
+
+    describe 'project invitation accepted' do
+      let(:project) { create(:project) }
+      let(:invited_user) { create(:user) }
+      let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
+      let(:project_member) do
+        invitee = invite_to_project(project: project, email: 'toto@example.com', inviter: master)
+        invitee.accept_invite!(invited_user)
+        invitee
+      end
+
+      subject { Notify.member_invite_accepted_email('project', project_member.id) }
+
+      it_behaves_like 'an email sent from GitLab'
+      it_behaves_like 'it should not have Gmail Actions links'
+      it_behaves_like "a user cannot unsubscribe through footer link"
+
+      it 'contains all the useful information' do
+        is_expected.to have_subject 'Invitation accepted'
+        is_expected.to have_body_text /#{project.name_with_namespace}/
+        is_expected.to have_body_text /#{project.web_url}/
+        is_expected.to have_body_text /#{project_member.invite_email}/
+        is_expected.to have_body_text /#{invited_user.name}/
+      end
+    end
+
+    describe 'project invitation declined' do
+      let(:project) { create(:project) }
+      let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
+      let(:project_member) do
+        invitee = invite_to_project(project: project, email: 'toto@example.com', inviter: master)
+        invitee.decline_invite!
+        invitee
+      end
+
+      subject { Notify.member_invite_declined_email('project', project.id, project_member.invite_email, master.id) }
+
+      it_behaves_like 'an email sent from GitLab'
+      it_behaves_like 'it should not have Gmail Actions links'
+      it_behaves_like "a user cannot unsubscribe through footer link"
+
+      it 'contains all the useful information' do
+        is_expected.to have_subject 'Invitation declined'
+        is_expected.to have_body_text /#{project.name_with_namespace}/
+        is_expected.to have_body_text /#{project.web_url}/
+        is_expected.to have_body_text /#{project_member.invite_email}/
       end
     end
 
@@ -535,27 +645,139 @@
     end
   end
 
-  describe 'group access changed' do
-    let(:group) { create(:group) }
-    let(:user) { create(:user) }
-    let(:membership) { create(:group_member, group: group, user: user) }
+  context 'for a group' do
+    describe 'group access requested' do
+      let(:group) { create(:group) }
+      let(:user) { create(:user) }
+      let(:group_member) do
+        group.request_access(user)
+        group.members.request.find_by(user_id: user.id)
+      end
+      subject { Notify.member_access_requested_email('group', group_member.id) }
 
-    subject { Notify.group_access_granted_email(membership.id) }
+      it_behaves_like 'an email sent from GitLab'
+      it_behaves_like 'it should not have Gmail Actions links'
+      it_behaves_like "a user cannot unsubscribe through footer link"
 
-    it_behaves_like 'an email sent from GitLab'
-    it_behaves_like 'it should not have Gmail Actions links'
-    it_behaves_like "a user cannot unsubscribe through footer link"
+      it 'contains all the useful information' do
+        is_expected.to have_subject "Request to join the #{group.name} group"
+        is_expected.to have_body_text /#{group.name}/
+        is_expected.to have_body_text /#{group_group_members_url(group)}/
+        is_expected.to have_body_text /#{group_member.human_access}/
+      end
+    end
 
-    it 'has the correct subject' do
-      is_expected.to have_subject /Access to group was granted/
+    describe 'group access denied' do
+      let(:group) { create(:group) }
+      let(:user) { create(:user) }
+      let(:group_member) do
+        group.request_access(user)
+        group.members.request.find_by(user_id: user.id)
+      end
+      subject { Notify.member_access_denied_email('group', group.id, user.id) }
+
+      it_behaves_like 'an email sent from GitLab'
+      it_behaves_like 'it should not have Gmail Actions links'
+      it_behaves_like "a user cannot unsubscribe through footer link"
+
+      it 'contains all the useful information' do
+        is_expected.to have_subject "Access to the #{group.name} group was denied"
+        is_expected.to have_body_text /#{group.name}/
+        is_expected.to have_body_text /#{group.web_url}/
+      end
+    end
+
+    describe 'group access changed' do
+      let(:group) { create(:group) }
+      let(:user) { create(:user) }
+      let(:group_member) { create(:group_member, group: group, user: user) }
+
+      subject { Notify.member_access_granted_email('group', group_member.id) }
+
+      it_behaves_like 'an email sent from GitLab'
+      it_behaves_like 'it should not have Gmail Actions links'
+      it_behaves_like "a user cannot unsubscribe through footer link"
+
+      it 'contains all the useful information' do
+        is_expected.to have_subject "Access to the #{group.name} group was granted"
+        is_expected.to have_body_text /#{group.name}/
+        is_expected.to have_body_text /#{group.web_url}/
+        is_expected.to have_body_text /#{group_member.human_access}/
+      end
+    end
+
+    def invite_to_group(group:, email:, inviter:)
+      GroupMember.add_user(group.group_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
+
+      group.group_members.invite.last
     end
 
-    it 'contains name of project' do
-      is_expected.to have_body_text /#{group.name}/
+    describe 'group invitation' do
+      let(:group) { create(:group) }
+      let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
+      let(:group_member) { invite_to_group(group: group, email: 'toto@example.com', inviter: owner) }
+
+      subject { Notify.member_invited_email('group', group_member.id, group_member.invite_token) }
+
+      it_behaves_like 'an email sent from GitLab'
+      it_behaves_like 'it should not have Gmail Actions links'
+      it_behaves_like "a user cannot unsubscribe through footer link"
+
+      it 'contains all the useful information' do
+        is_expected.to have_subject "Invitation to join the #{group.name} group"
+        is_expected.to have_body_text /#{group.name}/
+        is_expected.to have_body_text /#{group.web_url}/
+        is_expected.to have_body_text /#{group_member.human_access}/
+        is_expected.to have_body_text /#{group_member.invite_token}/
+      end
     end
 
-    it 'contains new user role' do
-      is_expected.to have_body_text /#{membership.human_access}/
+    describe 'group invitation accepted' do
+      let(:group) { create(:group) }
+      let(:invited_user) { create(:user) }
+      let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
+      let(:group_member) do
+        invitee = invite_to_group(group: group, email: 'toto@example.com', inviter: owner)
+        invitee.accept_invite!(invited_user)
+        invitee
+      end
+
+      subject { Notify.member_invite_accepted_email('group', group_member.id) }
+
+      it_behaves_like 'an email sent from GitLab'
+      it_behaves_like 'it should not have Gmail Actions links'
+      it_behaves_like "a user cannot unsubscribe through footer link"
+
+      it 'contains all the useful information' do
+        is_expected.to have_subject 'Invitation accepted'
+        is_expected.to have_body_text /#{group.name}/
+        is_expected.to have_body_text /#{group.web_url}/
+        is_expected.to have_body_text /#{group_member.invite_email}/
+        is_expected.to have_body_text /#{invited_user.name}/
+      end
+    end
+
+    describe 'group invitation declined' do
+      let(:group) { create(:group) }
+      let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
+      let(:group_member) do
+        invitee = invite_to_group(group: group, email: 'toto@example.com', inviter: owner)
+        invitee.decline_invite!
+        invitee
+      end
+
+      subject { Notify.member_invite_declined_email('group', group.id, group_member.invite_email, owner.id) }
+
+      it_behaves_like 'an email sent from GitLab'
+      it_behaves_like 'it should not have Gmail Actions links'
+      it_behaves_like "a user cannot unsubscribe through footer link"
+
+      it 'contains all the useful information' do
+        is_expected.to have_subject 'Invitation declined'
+        is_expected.to have_body_text /#{group.name}/
+        is_expected.to have_body_text /#{group.web_url}/
+        is_expected.to have_body_text /#{group_member.invite_email}/
+      end
     end
   end
 
diff --git a/spec/models/concerns/access_requestable_spec.rb b/spec/models/concerns/access_requestable_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..983078769626dfe26ea90408b86cad0cd0685b9e
--- /dev/null
+++ b/spec/models/concerns/access_requestable_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe AccessRequestable do
+  describe 'Group' do
+    describe '#request_access' do
+      let(:group) { create(:group, :public) }
+      let(:user) { create(:user) }
+
+      it { expect(group.request_access(user)).to be_a(GroupMember) }
+      it { expect(group.request_access(user).user).to eq(user) }
+    end
+
+    describe '#access_requested?' do
+      let(:group) { create(:group, :public) }
+      let(:user) { create(:user) }
+
+      before { group.request_access(user) }
+
+      it { expect(group.members.request.exists?(user_id: user)).to be_truthy }
+    end
+  end
+
+  describe 'Project' do
+    describe '#request_access' do
+      let(:project) { create(:empty_project, :public) }
+      let(:user) { create(:user) }
+
+      it { expect(project.request_access(user)).to be_a(ProjectMember) }
+    end
+
+    describe '#access_requested?' do
+      let(:project) { create(:empty_project, :public) }
+      let(:user) { create(:user) }
+
+      before { project.request_access(user) }
+
+      it { expect(project.members.request.exists?(user_id: user)).to be_truthy }
+    end
+  end
+end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 6fa16be7f045bb1e6be3e77800955dc820e6f168..ccdcb29f773016e5fdafe769c7bfa8b50347d87d 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -5,7 +5,11 @@
 
   describe 'associations' do
     it { is_expected.to have_many :projects }
-    it { is_expected.to have_many :group_members }
+    it { is_expected.to have_many(:group_members).dependent(:destroy) }
+    it { is_expected.to have_many(:users).through(:group_members) }
+    it { is_expected.to have_many(:project_group_links).dependent(:destroy) }
+    it { is_expected.to have_many(:shared_projects).through(:project_group_links) }
+    it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
   end
 
   describe 'modules' do
@@ -131,4 +135,46 @@
       expect(described_class.search(group.path.upcase)).to eq([group])
     end
   end
+
+  describe '#has_owner?' do
+    before { @members = setup_group_members(group) }
+
+    it { expect(group.has_owner?(@members[:owner])).to be_truthy }
+    it { expect(group.has_owner?(@members[:master])).to be_falsey }
+    it { expect(group.has_owner?(@members[:developer])).to be_falsey }
+    it { expect(group.has_owner?(@members[:reporter])).to be_falsey }
+    it { expect(group.has_owner?(@members[:guest])).to be_falsey }
+    it { expect(group.has_owner?(@members[:requester])).to be_falsey }
+  end
+
+  describe '#has_master?' do
+    before { @members = setup_group_members(group) }
+
+    it { expect(group.has_master?(@members[:owner])).to be_falsey }
+    it { expect(group.has_master?(@members[:master])).to be_truthy }
+    it { expect(group.has_master?(@members[:developer])).to be_falsey }
+    it { expect(group.has_master?(@members[:reporter])).to be_falsey }
+    it { expect(group.has_master?(@members[:guest])).to be_falsey }
+    it { expect(group.has_master?(@members[:requester])).to be_falsey }
+  end
+
+  def setup_group_members(group)
+    members = {
+      owner: create(:user),
+      master: create(:user),
+      developer: create(:user),
+      reporter: create(:user),
+      guest: create(:user),
+      requester: create(:user)
+    }
+
+    group.add_user(members[:owner], GroupMember::OWNER)
+    group.add_user(members[:master], GroupMember::MASTER)
+    group.add_user(members[:developer], GroupMember::DEVELOPER)
+    group.add_user(members[:reporter], GroupMember::REPORTER)
+    group.add_user(members[:guest], GroupMember::GUEST)
+    group.request_access(members[:requester])
+
+    members
+  end
 end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 6e51730eecd207ff08e016b9c28913cd9d8fdb89..3ed3202ac6c5e52821c42a1090b72752e8ceff1d 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -55,11 +55,97 @@
     end
   end
 
+  describe 'Scopes & finders' do
+    before do
+      project = create(:project)
+      group = create(:group)
+      @owner_user = create(:user).tap { |u| group.add_owner(u) }
+      @owner = group.members.find_by(user_id: @owner_user.id)
+
+      @master_user = create(:user).tap { |u| project.team << [u, :master] }
+      @master = project.members.find_by(user_id: @master_user.id)
+
+      ProjectMember.add_user(project.members, 'toto1@example.com', Gitlab::Access::DEVELOPER, @master_user)
+      @invited_member = project.members.invite.find_by_invite_email('toto1@example.com')
+
+      accepted_invite_user = build(:user)
+      ProjectMember.add_user(project.members, 'toto2@example.com', Gitlab::Access::DEVELOPER, @master_user)
+      @accepted_invite_member = project.members.invite.find_by_invite_email('toto2@example.com').tap { |u| u.accept_invite!(accepted_invite_user) }
+
+      requested_user = create(:user).tap { |u| project.request_access(u) }
+      @requested_member = project.members.request.find_by(user_id: requested_user.id)
+
+      accepted_request_user = create(:user).tap { |u| project.request_access(u) }
+      @accepted_request_member = project.members.request.find_by(user_id: accepted_request_user.id).tap { |m| m.accept_request }
+    end
+
+    describe '.invite' do
+      it { expect(described_class.invite).not_to include @master }
+      it { expect(described_class.invite).to include @invited_member }
+      it { expect(described_class.invite).not_to include @accepted_invite_member }
+      it { expect(described_class.invite).not_to include @requested_member }
+      it { expect(described_class.invite).not_to include @accepted_request_member }
+    end
+
+    describe '.non_invite' do
+      it { expect(described_class.non_invite).to include @master }
+      it { expect(described_class.non_invite).not_to include @invited_member }
+      it { expect(described_class.non_invite).to include @accepted_invite_member }
+      it { expect(described_class.non_invite).to include @requested_member }
+      it { expect(described_class.non_invite).to include @accepted_request_member }
+    end
+
+    describe '.request' do
+      it { expect(described_class.request).not_to include @master }
+      it { expect(described_class.request).not_to include @invited_member }
+      it { expect(described_class.request).not_to include @accepted_invite_member }
+      it { expect(described_class.request).to include @requested_member }
+      it { expect(described_class.request).not_to include @accepted_request_member }
+    end
+
+    describe '.non_request' do
+      it { expect(described_class.non_request).to include @master }
+      it { expect(described_class.non_request).to include @invited_member }
+      it { expect(described_class.non_request).to include @accepted_invite_member }
+      it { expect(described_class.non_request).not_to include @requested_member }
+      it { expect(described_class.non_request).to include @accepted_request_member }
+    end
+
+    describe '.non_pending' do
+      it { expect(described_class.non_pending).to include @master }
+      it { expect(described_class.non_pending).not_to include @invited_member }
+      it { expect(described_class.non_pending).to include @accepted_invite_member }
+      it { expect(described_class.non_pending).not_to include @requested_member }
+      it { expect(described_class.non_pending).to include @accepted_request_member }
+    end
+
+    describe '.owners_and_masters' do
+      it { expect(described_class.owners_and_masters).to include @owner }
+      it { expect(described_class.owners_and_masters).to include @master }
+      it { expect(described_class.owners_and_masters).not_to include @invited_member }
+      it { expect(described_class.owners_and_masters).not_to include @accepted_invite_member }
+      it { expect(described_class.owners_and_masters).not_to include @requested_member }
+      it { expect(described_class.owners_and_masters).not_to include @accepted_request_member }
+    end
+  end
+
   describe "Delegate methods" do
     it { is_expected.to respond_to(:user_name) }
     it { is_expected.to respond_to(:user_email) }
   end
 
+  describe 'Callbacks' do
+    describe 'after_destroy :post_decline_request, if: :request?' do
+      let(:member) { create(:project_member, requested_at: Time.now.utc) }
+
+      it 'calls #post_decline_request' do
+        expect(member).to receive(:post_decline_request)
+
+        member.destroy
+      end
+    end
+  end
+
   describe ".add_user" do
     let!(:user)    { create(:user) }
     let(:project) { create(:project) }
@@ -97,6 +183,44 @@
     end
   end
 
+  describe '#accept_request' do
+    let(:member) { create(:project_member, requested_at: Time.now.utc) }
+
+    it { expect(member.accept_request).to be_truthy }
+
+    it 'clears requested_at' do
+      member.accept_request
+
+      expect(member.requested_at).to be_nil
+    end
+
+    it 'calls #after_accept_request' do
+      expect(member).to receive(:after_accept_request)
+
+      member.accept_request
+    end
+  end
+
+  describe '#invite?' do
+    subject { create(:project_member, invite_email: "user@example.com", user: nil) }
+
+    it { is_expected.to be_invite }
+  end
+
+  describe '#request?' do
+    subject { create(:project_member, requested_at: Time.now.utc) }
+
+    it { is_expected.to be_request }
+  end
+
+  describe '#pending?' do
+    let(:invited_member) { create(:project_member, invite_email: "user@example.com", user: nil) }
+    let(:requester) { create(:project_member, requested_at: Time.now.utc) }
+
+    it { expect(invited_member).to be_invite }
+    it { expect(requester).to be_pending }
+  end
+
   describe "#accept_invite!" do
     let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) }
     let(:user) { create(:user) }
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 5424c9b9cba9fb4dc96b6aa40dee2597b45ce4b7..eeb74a462acb2b077c0f2d7dc438df81736a143e 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -20,7 +20,7 @@
 require 'spec_helper'
 
 describe GroupMember, models: true do
-  context 'notification' do
+  describe 'notifications' do
     describe "#after_create" do
       it "should send email to user" do
         membership = build(:group_member)
@@ -50,5 +50,31 @@
         @group_member.update_attribute(:access_level, GroupMember::OWNER)
       end
     end
+
+    describe '#after_accept_request' do
+      it 'calls NotificationService.accept_group_access_request' do
+        member = create(:group_member, user: build_stubbed(:user), requested_at: Time.now)
+
+        expect_any_instance_of(NotificationService).to receive(:new_group_member)
+
+        member.__send__(:after_accept_request)
+      end
+    end
+
+    describe '#post_decline_request' do
+      it 'calls NotificationService.decline_group_access_request' do
+        member = create(:group_member, user: build_stubbed(:user), requested_at: Time.now)
+
+        expect_any_instance_of(NotificationService).to receive(:decline_group_access_request)
+
+        member.__send__(:post_decline_request)
+      end
+    end
+
+    describe '#real_source_type' do
+      subject { create(:group_member).real_source_type }
+
+      it { is_expected.to eq 'Group' }
+    end
   end
 end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 9f13874b532acaf0b36c12e558ed08f8ae6df94c..1e466f9c62045ef243b5b175bb61dc6fbd4ec3eb 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -33,6 +33,12 @@
     it { is_expected.to include_module(Gitlab::ShellAdapter) }
   end
 
+  describe '#real_source_type' do
+    subject { create(:project_member).real_source_type }
+
+    it { is_expected.to eq 'Project' }
+  end
+
   describe "#destroy" do
     let(:owner)   { create(:project_member, access_level: ProjectMember::OWNER) }
     let(:project) { owner.project }
@@ -135,4 +141,26 @@
     it { expect(@project_1.users).to be_empty }
     it { expect(@project_2.users).to be_empty }
   end
+
+  describe 'notifications' do
+    describe '#after_accept_request' do
+      it 'calls NotificationService.new_project_member' do
+        member = create(:project_member, user: build_stubbed(:user), requested_at: Time.now)
+
+        expect_any_instance_of(NotificationService).to receive(:new_project_member)
+
+        member.__send__(:after_accept_request)
+      end
+    end
+
+    describe '#post_decline_request' do
+      it 'calls NotificationService.decline_project_access_request' do
+        member = create(:project_member, user: build_stubbed(:user), requested_at: Time.now)
+
+        expect_any_instance_of(NotificationService).to receive(:decline_project_access_request)
+
+        member.__send__(:post_decline_request)
+      end
+    end
+  end
 end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index de8815f5a38271560ccf24602e7562edc477715c..30aa2b70c8d343b291facb3cc7de6faf4ed76112 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -89,11 +89,17 @@
     it { is_expected.to respond_to(:repo_exists?) }
     it { is_expected.to respond_to(:update_merge_requests) }
     it { is_expected.to respond_to(:execute_hooks) }
-    it { is_expected.to respond_to(:name_with_namespace) }
     it { is_expected.to respond_to(:owner) }
     it { is_expected.to respond_to(:path_with_namespace) }
   end
 
+  describe '#name_with_namespace' do
+    let(:project) { build_stubbed(:empty_project) }
+
+    it { expect(project.name_with_namespace).to eq "#{project.namespace.human_name} / #{project.name}" }
+    it { expect(project.human_name).to eq project.name_with_namespace }
+  end
+
   describe '#to_reference' do
     let(:project) { create(:empty_project) }
 
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 8bebd6a944721de4fa468e25df124bbd18a3f57f..9262aeb6ed890a074f2971af20509474711e9b01 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -73,47 +73,42 @@
     end
   end
 
-  describe :max_invited_level do
-    let(:group) { create(:group) }
-    let(:project) { create(:empty_project) }
-
-    before do
-      project.project_group_links.create(
-        group: group,
-        group_access: Gitlab::Access::DEVELOPER
-      )
-
-      group.add_user(master, Gitlab::Access::MASTER)
-      group.add_user(reporter, Gitlab::Access::REPORTER)
-    end
-
-    it { expect(project.team.max_invited_level(master.id)).to eq(Gitlab::Access::DEVELOPER) }
-    it { expect(project.team.max_invited_level(reporter.id)).to eq(Gitlab::Access::REPORTER) }
-    it { expect(project.team.max_invited_level(nonmember.id)).to be_nil }
-  end
-
-  describe :max_member_access do
-    let(:group) { create(:group) }
-    let(:project) { create(:empty_project) }
-
-    before do
-      project.project_group_links.create(
-        group: group,
-        group_access: Gitlab::Access::DEVELOPER
-      )
-
-      group.add_user(master, Gitlab::Access::MASTER)
-      group.add_user(reporter, Gitlab::Access::REPORTER)
+  describe '#find_member' do
+    context 'personal project' do
+      let(:project) { create(:empty_project) }
+      let(:requester) { create(:user) }
+
+      before do
+        project.team << [master, :master]
+        project.team << [reporter, :reporter]
+        project.team << [guest, :guest]
+        project.request_access(requester)
+      end
+
+      it { expect(project.team.find_member(master.id)).to be_a(ProjectMember) }
+      it { expect(project.team.find_member(reporter.id)).to be_a(ProjectMember) }
+      it { expect(project.team.find_member(guest.id)).to be_a(ProjectMember) }
+      it { expect(project.team.find_member(nonmember.id)).to be_nil }
+      it { expect(project.team.find_member(requester.id)).to be_nil }
     end
 
-    it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::DEVELOPER) }
-    it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
-    it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
-
-    it "does not have an access" do
-      project.namespace.update(share_with_group_lock: true)
-      expect(project.team.max_member_access(master.id)).to be_nil
-      expect(project.team.max_member_access(reporter.id)).to be_nil
+    context 'group project' do
+      let(:group) { create(:group) }
+      let(:project) { create(:empty_project, group: group) }
+      let(:requester) { create(:user) }
+
+      before do
+        group.add_master(master)
+        group.add_reporter(reporter)
+        group.add_guest(guest)
+        group.request_access(requester)
+      end
+
+      it { expect(project.team.find_member(master.id)).to be_a(GroupMember) }
+      it { expect(project.team.find_member(reporter.id)).to be_a(GroupMember) }
+      it { expect(project.team.find_member(guest.id)).to be_a(GroupMember) }
+      it { expect(project.team.find_member(nonmember.id)).to be_nil }
+      it { expect(project.team.find_member(requester.id)).to be_nil }
     end
   end
 
@@ -138,4 +133,69 @@
       expect(project.team.human_max_access(user.id)).to eq 'Owner'
     end
   end
+
+  describe '#max_member_access' do
+    let(:requester) { create(:user) }
+
+    context 'personal project' do
+      let(:project) { create(:empty_project) }
+
+      context 'when project is not shared with group' do
+        before do
+          project.team << [master, :master]
+          project.team << [reporter, :reporter]
+          project.team << [guest, :guest]
+          project.request_access(requester)
+        end
+
+        it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::MASTER) }
+        it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
+        it { expect(project.team.max_member_access(guest.id)).to eq(Gitlab::Access::GUEST) }
+        it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
+        it { expect(project.team.max_member_access(requester.id)).to be_nil }
+      end
+
+      context 'when project is shared with group' do
+        before do
+          group = create(:group)
+          project.project_group_links.create(
+            group: group,
+            group_access: Gitlab::Access::DEVELOPER)
+
+          group.add_master(master)
+          group.add_reporter(reporter)
+        end
+
+        it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::DEVELOPER) }
+        it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
+        it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
+        it { expect(project.team.max_member_access(requester.id)).to be_nil }
+
+        context 'but share_with_group_lock is true' do
+          before { project.namespace.update(share_with_group_lock: true) }
+
+          it { expect(project.team.max_member_access(master.id)).to be_nil }
+          it { expect(project.team.max_member_access(reporter.id)).to be_nil }
+        end
+      end
+    end
+
+    context 'group project' do
+      let(:group) { create(:group) }
+      let(:project) { create(:empty_project, group: group) }
+
+      before do
+        group.add_master(master)
+        group.add_reporter(reporter)
+        group.add_guest(guest)
+        group.request_access(requester)
+      end
+
+      it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::MASTER) }
+      it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
+      it { expect(project.team.max_member_access(guest.id)).to eq(Gitlab::Access::GUEST) }
+      it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
+      it { expect(project.team.max_member_access(requester.id)).to be_nil }
+    end
+  end
 end