Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ef5bb32f95
commit
b6ecd9d9d4
|
|
@ -146,6 +146,12 @@ module Ci
|
|||
.includes(:metadata, :job_artifacts_metadata)
|
||||
end
|
||||
|
||||
scope :with_project_and_metadata, -> do
|
||||
if Feature.enabled?(:non_public_artifacts, type: :development)
|
||||
joins(:metadata).includes(:project, :metadata)
|
||||
end
|
||||
end
|
||||
|
||||
scope :with_artifacts_not_expired, -> { with_downloadable_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.current) }
|
||||
scope :with_expired_artifacts, -> { with_downloadable_artifacts.where('artifacts_expire_at < ?', Time.current) }
|
||||
scope :last_month, -> { where('created_at > ?', Date.today - 1.month) }
|
||||
|
|
@ -741,6 +747,16 @@ module Ci
|
|||
artifacts_metadata?
|
||||
end
|
||||
|
||||
def artifacts_public?
|
||||
return true unless Feature.enabled?(:non_public_artifacts, type: :development)
|
||||
|
||||
artifacts_public = options.dig(:artifacts, :public)
|
||||
|
||||
return true if artifacts_public.nil? # Default artifacts:public to true
|
||||
|
||||
options.dig(:artifacts, :public)
|
||||
end
|
||||
|
||||
def artifacts_metadata_entry(path, **options)
|
||||
artifacts_metadata.open do |metadata_stream|
|
||||
metadata = Gitlab::Ci::Build::Artifacts::Metadata.new(
|
||||
|
|
|
|||
|
|
@ -133,6 +133,12 @@ module Ci
|
|||
scope :for_sha, ->(sha, project_id) { joins(job: :pipeline).where(ci_pipelines: { sha: sha, project_id: project_id }) }
|
||||
scope :for_job_name, ->(name) { joins(:job).where(ci_builds: { name: name }) }
|
||||
|
||||
scope :with_job, -> do
|
||||
if Feature.enabled?(:non_public_artifacts, type: :development)
|
||||
joins(:job).includes(:job)
|
||||
end
|
||||
end
|
||||
|
||||
scope :with_file_types, -> (file_types) do
|
||||
types = self.file_types.select { |file_type| file_types.include?(file_type) }.values
|
||||
|
||||
|
|
|
|||
|
|
@ -68,8 +68,8 @@ module Ci
|
|||
has_many :variables, class_name: 'Ci::PipelineVariable'
|
||||
has_many :deployments, through: :builds
|
||||
has_many :environments, -> { distinct }, through: :deployments
|
||||
has_many :latest_builds, -> { latest }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build'
|
||||
has_many :downloadable_artifacts, -> { not_expired.downloadable }, through: :latest_builds, source: :job_artifacts
|
||||
has_many :latest_builds, -> { latest.with_project_and_metadata }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build'
|
||||
has_many :downloadable_artifacts, -> { not_expired.downloadable.with_job }, through: :latest_builds, source: :job_artifacts
|
||||
|
||||
has_many :messages, class_name: 'Ci::PipelineMessage', inverse_of: :pipeline
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ module Ci
|
|||
@subject.archived?
|
||||
end
|
||||
|
||||
condition(:artifacts_public, scope: :subject) do
|
||||
@subject.artifacts_public?
|
||||
end
|
||||
|
||||
condition(:terminal, scope: :subject) do
|
||||
@subject.has_terminal?
|
||||
end
|
||||
|
|
@ -57,6 +61,10 @@ module Ci
|
|||
can?(:update_build, @subject.project)
|
||||
end
|
||||
|
||||
condition(:project_developer) do
|
||||
can?(:developer_access, @subject.project)
|
||||
end
|
||||
|
||||
rule { project_read_build }.enable :read_build_trace
|
||||
rule { debug_mode & ~project_update_build }.prevent :read_build_trace
|
||||
|
||||
|
|
@ -94,6 +102,9 @@ module Ci
|
|||
rule { ~can?(:build_service_proxy_enabled) }.policy do
|
||||
prevent :create_build_service_proxy
|
||||
end
|
||||
|
||||
rule { project_read_build }.enable :read_job_artifacts
|
||||
rule { ~artifacts_public & ~project_developer }.prevent :read_job_artifacts
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class BuildDetailsEntity < JobEntity
|
|||
DeploymentClusterEntity.represent(build.deployment, options)
|
||||
end
|
||||
|
||||
expose :artifact, if: -> (*) { can?(current_user, :read_build, build) } do
|
||||
expose :artifact, if: -> (*) { can?(current_user, :read_job_artifacts, build) } do
|
||||
expose :download_path, if: -> (*) { build.locked_artifacts? || build.artifacts? } do |build|
|
||||
download_project_job_artifacts_path(project, build)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ class PipelineDetailsEntity < PipelineEntity
|
|||
expose :artifacts do |pipeline, options|
|
||||
rel = pipeline.downloadable_artifacts
|
||||
|
||||
if Feature.enabled?(:non_public_artifacts, type: :development)
|
||||
rel = rel.select { |artifact| can?(request.current_user, :read_job_artifacts, artifact.job) }
|
||||
end
|
||||
|
||||
BuildArtifactEntity.represent(rel, options)
|
||||
end
|
||||
expose :manual_actions, using: BuildActionEntity
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@
|
|||
|
||||
%td
|
||||
.gl-display-flex
|
||||
- if can?(current_user, :read_build, job) && job.artifacts?
|
||||
- if can?(current_user, :read_job_artifacts, job) && job.artifacts?
|
||||
= link_to download_project_job_artifacts_path(job.project, job), rel: 'nofollow', download: '', title: _('Download artifacts'), class: 'btn btn-build gl-button btn-icon btn-svg' do
|
||||
= sprite_icon('download')
|
||||
- if can?(current_user, :update_build, job)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add artifacts:public boolean
|
||||
merge_request: 49775
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: non_public_artifacts
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49775
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/294503
|
||||
milestone: '13.8'
|
||||
type: development
|
||||
group: group::configure
|
||||
default_enabled: false
|
||||
|
|
@ -275,6 +275,10 @@ module API
|
|||
authorize! :read_build_trace, build
|
||||
end
|
||||
|
||||
def authorize_read_job_artifacts!(build)
|
||||
authorize! :read_job_artifacts, build
|
||||
end
|
||||
|
||||
def authorize_destroy_artifacts!
|
||||
authorize! :destroy_artifacts, user_project
|
||||
end
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ module API
|
|||
authorize_download_artifacts!
|
||||
|
||||
latest_build = user_project.latest_successful_build_for_ref!(params[:job], params[:ref_name])
|
||||
authorize_read_job_artifacts!(latest_build)
|
||||
|
||||
present_carrierwave_file!(latest_build.artifacts_file)
|
||||
end
|
||||
|
|
@ -50,6 +51,7 @@ module API
|
|||
authorize_download_artifacts!
|
||||
|
||||
build = user_project.latest_successful_build_for_ref!(params[:job], params[:ref_name])
|
||||
authorize_read_job_artifacts!(build)
|
||||
|
||||
path = Gitlab::Ci::Build::Artifacts::Path
|
||||
.new(params[:artifact_path])
|
||||
|
|
@ -70,6 +72,7 @@ module API
|
|||
authorize_download_artifacts!
|
||||
|
||||
build = find_build!(params[:job_id])
|
||||
authorize_read_job_artifacts!(build)
|
||||
|
||||
present_carrierwave_file!(build.artifacts_file)
|
||||
end
|
||||
|
|
@ -82,9 +85,11 @@ module API
|
|||
requires :artifact_path, type: String, desc: 'Artifact path'
|
||||
end
|
||||
get ':id/jobs/:job_id/artifacts/*artifact_path', format: false do
|
||||
authorize_read_builds!
|
||||
authorize_download_artifacts!
|
||||
|
||||
build = find_build!(params[:job_id])
|
||||
authorize_read_job_artifacts!(build)
|
||||
|
||||
not_found! unless build.artifacts?
|
||||
|
||||
path = Gitlab::Ci::Build::Artifacts::Path
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ module Gitlab
|
|||
include ::Gitlab::Config::Entry::Validatable
|
||||
include ::Gitlab::Config::Entry::Attributable
|
||||
|
||||
ALLOWED_KEYS = %i[name untracked paths reports when expire_in expose_as exclude].freeze
|
||||
ALLOWED_KEYS = %i[name untracked paths reports when expire_in expose_as exclude public].freeze
|
||||
EXPOSE_AS_REGEX = /\A\w[-\w ]*\z/.freeze
|
||||
EXPOSE_AS_ERROR_MESSAGE = "can contain only letters, digits, '-', '_' and spaces"
|
||||
|
||||
|
|
@ -27,6 +27,7 @@ module Gitlab
|
|||
|
||||
with_options allow_nil: true do
|
||||
validates :name, type: String
|
||||
validates :public, boolean: true
|
||||
validates :untracked, boolean: true
|
||||
validates :paths, array_of_strings: true
|
||||
validates :paths, array_of_strings: {
|
||||
|
|
|
|||
|
|
@ -486,6 +486,14 @@ FactoryBot.define do
|
|||
end
|
||||
end
|
||||
|
||||
trait :non_public_artifacts do
|
||||
options do
|
||||
{
|
||||
artifacts: { public: false }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
trait :non_playable do
|
||||
status { 'created' }
|
||||
self.when { 'manual' }
|
||||
|
|
|
|||
|
|
@ -36,6 +36,14 @@ RSpec.describe Gitlab::Ci::Config::Entry::Artifacts do
|
|||
expect(entry.value).to eq config
|
||||
end
|
||||
end
|
||||
|
||||
context "when value includes 'public' keyword" do
|
||||
let(:config) { { paths: %w[results.txt], public: false } }
|
||||
|
||||
it 'returns general artifact and report-type artifacts configuration' do
|
||||
expect(entry.value).to eq config
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when entry value is not correct' do
|
||||
|
|
@ -67,6 +75,15 @@ RSpec.describe Gitlab::Ci::Config::Entry::Artifacts do
|
|||
end
|
||||
end
|
||||
|
||||
context "when 'public' is not a boolean" do
|
||||
let(:config) { { paths: %w[results.txt], public: 'false' } }
|
||||
|
||||
it 'reports error' do
|
||||
expect(entry.errors)
|
||||
.to include 'artifacts public should be a boolean value'
|
||||
end
|
||||
end
|
||||
|
||||
context "when 'expose_as' is not a string" do
|
||||
let(:config) { { paths: %w[results.txt], expose_as: 1 } }
|
||||
|
||||
|
|
|
|||
|
|
@ -717,6 +717,22 @@ RSpec.describe Ci::Build do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#artifacts_public?' do
|
||||
subject { build.artifacts_public? }
|
||||
|
||||
context 'artifacts with defaults' do
|
||||
let(:build) { create(:ci_build, :artifacts) }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
context 'non public artifacts' do
|
||||
let(:build) { create(:ci_build, :artifacts, :non_public_artifacts) }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#artifacts_expired?' do
|
||||
subject { build.artifacts_expired? }
|
||||
|
||||
|
|
|
|||
|
|
@ -260,6 +260,36 @@ RSpec.describe API::Jobs do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when project is public with artifacts that are non public' do
|
||||
let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline) }
|
||||
|
||||
it 'rejects access to artifacts' do
|
||||
project.update_column(:visibility_level,
|
||||
Gitlab::VisibilityLevel::PUBLIC)
|
||||
project.update_column(:public_builds, true)
|
||||
|
||||
get_artifact_file(artifact)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
|
||||
context 'with the non_public_artifacts feature flag disabled' do
|
||||
before do
|
||||
stub_feature_flags(non_public_artifacts: false)
|
||||
end
|
||||
|
||||
it 'allows access to artifacts' do
|
||||
project.update_column(:visibility_level,
|
||||
Gitlab::VisibilityLevel::PUBLIC)
|
||||
project.update_column(:public_builds, true)
|
||||
|
||||
get_artifact_file(artifact)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project is public with builds access disabled' do
|
||||
it 'rejects access to artifacts' do
|
||||
project.update_column(:visibility_level,
|
||||
|
|
@ -396,6 +426,33 @@ RSpec.describe API::Jobs do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when public project guest and artifacts are non public' do
|
||||
let(:api_user) { guest }
|
||||
let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline) }
|
||||
|
||||
before do
|
||||
project.update_column(:visibility_level,
|
||||
Gitlab::VisibilityLevel::PUBLIC)
|
||||
project.update_column(:public_builds, true)
|
||||
get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
|
||||
end
|
||||
|
||||
it 'rejects access and hides existence of artifacts' do
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
|
||||
context 'with the non_public_artifacts feature flag disabled' do
|
||||
before do
|
||||
stub_feature_flags(non_public_artifacts: false)
|
||||
get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
|
||||
end
|
||||
|
||||
it 'allows access to artifacts' do
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not return job artifacts if not uploaded' do
|
||||
get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
|
||||
|
||||
|
|
@ -580,6 +637,33 @@ RSpec.describe API::Jobs do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when project is public with non public artifacts' do
|
||||
let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline, user: api_user) }
|
||||
let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
|
||||
let(:public_builds) { true }
|
||||
|
||||
it 'rejects access and hides existence of artifacts', :sidekiq_might_not_need_inline do
|
||||
get_artifact_file(artifact)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
expect(json_response).to have_key('message')
|
||||
expect(response.headers.to_h)
|
||||
.not_to include('Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
|
||||
end
|
||||
|
||||
context 'with the non_public_artifacts feature flag disabled' do
|
||||
before do
|
||||
stub_feature_flags(non_public_artifacts: false)
|
||||
end
|
||||
|
||||
it 'allows access to artifacts', :sidekiq_might_not_need_inline do
|
||||
get_artifact_file(artifact)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project is private' do
|
||||
let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
|
||||
let(:public_builds) { true }
|
||||
|
|
|
|||
|
|
@ -225,5 +225,36 @@ RSpec.describe BuildDetailsEntity do
|
|||
expect(subject[:artifact].keys).to include(:download_path, :browse_path, :keep_path, :expire_at, :expired, :locked)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the project is public and the user is a guest' do
|
||||
let(:project) { create(:project, :repository, :public) }
|
||||
let(:user) { create(:project_member, :guest, project: project).user }
|
||||
|
||||
context 'when the build has public archive type artifacts' do
|
||||
let(:build) { create(:ci_build, :artifacts) }
|
||||
|
||||
it 'exposes public artifact details' do
|
||||
expect(subject[:artifact].keys).to include(:download_path, :browse_path, :locked)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the build has non public archive type artifacts' do
|
||||
let(:build) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline) }
|
||||
|
||||
it 'does not expose non public artifacts' do
|
||||
expect(subject.keys).not_to include(:artifact)
|
||||
end
|
||||
|
||||
context 'with the non_public_artifacts feature flag disabled' do
|
||||
before do
|
||||
stub_feature_flags(non_public_artifacts: false)
|
||||
end
|
||||
|
||||
it 'exposes artifact details' do
|
||||
expect(subject[:artifact].keys).to include(:download_path, :browse_path, :locked)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -183,5 +183,26 @@ RSpec.describe PipelineDetailsEntity do
|
|||
expect(source_jobs[child_pipeline.id][:name]).to eq('child')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a pipeline belongs to a public project' do
|
||||
let(:project) { create(:project, :public) }
|
||||
let(:pipeline) { create(:ci_empty_pipeline, status: :success, project: project) }
|
||||
|
||||
context 'that has artifacts' do
|
||||
let!(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
|
||||
|
||||
it 'contains information about artifacts' do
|
||||
expect(subject[:details][:artifacts].length).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'that has non public artifacts' do
|
||||
let!(:build) { create(:ci_build, :success, :artifacts, :non_public_artifacts, pipeline: pipeline) }
|
||||
|
||||
it 'does not contain information about artifacts' do
|
||||
expect(subject[:details][:artifacts].length).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue