summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorDouwe Maan <douwe@gitlab.com>2016-08-19 17:08:03 +0000
committerDouwe Maan <douwe@gitlab.com>2016-08-19 17:08:03 +0000
commit725036669cca45df17f1acbb1a45a031a1a84c85 (patch)
treef8fae23c197b4db87ed8ed3f387f943c17492b2f /app
parent8fc800cbcc9994ccf8e582df2e94e8e27b5e6fbe (diff)
parent1e00e4369d316d079e31227d4a45ad2101553ec2 (diff)
downloadgitlab-ce-725036669cca45df17f1acbb1a45a031a1a84c85.tar.gz
Merge branch 'expiration-date-on-memberships' into 'master'
Expiration date on memberships Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/17495 See merge request !5876
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/dispatcher.js3
-rw-r--r--app/assets/javascripts/member_expiration_date.js32
-rw-r--r--app/assets/javascripts/project_members.js3
-rw-r--r--app/assets/stylesheets/pages/projects.scss26
-rw-r--r--app/controllers/admin/groups_controller.rb2
-rw-r--r--app/controllers/groups/group_members_controller.rb9
-rw-r--r--app/controllers/projects/group_links_controller.rb4
-rw-r--r--app/controllers/projects/project_members_controller.rb9
-rw-r--r--app/models/concerns/expirable.rb15
-rw-r--r--app/models/group.rb24
-rw-r--r--app/models/member.rb4
-rw-r--r--app/models/members/project_member.rb10
-rw-r--r--app/models/project.rb4
-rw-r--r--app/models/project_group_link.rb4
-rw-r--r--app/models/project_team.rb13
-rw-r--r--app/services/members/authorized_destroy_service.rb19
-rw-r--r--app/services/members/destroy_service.rb7
-rw-r--r--app/views/groups/group_members/_new_group_member.html.haml9
-rw-r--r--app/views/groups/group_members/update.js.haml1
-rw-r--r--app/views/projects/group_links/index.html.haml11
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml9
-rw-r--r--app/views/projects/project_members/index.html.haml2
-rw-r--r--app/views/projects/project_members/update.js.haml1
-rw-r--r--app/views/shared/members/_member.html.haml20
-rw-r--r--app/workers/remove_expired_group_links_worker.rb7
-rw-r--r--app/workers/remove_expired_members_worker.rb13
26 files changed, 220 insertions, 41 deletions
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 32e3aa62358..ba64d2bcf0b 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -129,10 +129,12 @@
new NotificationsDropdown();
break;
case 'groups:group_members:index':
+ new gl.MemberExpirationDate();
new GroupMembers();
new UsersSelect();
break;
case 'projects:project_members:index':
+ new gl.MemberExpirationDate();
new ProjectMembers();
new UsersSelect();
break;
@@ -174,6 +176,7 @@
new BuildArtifacts();
break;
case 'projects:group_links:index':
+ new gl.MemberExpirationDate();
new GroupsSelect();
break;
case 'search:show':
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js
new file mode 100644
index 00000000000..1935af491f7
--- /dev/null
+++ b/app/assets/javascripts/member_expiration_date.js
@@ -0,0 +1,32 @@
+(function() {
+ // Add datepickers to all `js-access-expiration-date` elements. If those elements are
+ // children of an element with the `clearable-input` class, and have a sibling
+ // `js-clear-input` element, then show that element when there is a value in the
+ // datepicker, and make clicking on that element clear the field.
+ //
+ gl.MemberExpirationDate = function() {
+ function toggleClearInput() {
+ $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
+ }
+
+ var inputs = $('.js-access-expiration-date');
+
+ inputs.datepicker({
+ dateFormat: 'yy-mm-dd',
+ minDate: 1,
+ onSelect: toggleClearInput
+ });
+
+ inputs.next('.js-clear-input').on('click', function(event) {
+ event.preventDefault();
+
+ var input = $(this).closest('.clearable-input').find('.js-access-expiration-date');
+ input.datepicker('setDate', null);
+ toggleClearInput.call(input);
+ });
+
+ inputs.on('blur', toggleClearInput);
+
+ inputs.each(toggleClearInput);
+ };
+}).call(this);
diff --git a/app/assets/javascripts/project_members.js b/app/assets/javascripts/project_members.js
index f6a796b325a..78f7b48bc7d 100644
--- a/app/assets/javascripts/project_members.js
+++ b/app/assets/javascripts/project_members.js
@@ -5,9 +5,6 @@
return $(this).fadeOut();
});
}
-
return ProjectMembers;
-
})();
-
}).call(this);
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 27dc2b2a1fa..eaf2d3270b3 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -719,3 +719,29 @@ pre.light-well {
width: 300px;
}
}
+
+.clearable-input {
+ position: relative;
+
+ .clear-icon {
+ @extend .fa-times;
+ display: none;
+ position: absolute;
+ right: 7px;
+ top: 7px;
+ color: $location-icon-color;
+
+ &:before {
+ font-family: FontAwesome;
+ font-weight: normal;
+ font-style: normal;
+ }
+ }
+
+ &.has-value {
+ .clear-icon {
+ cursor: pointer;
+ display: block;
+ }
+ }
+}
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 4ce18321649..cdfa8d91a28 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -42,7 +42,7 @@ class Admin::GroupsController < Admin::ApplicationController
end
def members_update
- @group.add_users(params[:user_ids].split(','), params[:access_level], current_user)
+ @group.add_users(params[:user_ids].split(','), params[:access_level], current_user: current_user)
redirect_to [:admin, @group], notice: 'Users were successfully added.'
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 9fc41a12536..272164cd0cc 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -21,7 +21,12 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def create
- @group.add_users(params[:user_ids].split(','), params[:access_level], current_user)
+ @group.add_users(
+ params[:user_ids].split(','),
+ params[:access_level],
+ current_user: current_user,
+ expires_at: params[:expires_at]
+ )
redirect_to group_group_members_path(@group), notice: 'Users were successfully added.'
end
@@ -63,7 +68,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
protected
def member_params
- params.require(:group_member).permit(:access_level, :user_id)
+ params.require(:group_member).permit(:access_level, :user_id, :expires_at)
end
# MembershipActions concern
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index 606552fa853..d0c4550733c 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -11,7 +11,9 @@ class Projects::GroupLinksController < Projects::ApplicationController
return render_404 unless can?(current_user, :read_group, group)
project.project_group_links.create(
- group: group, group_access: params[:link_group_access]
+ group: group,
+ group_access: params[:link_group_access],
+ expires_at: params[:expires_at]
)
redirect_to namespace_project_group_links_path(project.namespace, project)
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 3435a118964..42a7e5a2c30 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -36,7 +36,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def create
- @project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user)
+ @project.team.add_users(
+ params[:user_ids].split(','),
+ params[:access_level],
+ expires_at: params[:expires_at],
+ current_user: current_user
+ )
redirect_to namespace_project_project_members_path(@project.namespace, @project)
end
@@ -94,7 +99,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
protected
def member_params
- params.require(:project_member).permit(:user_id, :access_level)
+ params.require(:project_member).permit(:user_id, :access_level, :expires_at)
end
# MembershipActions concern
diff --git a/app/models/concerns/expirable.rb b/app/models/concerns/expirable.rb
new file mode 100644
index 00000000000..be93435453b
--- /dev/null
+++ b/app/models/concerns/expirable.rb
@@ -0,0 +1,15 @@
+module Expirable
+ extend ActiveSupport::Concern
+
+ included do
+ scope :expired, -> { where('expires_at <= ?', Time.current) }
+ end
+
+ def expires?
+ expires_at.present?
+ end
+
+ def expires_soon?
+ expires_at < 7.days.from_now
+ end
+end
diff --git a/app/models/group.rb b/app/models/group.rb
index 37631b99701..c48869ae465 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -95,34 +95,40 @@ class Group < Namespace
end
end
- def add_users(user_ids, access_level, current_user = nil)
+ def add_users(user_ids, access_level, current_user: nil, expires_at: nil)
user_ids.each do |user_id|
- Member.add_user(self.group_members, user_id, access_level, current_user)
+ Member.add_user(
+ self.group_members,
+ user_id,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
end
end
- def add_user(user, access_level, current_user = nil)
- add_users([user], access_level, current_user)
+ def add_user(user, access_level, current_user: nil, expires_at: nil)
+ add_users([user], access_level, current_user: current_user, expires_at: expires_at)
end
def add_guest(user, current_user = nil)
- add_user(user, Gitlab::Access::GUEST, current_user)
+ add_user(user, Gitlab::Access::GUEST, current_user: current_user)
end
def add_reporter(user, current_user = nil)
- add_user(user, Gitlab::Access::REPORTER, current_user)
+ add_user(user, Gitlab::Access::REPORTER, current_user: current_user)
end
def add_developer(user, current_user = nil)
- add_user(user, Gitlab::Access::DEVELOPER, current_user)
+ add_user(user, Gitlab::Access::DEVELOPER, current_user: current_user)
end
def add_master(user, current_user = nil)
- add_user(user, Gitlab::Access::MASTER, current_user)
+ add_user(user, Gitlab::Access::MASTER, current_user: current_user)
end
def add_owner(user, current_user = nil)
- add_user(user, Gitlab::Access::OWNER, current_user)
+ add_user(user, Gitlab::Access::OWNER, current_user: current_user)
end
def has_owner?(user)
diff --git a/app/models/member.rb b/app/models/member.rb
index 24ab1276ee9..64e0d33fb20 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -1,6 +1,7 @@
class Member < ActiveRecord::Base
include Sortable
include Importable
+ include Expirable
include Gitlab::Access
attr_accessor :raw_invite_token
@@ -73,7 +74,7 @@ class Member < ActiveRecord::Base
user
end
- def add_user(members, user_id, access_level, current_user = nil)
+ def add_user(members, user_id, access_level, current_user: nil, expires_at: nil)
user = user_for_id(user_id)
# `user` can be either a User object or an email to be invited
@@ -87,6 +88,7 @@ class Member < ActiveRecord::Base
if can_update_member?(current_user, member) || project_creator?(member, access_level)
member.created_by ||= current_user
member.access_level = access_level
+ member.expires_at = expires_at
member.save
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 18e97c969d7..ec2d40eb11c 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -34,7 +34,7 @@ class ProjectMember < Member
# :master
# )
#
- def add_users_to_projects(project_ids, user_ids, access, current_user = nil)
+ def add_users_to_projects(project_ids, user_ids, access, current_user: nil, expires_at: nil)
access_level = if roles_hash.has_key?(access)
roles_hash[access]
elsif roles_hash.values.include?(access.to_i)
@@ -50,7 +50,13 @@ class ProjectMember < Member
project = Project.find(project_id)
users.each do |user|
- Member.add_user(project.project_members, user, access_level, current_user)
+ Member.add_user(
+ project.project_members,
+ user,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
end
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 043da030468..f9c48a546e6 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1003,8 +1003,8 @@ class Project < ActiveRecord::Base
project_members.find_by(user_id: user)
end
- def add_user(user, access_level, current_user = nil)
- team.add_user(user, access_level, current_user)
+ def add_user(user, access_level, current_user: nil, expires_at: nil)
+ team.add_user(user, access_level, current_user: current_user, expires_at: expires_at)
end
def default_branch
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index e52a6bd7c84..7613cbdea93 100644
--- a/app/models/project_group_link.rb
+++ b/app/models/project_group_link.rb
@@ -1,4 +1,6 @@
class ProjectGroupLink < ActiveRecord::Base
+ include Expirable
+
GUEST = 10
REPORTER = 20
DEVELOPER = 30
@@ -26,7 +28,7 @@ class ProjectGroupLink < ActiveRecord::Base
self.class.access_options.key(self.group_access)
end
- private
+ private
def different_group
if self.group && self.project && self.project.group == self.group
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index d0a714cd6fc..ab6ea2aae36 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -15,9 +15,9 @@ class ProjectTeam
users, access, current_user = *args
if users.respond_to?(:each)
- add_users(users, access, current_user)
+ add_users(users, access, current_user: current_user)
else
- add_user(users, access, current_user)
+ add_user(users, access, current_user: current_user)
end
end
@@ -33,17 +33,18 @@ class ProjectTeam
member
end
- def add_users(users, access, current_user = nil)
+ def add_users(users, access, current_user: nil, expires_at: nil)
ProjectMember.add_users_to_projects(
[project.id],
users,
access,
- current_user
+ current_user: current_user,
+ expires_at: expires_at
)
end
- def add_user(user, access, current_user = nil)
- add_users([user], access, current_user)
+ def add_user(user, access, current_user: nil, expires_at: nil)
+ add_users([user], access, current_user: current_user, expires_at: expires_at)
end
# Remove all users from project team
diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb
new file mode 100644
index 00000000000..ca9db59cac7
--- /dev/null
+++ b/app/services/members/authorized_destroy_service.rb
@@ -0,0 +1,19 @@
+module Members
+ class AuthorizedDestroyService < BaseService
+ attr_accessor :member, :user
+
+ def initialize(member, user = nil)
+ @member, @user = member, user
+ end
+
+ def execute
+ return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
+
+ member.destroy
+
+ if member.request? && member.user != user
+ notification_service.decline_access_request(member)
+ end
+ end
+ end
+end
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index 9e3f6af628d..9a2bf82ef51 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -11,12 +11,7 @@ module Members
unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member)
raise Gitlab::Access::AccessDeniedError
end
-
- member.destroy
-
- if member.request? && member.user != current_user
- notification_service.decline_access_request(member)
- end
+ AuthorizedDestroyService.new(member, current_user).execute
end
end
end
diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml
index 9bb9f962177..2fb3190ab11 100644
--- a/app/views/groups/group_members/_new_group_member.html.haml
+++ b/app/views/groups/group_members/_new_group_member.html.haml
@@ -14,5 +14,14 @@
Read more about role permissions
%strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
+ .form-group
+ = f.label :expires_at, 'Access expiration date', class: 'control-label'
+ .col-sm-10
+ .clearable-input
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
+ %i.clear-icon.js-clear-input
+ .help-block
+ On this date, the user(s) will automatically lose access to this group and all of its projects.
+
.form-actions
= f.submit 'Add users to group', class: "btn btn-create"
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index da71de4cd1e..742f9d7a433 100644
--- a/app/views/groups/group_members/update.js.haml
+++ b/app/views/groups/group_members/update.js.haml
@@ -1,2 +1,3 @@
:plain
$("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}');
+ new MemberExpirationDate();
diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml
index 2b904544f28..ca700cb3a3b 100644
--- a/app/views/projects/group_links/index.html.haml
+++ b/app/views/projects/group_links/index.html.haml
@@ -17,6 +17,13 @@
.select-wrapper
= select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
%span.caret
+ .form-group
+ = label_tag :expires_at, 'Access expiration date', class: 'label-light'
+ .clearable-input
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
+ %i.clear-icon.js-clear-input
+ .help-block
+ On this date, all users in the group will automatically lose access to this project.
= submit_tag "Share", class: "btn btn-create"
.col-lg-9.col-lg-offset-3
%hr
@@ -35,6 +42,10 @@
= group.name
%br
up to #{group_link.human_access}
+ - if group_link.expires?
+ ·
+ %span{ class: ('text-warning' if group_link.expires_soon?) }
+ expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
.pull-right
= link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: "btn btn-transparent" do
%span.sr-only disable sharing
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 978c4dfc5ec..fa8cbf71733 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -14,5 +14,14 @@
Read more about role permissions
%strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
+ .form-group
+ = f.label :expires_at, 'Access expiration date', class: 'control-label'
+ .col-sm-10
+ .clearable-input
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
+ %i.clear-icon.js-clear-input
+ .help-block
+ On this date, the user(s) will automatically lose access to this project.
+
.form-actions
= f.submit 'Add users to project', class: "btn btn-create"
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 9031f01b496..9d063b3081f 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,6 +1,6 @@
- page_title "Members"
-.project-members-page.prepend-top-default
+.project-members-page.js-project-members-page.prepend-top-default
- if can?(current_user, :admin_project_member, @project)
.panel.panel-default
.panel-heading
diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml
index 45f8ef89060..833954bc039 100644
--- a/app/views/projects/project_members/update.js.haml
+++ b/app/views/projects/project_members/update.js.haml
@@ -1,2 +1,3 @@
:plain
$("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}');
+ new MemberExpirationDate();
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index fc6e206d082..5f20e4bd42a 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -16,7 +16,7 @@
= button_tag icon('pencil'),
type: 'button',
class: 'btn inline js-toggle-button',
- title: 'Edit access level'
+ title: 'Edit'
- if member.request?
= link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]),
@@ -59,6 +59,10 @@
= time_ago_with_tooltip(member.requested_at)
- else
Joined #{time_ago_with_tooltip(member.created_at)}
+ - if member.expires?
+ ·
+ %span{ class: ('text-warning' if member.expires_soon?) }
+ Expires in #{distance_of_time_in_words_to_now(member.expires_at)}
- else
= image_tag avatar_icon(member.invite_email, 40), class: "avatar s40", alt: ''
@@ -73,8 +77,16 @@
- if show_roles
.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'
+ = form_for member, remote: true, html: { class: 'form-horizontal' } do |f|
+ .form-group
+ = label_tag "member_access_level_#{member.id}", 'Project access', class: 'control-label'
+ .col-sm-10
+ = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control', id: "member_access_level_#{member.id}"
+ .form-group
+ = label_tag "member_expires_at_#{member.id}", 'Access expiration date', class: 'control-label'
+ .col-sm-10
+ .clearable-input
+ = f.text_field :expires_at, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date', id: "member_expires_at_#{member.id}"
+ %i.clear-icon.js-clear-input
.prepend-top-10
= f.submit 'Save', class: 'btn btn-save btn-sm'
diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb
new file mode 100644
index 00000000000..246c8b6650a
--- /dev/null
+++ b/app/workers/remove_expired_group_links_worker.rb
@@ -0,0 +1,7 @@
+class RemoveExpiredGroupLinksWorker
+ include Sidekiq::Worker
+
+ def perform
+ ProjectGroupLink.expired.destroy_all
+ end
+end
diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb
new file mode 100644
index 00000000000..cf765af97ce
--- /dev/null
+++ b/app/workers/remove_expired_members_worker.rb
@@ -0,0 +1,13 @@
+class RemoveExpiredMembersWorker
+ include Sidekiq::Worker
+
+ def perform
+ Member.expired.find_each do |member|
+ begin
+ Members::AuthorizedDestroyService.new(member).execute
+ rescue => ex
+ logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}")
+ end
+ end
+ end
+end