Commit 0675d91b authored by Thong Kuah's avatar Thong Kuah

Merge branch 'mattkasa-downloadable-artifacts' into 'master'

Add artifacts:public boolean

See merge request gitlab-org/gitlab!49775
parents 8ef35df0 cfaea034
...@@ -146,6 +146,12 @@ module Ci ...@@ -146,6 +146,12 @@ module Ci
.includes(:metadata, :job_artifacts_metadata) .includes(:metadata, :job_artifacts_metadata)
end 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_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 :with_expired_artifacts, -> { with_downloadable_artifacts.where('artifacts_expire_at < ?', Time.current) }
scope :last_month, -> { where('created_at > ?', Date.today - 1.month) } scope :last_month, -> { where('created_at > ?', Date.today - 1.month) }
...@@ -741,6 +747,16 @@ module Ci ...@@ -741,6 +747,16 @@ module Ci
artifacts_metadata? artifacts_metadata?
end 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) def artifacts_metadata_entry(path, **options)
artifacts_metadata.open do |metadata_stream| artifacts_metadata.open do |metadata_stream|
metadata = Gitlab::Ci::Build::Artifacts::Metadata.new( metadata = Gitlab::Ci::Build::Artifacts::Metadata.new(
......
...@@ -133,6 +133,12 @@ module Ci ...@@ -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_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 :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 scope :with_file_types, -> (file_types) do
types = self.file_types.select { |file_type| file_types.include?(file_type) }.values types = self.file_types.select { |file_type| file_types.include?(file_type) }.values
......
...@@ -68,8 +68,8 @@ module Ci ...@@ -68,8 +68,8 @@ module Ci
has_many :variables, class_name: 'Ci::PipelineVariable' has_many :variables, class_name: 'Ci::PipelineVariable'
has_many :deployments, through: :builds has_many :deployments, through: :builds
has_many :environments, -> { distinct }, through: :deployments has_many :environments, -> { distinct }, through: :deployments
has_many :latest_builds, -> { latest }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build' 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 }, through: :latest_builds, source: :job_artifacts 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 has_many :messages, class_name: 'Ci::PipelineMessage', inverse_of: :pipeline
......
...@@ -37,6 +37,10 @@ module Ci ...@@ -37,6 +37,10 @@ module Ci
@subject.archived? @subject.archived?
end end
condition(:artifacts_public, scope: :subject) do
@subject.artifacts_public?
end
condition(:terminal, scope: :subject) do condition(:terminal, scope: :subject) do
@subject.has_terminal? @subject.has_terminal?
end end
...@@ -57,6 +61,10 @@ module Ci ...@@ -57,6 +61,10 @@ module Ci
can?(:update_build, @subject.project) can?(:update_build, @subject.project)
end end
condition(:project_developer) do
can?(:developer_access, @subject.project)
end
rule { project_read_build }.enable :read_build_trace rule { project_read_build }.enable :read_build_trace
rule { debug_mode & ~project_update_build }.prevent :read_build_trace rule { debug_mode & ~project_update_build }.prevent :read_build_trace
...@@ -94,6 +102,9 @@ module Ci ...@@ -94,6 +102,9 @@ module Ci
rule { ~can?(:build_service_proxy_enabled) }.policy do rule { ~can?(:build_service_proxy_enabled) }.policy do
prevent :create_build_service_proxy prevent :create_build_service_proxy
end end
rule { project_read_build }.enable :read_job_artifacts
rule { ~artifacts_public & ~project_developer }.prevent :read_job_artifacts
end end
end end
......
...@@ -26,7 +26,7 @@ class BuildDetailsEntity < JobEntity ...@@ -26,7 +26,7 @@ class BuildDetailsEntity < JobEntity
DeploymentClusterEntity.represent(build.deployment, options) DeploymentClusterEntity.represent(build.deployment, options)
end 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| expose :download_path, if: -> (*) { build.locked_artifacts? || build.artifacts? } do |build|
download_project_job_artifacts_path(project, build) download_project_job_artifacts_path(project, build)
end end
......
...@@ -11,6 +11,10 @@ class PipelineDetailsEntity < PipelineEntity ...@@ -11,6 +11,10 @@ class PipelineDetailsEntity < PipelineEntity
expose :artifacts do |pipeline, options| expose :artifacts do |pipeline, options|
rel = pipeline.downloadable_artifacts 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) BuildArtifactEntity.represent(rel, options)
end end
expose :manual_actions, using: BuildActionEntity expose :manual_actions, using: BuildActionEntity
......
...@@ -98,7 +98,7 @@ ...@@ -98,7 +98,7 @@
%td %td
.gl-display-flex .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 = 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') = sprite_icon('download')
- if can?(current_user, :update_build, job) - if can?(current_user, :update_build, job)
......
---
title: Add artifacts:public boolean
merge_request: 49775
author:
type: added
---
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 ...@@ -275,6 +275,10 @@ module API
authorize! :read_build_trace, build authorize! :read_build_trace, build
end end
def authorize_read_job_artifacts!(build)
authorize! :read_job_artifacts, build
end
def authorize_destroy_artifacts! def authorize_destroy_artifacts!
authorize! :destroy_artifacts, user_project authorize! :destroy_artifacts, user_project
end end
......
...@@ -32,6 +32,7 @@ module API ...@@ -32,6 +32,7 @@ module API
authorize_download_artifacts! authorize_download_artifacts!
latest_build = user_project.latest_successful_build_for_ref!(params[:job], params[:ref_name]) 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) present_carrierwave_file!(latest_build.artifacts_file)
end end
...@@ -50,6 +51,7 @@ module API ...@@ -50,6 +51,7 @@ module API
authorize_download_artifacts! authorize_download_artifacts!
build = user_project.latest_successful_build_for_ref!(params[:job], params[:ref_name]) build = user_project.latest_successful_build_for_ref!(params[:job], params[:ref_name])
authorize_read_job_artifacts!(build)
path = Gitlab::Ci::Build::Artifacts::Path path = Gitlab::Ci::Build::Artifacts::Path
.new(params[:artifact_path]) .new(params[:artifact_path])
...@@ -70,6 +72,7 @@ module API ...@@ -70,6 +72,7 @@ module API
authorize_download_artifacts! authorize_download_artifacts!
build = find_build!(params[:job_id]) build = find_build!(params[:job_id])
authorize_read_job_artifacts!(build)
present_carrierwave_file!(build.artifacts_file) present_carrierwave_file!(build.artifacts_file)
end end
...@@ -82,9 +85,11 @@ module API ...@@ -82,9 +85,11 @@ module API
requires :artifact_path, type: String, desc: 'Artifact path' requires :artifact_path, type: String, desc: 'Artifact path'
end end
get ':id/jobs/:job_id/artifacts/*artifact_path', format: false do get ':id/jobs/:job_id/artifacts/*artifact_path', format: false do
authorize_read_builds! authorize_download_artifacts!
build = find_build!(params[:job_id]) build = find_build!(params[:job_id])
authorize_read_job_artifacts!(build)
not_found! unless build.artifacts? not_found! unless build.artifacts?
path = Gitlab::Ci::Build::Artifacts::Path path = Gitlab::Ci::Build::Artifacts::Path
......
...@@ -12,7 +12,7 @@ module Gitlab ...@@ -12,7 +12,7 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable 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_REGEX = /\A\w[-\w ]*\z/.freeze
EXPOSE_AS_ERROR_MESSAGE = "can contain only letters, digits, '-', '_' and spaces" EXPOSE_AS_ERROR_MESSAGE = "can contain only letters, digits, '-', '_' and spaces"
...@@ -27,6 +27,7 @@ module Gitlab ...@@ -27,6 +27,7 @@ module Gitlab
with_options allow_nil: true do with_options allow_nil: true do
validates :name, type: String validates :name, type: String
validates :public, boolean: true
validates :untracked, boolean: true validates :untracked, boolean: true
validates :paths, array_of_strings: true validates :paths, array_of_strings: true
validates :paths, array_of_strings: { validates :paths, array_of_strings: {
......
...@@ -486,6 +486,14 @@ FactoryBot.define do ...@@ -486,6 +486,14 @@ FactoryBot.define do
end end
end end
trait :non_public_artifacts do
options do
{
artifacts: { public: false }
}
end
end
trait :non_playable do trait :non_playable do
status { 'created' } status { 'created' }
self.when { 'manual' } self.when { 'manual' }
......
...@@ -36,6 +36,14 @@ RSpec.describe Gitlab::Ci::Config::Entry::Artifacts do ...@@ -36,6 +36,14 @@ RSpec.describe Gitlab::Ci::Config::Entry::Artifacts do
expect(entry.value).to eq config expect(entry.value).to eq config
end end
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 end
context 'when entry value is not correct' do context 'when entry value is not correct' do
...@@ -67,6 +75,15 @@ RSpec.describe Gitlab::Ci::Config::Entry::Artifacts do ...@@ -67,6 +75,15 @@ RSpec.describe Gitlab::Ci::Config::Entry::Artifacts do
end end
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 context "when 'expose_as' is not a string" do
let(:config) { { paths: %w[results.txt], expose_as: 1 } } let(:config) { { paths: %w[results.txt], expose_as: 1 } }
......
...@@ -717,6 +717,22 @@ RSpec.describe Ci::Build do ...@@ -717,6 +717,22 @@ RSpec.describe Ci::Build do
end end
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 describe '#artifacts_expired?' do
subject { build.artifacts_expired? } subject { build.artifacts_expired? }
......
...@@ -260,6 +260,36 @@ RSpec.describe API::Jobs do ...@@ -260,6 +260,36 @@ RSpec.describe API::Jobs do
end end
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 context 'when project is public with builds access disabled' do
it 'rejects access to artifacts' do it 'rejects access to artifacts' do
project.update_column(:visibility_level, project.update_column(:visibility_level,
...@@ -396,6 +426,33 @@ RSpec.describe API::Jobs do ...@@ -396,6 +426,33 @@ RSpec.describe API::Jobs do
end end
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 it 'does not return job artifacts if not uploaded' do
get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
...@@ -580,6 +637,33 @@ RSpec.describe API::Jobs do ...@@ -580,6 +637,33 @@ RSpec.describe API::Jobs do
end end
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 context 'when project is private' do
let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE } let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
let(:public_builds) { true } let(:public_builds) { true }
......
...@@ -225,5 +225,36 @@ RSpec.describe BuildDetailsEntity do ...@@ -225,5 +225,36 @@ RSpec.describe BuildDetailsEntity do
expect(subject[:artifact].keys).to include(:download_path, :browse_path, :keep_path, :expire_at, :expired, :locked) expect(subject[:artifact].keys).to include(:download_path, :browse_path, :keep_path, :expire_at, :expired, :locked)
end end
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
end end
...@@ -183,5 +183,26 @@ RSpec.describe PipelineDetailsEntity do ...@@ -183,5 +183,26 @@ RSpec.describe PipelineDetailsEntity do
expect(source_jobs[child_pipeline.id][:name]).to eq('child') expect(source_jobs[child_pipeline.id][:name]).to eq('child')
end end
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
end end
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