Commit cd876730 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch '217115-assign-epic-to-a-group-when-creating-within-another-epic' into 'master'

Add API endpoint to list group descendants

See merge request gitlab-org/gitlab!42620
parents 98fb0ef8 75969000
......@@ -12,6 +12,8 @@
# all_available: boolean (defaults to true)
# min_access_level: integer
# exclude_group_ids: array of integers
# include_parent_descendants: boolean (defaults to false) - includes descendant groups when
# filtering by parent. The parent param must be present.
#
# Users with full private access can see all groups. The `owned` and `parent`
# params can be used to restrict the groups that are returned.
......@@ -84,7 +86,11 @@ class GroupsFinder < UnionFinder
def by_parent(groups)
return groups unless params[:parent]
groups.where(parent: params[:parent])
if include_parent_descendants?
groups.id_in(params[:parent].descendants)
else
groups.where(parent: params[:parent])
end
end
# rubocop: enable CodeReuse/ActiveRecord
......@@ -100,6 +106,10 @@ class GroupsFinder < UnionFinder
params.fetch(:all_available, true)
end
def include_parent_descendants?
params.fetch(:include_parent_descendants, false)
end
def min_access_level?
current_user && params[:min_access_level].present?
end
......
---
title: Add a REST API endpoint to list group's descendants
merge_request: 42620
author:
type: added
......@@ -167,6 +167,89 @@ GET /groups/:id/subgroups
]
```
## List a group's descendant groups
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217115) in GitLab 13.5
Get a list of visible descendant groups of this group.
When accessed without authentication, only public groups are returned.
By default, this request returns 20 results at a time because the API results [are paginated](README.md#pagination).
Parameters:
| Attribute | Type | Required | Description |
| ------------------------ | ----------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) of the immediate parent group |
| `skip_groups` | array of integers | no | Skip the group IDs passed |
| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users, `true` for admin). Attributes `owned` and `min_access_level` have precedence |
| `search` | string | no | Return the list of authorized groups matching the search criteria |
| `order_by` | string | no | Order groups by `name`, `path`, or `id`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
| `statistics` | boolean | no | Include group statistics (admins only) |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
| `owned` | boolean | no | Limit to groups explicitly owned by the current user |
| `min_access_level` | integer | no | Limit to groups where current user has at least this [access level](members.md#valid-access-levels) |
```plaintext
GET /groups/:id/descendant_groups
```
```json
[
{
"id": 2,
"name": "Bar Group",
"path": "foo/bar",
"description": "A subgroup of Foo Group",
"visibility": "public",
"share_with_group_lock": false,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"project_creation_level": "developer",
"auto_devops_enabled": null,
"subgroup_creation_level": "owner",
"emails_disabled": null,
"mentions_disabled": null,
"lfs_enabled": true,
"default_branch_protection": 2,
"avatar_url": "http://gitlab.example.com/uploads/group/avatar/1/bar.jpg",
"web_url": "http://gitlab.example.com/groups/foo/bar",
"request_access_enabled": false,
"full_name": "Bar Group",
"full_path": "foo/bar",
"file_template_project_id": 1,
"parent_id": 123,
"created_at": "2020-01-15T12:36:29.590Z"
},
{
"id": 3,
"name": "Baz Group",
"path": "foo/bar/baz",
"description": "A subgroup of Bar Group",
"visibility": "public",
"share_with_group_lock": false,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"project_creation_level": "developer",
"auto_devops_enabled": null,
"subgroup_creation_level": "owner",
"emails_disabled": null,
"mentions_disabled": null,
"lfs_enabled": true,
"default_branch_protection": 2,
"avatar_url": "http://gitlab.example.com/uploads/group/avatar/1/baz.jpg",
"web_url": "http://gitlab.example.com/groups/foo/bar/baz",
"request_access_enabled": false,
"full_name": "Baz Group",
"full_path": "foo/bar/baz",
"file_template_project_id": 1,
"parent_id": 123,
"created_at": "2020-01-15T12:36:29.590Z"
}
]
```
## List a group's projects
Get a list of projects in this group. When accessed without authentication, only public projects are returned.
......
......@@ -29,7 +29,12 @@ module API
# rubocop: disable CodeReuse/ActiveRecord
def find_groups(params, parent_id = nil)
find_params = params.slice(:all_available, :custom_attributes, :owned, :min_access_level)
find_params = params.slice(
:all_available,
:custom_attributes,
:owned, :min_access_level,
:include_parent_descendants
)
find_params[:parent] = if params[:top_level_only]
[nil]
......@@ -309,6 +314,19 @@ module API
present_groups params, groups
end
desc 'Get a list of descendant groups of this group.' do
success Entities::Group
end
params do
use :group_list_params
use :with_custom_attributes
end
get ":id/descendant_groups" do
finder_params = declared_params(include_missing: false).merge(include_parent_descendants: true)
groups = find_groups(finder_params, params[:id])
present_groups params, groups
end
desc 'Transfer a project to the group namespace. Available only for admin.' do
success Entities::GroupDetail
end
......
......@@ -161,5 +161,61 @@ RSpec.describe GroupsFinder do
end
end
end
context 'with include parent group descendants' do
let_it_be(:user) { create(:user) }
let_it_be(:parent_group) { create(:group, :public) }
let_it_be(:public_subgroup) { create(:group, :public, parent: parent_group) }
let_it_be(:internal_sub_subgroup) { create(:group, :internal, parent: public_subgroup) }
let_it_be(:private_sub_subgroup) { create(:group, :private, parent: public_subgroup) }
let_it_be(:public_sub_subgroup) { create(:group, :public, parent: public_subgroup) }
let(:params) { { include_parent_descendants: true, parent: parent_group } }
context 'with nil parent' do
it 'returns all accessible groups' do
params[:parent] = nil
expect(described_class.new(user, params).execute).to contain_exactly(
parent_group,
public_subgroup,
internal_sub_subgroup,
public_sub_subgroup
)
end
end
context 'without a user' do
it 'only returns the group public descendants' do
expect(described_class.new(nil, params).execute).to contain_exactly(
public_subgroup,
public_sub_subgroup
)
end
end
context 'when a user is present' do
it 'returns the group public and internal descendants' do
expect(described_class.new(user, params).execute).to contain_exactly(
public_subgroup,
public_sub_subgroup,
internal_sub_subgroup
)
end
end
context 'when a parent group member is present' do
before do
parent_group.add_developer(user)
end
it 'returns all group descendants' do
expect(described_class.new(user, params).execute).to contain_exactly(
public_subgroup,
public_sub_subgroup,
internal_sub_subgroup,
private_sub_subgroup
)
end
end
end
end
end
......@@ -1391,6 +1391,139 @@ RSpec.describe API::Groups do
end
end
describe 'GET /groups/:id/descendant_groups' do
let_it_be(:child_group1) { create(:group, parent: group1) }
let_it_be(:private_child_group1) { create(:group, :private, parent: group1) }
let_it_be(:sub_child_group1) { create(:group, parent: child_group1) }
let_it_be(:child_group2) { create(:group, :private, parent: group2) }
let_it_be(:sub_child_group2) { create(:group, :private, parent: child_group2) }
let(:response_groups) { json_response.map { |group| group['name'] } }
context 'when unauthenticated' do
it 'returns only public descendants' do
get api("/groups/#{group1.id}/descendant_groups")
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(response_groups).to contain_exactly(child_group1.name, sub_child_group1.name)
end
it 'returns 404 for a private group' do
get api("/groups/#{group2.id}/descendant_groups")
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when authenticated as user' do
context 'when user is not member of a public group' do
it 'returns no descendants for the public group' do
get api("/groups/#{group1.id}/descendant_groups", user2)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
context 'when using all_available in request' do
it 'returns public descendants' do
get api("/groups/#{group1.id}/descendant_groups", user2), params: { all_available: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(response_groups).to contain_exactly(child_group1.name, sub_child_group1.name)
end
end
end
context 'when user is not member of a private group' do
it 'returns 404 for the private group' do
get api("/groups/#{group2.id}/descendant_groups", user1)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when user is member of public group' do
before do
group1.add_guest(user2)
end
it 'returns private descendants' do
get api("/groups/#{group1.id}/descendant_groups", user2)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(response_groups).to contain_exactly(child_group1.name, sub_child_group1.name, private_child_group1.name)
end
context 'when using statistics in request' do
it 'does not include statistics' do
get api("/groups/#{group1.id}/descendant_groups", user2), params: { statistics: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response.first).not_to include 'statistics'
end
end
end
context 'when user is member of private group' do
before do
group2.add_guest(user1)
end
it 'returns descendants' do
get api("/groups/#{group2.id}/descendant_groups", user1)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(response_groups).to contain_exactly(child_group2.name, sub_child_group2.name)
end
end
end
context 'when authenticated as admin' do
it 'returns private descendants of a public group' do
get api("/groups/#{group1.id}/descendant_groups", admin)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
end
it 'returns descendants of a private group' do
get api("/groups/#{group2.id}/descendant_groups", admin)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
it 'does not include statistics by default' do
get api("/groups/#{group1.id}/descendant_groups", admin)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response.first).not_to include('statistics')
end
it 'includes statistics if requested' do
get api("/groups/#{group1.id}/descendant_groups", admin), params: { statistics: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response.first).to include('statistics')
end
end
end
describe "POST /groups" do
it_behaves_like 'group avatar upload' do
def make_upload_request
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment