Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-03-06 00:09:02 +00:00
parent 250c4c2596
commit 0313282761
10 changed files with 397 additions and 3 deletions

View File

@ -1 +1 @@
a9ae63b728760199653b4b79a931d91f8a873df7
4ffdf88c407165df85cd3d887f77b265ee29171e

View File

@ -0,0 +1,68 @@
# frozen_string_literal: true
# Used to filter ancestor and shared project's Groups by a set of params
#
# Arguments:
# project
# current_user - which user is requesting groups
# params:
# with_shared: boolean (optional)
# shared_min_access_level: integer (optional)
# skip_groups: array of integers (optional)
#
module Projects
class GroupsFinder < UnionFinder
def initialize(project:, current_user: nil, params: {})
@project = project
@current_user = current_user
@params = params
end
def execute
return Group.none unless authorized?
items = all_groups.map do |item|
item = exclude_group_ids(item)
item
end
find_union(items, Group).with_route.order_id_desc
end
private
attr_reader :project, :current_user, :params
def authorized?
Ability.allowed?(current_user, :read_project, project)
end
# rubocop: disable CodeReuse/ActiveRecord
def all_groups
groups = []
groups << project.group.self_and_ancestors if project.group
if params[:with_shared]
shared_groups = project.invited_groups
if params[:shared_min_access_level]
shared_groups = shared_groups.where(
'project_group_links.group_access >= ?', params[:shared_min_access_level]
)
end
groups << shared_groups
end
groups << Group.none if groups.compact.empty?
groups
end
# rubocop: enable CodeReuse/ActiveRecord
def exclude_group_ids(groups)
return groups unless params[:skip_groups]
groups.id_not_in(params[:skip_groups])
end
end
end

View File

@ -0,0 +1,5 @@
---
title: Add groups endpoint for Projects API
merge_request: 53642
author:
type: added

View File

