From ea5f4cae53eb571b250fcbaa3649cefe3083a636 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 11 Mar 2016 17:47:05 +0100 Subject: Bring ProjectGroupLink model and migrations from EE Signed-off-by: Dmitriy Zaporozhets --- app/models/group.rb | 2 ++ app/models/project.rb | 2 ++ app/models/project_group_link.rb | 36 ++++++++++++++++++++++ .../20130711063759_create_project_group_links.rb | 10 ++++++ ...30820102832_add_access_to_project_group_link.rb | 5 +++ db/schema.rb | 14 +++++++-- spec/factories/project_group_links.rb | 6 ++++ spec/models/project_group_link_spec.rb | 17 ++++++++++ 8 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 app/models/project_group_link.rb create mode 100644 db/migrate/20130711063759_create_project_group_links.rb create mode 100644 db/migrate/20130820102832_add_access_to_project_group_link.rb create mode 100644 spec/factories/project_group_links.rb create mode 100644 spec/models/project_group_link_spec.rb diff --git a/app/models/group.rb b/app/models/group.rb index 76042b3e3fd..e307624e965 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -23,6 +23,8 @@ class Group < Namespace has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember' alias_method :members, :group_members has_many :users, through: :group_members + has_many :project_group_links, dependent: :destroy + has_many :shared_projects, through: :project_group_links, source: :project validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } diff --git a/app/models/project.rb b/app/models/project.rb index 65829bec77a..2232637f1f1 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -151,6 +151,8 @@ class Project < ActiveRecord::Base has_many :releases, dependent: :destroy has_many :lfs_objects_projects, dependent: :destroy has_many :lfs_objects, through: :lfs_objects_projects + has_many :project_group_links, dependent: :destroy + has_many :invited_groups, through: :project_group_links, source: :group has_many :todos, dependent: :destroy has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb new file mode 100644 index 00000000000..e52a6bd7c84 --- /dev/null +++ b/app/models/project_group_link.rb @@ -0,0 +1,36 @@ +class ProjectGroupLink < ActiveRecord::Base + GUEST = 10 + REPORTER = 20 + DEVELOPER = 30 + MASTER = 40 + + belongs_to :project + belongs_to :group + + validates :project_id, presence: true + validates :group_id, presence: true + validates :group_id, uniqueness: { scope: [:project_id], message: "already shared with this group" } + validates :group_access, presence: true + validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true + validate :different_group + + def self.access_options + Gitlab::Access.options + end + + def self.default_access + DEVELOPER + end + + def human_access + self.class.access_options.key(self.group_access) + end + + private + + def different_group + if self.group && self.project && self.project.group == self.group + errors.add(:base, "Project cannot be shared with the project it is in.") + end + end +end diff --git a/db/migrate/20130711063759_create_project_group_links.rb b/db/migrate/20130711063759_create_project_group_links.rb new file mode 100644 index 00000000000..395083f2a03 --- /dev/null +++ b/db/migrate/20130711063759_create_project_group_links.rb @@ -0,0 +1,10 @@ +class CreateProjectGroupLinks < ActiveRecord::Migration + def change + create_table :project_group_links do |t| + t.integer :project_id, null: false + t.integer :group_id, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20130820102832_add_access_to_project_group_link.rb b/db/migrate/20130820102832_add_access_to_project_group_link.rb new file mode 100644 index 00000000000..00e3947a6bb --- /dev/null +++ b/db/migrate/20130820102832_add_access_to_project_group_link.rb @@ -0,0 +1,5 @@ +class AddAccessToProjectGroupLink < ActiveRecord::Migration + def change + add_column :project_group_links, :group_access, :integer, null: false, default: ProjectGroupLink.default_access + end +end diff --git a/db/schema.rb b/db/schema.rb index a74b86d8e2f..89e8c9e67b0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -656,6 +656,14 @@ ActiveRecord::Schema.define(version: 20160309140734) do add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree + create_table "project_group_links", force: :cascade do |t| + t.integer "project_id", null: false + t.integer "group_id", null: false + t.datetime "created_at" + t.datetime "updated_at" + t.integer "group_access", default: 30, null: false + end + create_table "project_import_data", force: :cascade do |t| t.integer "project_id" t.text "data" @@ -749,9 +757,9 @@ ActiveRecord::Schema.define(version: 20160309140734) do t.string "type" t.string "title" t.integer "project_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "active", null: false + t.datetime "created_at" + t.datetime "updated_at" + t.boolean "active", default: false, null: false t.text "properties" t.boolean "template", default: false t.boolean "push_events", default: true diff --git a/spec/factories/project_group_links.rb b/spec/factories/project_group_links.rb new file mode 100644 index 00000000000..e73cc05f9d7 --- /dev/null +++ b/spec/factories/project_group_links.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :project_group_link do + project + group + end +end diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb new file mode 100644 index 00000000000..2fa6715fcaf --- /dev/null +++ b/spec/models/project_group_link_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe ProjectGroupLink do + describe "Associations" do + it { should belong_to(:group) } + it { should belong_to(:project) } + end + + describe "Validation" do + let!(:project_group_link) { create(:project_group_link) } + + it { should validate_presence_of(:project_id) } + it { should validate_uniqueness_of(:group_id).scoped_to(:project_id).with_message(/already shared/) } + it { should validate_presence_of(:group_id) } + it { should validate_presence_of(:group_access) } + end +end -- cgit v1.2.3 From 746ac56b6f92991bc15d3cd787094b9555825ac3 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 11 Mar 2016 18:37:46 +0100 Subject: Add functionality to setup share of project with group via project settings Signed-off-by: Dmitriy Zaporozhets --- app/assets/javascripts/dispatcher.js.coffee | 2 ++ app/controllers/projects/group_links_controller.rb | 23 ++++++++++++ app/models/project.rb | 5 +++ app/views/layouts/nav/_project_settings.html.haml | 6 ++++ app/views/projects/group_links/index.html.haml | 41 ++++++++++++++++++++++ config/routes.rb | 2 ++ 6 files changed, 79 insertions(+) create mode 100644 app/controllers/projects/group_links_controller.rb create mode 100644 app/views/projects/group_links/index.html.haml diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 54b28f2dd8d..e964bba1325 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -103,6 +103,8 @@ class Dispatcher new ProjectFork() when 'projects:artifacts:browse' new BuildArtifacts() + when 'projects:group_links:index' + new GroupsSelect() switch path.first() when 'admin' diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb new file mode 100644 index 00000000000..4159e53bfa9 --- /dev/null +++ b/app/controllers/projects/group_links_controller.rb @@ -0,0 +1,23 @@ +class Projects::GroupLinksController < Projects::ApplicationController + layout 'project_settings' + before_action :authorize_admin_project! + + def index + @group_links = project.project_group_links.all + end + + def create + link = project.project_group_links.new + link.group_id = params[:link_group_id] + link.group_access = params[:link_group_access] + link.save + + redirect_to namespace_project_group_links_path(project.namespace, project) + end + + def destroy + project.project_group_links.find(params[:id]).destroy + + redirect_to namespace_project_group_links_path(project.namespace, project) + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 2232637f1f1..56865459724 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -880,6 +880,11 @@ class Project < ActiveRecord::Base jira_tracker? && jira_service.active end + def allowed_to_share_with_group? + # TODO: replace with logic + true + end + def ci_commit(sha) ci_commits.find_by(sha: sha) end diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 3359716202f..dc3050f02e5 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -13,6 +13,12 @@ = icon('pencil-square-o fw') %span Project Settings + - if @project.allowed_to_share_with_group? + = nav_link(controller: :group_links) do + = link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do + = icon('share-square-o fw') + %span + Groups = nav_link(controller: :deploy_keys) do = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do = icon('key fw') diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml new file mode 100644 index 00000000000..13f5fc141fa --- /dev/null +++ b/app/views/projects/group_links/index.html.haml @@ -0,0 +1,41 @@ +- page_title "Groups" +%h3.page_title Share project with other groups +%p.light + Projects can be stored in only one group at once. However you can share a project with other groups here. +%hr +- if @group_links.present? + .enabled-groups.panel.panel-default + .panel-heading + Already shared with + %ul.well-list + - @group_links.each do |group_link| + - group = group_link.group + %li + .pull-right + = link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: 'btn btn-sm' do + %i.icon-remove + disable sharing + = link_to group do + %strong + %i.icon-folder-open + = group.name + %br + .light up to #{group_link.human_access} + + +.available-groups + %h4 + Can be shared with + %div + = form_tag namespace_project_group_links_path(@project.namespace, @project), method: :post, class: 'form-horizontal' do + .form-group + = label_tag :link_group_id, 'Group', class: 'control-label' + .col-sm-10 + = groups_select_tag(:link_group_id, skip_group: @project.group.try(:path)) + .form-group + = label_tag :link_group_access, 'Max access level', class: 'control-label' + .col-sm-10 + = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control" + .form-actions + = submit_tag "Share", class: "btn btn-create" + diff --git a/config/routes.rb b/config/routes.rb index a918b5bd3f0..a0e0cc87f51 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -701,6 +701,8 @@ Rails.application.routes.draw do end end + resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ } + resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do member do delete :delete_attachment -- cgit v1.2.3 From f8163c81e7c765ea654ea81818eb3f8a9da7648e Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 11 Mar 2016 18:42:16 +0100 Subject: Show shared projects on group page Signed-off-by: Dmitriy Zaporozhets --- app/controllers/groups_controller.rb | 2 ++ app/views/groups/_shared_projects.html.haml | 18 ++++++++++++++++++ app/views/groups/show.html.haml | 7 +++++++ 3 files changed, 27 insertions(+) create mode 100644 app/views/groups/_shared_projects.html.haml diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index f05c29e9974..bd6b44bdbbc 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -46,6 +46,8 @@ class GroupsController < Groups::ApplicationController @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? + @shared_projects = @group.shared_projects + respond_to do |format| format.html diff --git a/app/views/groups/_shared_projects.html.haml b/app/views/groups/_shared_projects.html.haml new file mode 100644 index 00000000000..d707ad4272d --- /dev/null +++ b/app/views/groups/_shared_projects.html.haml @@ -0,0 +1,18 @@ +- if projects.present? + .panel.panel-default + .panel-heading + Projects shared with + %strong #{@group.name} + (#{projects.count}) + %ul.well-list + - projects.each do |project| + %li.project-row + = link_to namespace_project_path(project.namespace, project), class: dom_class(project) do + %span.namespace-name + - if project.namespace + = project.namespace.human_name + \/ + %span.project-name + = truncate(project.name, length: 25) + %span.arrow + %i.icon-angle-right diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 6148d8cb3d2..c64c986b5a4 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -35,6 +35,10 @@ %li = link_to "#projects", 'data-toggle' => 'tab' do Projects + - if @shared_projects.present? + %li + = link_to "#shared", 'data-toggle' => 'tab' do + Shared Projects - if can?(current_user, :read_group, @group) %div{ class: container_class } @@ -52,6 +56,9 @@ .tab-pane#projects = render "projects", projects: @projects + .tab-pane#shared + = render "shared_projects", projects: @shared_projects + - else %p.nav-links.no-top No projects to show -- cgit v1.2.3 From 8901336c78a9075a6a64205500e6019c40fd632f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 11 Mar 2016 18:46:01 +0100 Subject: Allow users to access project shared with their group Signed-off-by: Dmitriy Zaporozhets --- app/models/project_team.rb | 52 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 9629c7e1bb9..70a8bbaba65 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -160,7 +160,27 @@ class ProjectTeam end end - access.max + if project.invited_groups.any? && project.allowed_to_share_with_group? + access << max_invited_level(user_id) + end + + access.compact.max + end + + + def max_invited_level(user_id) + project.project_group_links.map do |group_link| + invited_group = group_link.group + access = invited_group.group_members.find_by(user_id: user_id).try(:access_field) + + # If group member has higher access level we should restrict it + # to max allowed access level + if access && access > group_link.group_access + access = group_link.group_access + end + + access + end.compact.max end private @@ -168,6 +188,35 @@ class ProjectTeam def fetch_members(level = nil) project_members = project.project_members group_members = group ? group.group_members : [] + 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 + + if level + int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize] + + # Skip group members if we ask for masters + # but max group access is developers + next if int_level > group_link.group_access + + # If we ask for developers and max + # group access is developers we need to provide + # both group master, developers as devs + if int_level == group_link.group_access + im.where("access_level >= ?)", group_link.group_access) + else + im.send(level) + end + end + + invited_members << im + end + + invited_members = invited_members.flatten.compact + end if level project_members = project_members.send(level) @@ -175,6 +224,7 @@ class ProjectTeam end user_ids = project_members.pluck(:user_id) + user_ids.push(*invited_members.map(&:user_id)) if invited_members.any? user_ids.push(*group_members.pluck(:user_id)) if group User.where(id: user_ids) -- cgit v1.2.3 From 068fd5de8a45ef0814c500df10d3b9d39496fcd9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 11 Mar 2016 18:55:17 +0100 Subject: Add finders logic and tests for shared projects feature Signed-off-by: Dmitriy Zaporozhets --- app/finders/projects_finder.rb | 3 +- app/models/user.rb | 3 +- app/views/admin/groups/show.html.haml | 16 +++++++++ features/project/group_links.feature | 16 +++++++++ features/steps/project/project_group_links.rb | 50 +++++++++++++++++++++++++++ 5 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 features/project/group_links.feature create mode 100644 features/steps/project/project_group_links.rb diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 3b4e0362e04..93fc7c4b01c 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -43,7 +43,8 @@ class ProjectsFinder if current_user [ group_projects_for_user(current_user, group), - group.projects.public_and_internal_only + group.projects.public_and_internal_only, + group.shared_projects.visible_to_user(current_user) ] else [group.projects.public_only] diff --git a/app/models/user.rb b/app/models/user.rb index 505a547d8ec..e9d1f479433 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -818,7 +818,8 @@ class User < ActiveRecord::Base def projects_union Gitlab::SQL::Union.new([personal_projects.select(:id), groups_projects.select(:id), - projects.select(:id)]) + projects.select(:id), + groups.joins(:shared_projects).select(:project_id)]) end def ci_projects_union diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index f7fd156b84a..264fa1bf0cd 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -50,6 +50,22 @@ .panel-footer = paginate @projects, param_name: 'projects_page', theme: 'gitlab' + - if @group.shared_projects.any? + .panel.panel-default + .panel-heading + Projects shared with #{@group.name} + %span.badge + #{@group.shared_projects.count} + %ul.well-list + - @group.shared_projects.sort_by(&:name).each do |project| + %li + %strong + = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] + %span.label.label-gray + = repository_size(project) + %span.pull-right.light + %span.monospace= project.path_with_namespace + ".git" + .col-md-6 - if can?(current_user, :admin_group_member, @group) .panel.panel-default diff --git a/features/project/group_links.feature b/features/project/group_links.feature new file mode 100644 index 00000000000..2657c4487ad --- /dev/null +++ b/features/project/group_links.feature @@ -0,0 +1,16 @@ +Feature: Project Group Links + Background: + Given I sign in as a user + And I own project "Shop" + And project "Shop" is shared with group "Ops" + And project "Shop" is not shared with group "Market" + And I visit project group links page + + Scenario: I should see list of groups + Then I should see project already shared with group "Ops" + Then I should see project is not shared with group "Market" + + @javascript + Scenario: I share project with group + When I select group "Market" for share + Then I should see project is shared with group "Market" diff --git a/features/steps/project/project_group_links.rb b/features/steps/project/project_group_links.rb new file mode 100644 index 00000000000..739a85e5fa4 --- /dev/null +++ b/features/steps/project/project_group_links.rb @@ -0,0 +1,50 @@ +class Spinach::Features::ProjectGroupLinks < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedPaths + include Select2Helper + + step 'I should see project already shared with group "Ops"' do + page.within '.enabled-groups' do + expect(page).to have_content "Ops" + end + end + + step 'I should see project is not shared with group "Market"' do + page.within '.enabled-groups' do + expect(page).not_to have_content "Market" + end + end + + step 'I select group "Market" for share' do + group = Group.find_by(path: 'market') + select2(group.id, from: "#link_group_id") + select "Master", from: 'link_group_access' + click_button "Share" + end + + step 'I should see project is shared with group "Market"' do + page.within '.enabled-groups' do + expect(page).to have_content "Market" + end + end + + step 'project "Shop" is shared with group "Ops"' do + group = create(:group, name: 'Ops') + share_link = project.project_group_links.new(group_access: Gitlab::Access::MASTER) + share_link.group_id = group.id + share_link.save! + end + + step 'project "Shop" is not shared with group "Market"' do + create(:group, name: 'Market', path: 'market') + end + + step 'I visit project group links page' do + visit namespace_project_group_links_path(project.namespace, project) + end + + def project + @project ||= Project.find_by_name "Shop" + end +end -- cgit v1.2.3 From 9a95b15552fa8920800274324aa65900360e8038 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 11 Mar 2016 19:03:19 +0100 Subject: Add share project from group lock Signed-off-by: Dmitriy Zaporozhets --- app/controllers/groups_controller.rb | 2 +- app/models/project.rb | 3 +-- app/views/groups/edit.html.haml | 9 +++++++++ db/migrate/20150930110012_add_group_share_lock.rb | 5 +++++ db/schema.rb | 7 ++++--- 5 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20150930110012_add_group_share_lock.rb diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index bd6b44bdbbc..e8d9d9fb344 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -133,7 +133,7 @@ class GroupsController < Groups::ApplicationController end def group_params - params.require(:group).permit(:name, :description, :path, :avatar, :public) + params.require(:group).permit(:name, :description, :path, :avatar, :public, :share_with_group_lock) end def load_events diff --git a/app/models/project.rb b/app/models/project.rb index 56865459724..40f8c4c83da 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -881,8 +881,7 @@ class Project < ActiveRecord::Base end def allowed_to_share_with_group? - # TODO: replace with logic - true + !namespace.share_with_group_lock end def ci_commit(sha) diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 3430f56a9c9..83936d39b16 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -23,6 +23,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-sm remove-avatar" + .form-group + %hr + = f.label :share_with_group_lock, class: 'control-label' do + Share with group lock + .col-sm-10 + .checkbox + = f.check_box :share_with_group_lock + %span.descr Prevent sharing a project with another group within this group + .form-actions = f.submit 'Save group', class: "btn btn-save" diff --git a/db/migrate/20150930110012_add_group_share_lock.rb b/db/migrate/20150930110012_add_group_share_lock.rb new file mode 100644 index 00000000000..78d1a4538f2 --- /dev/null +++ b/db/migrate/20150930110012_add_group_share_lock.rb @@ -0,0 +1,5 @@ +class AddGroupShareLock < ActiveRecord::Migration + def change + add_column :namespaces, :share_with_group_lock, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 89e8c9e67b0..20dbb4f5a3e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -568,14 +568,15 @@ ActiveRecord::Schema.define(version: 20160309140734) do add_index "milestones", ["title"], name: "index_milestones_on_title", using: :btree create_table "namespaces", force: :cascade do |t| - t.string "name", null: false - t.string "path", null: false + t.string "name", null: false + t.string "path", null: false t.integer "owner_id" t.datetime "created_at" t.datetime "updated_at" t.string "type" - t.string "description", default: "", null: false + t.string "description", default: "", null: false t.string "avatar" + t.boolean "share_with_group_lock", default: false end add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree -- cgit v1.2.3 From 55ceda12045ffb59187b7104a3b28a34e28a8b87 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 12 Mar 2016 14:45:14 +0100 Subject: Bring shared project feature tests from EE Signed-off-by: Dmitriy Zaporozhets --- features/admin/groups.feature | 5 ++++ features/project/team_management.feature | 5 ++++ features/steps/admin/groups.rb | 19 +++++++++++++ features/steps/project/team_management.rb | 19 +++++++++++++ spec/finders/projects_finder_spec.rb | 34 +++++++++++++++++++++++- spec/models/project_team_spec.rb | 44 +++++++++++++++++++++++++++++++ 6 files changed, 125 insertions(+), 1 deletion(-) diff --git a/features/admin/groups.feature b/features/admin/groups.feature index 2edb3964f70..ab7de7ac315 100644 --- a/features/admin/groups.feature +++ b/features/admin/groups.feature @@ -21,6 +21,11 @@ Feature: Admin Groups When I select user "John Doe" from user list as "Reporter" Then I should see "John Doe" in team list in every project as "Reporter" + Scenario: Shared projects + Given group has shared projects + When I visit group page + Then I should see project shared with group + @javascript Scenario: Remove user from group Given we have user "John Doe" in group diff --git a/features/project/team_management.feature b/features/project/team_management.feature index 06fb45c8bde..5888662fc3f 100644 --- a/features/project/team_management.feature +++ b/features/project/team_management.feature @@ -39,3 +39,8 @@ Feature: Project Team Management And I click link "Import team from another project" And I submit "Website" project for import team Then I should see "Mike" in team list as "Reporter" + + Scenario: See all members of projects shared group + Given I share project with group "OpenSource" + And I visit project "Shop" team page + Then I should see "Opensource" group user listing diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb index 43fd91d0d4c..e1f1db2872f 100644 --- a/features/steps/admin/groups.rb +++ b/features/steps/admin/groups.rb @@ -73,6 +73,21 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps end end + step 'group has shared projects' do + share_link = shared_project.project_group_links.new(group_access: Gitlab::Access::MASTER) + share_link.group_id = current_group.id + share_link.save! + end + + step 'I visit group page' do + visit admin_group_path(current_group) + end + + step 'I should see project shared with group' do + expect(page).to have_content(shared_project.name_with_namespace) + expect(page).to have_content "Projects shared with" + end + step 'we have user "John Doe" in group' do current_group.add_reporter(user_john) end @@ -123,6 +138,10 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps @group ||= Group.first end + def shared_project + @shared_project ||= create(:empty_project) + end + def user_john @user_john ||= User.find_by(name: "John Doe") end diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb index caad52def79..3fbcf770b62 100644 --- a/features/steps/project/team_management.rb +++ b/features/steps/project/team_management.rb @@ -123,4 +123,23 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps click_link('Remove user from team') end end + + step 'I share project with group "OpenSource"' do + project = Project.find_by(name: 'Shop') + os_group = create(:group, name: 'OpenSource') + create(:project, group: os_group) + @os_user1 = create(:user) + @os_user2 = create(:user) + os_group.add_owner(@os_user1) + os_group.add_user(@os_user2, Gitlab::Access::DEVELOPER) + share_link = project.project_group_links.new(group_access: Gitlab::Access::MASTER) + share_link.group_id = os_group.id + share_link.save! + end + + step 'I should see "Opensource" group user listing' do + expect(page).to have_content("Shared with OpenSource group, members with Master role (2)") + expect(page).to have_content(@os_user1.name) + expect(page).to have_content(@os_user2.name) + end end diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb index f32641ef0f6..fae0da9d898 100644 --- a/spec/finders/projects_finder_spec.rb +++ b/spec/finders/projects_finder_spec.rb @@ -17,6 +17,10 @@ describe ProjectsFinder do create(:project, :public, group: group, name: 'C', path: 'C') end + let!(:shared_project) do + create(:project, :private, name: 'D', path: 'D') + end + let(:finder) { described_class.new } describe 'without a group' do @@ -56,7 +60,35 @@ describe ProjectsFinder do describe 'with a user' do subject { finder.execute(user, group: group) } - it { is_expected.to eq([public_project, internal_project]) } + describe 'without shared projects' do + it { is_expected.to eq([public_project, internal_project]) } + end + + describe 'with shared projects and group membership' do + before do + group.add_user(user, Gitlab::Access::DEVELOPER) + + shared_project.project_group_links. + create(group_access: Gitlab::Access::MASTER, group: group) + end + + it do + is_expected.to eq([shared_project, public_project, internal_project]) + end + end + + describe 'with shared projects and project membership' do + before do + shared_project.team.add_user(user, Gitlab::Access::DEVELOPER) + + shared_project.project_group_links. + create(group_access: Gitlab::Access::MASTER, group: group) + end + + it do + is_expected.to eq([shared_project, public_project, internal_project]) + end + end end end end diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 7b63da005f0..bacb17a8883 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -67,6 +67,50 @@ describe ProjectTeam, models: true do 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) + 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 + end + end + describe "#human_max_access" do it 'returns Master role' do user = create(:user) -- cgit v1.2.3 From e12e937b943be2d9dce062cde10b02b6474e170b Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 12 Mar 2016 15:05:52 +0100 Subject: Bring share with group feature documentation from EE Signed-off-by: Dmitriy Zaporozhets --- doc/workflow/README.md | 2 ++ doc/workflow/groups/max_access_level.png | Bin 0 -> 135354 bytes .../groups/other_group_sees_shared_project.png | Bin 0 -> 118382 bytes doc/workflow/groups/share_project_with_groups.png | Bin 0 -> 118868 bytes doc/workflow/share_projects_with_other_groups.md | 30 +++++++++++++++++++++ doc/workflow/share_with_group.md | 13 +++++++++ doc/workflow/share_with_group.png | Bin 0 -> 53784 bytes 7 files changed, 45 insertions(+) create mode 100644 doc/workflow/groups/max_access_level.png create mode 100644 doc/workflow/groups/other_group_sees_shared_project.png create mode 100644 doc/workflow/groups/share_project_with_groups.png create mode 100644 doc/workflow/share_projects_with_other_groups.md create mode 100644 doc/workflow/share_with_group.md create mode 100644 doc/workflow/share_with_group.png diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 2ac32373ce9..25893f948ea 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -13,6 +13,8 @@ - [Project forking workflow](forking_workflow.md) - [Project users](add-user/add-user.md) - [Protected branches](protected_branches.md) +- [Sharing a project with a group](share_with_group.md) +- [Share projects with other groups](share_projects_with_other_groups.md) - [Web Editor](web_editor.md) - [Releases](releases.md) - [Milestones](milestones.md) diff --git a/doc/workflow/groups/max_access_level.png b/doc/workflow/groups/max_access_level.png new file mode 100644 index 00000000000..71106a8a5a0 Binary files /dev/null and b/doc/workflow/groups/max_access_level.png differ diff --git a/doc/workflow/groups/other_group_sees_shared_project.png b/doc/workflow/groups/other_group_sees_shared_project.png new file mode 100644 index 00000000000..cbf2c3c1fdc Binary files /dev/null and b/doc/workflow/groups/other_group_sees_shared_project.png differ diff --git a/doc/workflow/groups/share_project_with_groups.png b/doc/workflow/groups/share_project_with_groups.png new file mode 100644 index 00000000000..a5dbc89fe90 Binary files /dev/null and b/doc/workflow/groups/share_project_with_groups.png differ diff --git a/doc/workflow/share_projects_with_other_groups.md b/doc/workflow/share_projects_with_other_groups.md new file mode 100644 index 00000000000..4c59f59c587 --- /dev/null +++ b/doc/workflow/share_projects_with_other_groups.md @@ -0,0 +1,30 @@ +# Share Projects with other Groups + +In GitLab Enterprise Edition you can share projects with other groups. +This makes it possible to add a group of users to a project with a single action. + +## Groups as collections of users + +In GitLab Community Edition groups are used primarily to [create collections of projects](groups.md). +In GitLab Enterprise Edition you can also take advantage of the fact that groups define collections of _users_, namely the group members. + +## Sharing a project with a group of users + +The primary mechanism to give a group of users, say 'Engineering', access to a project, say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project Acme'. +But what if 'Project Acme' already belongs to another group, say 'Open Source'? +This is where the (Enterprise Edition only) group sharing feature can be of use. + +To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the 'Groups' section. + +![The 'Groups' section in the project settings screen (Enterprise Edition only)](groups/share_project_with_groups.png) + +Now you can add the 'Engineering' group with the maximum access level of your choice. +After sharing 'Project Acme' with 'Engineering', the project is listed on the group dashboard. + +!['Project Acme' is listed as a shared project for 'Engineering'](groups/other_group_sees_shared_project.png) + +## Maximum access level + +!['Project Acme' is shared with 'Engineering' with a maximum access level of 'Developer'](groups/max_access_level.png) + +In the screenshot above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Master' or 'Owner') will only have 'Developer' access to 'Project Acme'. diff --git a/doc/workflow/share_with_group.md b/doc/workflow/share_with_group.md new file mode 100644 index 00000000000..3b7690973cb --- /dev/null +++ b/doc/workflow/share_with_group.md @@ -0,0 +1,13 @@ +# Sharing a project with a group + +If you want to share a single project in a group with another group, +you can do so easily. By setting the permission you can quickly +give a select group of users access to a project in a restricted manner. + +In a project go to the project settings -> groups. + +Now you can select a group that you want to share this project with and with +which maximum access level. Users in that group are able to access this project +with their set group access level, up to the maximum level that you've set. + +![Share a project with a group](share_with_group.png) diff --git a/doc/workflow/share_with_group.png b/doc/workflow/share_with_group.png new file mode 100644 index 00000000000..a0ca6f14552 Binary files /dev/null and b/doc/workflow/share_with_group.png differ -- cgit v1.2.3 From e1dffa32db3d2ac877c0fc6f1728566d46c9e93f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 12 Mar 2016 18:01:55 +0100 Subject: Render project members from shared group Signed-off-by: Dmitriy Zaporozhets --- .../projects/project_members_controller.rb | 1 + .../project_members/_shared_group_members.html.haml | 21 +++++++++++++++++++++ app/views/projects/project_members/index.html.haml | 3 +++ 3 files changed, 25 insertions(+) create mode 100644 app/views/projects/project_members/_shared_group_members.html.haml diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 8364fc293b7..e7bddc4a6f1 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -27,6 +27,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController end @project_member = @project.project_members.new + @project_group_links = @project.project_group_links end def create diff --git a/app/views/projects/project_members/_shared_group_members.html.haml b/app/views/projects/project_members/_shared_group_members.html.haml new file mode 100644 index 00000000000..62888e41935 --- /dev/null +++ b/app/views/projects/project_members/_shared_group_members.html.haml @@ -0,0 +1,21 @@ +- @project_group_links.each do |group_links| + - shared_group = group_links.group + - shared_group_users_count = group_links.group.group_members.count + .panel.panel-default + .panel-heading + Shared with + %strong #{shared_group.name} + group, members with + %strong #{group_links.human_access} + role (#{shared_group_users_count}) + - if current_user.can?(:admin_group, shared_group) + .panel-head-actions + = link_to group_group_members_path(shared_group), class: 'btn btn-sm' do + %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 + - 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/index.html.haml b/app/views/projects/project_members/index.html.haml index 0f8848a5cbe..ebcfc907ebb 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -18,3 +18,6 @@ - if @group = render "group_members", members: @group_members + + - if @project_group_links.any? && @project.allowed_to_share_with_group? + = render "shared_group_members" -- cgit v1.2.3 From e8b3b92ddebc47595fe4b69dc5b5a3a6dd1365ab Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 13 Mar 2016 11:46:16 +0100 Subject: Bring share project with group API from EE Signed-off-by: Dmitriy Zaporozhets --- doc/api/projects.md | 14 ++++++++++++++ lib/api/entities.rb | 4 ++++ lib/api/projects.rb | 27 +++++++++++++++++++++++++++ spec/requests/api/projects_spec.rb | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+) diff --git a/doc/api/projects.md b/doc/api/projects.md index 9e9486cd87a..3703f4b327a 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -619,6 +619,20 @@ Revoking team membership for a user who is not currently a team member is consid Please note that the returned JSON currently differs slightly. Thus you should not rely on the returned JSON structure. +### Share project with group + +Allow to share project with group. + +``` +POST /projects/:id/share +``` + +Parameters: + +- `id` (required) - The ID of a project +- `group_id` (required) - The ID of a group +- `group_access` (required) - Level of permissions for sharing + ## Hooks Also called Project Hooks and Webhooks. diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 5b5b8bd044b..d96eaef7c0a 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -243,6 +243,10 @@ module API end end + class ProjectGroupLink < Grape::Entity + expose :id, :project_id, :group_id, :group_access + end + class Namespace < Grape::Entity expose :id, :path, :kind end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 6067c8b4a5e..6fcb5261e40 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -290,6 +290,33 @@ module API end end + # Share project with group + # + # Parameters: + # id (required) - The ID of a project + # group_id (required) - The ID of a group + # group_access (required) - Level of permissions for sharing + # + # Example Request: + # POST /projects/:id/share + post ":id/share" do + authorize! :admin_project, user_project + required_attributes! [:group_id, :group_access] + + unless user_project.allowed_to_share_with_group? + return render_api_error!("The project sharing with group is disabled", 400) + end + + link = user_project.project_group_links.new + link.group_id = params[:group_id] + link.group_access = params[:group_access] + if link.save + present link, with: Entities::ProjectGroupLink + else + render_api_error!(link.errors.full_messages.first, 409) + end + end + # Upload a file # # Parameters: diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 9f2365a4832..a6699cdc81c 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -747,6 +747,42 @@ describe API::API, api: true do end end + describe "POST /projects/:id/share" do + let(:group) { create(:group) } + + it "should share project with group" do + expect do + post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER + end.to change { ProjectGroupLink.count }.by(1) + + expect(response.status).to eq 201 + expect(json_response['group_id']).to eq group.id + expect(json_response['group_access']).to eq Gitlab::Access::DEVELOPER + end + + it "should return a 400 error when group id is not given" do + post api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER + expect(response.status).to eq 400 + end + + it "should return a 400 error when access level is not given" do + post api("/projects/#{project.id}/share", user), group_id: group.id + expect(response.status).to eq 400 + end + + it "should return a 400 error when sharing is disabled" do + project.namespace.update(share_with_group_lock: true) + post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER + expect(response.status).to eq 400 + end + + it "should return a 409 error when wrong params passed" do + post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234 + expect(response.status).to eq 409 + expect(json_response['message']).to eq 'Group access is not included in the list' + end + end + describe 'GET /projects/search/:query' do let!(:query) { 'query'} let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) } -- cgit v1.2.3