summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZ.J. van de Weg <git@zjvandeweg.nl>2017-06-06 09:54:51 +0200
committerZ.J. van de Weg <git@zjvandeweg.nl>2017-06-06 09:54:51 +0200
commita28fa4b6d81ae1d74d313f8a04a60ccb2390e72a (patch)
tree6d8a993e5dbe4b92b8fbcb09d6752eb0f44a0ec4
parentc34107608ecc5c36e80a748eb4c9b88d2b1157cf (diff)
downloadgitlab-ce-zj-api-artifacts-ci-token-access.tar.gz
Allow artifacts downloads with gitlab-ci-tokenzj-api-artifacts-ci-token-access
By extracting the authentication code using gitlab-ci-tokens, this is now used when one wants to download the artifacts only authenticating using the gitlab_ci_token.
-rw-r--r--app/services/auth/job_token_authentication_service.rb18
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/artifacts.rb85
-rw-r--r--lib/api/jobs.rb50
-rw-r--r--lib/gitlab/auth.rb19
-rw-r--r--lib/gitlab/auth/result.rb11
-rw-r--r--spec/requests/api/artifacts_spec.rb180
-rw-r--r--spec/requests/api/jobs_spec.rb33
8 files changed, 298 insertions, 99 deletions
diff --git a/app/services/auth/job_token_authentication_service.rb b/app/services/auth/job_token_authentication_service.rb
new file mode 100644
index 00000000000..55f3d647fc6
--- /dev/null
+++ b/app/services/auth/job_token_authentication_service.rb
@@ -0,0 +1,18 @@
+module Auth
+ class JobTokenAuthenticationService
+ def execute(login = 'gitlab-ci-token', password)
+ return unless login == 'gitlab-ci-token' && password.present?
+
+ build = ::Ci::Build.running.find_by_token(password)
+ return unless build&.project&.builds_enabled?
+
+ if build.user
+ # If user is assigned to build, use restricted credentials of user
+ Gitlab::Auth::Result.new(build.user, build.project, :build)
+ else
+ # Otherwise use generic CI credentials (backward compatibility)
+ Gitlab::Auth::Result.new(nil, build.project, :ci)
+ end
+ end
+ end
+end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 7ae2f3cad40..2169b939f22 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -85,6 +85,7 @@ module API
# Keep in alphabetical order
mount ::API::AccessRequests
+ mount ::API::Artifacts
mount ::API::AwardEmoji
mount ::API::Boards
mount ::API::Branches
diff --git a/lib/api/artifacts.rb b/lib/api/artifacts.rb
new file mode 100644
index 00000000000..dfb4751dbcd
--- /dev/null
+++ b/lib/api/artifacts.rb
@@ -0,0 +1,85 @@
+module API
+ class Artifacts < Grape::API
+ include PaginationParams
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc 'Download the artifacts file from a job' do
+ detail 'This feature was introduced in GitLab 8.5'
+ end
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a job'
+ end
+ get ':id/jobs/:job_id/artifacts' do
+ load_gitlab_ci_token_user!
+ authorize_read_builds!
+
+ build = get_build!(params[:job_id])
+
+ present_artifacts!(build.artifacts_file)
+ end
+
+ desc 'Download the artifacts file from a job' do
+ detail 'This feature was introduced in GitLab 8.10'
+ end
+ params do
+ requires :ref_name, type: String, desc: 'The ref from repository'
+ requires :job, type: String, desc: 'The name for the job'
+ end
+ get ':id/jobs/artifacts/:ref_name/download',
+ requirements: { ref_name: /.+/ } do
+ authenticate!
+ authorize_read_builds!
+
+ builds = user_project.latest_successful_builds_for(params[:ref_name])
+ latest_build = builds.find_by!(name: params[:job])
+
+ present_artifacts!(latest_build.artifacts_file)
+ end
+
+ desc 'Keep the artifacts to prevent them from being deleted' do
+ success Entities::Job
+ end
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a job'
+ end
+ post ':id/jobs/:job_id/artifacts/keep' do
+ authenticate!
+ authorize! :update_build, user_project
+
+ build = get_build!(params[:job_id])
+ authorize!(:update_build, build)
+ return not_found!(build) unless build.artifacts?
+
+ build.keep_artifacts!
+
+ status 200
+ present build, with: Entities::Job
+ end
+ end
+
+ helpers do
+ def find_build(id)
+ user_project.builds.find_by(id: id)
+ end
+
+ def get_build!(id)
+ find_build(id) || not_found!
+ end
+
+ def authorize_read_builds!
+ authorize! :read_build, user_project
+ end
+
+ def load_gitlab_ci_token_user!
+ @current_user ||=
+ begin
+ result = ::Auth::JobTokenAuthenticationService.new.execute(params[:gitlab_ci_token])
+ result&.actor
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb
index 8a67de10bca..9cdd0168a98 100644
--- a/lib/api/jobs.rb
+++ b/lib/api/jobs.rb
@@ -71,37 +71,6 @@ module API
present build, with: Entities::Job
end
- desc 'Download the artifacts file from a job' do
- detail 'This feature was introduced in GitLab 8.5'
- end
- params do
- requires :job_id, type: Integer, desc: 'The ID of a job'
- end
- get ':id/jobs/:job_id/artifacts' do
- authorize_read_builds!
-
- build = get_build!(params[:job_id])
-
- present_artifacts!(build.artifacts_file)
- end
-
- desc 'Download the artifacts file from a job' do
- detail 'This feature was introduced in GitLab 8.10'
- end
- params do
- requires :ref_name, type: String, desc: 'The ref from repository'
- requires :job, type: String, desc: 'The name for the job'
- end
- get ':id/jobs/artifacts/:ref_name/download',
- requirements: { ref_name: /.+/ } do
- authorize_read_builds!
-
- builds = user_project.latest_successful_builds_for(params[:ref_name])
- latest_build = builds.find_by!(name: params[:job])
-
- present_artifacts!(latest_build.artifacts_file)
- end
-
# TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace
# is saved in the DB instead of file). But before that, we need to consider how to replace the value of
# `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse.
@@ -174,25 +143,6 @@ module API
present build, with: Entities::Job
end
- desc 'Keep the artifacts to prevent them from being deleted' do
- success Entities::Job
- end
- params do
- requires :job_id, type: Integer, desc: 'The ID of a job'
- end
- post ':id/jobs/:job_id/artifacts/keep' do
- authorize_update_builds!
-
- build = get_build!(params[:job_id])
- authorize!(:update_build, build)
- return not_found!(build) unless build.artifacts?
-
- build.keep_artifacts!
-
- status 200
- present build, with: Entities::Job
- end
-
desc 'Trigger a manual job' do
success Entities::Job
detail 'This feature was added in GitLab 8.11'
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 099c45dcfb7..e31d2567b2e 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -23,7 +23,7 @@ module Gitlab
# is enabled.
result =
service_request_check(login, password, project) ||
- build_access_token_check(login, password) ||
+ job_access_token_check(login, password) ||
lfs_token_check(login, password) ||
oauth_access_token_check(login, password) ||
user_with_password_for_git(login, password) ||
@@ -160,20 +160,9 @@ module Gitlab
end
end
- def build_access_token_check(login, password)
- return unless login == 'gitlab-ci-token'
- return unless password
-
- build = ::Ci::Build.running.find_by_token(password)
- return unless build
- return unless build.project.builds_enabled?
-
- if build.user
- # If user is assigned to build, use restricted credentials of user
- Gitlab::Auth::Result.new(build.user, build.project, :build, build_authentication_abilities)
- else
- # Otherwise use generic CI credentials (backward compatibility)
- Gitlab::Auth::Result.new(nil, build.project, :ci, build_authentication_abilities)
+ def job_access_token_check(login, password)
+ ::Auth::JobTokenAuthenticationService.new.execute(login, password)&.tap do |result|
+ authentication_abilities = build_authentication_abilities
end
end
diff --git a/lib/gitlab/auth/result.rb b/lib/gitlab/auth/result.rb
index 39b86c61a18..165a48498ab 100644
--- a/lib/gitlab/auth/result.rb
+++ b/lib/gitlab/auth/result.rb
@@ -1,6 +1,15 @@
module Gitlab
module Auth
- Result = Struct.new(:actor, :project, :type, :authentication_abilities) do
+ class Result
+ attr_accessor :actor, :project, :type, :authentication_abilities
+
+ def initialize(actor = nil, project = nil, type = nil, abilities = [])
+ @actor = actor
+ @project = project
+ @type = type
+ @authentication_abilities = abilities
+ end
+
def ci?(for_project)
type == :ci &&
project &&
diff --git a/spec/requests/api/artifacts_spec.rb b/spec/requests/api/artifacts_spec.rb
new file mode 100644
index 00000000000..6de57464e7f
--- /dev/null
+++ b/spec/requests/api/artifacts_spec.rb
@@ -0,0 +1,180 @@
+require 'spec_helper'
+
+describe API::Artifacts, :api do
+ set(:project) { create(:project, :repository, public_builds: false) }
+
+ set(:pipeline) do
+ create(:ci_empty_pipeline, project: project,
+ sha: project.commit.id,
+ ref: project.default_branch)
+ end
+
+ set(:user) { create(:user) }
+
+ let(:reporter) { create(:project_member, :reporter, project: project).user }
+ let(:guest) { create(:project_member, :guest, project: project).user }
+
+
+ before do
+ project.add_developer(user)
+ end
+
+ describe 'GET /projects/:id/jobs/:job_id/artifacts' do
+ let(:build) do
+ create(:ci_build,
+ :artifacts,
+ :running,
+ pipeline: pipeline,
+ project: project,
+ user: user)
+ end
+
+ context 'job with artifacts' do
+ let(:download_headers) do
+ { 'Content-Transfer-Encoding' => 'binary',
+ 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
+ end
+
+ context 'authorized user' do
+ before do
+ get api("/projects/#{project.id}/jobs/#{build.id}/artifacts", user)
+ end
+
+ it 'returns specific job artifacts' do
+ expect(response).to have_http_status(200)
+ expect(response.headers).to include(download_headers)
+ expect(response.body).to match_file(build.artifacts_file.file.file)
+ end
+ end
+
+ context 'authenticating with a gitlab-ci token' do
+ it 'returns the specific job artifacts' do
+ get api("/projects/#{project.id}/jobs/#{build.id}/artifacts"), gitlab_ci_token: build.token
+
+ expect(response).to have_http_status(200)
+ expect(response.headers).to include(download_headers)
+ expect(response.body).to match_file(build.artifacts_file.file.file)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not return specific job artifacts' do
+ get api("/projects/#{project.id}/jobs/#{build.id}/artifacts")
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+ before do
+ build.success
+ end
+
+ def get_for_ref(ref = pipeline.ref, job = build.name)
+ get api("/projects/#{project.id}/jobs/artifacts/#{ref}/download", user), job: job
+ end
+
+ context 'when not logged in' do
+ let(:user) { nil }
+
+ before do
+ get_for_ref
+ end
+
+ it 'gives 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'non-existing job' do
+ shared_examples 'not found' do
+ it { expect(response).to have_http_status(:not_found) }
+ end
+
+ context 'has no such ref' do
+ before do
+ get_for_ref('TAIL')
+ end
+
+ it_behaves_like 'not found'
+ end
+
+ context 'has no such job' do
+ before do
+ get_for_ref(pipeline.ref, 'NOBUILD')
+ end
+
+ it_behaves_like 'not found'
+ end
+ end
+
+ context 'find proper job' do
+ shared_examples 'a valid file' do
+ let(:download_headers) do
+ { 'Content-Transfer-Encoding' => 'binary',
+ 'Content-Disposition' =>
+ "attachment; filename=#{build.artifacts_file.filename}" }
+ end
+
+ it { expect(response).to have_http_status(200) }
+ it { expect(response.headers).to include(download_headers) }
+ end
+
+ context 'with regular branch' do
+ before do
+ pipeline.update(ref: 'master',
+ sha: project.commit('master').sha)
+
+ get_for_ref('master')
+ end
+
+ it_behaves_like 'a valid file'
+ end
+
+ context 'with branch name containing slash' do
+ before do
+ pipeline.reload
+ pipeline.update(ref: 'improve/awesome',
+ sha: project.commit('improve/awesome').sha)
+ end
+
+ before do
+ get_for_ref('improve/awesome')
+ end
+
+ it_behaves_like 'a valid file'
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/jobs/:build_id/artifacts/keep' do
+ before do
+ post api("/projects/#{project.id}/jobs/#{build.id}/artifacts/keep", user)
+ end
+
+ context 'artifacts did not expire' do
+ let(:build) do
+ create(:ci_build, :trace, :artifacts, :success,
+ project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days)
+ end
+
+ it 'keeps artifacts' do
+ expect(response).to have_http_status(200)
+ expect(build.reload.artifacts_expire_at).to be_nil
+ end
+ end
+
+ context 'no artifacts' do
+ let(:build) { create(:ci_build, project: project, pipeline: pipeline) }
+
+ it 'responds with not found' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
+
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index e5e5872dc1f..ff84e1f9b6c 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -187,39 +187,6 @@ describe API::Jobs, :api do
end
end
- describe 'GET /projects/:id/jobs/:job_id/artifacts' do
- before do
- get api("/projects/#{project.id}/jobs/#{build.id}/artifacts", api_user)
- end
-
- context 'job with artifacts' do
- let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
-
- context 'authorized user' do
- let(:download_headers) do
- { 'Content-Transfer-Encoding' => 'binary',
- 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
- end
-
- it 'returns specific job artifacts' do
- expect(response).to have_http_status(200)
- expect(response.headers).to include(download_headers)
- expect(response.body).to match_file(build.artifacts_file.file.file)
- end
- end
-
- context 'unauthorized user' do
- let(:api_user) { nil }
-
- it 'does not return specific job artifacts' do
- expect(response).to have_http_status(401)
- end
- end
- end
-
- it 'does not return job artifacts if not uploaded' do
- expect(response).to have_http_status(404)
- end
end
describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do