diff options
author | Z.J. van de Weg <git@zjvandeweg.nl> | 2017-06-06 09:54:51 +0200 |
---|---|---|
committer | Z.J. van de Weg <git@zjvandeweg.nl> | 2017-06-06 09:54:51 +0200 |
commit | a28fa4b6d81ae1d74d313f8a04a60ccb2390e72a (patch) | |
tree | 6d8a993e5dbe4b92b8fbcb09d6752eb0f44a0ec4 | |
parent | c34107608ecc5c36e80a748eb4c9b88d2b1157cf (diff) | |
download | gitlab-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.rb | 18 | ||||
-rw-r--r-- | lib/api/api.rb | 1 | ||||
-rw-r--r-- | lib/api/artifacts.rb | 85 | ||||
-rw-r--r-- | lib/api/jobs.rb | 50 | ||||
-rw-r--r-- | lib/gitlab/auth.rb | 19 | ||||
-rw-r--r-- | lib/gitlab/auth/result.rb | 11 | ||||
-rw-r--r-- | spec/requests/api/artifacts_spec.rb | 180 | ||||
-rw-r--r-- | spec/requests/api/jobs_spec.rb | 33 |
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 |