Merge branch '31409-fix-group-and-project-search-for-anonymous-users' into 'master'
Fix group and project search for anonymous users Closes #31409 See merge request !13745
This commit is contained in:
commit
c842e29aad
|
@ -55,13 +55,18 @@ const Api = {
|
||||||
// Return projects list. Filtered by query
|
// Return projects list. Filtered by query
|
||||||
projects(query, options, callback) {
|
projects(query, options, callback) {
|
||||||
const url = Api.buildUrl(Api.projectsPath);
|
const url = Api.buildUrl(Api.projectsPath);
|
||||||
|
const defaults = {
|
||||||
|
search: query,
|
||||||
|
per_page: 20,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (gon.current_user_id) {
|
||||||
|
defaults.membership = true;
|
||||||
|
}
|
||||||
|
|
||||||
return $.ajax({
|
return $.ajax({
|
||||||
url,
|
url,
|
||||||
data: Object.assign({
|
data: Object.assign(defaults, options),
|
||||||
search: query,
|
|
||||||
per_page: 20,
|
|
||||||
membership: true,
|
|
||||||
}, options),
|
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
})
|
})
|
||||||
.done(projects => callback(projects));
|
.done(projects => callback(projects));
|
||||||
|
|
|
@ -1,3 +1,19 @@
|
||||||
|
# GroupsFinder
|
||||||
|
#
|
||||||
|
# Used to filter Groups by a set of params
|
||||||
|
#
|
||||||
|
# Arguments:
|
||||||
|
# current_user - which user is requesting groups
|
||||||
|
# params:
|
||||||
|
# owned: boolean
|
||||||
|
# parent: Group
|
||||||
|
# all_available: boolean (defaults to true)
|
||||||
|
#
|
||||||
|
# Users with full private access can see all groups. The `owned` and `parent`
|
||||||
|
# params can be used to restrict the groups that are returned.
|
||||||
|
#
|
||||||
|
# Anonymous users will never return any `owned` groups. They will return all
|
||||||
|
# public groups instead, even if `all_available` is set to false.
|
||||||
class GroupsFinder < UnionFinder
|
class GroupsFinder < UnionFinder
|
||||||
def initialize(current_user = nil, params = {})
|
def initialize(current_user = nil, params = {})
|
||||||
@current_user = current_user
|
@current_user = current_user
|
||||||
|
@ -16,13 +32,13 @@ class GroupsFinder < UnionFinder
|
||||||
attr_reader :current_user, :params
|
attr_reader :current_user, :params
|
||||||
|
|
||||||
def all_groups
|
def all_groups
|
||||||
|
return [owned_groups] if params[:owned]
|
||||||
|
return [Group.all] if current_user&.full_private_access?
|
||||||
|
|
||||||
groups = []
|
groups = []
|
||||||
|
groups << Gitlab::GroupHierarchy.new(groups_for_ancestors, groups_for_descendants).all_groups if current_user
|
||||||
if current_user
|
groups << Group.unscoped.public_to_user(current_user) if include_public_groups?
|
||||||
groups << Gitlab::GroupHierarchy.new(groups_for_ancestors, groups_for_descendants).all_groups
|
groups << Group.none if groups.empty?
|
||||||
end
|
|
||||||
groups << Group.unscoped.public_to_user(current_user)
|
|
||||||
|
|
||||||
groups
|
groups
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -39,4 +55,12 @@ class GroupsFinder < UnionFinder
|
||||||
|
|
||||||
groups.where(parent: params[:parent])
|
groups.where(parent: params[:parent])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def owned_groups
|
||||||
|
current_user&.groups || Group.none
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_public_groups?
|
||||||
|
current_user.nil? || params.fetch(:all_available, true)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,5 +11,5 @@
|
||||||
%span.sr-only
|
%span.sr-only
|
||||||
Clear search
|
Clear search
|
||||||
- unless params[:snippets].eql? 'true'
|
- unless params[:snippets].eql? 'true'
|
||||||
= render 'filter' if current_user
|
= render 'filter'
|
||||||
= button_tag "Search", class: "btn btn-success btn-search"
|
= button_tag "Search", class: "btn btn-success btn-search"
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix group and project search for anonymous users
|
||||||
|
merge_request: 13745
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
## List groups
|
## List groups
|
||||||
|
|
||||||
Get a list of groups. (As user: my groups or all available, as admin: all groups).
|
Get a list of visible groups for the authenticated user. When accessed without
|
||||||
|
authentication, only public groups are returned.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
|
|
||||||
|
@ -43,7 +44,8 @@ You can search for groups by name or path, see below.
|
||||||
|
|
||||||
## List a group's projects
|
## List a group's projects
|
||||||
|
|
||||||
Get a list of projects in this group.
|
Get a list of projects in this group. When accessed without authentication, only
|
||||||
|
public projects are returned.
|
||||||
|
|
||||||
```
|
```
|
||||||
GET /groups/:id/projects
|
GET /groups/:id/projects
|
||||||
|
@ -109,7 +111,8 @@ Example response:
|
||||||
|
|
||||||
## Details of a group
|
## Details of a group
|
||||||
|
|
||||||
Get all details of a group.
|
Get all details of a group. This endpoint can be accessed without authentication
|
||||||
|
if the group is publicly accessible.
|
||||||
|
|
||||||
```
|
```
|
||||||
GET /groups/:id
|
GET /groups/:id
|
||||||
|
|
|
@ -2,7 +2,7 @@ module API
|
||||||
class Groups < Grape::API
|
class Groups < Grape::API
|
||||||
include PaginationParams
|
include PaginationParams
|
||||||
|
|
||||||
before { authenticate! }
|
before { authenticate_non_get! }
|
||||||
|
|
||||||
helpers do
|
helpers do
|
||||||
params :optional_params_ce do
|
params :optional_params_ce do
|
||||||
|
@ -47,16 +47,8 @@ module API
|
||||||
use :pagination
|
use :pagination
|
||||||
end
|
end
|
||||||
get do
|
get do
|
||||||
groups = if params[:owned]
|
find_params = { all_available: params[:all_available], owned: params[:owned] }
|
||||||
current_user.owned_groups
|
groups = GroupsFinder.new(current_user, find_params).execute
|
||||||
elsif current_user.admin
|
|
||||||
Group.all
|
|
||||||
elsif params[:all_available]
|
|
||||||
GroupsFinder.new(current_user).execute
|
|
||||||
else
|
|
||||||
current_user.groups
|
|
||||||
end
|
|
||||||
|
|
||||||
groups = groups.search(params[:search]) if params[:search].present?
|
groups = groups.search(params[:search]) if params[:search].present?
|
||||||
groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
|
groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
|
||||||
groups = groups.reorder(params[:order_by] => params[:sort])
|
groups = groups.reorder(params[:order_by] => params[:sort])
|
||||||
|
|
|
@ -281,4 +281,30 @@ describe "Search" do
|
||||||
expect(page).to have_selector('.commit-row-description', count: 9)
|
expect(page).to have_selector('.commit-row-description', count: 9)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'anonymous user' do
|
||||||
|
let(:project) { create(:project, :public) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_out(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'preserves the group being searched in' do
|
||||||
|
visit search_path(group_id: project.namespace.id)
|
||||||
|
|
||||||
|
fill_in 'search', with: 'foo'
|
||||||
|
click_button 'Search'
|
||||||
|
|
||||||
|
expect(find('#group_id').value).to eq(project.namespace.id.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'preserves the project being searched in' do
|
||||||
|
visit search_path(project_id: project.id)
|
||||||
|
|
||||||
|
fill_in 'search', with: 'foo'
|
||||||
|
click_button 'Search'
|
||||||
|
|
||||||
|
expect(find('#project_id').value).to eq(project.id.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,7 +17,7 @@ describe('Api', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
originalGon = window.gon;
|
originalGon = window.gon;
|
||||||
window.gon = dummyGon;
|
window.gon = Object.assign({}, dummyGon);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -98,14 +98,36 @@ describe('Api', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('projects', () => {
|
describe('projects', () => {
|
||||||
it('fetches projects', (done) => {
|
it('fetches projects with membership when logged in', (done) => {
|
||||||
|
const query = 'dummy query';
|
||||||
|
const options = { unused: 'option' };
|
||||||
|
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json?simple=true`;
|
||||||
|
window.gon.current_user_id = 1;
|
||||||
|
const expectedData = Object.assign({
|
||||||
|
search: query,
|
||||||
|
per_page: 20,
|
||||||
|
membership: true,
|
||||||
|
}, options);
|
||||||
|
spyOn(jQuery, 'ajax').and.callFake((request) => {
|
||||||
|
expect(request.url).toEqual(expectedUrl);
|
||||||
|
expect(request.dataType).toEqual('json');
|
||||||
|
expect(request.data).toEqual(expectedData);
|
||||||
|
return sendDummyResponse();
|
||||||
|
});
|
||||||
|
|
||||||
|
Api.projects(query, options, (response) => {
|
||||||
|
expect(response).toBe(dummyResponse);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fetches projects without membership when not logged in', (done) => {
|
||||||
const query = 'dummy query';
|
const query = 'dummy query';
|
||||||
const options = { unused: 'option' };
|
const options = { unused: 'option' };
|
||||||
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json?simple=true`;
|
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json?simple=true`;
|
||||||
const expectedData = Object.assign({
|
const expectedData = Object.assign({
|
||||||
search: query,
|
search: query,
|
||||||
per_page: 20,
|
per_page: 20,
|
||||||
membership: true,
|
|
||||||
}, options);
|
}, options);
|
||||||
spyOn(jQuery, 'ajax').and.callFake((request) => {
|
spyOn(jQuery, 'ajax').and.callFake((request) => {
|
||||||
expect(request.url).toEqual(expectedUrl);
|
expect(request.url).toEqual(expectedUrl);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import '~/project_select';
|
||||||
import '~/project';
|
import '~/project';
|
||||||
|
|
||||||
describe('Project Title', () => {
|
describe('Project Title', () => {
|
||||||
|
const dummyApiVersion = 'v3000';
|
||||||
preloadFixtures('issues/open-issue.html.raw');
|
preloadFixtures('issues/open-issue.html.raw');
|
||||||
loadJSONFixtures('projects.json');
|
loadJSONFixtures('projects.json');
|
||||||
|
|
||||||
|
@ -14,7 +15,7 @@ describe('Project Title', () => {
|
||||||
loadFixtures('issues/open-issue.html.raw');
|
loadFixtures('issues/open-issue.html.raw');
|
||||||
|
|
||||||
window.gon = {};
|
window.gon = {};
|
||||||
window.gon.api_version = 'v3';
|
window.gon.api_version = dummyApiVersion;
|
||||||
|
|
||||||
// eslint-disable-next-line no-new
|
// eslint-disable-next-line no-new
|
||||||
new Project();
|
new Project();
|
||||||
|
@ -37,9 +38,10 @@ describe('Project Title', () => {
|
||||||
|
|
||||||
it('toggles dropdown', () => {
|
it('toggles dropdown', () => {
|
||||||
const $menu = $('.js-dropdown-menu-projects');
|
const $menu = $('.js-dropdown-menu-projects');
|
||||||
|
window.gon.current_user_id = 1;
|
||||||
$('.js-projects-dropdown-toggle').click();
|
$('.js-projects-dropdown-toggle').click();
|
||||||
expect($menu).toHaveClass('open');
|
expect($menu).toHaveClass('open');
|
||||||
expect(reqUrl).toBe('/api/v3/projects.json?simple=true');
|
expect(reqUrl).toBe(`/api/${dummyApiVersion}/projects.json?simple=true`);
|
||||||
expect(reqData).toEqual({
|
expect(reqData).toEqual({
|
||||||
search: '',
|
search: '',
|
||||||
order_by: 'last_activity_at',
|
order_by: 'last_activity_at',
|
||||||
|
|
|
@ -20,10 +20,15 @@ describe API::Groups do
|
||||||
|
|
||||||
describe "GET /groups" do
|
describe "GET /groups" do
|
||||||
context "when unauthenticated" do
|
context "when unauthenticated" do
|
||||||
it "returns authentication error" do
|
it "returns public groups" do
|
||||||
get api("/groups")
|
get api("/groups")
|
||||||
|
|
||||||
expect(response).to have_http_status(401)
|
expect(response).to have_http_status(200)
|
||||||
|
expect(response).to include_pagination_headers
|
||||||
|
expect(json_response).to be_an Array
|
||||||
|
expect(json_response.length).to eq(1)
|
||||||
|
expect(json_response)
|
||||||
|
.to satisfy_one { |group| group['name'] == group1.name }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -165,6 +170,18 @@ describe API::Groups do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET /groups/:id" do
|
describe "GET /groups/:id" do
|
||||||
|
context 'when unauthenticated' do
|
||||||
|
it 'returns 404 for a private group' do
|
||||||
|
get api("/groups/#{group2.id}")
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 200 for a public group' do
|
||||||
|
get api("/groups/#{group1.id}")
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "when authenticated as user" do
|
context "when authenticated as user" do
|
||||||
it "returns one of user1's groups" do
|
it "returns one of user1's groups" do
|
||||||
project = create(:project, namespace: group2, path: 'Foo')
|
project = create(:project, namespace: group2, path: 'Foo')
|
||||||
|
|
Loading…
Reference in New Issue