...@@ -476,8 +476,18 @@ module API ...@@ -476,8 +476,18 @@ module API
class CommitDetail < Commit class CommitDetail < Commit
expose :stats, using: Entities::CommitStats, if: :stats expose :stats, using: Entities::CommitStats, if: :stats
expose :status expose :status
expose :last_pipeline, using: 'API::Entities::PipelineBasic'
expose :project_id expose :project_id
expose :last_pipeline do |commit, options|
pipeline = commit.last_pipeline if can_read_pipeline?
::API::Entities::PipelineBasic.represent(pipeline, options)
end
private
def can_read_pipeline?
Ability.allowed?(options[:current_user], :read_pipeline, object.last_pipeline)
end
end end
class CommitSignature < Grape::Entity class CommitSignature < Grape::Entity
... ...
......
...@@ -127,6 +127,7 @@ module API ...@@ -127,6 +127,7 @@ module API
get ":id/repository/files/:file_path/raw", requirements: FILE_ENDPOINT_REQUIREMENTS do get ":id/repository/files/:file_path/raw", requirements: FILE_ENDPOINT_REQUIREMENTS do
assign_file_vars! assign_file_vars!
no_cache_headers
set_http_headers(blob_data) set_http_headers(blob_data)
send_git_blob @repo, @blob send_git_blob @repo, @blob
... ...
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
module API module API
module Helpers module Helpers
module HeadersHelpers module HeadersHelpers
include Gitlab::NoCacheHeaders
def set_http_headers(header_data) def set_http_headers(header_data)
header_data.each do |key, value| header_data.each do |key, value|
if value.is_a?(Enumerable) if value.is_a?(Enumerable)
...@@ -12,6 +14,12 @@ module API ...@@ -12,6 +14,12 @@ module API
header "X-Gitlab-#{key.to_s.split('_').collect(&:capitalize).join('-')}", value.to_s header "X-Gitlab-#{key.to_s.split('_').collect(&:capitalize).join('-')}", value.to_s
end end
end end
def no_cache_headers
DEFAULT_GITLAB_NO_CACHE_HEADERS.each do |k, v|
header k, v
end
end
end end
end end
end end
# frozen_string_literal: true
module Gitlab
module NoCacheHeaders
DEFAULT_GITLAB_NO_CACHE_HEADERS = {
'Cache-Control' => "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store, no-cache",
'Pragma' => 'no-cache', # HTTP 1.0 compatibility
'Expires' => 'Fri, 01 Jan 1990 00:00:00 GMT'
}.freeze
def no_cache_headers
raise "#no_cache_headers is not implemented for this object"
end
end
end
...@@ -59,5 +59,48 @@ module QA ...@@ -59,5 +59,48 @@ module QA
a_hash_including(message: '202 Accepted') a_hash_including(message: '202 Accepted')
) )
end end
describe 'raw file access' do
let(:svg_file) do
<<-SVG
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
<polygon id="triangle" points="0,0 0,50 50,0" fill="#009900" stroke="#004400"/>
<script type="text/javascript">
alert("surprise");
</script>
</svg>
SVG
end
it 'sets no-cache headers as expected' do
create_project_request = Runtime::API::Request.new(@api_client, '/projects')
post create_project_request.url, path: project_name, name: project_name
create_file_request = Runtime::API::Request.new(@api_client, "/projects/#{sanitized_project_path}/repository/files/test.svg")
post create_file_request.url, branch: 'master', content: svg_file, commit_message: 'Add test.svg'
get_file_request = Runtime::API::Request.new(@api_client, "/projects/#{sanitized_project_path}/repository/files/test.svg/raw", ref: 'master')
3.times do
response = get get_file_request.url
# Subsequent responses aren't cached, so headers should match from
# request to request, especially a 200 response rather than a 304
# (indicating a cached response.) Further, :content_disposition
# should include `attachment` for all responses.
#
expect(response.headers[:cache_control]).to include("no-store")
expect(response.headers[:cache_control]).to include("no-cache")
expect(response.headers[:pragma]).to eq("no-cache")
expect(response.headers[:expires]).to eq("Fri, 01 Jan 1990 00:00:00 GMT")
expect(response.headers[:content_disposition]).to include("attachment")
expect(response.headers[:content_disposition]).not_to include("inline")
expect(response.headers[:content_type]).to include("image/svg+xml")
end
end
end
end end
end end
...@@ -23,6 +23,47 @@ describe DashboardController do ...@@ -23,6 +23,47 @@ describe DashboardController do
end end
end end
describe "GET activity as JSON" do
render_views
let(:user) { create(:user) }
let(:project) { create(:project, :public, issues_access_level: ProjectFeature::PRIVATE) }
before do
create(:event, :created, project: project, target: create(:issue))
sign_in(user)
request.cookies[:event_filter] = 'all'
end
context 'when user has permission to see the event' do
before do
project.add_developer(user)
end
it 'returns count' do
get :activity, params: { format: :json }
expect(json_response['count']).to eq(1)
end
end
context 'when user has no permission to see the event' do
it 'filters out invisible event' do
get :activity, params: { format: :json }
expect(json_response['html']).to include(_('No activities found'))
end
it 'filters out invisible event when calculating the count' do
get :activity, params: { format: :json }
expect(json_response['count']).to eq(0)
end
end
end
it_behaves_like 'authenticates sessionless user', :issues, :atom, author_id: User.first it_behaves_like 'authenticates sessionless user', :issues, :atom, author_id: User.first
it_behaves_like 'authenticates sessionless user', :issues_calendar, :ics it_behaves_like 'authenticates sessionless user', :issues_calendar, :ics
... ...
......
...@@ -47,7 +47,7 @@ describe GroupsController do ...@@ -47,7 +47,7 @@ describe GroupsController do
it 'assigns events for all the projects in the group', :sidekiq_might_not_need_inline do it 'assigns events for all the projects in the group', :sidekiq_might_not_need_inline do
subject subject
expect(assigns(:events)).to contain_exactly(event) expect(assigns(:events).map(&:id)).to contain_exactly(event.id)
end end
end end
end end
...@@ -119,12 +119,12 @@ describe GroupsController do ...@@ -119,12 +119,12 @@ describe GroupsController do
describe 'GET #activity' do describe 'GET #activity' do
render_views render_views
context 'as json' do
before do before do
sign_in(user) sign_in(user)
project project
end end
context 'as json' do
it 'includes events from all projects in group and subgroups', :sidekiq_might_not_need_inline do it 'includes events from all projects in group and subgroups', :sidekiq_might_not_need_inline do
2.times do 2.times do
project = create(:project, group: group) project = create(:project, group: group)
...@@ -141,6 +141,31 @@ describe GroupsController do ...@@ -141,6 +141,31 @@ describe GroupsController do
expect(assigns(:projects).limit_value).to be_nil expect(assigns(:projects).limit_value).to be_nil
end end
end end
context 'when user has no permission to see the event' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
let(:project_with_restricted_access) do
create(:project, :public, issues_access_level: ProjectFeature::PRIVATE, group: group)
end
before do
create(:event, project: project)
create(:event, :created, project: project_with_restricted_access, target: create(:issue))
group.add_guest(user)
sign_in(user)
end
it 'filters out invisible event' do
get :activity, params: { id: group.to_param }, format: :json
expect(json_response['count']).to eq(1)
end
end
end end
describe 'POST #create' do describe 'POST #create' do
... ...
......
...@@ -64,6 +64,46 @@ describe ProjectsController do ...@@ -64,6 +64,46 @@ describe ProjectsController do
end end
end end
describe "GET #activity as JSON" do
render_views
let(:project) { create(:project, :public, issues_access_level: ProjectFeature::PRIVATE) }
before do
create(:event, :created, project: project, target: create(:issue))
sign_in(user)
request.cookies[:event_filter] = 'all'
end
context 'when user has permission to see the event' do
before do
project.add_developer(user)
end
it 'returns count' do
get :activity, params: { namespace_id: project.namespace, id: project, format: :json }
expect(json_response['count']).to eq(1)
end
end
context 'when user has no permission to see the event' do
it 'filters out invisible event' do
get :activity, params: { namespace_id: project.namespace, id: project, format: :json }
expect(json_response['html']).to eq("\n")
end
it 'filters out invisible event when calculating the count' do
get :activity, params: { namespace_id: project.namespace, id: project, format: :json }
expect(json_response['count']).to eq(0)
end
end
end
describe "GET show" do describe "GET show" do
context "user not project member" do context "user not project member" do
before do before do
... ...
......
import bp from '~/breakpoints'; import bp from '~/breakpoints';
import { isMobile, getTopFrequentItems, updateExistingFrequentItem } from '~/frequent_items/utils'; import {
isMobile,
getTopFrequentItems,
updateExistingFrequentItem,
sanitizeItem,
} from '~/frequent_items/utils';
import { HOUR_IN_MS, FREQUENT_ITEMS } from '~/frequent_items/constants'; import { HOUR_IN_MS, FREQUENT_ITEMS } from '~/frequent_items/constants';
import { mockProject, unsortedFrequentItems, sortedFrequentItems } from './mock_data'; import { mockProject, unsortedFrequentItems, sortedFrequentItems } from './mock_data';
...@@ -86,4 +91,16 @@ describe('Frequent Items utils spec', () => { ...@@ -86,4 +91,16 @@ describe('Frequent Items utils spec', () => {
expect(result.frequency).toBe(mockedProject.frequency); expect(result.frequency).toBe(mockedProject.frequency);
}); });
}); });
describe('sanitizeItem', () => {
it('strips HTML tags for name and namespace', () => {
const input = {
name: '<br><b>test</b>',
namespace: '<br>test',
id: 1,
};
expect(sanitizeItem(input)).toEqual({ name: 'test', namespace: 'test', id: 1 });
});
});
}); });
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::NoCacheHeaders do
class NoCacheTester
include Gitlab::NoCacheHeaders
end
describe "#no_cache_headers" do
subject { NoCacheTester.new }
it "raises a RuntimeError" do
expect { subject.no_cache_headers }.to raise_error(RuntimeError)
end
end
end
...@@ -141,6 +141,34 @@ describe GlobalPolicy do ...@@ -141,6 +141,34 @@ describe GlobalPolicy do
it { is_expected.to be_allowed(:access_api) } it { is_expected.to be_allowed(:access_api) }
end end
end end
context 'inactive user' do
before do
current_user.update!(confirmed_at: nil, confirmation_sent_at: 5.days.ago)
end
context 'when within the confirmation grace period' do
before do
allow(User).to receive(:allow_unconfirmed_access_for).and_return(10.days)
end
it { is_expected.to be_allowed(:access_api) }
end
context 'when confirmation grace period is expired' do
before do
allow(User).to receive(:allow_unconfirmed_access_for).and_return(2.days)
end
it { is_expected.not_to be_allowed(:access_api) }
end
it 'when `inactive_policy_condition` feature flag is turned off' do
stub_feature_flags(inactive_policy_condition: false)
is_expected.to be_allowed(:access_api)
end
end
end end
describe 'receive notifications' do describe 'receive notifications' do
...@@ -202,6 +230,20 @@ describe GlobalPolicy do ...@@ -202,6 +230,20 @@ describe GlobalPolicy do
it { is_expected.not_to be_allowed(:access_git) } it { is_expected.not_to be_allowed(:access_git) }
end end
describe 'inactive user' do
before do
current_user.update!(confirmed_at: nil)
end
it { is_expected.not_to be_allowed(:access_git) }
it 'when `inactive_policy_condition` feature flag is turned off' do
stub_feature_flags(inactive_policy_condition: false)
is_expected.to be_allowed(:access_git)
end
end
context 'when terms are enforced' do context 'when terms are enforced' do
before do before do
enforce_terms enforce_terms
...@@ -298,6 +340,20 @@ describe GlobalPolicy do ...@@ -298,6 +340,20 @@ describe GlobalPolicy do
it { is_expected.not_to be_allowed(:use_slash_commands) } it { is_expected.not_to be_allowed(:use_slash_commands) }
end end
describe 'inactive user' do
before do
current_user.update!(confirmed_at: nil)
end
it { is_expected.not_to be_allowed(:use_slash_commands) }
it 'when `inactive_policy_condition` feature flag is turned off' do
stub_feature_flags(inactive_policy_condition: false)
is_expected.to be_allowed(:use_slash_commands)
end
end
context 'when access locked' do context 'when access locked' do
before do before do
current_user.lock_access! current_user.lock_access!
... ...
......
...@@ -8,6 +8,7 @@ describe API::Commits do ...@@ -8,6 +8,7 @@ describe API::Commits do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:guest) { create(:user).tap { |u| project.add_guest(u) } } let(:guest) { create(:user).tap { |u| project.add_guest(u) } }
let(:developer) { create(:user).tap { |u| project.add_developer(u) } }
let(:project) { create(:project, :repository, creator: user, path: 'my.project') } let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
let(:branch_with_dot) { project.repository.find_branch('ends-with.json') } let(:branch_with_dot) { project.repository.find_branch('ends-with.json') }
let(:branch_with_slash) { project.repository.find_branch('improve/awesome') } let(:branch_with_slash) { project.repository.find_branch('improve/awesome') }
...@@ -964,6 +965,56 @@ describe API::Commits do ...@@ -964,6 +965,56 @@ describe API::Commits do
end end
end end
shared_examples_for 'ref with pipeline' do
let!(:pipeline) do
project
.ci_pipelines
.create!(source: :push, ref: 'master', sha: commit.sha, protected: false)
end
it 'includes status as "created" and a last_pipeline object' do
get api(route, current_user)
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/commit/detail')
expect(json_response['status']).to eq('created')
expect(json_response['last_pipeline']['id']).to eq(pipeline.id)
expect(json_response['last_pipeline']['ref']).to eq(pipeline.ref)
expect(json_response['last_pipeline']['sha']).to eq(pipeline.sha)
expect(json_response['last_pipeline']['status']).to eq(pipeline.status)
end
context 'when pipeline succeeds' do
before do
pipeline.update!(status: 'success')
end
it 'includes a "success" status' do
get api(route, current_user)
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/commit/detail')
expect(json_response['status']).to eq('success')
end
end
end
shared_examples_for 'ref with unaccessible pipeline' do
let!(:pipeline) do
project
.ci_pipelines
.create!(source: :push, ref: 'master', sha: commit.sha, protected: false)
end
it 'does not include last_pipeline' do
get api(route, current_user)
expect(response).to match_response_schema('public_api/v4/commit/detail')
expect(response).to have_gitlab_http_status(200)
expect(json_response['last_pipeline']).to be_nil
end
end
context 'when stat param' do context 'when stat param' do
let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}" } let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}" }
...@@ -993,6 +1044,15 @@ describe API::Commits do ...@@ -993,6 +1044,15 @@ describe API::Commits do
let(:project) { create(:project, :public, :repository) } let(:project) { create(:project, :public, :repository) }
it_behaves_like 'ref commit' it_behaves_like 'ref commit'
it_behaves_like 'ref with pipeline'
context 'with private builds' do
before do
project.project_feature.update!(builds_access_level: ProjectFeature::PRIVATE)
end
it_behaves_like 'ref with unaccessible pipeline'
end
end end
context 'when unauthenticated', 'and project is private' do context 'when unauthenticated', 'and project is private' do
...@@ -1006,6 +1066,17 @@ describe API::Commits do ...@@ -1006,6 +1066,17 @@ describe API::Commits do
let(:current_user) { user } let(:current_user) { user }
it_behaves_like 'ref commit' it_behaves_like 'ref commit'
it_behaves_like 'ref with pipeline'
context 'when builds are disabled' do
before do
project
.project_feature
.update!(builds_access_level: ProjectFeature::DISABLED)
end
it_behaves_like 'ref with unaccessible pipeline'
end
context 'when branch contains a dot' do context 'when branch contains a dot' do
let(:commit) { project.repository.commit(branch_with_dot.name) } let(:commit) { project.repository.commit(branch_with_dot.name) }
...@@ -1041,35 +1112,53 @@ describe API::Commits do ...@@ -1041,35 +1112,53 @@ describe API::Commits do
it_behaves_like 'ref commit' it_behaves_like 'ref commit'
end end
end end
end
context 'when the ref has a pipeline' do context 'when authenticated', 'as a developer' do
let!(:pipeline) { project.ci_pipelines.create(source: :push, ref: 'master', sha: commit.sha, protected: false) } let(:current_user) { developer }
it 'includes a "created" status' do it_behaves_like 'ref commit'
get api(route, current_user) it_behaves_like 'ref with pipeline'
expect(response).to have_gitlab_http_status(200) context 'with private builds' do
expect(response).to match_response_schema('public_api/v4/commit/detail') before do
expect(json_response['status']).to eq('created') project.project_feature.update!(builds_access_level: ProjectFeature::PRIVATE)
expect(json_response['last_pipeline']['id']).to eq(pipeline.id)
expect(json_response['last_pipeline']['ref']).to eq(pipeline.ref)
expect(json_response['last_pipeline']['sha']).to eq(pipeline.sha)
expect(json_response['last_pipeline']['status']).to eq(pipeline.status)
end end
context 'when pipeline succeeds' do it_behaves_like 'ref with pipeline'
before do end
pipeline.update(status: 'success')
end end
it 'includes a "success" status' do context 'when authenticated', 'as a guest' do
get api(route, current_user) let(:current_user) { guest }
expect(response).to have_gitlab_http_status(200) it_behaves_like '403 response' do
expect(response).to match_response_schema('public_api/v4/commit/detail') let(:request) { get api(route, guest) }
expect(json_response['status']).to eq('success') let(:message) { '403 Forbidden' }
end
end
context 'when authenticated', 'as a non member' do
let(:current_user) { create(:user) }
it_behaves_like '403 response' do
let(:request) { get api(route, guest) }
let(:message) { '403 Forbidden' }
end end
end end
context 'when authenticated', 'as non_member and project is public' do
let(:current_user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
it_behaves_like 'ref with pipeline'
context 'with private builds' do
before do
project.project_feature.update!(builds_access_level: ProjectFeature::PRIVATE)
end
it_behaves_like 'ref with unaccessible pipeline'
end end
end end
end end
... ...
......
...@@ -447,6 +447,18 @@ describe API::Files do ...@@ -447,6 +447,18 @@ describe API::Files do
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
end end
it 'sets no-cache headers' do
url = route('.gitignore') + "/raw"
expect(Gitlab::Workhorse).to receive(:send_git_blob)
get api(url, current_user), params: params
expect(response.headers["Cache-Control"]).to include("no-store")
expect(response.headers["Cache-Control"]).to include("no-cache")
expect(response.headers["Pragma"]).to eq("no-cache")
expect(response.headers["Expires"]).to eq("Fri, 01 Jan 1990 00:00:00 GMT")
end
context 'when mandatory params are not given' do context 'when mandatory params are not given' do
it_behaves_like '400 response' do it_behaves_like '400 response' do
let(:request) { get api(route("any%2Ffile"), current_user) } let(:request) { get api(route("any%2Ffile"), current_user) }
... ...
......
...@@ -30,26 +30,40 @@ describe 'OAuth tokens' do ...@@ -30,26 +30,40 @@ describe 'OAuth tokens' do
end end
end end
context "when user is blocked" do shared_examples 'does not create an access token' do
it "does not create an access token" do let(:user) { create(:user) }
user = create(:user)
it { expect(response).to have_gitlab_http_status(401) }
end
context 'when user is blocked' do
before do
user.block user.block
request_oauth_token(user) request_oauth_token(user)
expect(response).to have_gitlab_http_status(401)
end end
include_examples 'does not create an access token'
end end
context "when user is ldap_blocked" do context 'when user is ldap_blocked' do
it "does not create an access token" do before do
user = create(:user)
user.ldap_block user.ldap_block
request_oauth_token(user) request_oauth_token(user)
end
expect(response).to have_gitlab_http_status(401) include_examples 'does not create an access token'
end end
context 'when user account is not confirmed' do
before do
user.update!(confirmed_at: nil)
request_oauth_token(user)
end
include_examples 'does not create an access token'
end end
end end
end end
...@@ -10,6 +10,10 @@ describe Projects::ImportExport::ExportService do ...@@ -10,6 +10,10 @@ describe Projects::ImportExport::ExportService do
let(:service) { described_class.new(project, user) } let(:service) { described_class.new(project, user) }
let!(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new } let!(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new }
before do
project.add_maintainer(user)
end
it 'saves the version' do it 'saves the version' do
expect(Gitlab::ImportExport::VersionSaver).to receive(:new).and_call_original expect(Gitlab::ImportExport::VersionSaver).to receive(:new).and_call_original
...@@ -133,5 +137,18 @@ describe Projects::ImportExport::ExportService do ...@@ -133,5 +137,18 @@ describe Projects::ImportExport::ExportService do
expect(service).not_to receive(:execute_after_export_action) expect(service).not_to receive(:execute_after_export_action)
end end
end end
context 'when user does not have admin_project permission' do
let!(:another_user) { create(:user) }
subject(:service) { described_class.new(project, another_user) }
it 'fails' do
expected_message =
"User with ID: %s does not have permission to Project %s with ID: %s." %
[another_user.id, project.name, project.id]
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error).with_message(expected_message)
end
end
end end
end end