@ -1040,6 +1040,43 @@ GET /projects/:id/users
]
```
## List a project's groups
Get a list of ancestor groups for this project.
```plaintext
GET /projects/:id/groups
```
| Attribute | Type | Required | Description |
|-----------------------------|-------------------|------------------------|-------------|
| `id` | integer/string | **{check-circle}** Yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
| `search` | string | **{dotted-circle}** No | Search for specific groups. |
| `skip_groups` | array of integers | **{dotted-circle}** No | Skip the group IDs passed. |
| `with_shared` | boolean | **{dotted-circle}** No | Include projects shared with this group. Default is `false`. |
| `shared_min_access_level` | integer | **{dotted-circle}** No | Limit to shared groups with at least this [access level](members.md#valid-access-levels). |
```json
[
{
"id": 1,
"name": "Foobar Group",
"avatar_url": "http://localhost:3000/uploads/group/avatar/1/foo.jpg",
"web_url": "http://localhost:3000/groups/foo-bar",
"full_name": "Foobar Group",
"full_path": "foo-bar",
},
{
"id": 2,
"name": "Shared Group",
"avatar_url": "http://gitlab.example.com/uploads/group/avatar/1/bar.jpg",
"web_url": "http://gitlab.example.com/groups/foo/bar",
"full_name": "Shared Group",
"full_path": "foo/shared",
}
]
```
## Get project events
Refer to the [Events API documentation](events.md#list-a-projects-visible-events).

View File

@ -68,10 +68,11 @@ request is as follows:
1. Use the syntax `Solves #XXX`, `Closes #XXX`, or `Refs #XXX` to mention the issue(s) your merge
request addresses. Referenced issues do not [close automatically](../../user/project/issues/managing_issues.md#closing-issues-automatically).
You must close them manually once the merge request is merged.
1. The MR must include *Before* and *After* screenshots if UI changes are made.
1. Include any steps or setup required to ensure reviewers can view the changes you've made (e.g. include any information about feature flags).
1. If you're allowed to, set a relevant milestone and [labels](issue_workflow.md).
1. UI changes should use available components from the GitLab Design System,
[Pajamas](https://design.gitlab.com/). The MR must include *Before* and
*After* screenshots.
[Pajamas](https://design.gitlab.com/).
1. If the MR changes CSS classes, please include the list of affected pages, which
can be found by running `grep css-class ./app -R`.
1. If your MR touches code that executes shell commands, reads or opens files, or

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
module API
module Entities
class PublicGroupDetails < BasicGroupDetails
expose :avatar_url do |group, options|
group.avatar_url(only_path: false)
end
expose :full_name, :full_path
end
end
end

View File

@ -136,6 +136,17 @@ module API
present records, options
end
def present_groups(groups)
options = {
with: Entities::PublicGroupDetails,
current_user: current_user
}
groups, options = with_custom_attributes(groups, options)
present paginate(groups), options
end
def translate_params_for_compatibility(params)
params[:builds_enabled] = params.delete(:jobs_enabled) if params.key?(:jobs_enabled)
params
@ -561,6 +572,25 @@ module API
present paginate(users), with: Entities::UserBasic
end
desc 'Get ancestor and shared groups for a project' do
success Entities::PublicGroupDetails
end
params do
optional :search, type: String, desc: 'Return list of groups matching the search criteria'
optional :skip_groups, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'Array of group ids to exclude from list'
optional :with_shared, type: Boolean, default: false,
desc: 'Include shared groups'
optional :shared_min_access_level, type: Integer, values: Gitlab::Access.all_values,
desc: 'Limit returned shared groups by minimum access level to the project'
use :pagination
end
get ':id/groups', feature_category: :source_code_management do
groups = ::Projects::GroupsFinder.new(project: user_project, current_user: current_user, params: declared_params(include_missing: false)).execute
groups = groups.search(params[:search]) if params[:search].present?
present_groups groups
end
desc 'Start the housekeeping task for a project' do
detail 'This feature was introduced in GitLab 9.0.'
end

View File

@ -0,0 +1,103 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::GroupsFinder do
describe '#execute' do
let_it_be(:user) { create(:user) }
let_it_be(:root_group) { create(:group, :public) }
let_it_be(:project_group) { create(:group, :public, parent: root_group) }
let_it_be(:shared_group_with_dev_access) { create(:group, :private, parent: root_group) }
let_it_be(:shared_group_with_reporter_access) { create(:group, :private) }
let_it_be(:public_project) { create(:project, :public, group: project_group) }
let_it_be(:private_project) { create(:project, :private, group: project_group) }
before_all do
[public_project, private_project].each do |project|
create(:project_group_link, :developer, group: shared_group_with_dev_access, project: project)
create(:project_group_link, :reporter, group: shared_group_with_reporter_access, project: project)
end
end
let(:params) { {} }
let(:current_user) { user }
let(:finder) { described_class.new(project: project, current_user: current_user, params: params) }
subject { finder.execute }
shared_examples 'finding related groups' do
it 'returns ancestor groups for this project' do
is_expected.to match_array([project_group, root_group])
end
context 'when the project does not belong to any group' do
before do
allow(project).to receive(:group) { nil }
end
it { is_expected.to eq([]) }
end
context 'when shared groups option is on' do
let(:params) { { with_shared: true } }
it 'returns ancestor and all shared groups' do
is_expected.to match_array([project_group, root_group, shared_group_with_dev_access, shared_group_with_reporter_access])
end
context 'when shared_min_access_level is developer' do
let(:params) { super().merge(shared_min_access_level: Gitlab::Access::DEVELOPER) }
it 'returns ancestor and shared groups with at least developer access' do
is_expected.to match_array([project_group, root_group, shared_group_with_dev_access])
end
end
end
context 'when skip group option is on' do
let(:params) { { skip_groups: [project_group.id] } }
it 'excludes provided groups' do
is_expected.to match_array([root_group])
end
end
end
context 'Public project' do
it_behaves_like 'finding related groups' do
let(:project) { public_project }
context 'when user is not authorized' do
let(:current_user) { nil }
it 'returns ancestor groups for this project' do
is_expected.to match_array([project_group, root_group])
end
end
end
end
context 'Private project' do
it_behaves_like 'finding related groups' do
let(:project) { private_project }
before do
project.add_developer(user)
end
context 'when user is not authorized' do
let(:current_user) { nil }
it { is_expected.to eq([]) }
end
end
end
context 'Missing project' do
let(:project) { nil }
it { is_expected.to eq([]) }
end
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Entities::PublicGroupDetails do
subject(:entity) { described_class.new(group) }
let(:group) { create(:group, :with_avatar) }
describe '#as_json' do
subject { entity.as_json }
it 'includes public group fields' do
is_expected.to eq(
id: group.id,
name: group.name,
web_url: group.web_url,
avatar_url: group.avatar_url(only_path: false),
full_name: group.full_name,
full_path: group.full_path
)
end
end
end

View File

@ -1478,6 +1478,120 @@ RSpec.describe API::Projects do
end
end
describe "GET /projects/:id/groups" do
let_it_be(:root_group) { create(:group, :public, name: 'root group') }
let_it_be(:project_group) { create(:group, :public, parent: root_group, name: 'project group') }
let_it_be(:shared_group_with_dev_access) { create(:group, :private, parent: root_group, name: 'shared group') }
let_it_be(:shared_group_with_reporter_access) { create(:group, :private) }
let_it_be(:private_project) { create(:project, :private, group: project_group) }
let_it_be(:public_project) { create(:project, :public, group: project_group) }
before_all do
create(:project_group_link, :developer, group: shared_group_with_dev_access, project: private_project)
create(:project_group_link, :reporter, group: shared_group_with_reporter_access, project: private_project)
end
shared_examples_for 'successful groups response' do
it 'returns an array of groups' do
request
aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |g| g['name'] }).to match_array(expected_groups.map(&:name))
end
end
end
context 'when unauthenticated' do
it 'does not return groups for private projects' do
get api("/projects/#{private_project.id}/groups")
expect(response).to have_gitlab_http_status(:not_found)
end
context 'for public projects' do
let(:request) { get api("/projects/#{public_project.id}/groups") }
it_behaves_like 'successful groups response' do
let(:expected_groups) { [root_group, project_group] }
end
end
end
context 'when authenticated as user' do
context 'when user does not have access to the project' do
it 'does not return groups' do
get api("/projects/#{private_project.id}/groups", user)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when user has access to the project' do
let(:request) { get api("/projects/#{private_project.id}/groups", user), params: params }
let(:params) { {} }
before do
private_project.add_developer(user)
end
it_behaves_like 'successful groups response' do
let(:expected_groups) { [root_group, project_group] }
end
context 'when search by root group name' do
let(:params) { { search: 'root' } }
it_behaves_like 'successful groups response' do
let(:expected_groups) { [root_group] }
end
end
context 'with_shared option is on' do
let(:params) { { with_shared: true } }
it_behaves_like 'successful groups response' do
let(:expected_groups) { [root_group, project_group, shared_group_with_dev_access, shared_group_with_reporter_access] }
end
context 'when shared_min_access_level is set' do
let(:params) { super().merge(shared_min_access_level: Gitlab::Access::DEVELOPER) }
it_behaves_like 'successful groups response' do
let(:expected_groups) { [root_group, project_group, shared_group_with_dev_access] }
end
end
context 'when search by shared group name' do
let(:params) { super().merge(search: 'shared') }
it_behaves_like 'successful groups response' do
let(:expected_groups) { [shared_group_with_dev_access] }
end
end
context 'when skip_groups is set' do
let(:params) { super().merge(skip_groups: [shared_group_with_dev_access.id, root_group.id]) }
it_behaves_like 'successful groups response' do
let(:expected_groups) { [shared_group_with_reporter_access, project_group] }
end
end
end
end
end
context 'when authenticated as admin' do
let(:request) { get api("/projects/#{private_project.id}/groups", admin) }
it_behaves_like 'successful groups response' do
let(:expected_groups) { [root_group, project_group] }
end
end
end
describe 'GET /projects/:id' do
context 'when unauthenticated' do
it 'does not return private projects' do