diff --git a/CHANGELOG-EE b/CHANGELOG-EE index 4d57fb8d74..71b38341a7 100644 --- a/CHANGELOG-EE +++ b/CHANGELOG-EE @@ -2,6 +2,7 @@ v 7.6.0 - Added Audit events related to membership changes for groups and projects - Added option to attempt a rebase before merging merge request - Dont show LDAP groups settings if LDAP disabled + - Added member lock for groups to disallow membership additions on project level - Rebase on merge request. Introduced merge request option to rebase before merging - Better message for failed pushes because of git hooks - Kerberos support for web interface and git HTTP diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index df298ee1e5..8251ee9240 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -163,6 +163,6 @@ class GroupsController < ApplicationController end def group_params - params.require(:group).permit(:name, :description, :path, :avatar) + params.require(:group).permit(:name, :description, :path, :avatar, :membership_lock) end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index fb5470d98e..768c8e2250 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -279,4 +279,12 @@ module ProjectsHelper result.password = '*****' if result.password.present? result end + + def membership_locked? + if @project.group && @project.group.membership_lock + true + else + false + end + end end diff --git a/app/models/project_team.rb b/app/models/project_team.rb index fc017b1240..a8cdeb6836 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -52,6 +52,7 @@ class ProjectTeam end def add_users_ids(user_ids, access) + return false if group_member_lock ProjectMember.add_users_into_projects( [project.id], user_ids, @@ -218,4 +219,8 @@ class ProjectTeam def group project.group end + + def group_member_lock + group && group.membership_lock + end end diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 0eda33eeb5..436211f402 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -27,6 +27,15 @@ %hr = link_to 'Remove avatar', group_avatar_path(@group.to_param), data: { confirm: "Group avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-avatar" + .form-group + %hr + = f.label :name, class: 'control-label' do + Member lock + .col-sm-10 + .checkbox + = f.check_box :membership_lock + %span.descr Prevent adding new members to project membership within this group + .form-actions = f.submit 'Save group', class: "btn btn-save" diff --git a/app/views/projects/team_members/_team_member.html.haml b/app/views/projects/team_members/_team_member.html.haml index 7a9c0939ba..6925f9cfb3 100644 --- a/app/views/projects/team_members/_team_member.html.haml +++ b/app/views/projects/team_members/_team_member.html.haml @@ -13,5 +13,3 @@ %p %strong= user.name %span.cgray= user.username - - diff --git a/app/views/projects/team_members/index.html.haml b/app/views/projects/team_members/index.html.haml index 37105c8676..08e15b63fe 100644 --- a/app/views/projects/team_members/index.html.haml +++ b/app/views/projects/team_members/index.html.haml @@ -1,16 +1,22 @@ %h3.page-title Users with access to this project - - if can? current_user, :admin_team_member, @project - %span.pull-right - = link_to new_project_team_member_path(@project), class: "btn btn-new btn-grouped", title: "New project member" do - New project member - = link_to import_project_team_members_path(@project), class: "btn btn-grouped", title: "Import members from another project" do - Import members + - unless membership_locked? + - if can?(current_user, :admin_team_member, @project) + %span.pull-right + = link_to new_project_team_member_path(@project), class: "btn btn-new btn-grouped", title: "New project member" do + New project member + = link_to import_project_team_members_path(@project), class: "btn btn-grouped", title: "Import members from another project" do + Import members + %p.light Read more about project permissions %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" + - if membership_locked? + %span.pull-right + Adding new users is disabled at group level + = render "team", members: @project_members - if @group = render "group_members" diff --git a/db/migrate/20141212124604_add_group_membership_lock.rb b/db/migrate/20141212124604_add_group_membership_lock.rb new file mode 100644 index 0000000000..5dfd0835cb --- /dev/null +++ b/db/migrate/20141212124604_add_group_membership_lock.rb @@ -0,0 +1,5 @@ +class AddGroupMembershipLock < ActiveRecord::Migration + def change + add_column :namespaces, :membership_lock, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 5acf6f8ee9..0e350e6957 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20141205134006) do +ActiveRecord::Schema.define(version: 20141212124604) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -265,6 +265,7 @@ ActiveRecord::Schema.define(version: 20141205134006) do t.string "avatar" t.string "ldap_cn" t.integer "ldap_access" + t.boolean "membership_lock", default: false end add_index "namespaces", ["name"], name: "index_namespaces_on_name", using: :btree diff --git a/doc/workflow/groups.md b/doc/workflow/groups.md index 52bf611dc5..e8bc36ba18 100644 --- a/doc/workflow/groups.md +++ b/doc/workflow/groups.md @@ -69,3 +69,15 @@ gitlab_rails['gitlab_default_can_create_group'] = false # For installations from source, uncomment the 'default_can_create_group' # line in /home/git/gitlab/config/gitlab.yml ``` + +## Lock project membership to members of the group + +In GitLab Enterprise Edition it is possible to lock membership in project to the level of members in group. + +This allows group owner to lock down any new project membership to any of the projects within the group allowing tighter control over project membership. + +To enable this feature, navigate to group settings page, select `Member lock` and `Save group`. + +![Checkbox for membership lock](groups/membership_lock.png) + +This will disable the option for all users who previously had permissions to operate project memberships so no new users can be added. Furthermore, any request to add new user to project through API will not be possible. diff --git a/doc/workflow/groups/membership_lock.png b/doc/workflow/groups/membership_lock.png new file mode 100644 index 0000000000..e32fe2881e Binary files /dev/null and b/doc/workflow/groups/membership_lock.png differ diff --git a/features/groups_management.feature b/features/groups_management.feature new file mode 100644 index 0000000000..35f471fac6 --- /dev/null +++ b/features/groups_management.feature @@ -0,0 +1,22 @@ +Feature: Groups Management + Background: + Given "Pete Peters" is owner of group "Sourcing" + And "Open" is in group "Sourcing" + And "Mary Jane" has master access for project "Open" + + Scenario: Project master can add members before lock + Given I sign in as "Mary Jane" + And I go to "Open" project members page + Then I can control user membership + When Group membership lock is enabled + And I reload "Open" project members page + Then I cannot control user membership from project page + And I logout + + Scenario: Group owner lock membership controls + Given I sign in as "Pete Peters" + And I go to group settings page + And I enable membership lock + And I go to project settings + Then I cannot control user membership from project page + And I logout diff --git a/features/steps/groups_management.rb b/features/steps/groups_management.rb new file mode 100644 index 0000000000..42efd8e7e8 --- /dev/null +++ b/features/steps/groups_management.rb @@ -0,0 +1,65 @@ +class Spinach::Features::GroupsManagement < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedGroup + include SharedUser + include Select2Helper + + step '"Open" is in group "Sourcing"' do + @group = Group.find_by(name: "Sourcing") + @project ||= create(:project, name: "Open", namespace: @group) + + end + + step '"Mary Jane" has master access for project "Open"' do + @user = User.find_by(name: "Mary Jane") || create(:user, name: "Mary Jane") + @project = Project.find_by(name: "Open") + @project.team << [@user, :master] + end + + step "Group membership lock is enabled" do + @group = Group.find_by(name: "Sourcing") + @group.update_attributes(membership_lock: true) + end + + step 'I go to "Open" project members page' do + click_link 'Sourcing / Open' + click_link 'Settings' + click_link 'Members' + end + + step 'I can control user membership' do + page.should have_link 'New project member' + page.should have_link 'Import members' + page.should have_selector '#project_member_access_level', text: 'Master' + end + + step 'I reload "Open" project members page' do + click_link 'Members' + end + + step 'I go to group settings page' do + click_link 'sidebar-groups-tab' + click_link 'Sourcing' + click_link 'Settings' + end + + step 'I enable membership lock' do + check 'group_membership_lock' + click_button 'Save group' + end + + step 'I go to project settings' do + @project = Project.find_by(name: "Open") + click_link 'Projects' + + link = "/#{@project.path_with_namespace}/team" + find(:xpath, "//a[@href=\"#{link}\"]").click + end + + step 'I cannot control user membership from project page' do + page.should_not have_link 'New project member' + page.should_not have_link 'Import members' + page.should have_selector '#project_member_access_level', text: 'Master' + end +end diff --git a/features/steps/shared/authentication.rb b/features/steps/shared/authentication.rb index ac8a3df6bb..22d44add04 100644 --- a/features/steps/shared/authentication.rb +++ b/features/steps/shared/authentication.rb @@ -20,6 +20,10 @@ module SharedAuthentication login_with(user_exists("Mary Jane")) end + step 'I sign in as "Pete Peters"' do + login_with(user_exists("Pete Peters")) + end + step 'I should be redirected to sign in page' do current_path.should == new_user_session_path end diff --git a/features/steps/shared/group.rb b/features/steps/shared/group.rb index 1b225dd61a..4f9a77283b 100644 --- a/features/steps/shared/group.rb +++ b/features/steps/shared/group.rb @@ -21,6 +21,10 @@ module SharedGroup is_member_of("Mary Jane", "Guest", Gitlab::Access::GUEST) end + step '"Pete Peters" is owner of group "Sourcing"' do + is_member_of("Pete Peters", "Sourcing", Gitlab::Access::OWNER) + end + step 'I should see group "TestGroup"' do page.should have_content "TestGroup" end diff --git a/lib/api/project_members.rb b/lib/api/project_members.rb index 1595ed0bc3..7d83c1bebd 100644 --- a/lib/api/project_members.rb +++ b/lib/api/project_members.rb @@ -53,6 +53,10 @@ module API authorize! :admin_project, user_project required_attributes! [:user_id, :access_level] + if user_project.group && user_project.group.membership_lock + not_allowed! + end + # either the user is already a team member or a new one team_member = user_project.team_member_by_id(params[:user_id]) if team_member.nil? diff --git a/spec/requests/api/project_members_spec.rb b/spec/requests/api/project_members_spec.rb index 836f21f3e0..fe35f17a02 100644 --- a/spec/requests/api/project_members_spec.rb +++ b/spec/requests/api/project_members_spec.rb @@ -90,6 +90,18 @@ describe API::API, api: true do post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: 1234 response.status.should == 422 end + + context 'project in a group' do + before do + project2 = create(:project, group: create(:group, membership_lock: true)) + project2.group.add_owner(user) + post api("/projects/#{project2.id}/members", user), user_id: user2.id, access_level: ProjectMember::MASTER + end + + it 'should return a 405 method not allowed error when group membership lock is enabled' do + response.status.should == 405 + end + end end describe "PUT /projects/:id/members/:user_id" do