diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 75e590f3f33815d806058c6d92f4af3c48fab54e..f2f63e986bb9ba8b9d6df462d3d99e93e77b2335 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -99,7 +99,9 @@ module Projects def define_triggers_variables @triggers = @project.triggers + .present(current_user: current_user) @trigger = ::Ci::Trigger.new + .present(current_user: current_user) end def define_badges_variables diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb index f5fdfb8accca98d86128aa40d86b8eb25ae05608..c7b4ebb2b248631cf89c0c8714f9a90fb1ec8f94 100644 --- a/app/controllers/projects/triggers_controller.rb +++ b/app/controllers/projects/triggers_controller.rb @@ -66,12 +66,11 @@ class Projects::TriggersController < Projects::ApplicationController end def trigger - @trigger ||= project.triggers.find(params[:id]) || render_404 + @trigger ||= project.triggers.find(params[:id]) + .present(current_user: current_user) end def trigger_params - params.require(:trigger).permit( - :description - ) + params.require(:trigger).permit(:description) end end diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index 55db42162ca0cd82b5a56543548b8b0976797d05..637148c4ce47c94021883ffc2773fee97148eea6 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -4,6 +4,7 @@ module Ci class Trigger < ActiveRecord::Base extend Gitlab::Ci::Model include IgnorableColumn + include Presentable ignore_column :deleted_at @@ -29,7 +30,7 @@ module Ci end def short_token - token[0...4] + token[0...4] if token.present? end def legacy? diff --git a/app/presenters/ci/trigger_presenter.rb b/app/presenters/ci/trigger_presenter.rb new file mode 100644 index 0000000000000000000000000000000000000000..605c8f328a47a4f075314d0ceaca9c899974cbc8 --- /dev/null +++ b/app/presenters/ci/trigger_presenter.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Ci + class TriggerPresenter < Gitlab::View::Presenter::Delegated + presents :trigger + + def has_token_exposed? + can?(current_user, :admin_trigger, trigger) + end + + def token + if has_token_exposed? + trigger.token + else + trigger.short_token + end + end + end +end diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml index 7e4618e1a88444c011758cd5b67282469473445d..6f6f1e5e0c5e01f5cab636437ff8ba745c1648d0 100644 --- a/app/views/projects/triggers/_trigger.html.haml +++ b/app/views/projects/triggers/_trigger.html.haml @@ -1,6 +1,6 @@ %tr %td - - if can?(current_user, :admin_trigger, trigger) + - if trigger.has_token_exposed? %span= trigger.token = clipboard_button(text: trigger.token, title: "Copy trigger token to clipboard") - else diff --git a/changelogs/unreleased/security-pipeline-trigger-tokens-exposure.yml b/changelogs/unreleased/security-pipeline-trigger-tokens-exposure.yml new file mode 100644 index 0000000000000000000000000000000000000000..97d743eead13482cb8fd63ec73aadbc0e7349da2 --- /dev/null +++ b/changelogs/unreleased/security-pipeline-trigger-tokens-exposure.yml @@ -0,0 +1,5 @@ +--- +title: Expose CI/CD trigger token only to the trigger owner +merge_request: +author: +type: security diff --git a/lib/api/entities.rb b/lib/api/entities.rb index e0a48908122d5fc02cb5697b4a2aae7449053c89..622783603292da5eacaed9a960dcb1dfc144be61 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1223,8 +1223,11 @@ module API end class Trigger < Grape::Entity + include ::API::Helpers::Presentable + expose :id - expose :token, :description + expose :token + expose :description expose :created_at, :updated_at, :last_used expose :owner, using: Entities::UserBasic end diff --git a/lib/api/helpers/presentable.rb b/lib/api/helpers/presentable.rb new file mode 100644 index 0000000000000000000000000000000000000000..973c2132efeb42c062fd7ac4340b5b5007d9c7bf --- /dev/null +++ b/lib/api/helpers/presentable.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module API + module Helpers + ## + # This module makes it possible to use `app/presenters` with + # Grape Entities. It instantiates model presenter and passes + # options defined in the API endpoint to the presenter itself. + # + # present object, with: Entities::Something, + # current_user: current_user, + # another_option: 'my options' + # + # Example above will make `current_user` and `another_option` + # values available in the subclass of `Gitlab::View::Presenter` + # thorough a separate method in the presenter. + # + # The model class needs to have `::Presentable` module mixed in + # if you want to use `API::Helpers::Presentable`. + # + module Presentable + extend ActiveSupport::Concern + + def initialize(object, options = {}) + super(object.present(options), options) + end + end + end +end diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index 3ce1529f2596bf7e6f47cdf942263fbda02e02ac..b67f056f4919697fa95f66cbaa1f9be3a54838f4 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -51,7 +51,7 @@ module API triggers = user_project.triggers.includes(:trigger_requests) - present paginate(triggers), with: Entities::Trigger + present paginate(triggers), with: Entities::Trigger, current_user: current_user end # rubocop: enable CodeReuse/ActiveRecord @@ -68,7 +68,7 @@ module API trigger = user_project.triggers.find(params.delete(:trigger_id)) break not_found!('Trigger') unless trigger - present trigger, with: Entities::Trigger + present trigger, with: Entities::Trigger, current_user: current_user end desc 'Create a trigger' do @@ -85,7 +85,7 @@ module API declared_params(include_missing: false).merge(owner: current_user)) if trigger.valid? - present trigger, with: Entities::Trigger + present trigger, with: Entities::Trigger, current_user: current_user else render_validation_error!(trigger) end @@ -106,7 +106,7 @@ module API break not_found!('Trigger') unless trigger if trigger.update(declared_params(include_missing: false)) - present trigger, with: Entities::Trigger + present trigger, with: Entities::Trigger, current_user: current_user else render_validation_error!(trigger) end @@ -127,7 +127,7 @@ module API if trigger.update(owner: current_user) status :ok - present trigger, with: Entities::Trigger + present trigger, with: Entities::Trigger, current_user: current_user else render_validation_error!(trigger) end diff --git a/spec/presenters/ci/trigger_presenter_spec.rb b/spec/presenters/ci/trigger_presenter_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..231b539c188feb2771fc80908e608fc5f13e7de1 --- /dev/null +++ b/spec/presenters/ci/trigger_presenter_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe Ci::TriggerPresenter do + set(:user) { create(:user) } + set(:project) { create(:project) } + + set(:trigger) do + create(:ci_trigger, token: '123456789abcd', project: project) + end + + subject do + described_class.new(trigger, current_user: user) + end + + before do + project.add_maintainer(user) + end + + context 'when user is not a trigger owner' do + describe '#token' do + it 'exposes only short token' do + expect(subject.token).not_to eq trigger.token + expect(subject.token).to eq '1234' + end + end + + describe '#has_token_exposed?' do + it 'does not have token exposed' do + expect(subject).not_to have_token_exposed + end + end + end + + context 'when user is a trigger owner and builds admin' do + before do + trigger.update(owner: user) + end + + describe '#token' do + it 'exposes full token' do + expect(subject.token).to eq trigger.token + end + end + + describe '#has_token_exposed?' do + it 'has token exposed' do + expect(subject).to have_token_exposed + end + end + end +end diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 15dc901d06ee6d0fd18270ab974fd12d66d1e28a..f0f01e97f1d69b1414a77d1af91e479ffd231b32 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -1,8 +1,9 @@ require 'spec_helper' describe API::Triggers do - let(:user) { create(:user) } - let(:user2) { create(:user) } + set(:user) { create(:user) } + set(:user2) { create(:user) } + let!(:trigger_token) { 'secure_token' } let!(:trigger_token_2) { 'secure_token_2' } let!(:project) { create(:project, :repository, creator: user) } @@ -132,14 +133,17 @@ describe API::Triggers do end describe 'GET /projects/:id/triggers' do - context 'authenticated user with valid permissions' do - it 'returns list of triggers' do + context 'authenticated user who can access triggers' do + it 'returns a list of triggers with tokens exposed correctly' do get api("/projects/#{project.id}/triggers", user) expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers + expect(json_response).to be_a(Array) - expect(json_response[0]).to have_key('token') + expect(json_response.size).to eq 2 + expect(json_response.dig(0, 'token')).to eq trigger_token + expect(json_response.dig(1, 'token')).to eq trigger_token_2[0..3] end end