diff options
-rw-r--r-- | .gitleaksignore | 2 | ||||
-rw-r--r-- | app/assets/javascripts/boards/components/board_list.vue | 26 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/secret_detection.js | 4 | ||||
-rw-r--r-- | app/models/ci/build.rb | 12 | ||||
-rw-r--r-- | config/feature_flags/beta/prefix_ci_build_tokens.yml | 9 | ||||
-rw-r--r-- | config/gitleaks.toml | 2 | ||||
-rw-r--r-- | doc/api/jobs.md | 11 | ||||
-rw-r--r-- | doc/development/pipelines/index.md | 20 | ||||
-rw-r--r-- | doc/security/token_overview.md | 2 | ||||
-rw-r--r-- | lib/api/ci/pipelines.rb | 2 | ||||
-rw-r--r-- | lib/api/entities/ci/job.rb | 1 | ||||
-rw-r--r-- | spec/fixtures/api/schemas/public_api/v4/job.json | 6 | ||||
-rw-r--r-- | spec/frontend/boards/board_list_spec.js | 83 | ||||
-rw-r--r-- | spec/frontend/lib/utils/secret_detection_spec.js | 4 | ||||
-rw-r--r-- | spec/models/ci/build_spec.rb | 56 | ||||
-rw-r--r-- | spec/requests/api/ci/jobs_spec.rb | 1 |
16 files changed, 194 insertions, 47 deletions
diff --git a/.gitleaksignore b/.gitleaksignore index d52f10ebdc4..9ff1dcb3a70 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -1,3 +1,5 @@ afedb913baf4203aa688421873fdb9f94649578e:doc/api/users.md:generic-api-key:2201 spec/frontend/lib/utils/secret_detection_spec.js:generic-api-key:34 spec/frontend/lib/utils/secret_detection_spec.js:generic-api-key:35 +spec/frontend/lib/utils/secret_detection_spec.js:generic-api-key:38 +spec/frontend/lib/utils/secret_detection_spec.js:generic-api-key:39
\ No newline at end of file diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 8a5c6882e56..58c20c0da91 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -3,6 +3,7 @@ import { GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui'; import Draggable from 'vuedraggable'; import { STATUS_CLOSED } from '~/issues/constants'; import { sprintf, __, s__ } from '~/locale'; +import { ESC_KEY_CODE } from '~/lib/utils/keycodes'; import { defaultSortableOptions, DRAG_DELAY } from '~/sortable/constants'; import { sortableStart, sortableEnd } from '~/sortable/utils'; import Tracking from '~/tracking'; @@ -82,6 +83,7 @@ export default { toList: {}, addItemToListInProgress: false, updateIssueOrderInProgress: false, + dragCancelled: false, }; }, apollo: { @@ -307,6 +309,11 @@ export default { return; } + // Reset dragCancelled flag + this.dragCancelled = false; + // Attach listener to detect `ESC` key press to cancel drag. + document.addEventListener('keyup', this.handleKeyUp.bind(this)); + sortableStart(); this.track('drag_card', { label: 'board' }); }, @@ -323,6 +330,11 @@ export default { return; } + // Detach listener as soon as drag ends. + document.removeEventListener('keyup', this.handleKeyUp.bind(this)); + // Drag was cancelled, prevent reordering. + if (this.dragCancelled) return; + sortableEnd(); let newIndex = originalNewIndex; let { children } = to; @@ -375,6 +387,20 @@ export default { this.updateIssueOrderInProgress = false; }); }, + /** + * This implementation is needed to support `Esc` key press to cancel drag. + * It matches with what we already shipped in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119311 + */ + handleKeyUp(e) { + if (e.keyCode === ESC_KEY_CODE) { + this.dragCancelled = true; + // Sortable.js internally listens for `mouseup` event on document + // to register drop event, see https://github.com/SortableJS/Sortable/blob/master/src/Sortable.js#L625 + // We need to manually trigger it to simulate cancel behaviour as VueDraggable doesn't + // natively support it, see https://github.com/SortableJS/Vue.Draggable/issues/968. + document.dispatchEvent(new Event('mouseup')); + } + }, isItemInTheList(itemIid) { const items = this.toList?.[`${this.issuableType}s`]?.nodes || []; return items.some((item) => item.iid === itemIid); diff --git a/app/assets/javascripts/lib/utils/secret_detection.js b/app/assets/javascripts/lib/utils/secret_detection.js index 92edd286c76..dad4af004cc 100644 --- a/app/assets/javascripts/lib/utils/secret_detection.js +++ b/app/assets/javascripts/lib/utils/secret_detection.js @@ -36,6 +36,10 @@ export const containsSensitiveToken = (message) => { name: 'GitLab SCIM OAuth Access Token', regex: `glsoat-[0-9a-zA-Z_-]{20}`, }, + { + name: 'GitLab CI Build (Job) Token', + regex: `glcbt-[0-9a-zA-Z]{1,5}_[0-9a-zA-Z_-]{20}`, + }, ]; for (const rule of sensitiveDataPatterns) { diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index e56f3d2536c..23185548554 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -42,6 +42,8 @@ module Ci DEPLOYMENT_NAMES = %w[deploy release rollout].freeze + TOKEN_PREFIX = 'glcbt-' + has_one :pending_state, class_name: 'Ci::BuildPendingState', foreign_key: :build_id, inverse_of: :build has_one :queuing_entry, class_name: 'Ci::PendingBuild', foreign_key: :build_id, inverse_of: :build has_one :runtime_metadata, class_name: 'Ci::RunningBuild', foreign_key: :build_id, inverse_of: :build @@ -204,7 +206,7 @@ module Ci add_authentication_token_field :token, encrypted: :required, - format_with_prefix: :partition_id_prefix_in_16_bit_encode + format_with_prefix: :prefix_and_partition_for_token after_save :stick_build_if_status_changed @@ -1232,6 +1234,14 @@ module Ci def partition_id_prefix_in_16_bit_encode "#{partition_id.to_s(16)}_" end + + def prefix_and_partition_for_token + if Feature.enabled?(:prefix_ci_build_tokens, project, type: :beta) + TOKEN_PREFIX + partition_id_prefix_in_16_bit_encode + else + partition_id_prefix_in_16_bit_encode + end + end end end diff --git a/config/feature_flags/beta/prefix_ci_build_tokens.yml b/config/feature_flags/beta/prefix_ci_build_tokens.yml new file mode 100644 index 00000000000..ed0838b36bd --- /dev/null +++ b/config/feature_flags/beta/prefix_ci_build_tokens.yml @@ -0,0 +1,9 @@ +--- +name: prefix_ci_build_tokens +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/426137 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140159 +rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/production/-/issues/17299 +milestone: '16.8' +group: group::pipeline security +type: beta +default_enabled: false diff --git a/config/gitleaks.toml b/config/gitleaks.toml index 0983a34ca71..d0d140c8414 100644 --- a/config/gitleaks.toml +++ b/config/gitleaks.toml @@ -14,6 +14,8 @@ path = "/gitleaks.toml" "glpat-cgyKc1k_AsnEpmP-5fRL", "gldt-cgyKc1k_AsnEpmP-5fRL", "glsoat-cgyKc1k_AsnEpmP-5fRL", + "glcbt-FFFF_cgyKc1k_AsnEpmP-5fRL", + "glcbt-1_cgyKc1k_AsnEpmP-5fRL", # spec/frontend/lib/utils/secret_detection_spec.js "GlPat-abcdefghijklmnopqrstuvwxyz", # doc/development/sec/token_revocation_api.md diff --git a/doc/api/jobs.md b/doc/api/jobs.md index 739d5426082..886f209520d 100644 --- a/doc/api/jobs.md +++ b/doc/api/jobs.md @@ -46,6 +46,7 @@ Example of response "title": "Test the CI integration." }, "coverage": null, + "archived": false, "allow_failure": false, "created_at": "2015-12-24T15:51:21.802Z", "started_at": "2015-12-24T17:54:27.722Z", @@ -115,6 +116,7 @@ Example of response "title": "Test the CI integration." }, "coverage": null, + "archived": false, "allow_failure": false, "created_at": "2015-12-24T15:51:21.727Z", "started_at": "2015-12-24T17:54:24.729Z", @@ -209,6 +211,7 @@ Example of response "title": "Test the CI integration." }, "coverage": null, + "archived": false, "allow_failure": false, "created_at": "2015-12-24T15:51:21.727Z", "started_at": "2015-12-24T17:54:24.729Z", @@ -269,6 +272,7 @@ Example of response "title": "Test the CI integration." }, "coverage": null, + "archived": false, "allow_failure": false, "created_at": "2015-12-24T15:51:21.802Z", "started_at": "2015-12-24T17:54:27.722Z", @@ -363,6 +367,7 @@ Example of response "title": "Test the CI integration." }, "coverage": null, + "archived": false, "allow_failure": false, "created_at": "2015-12-24T15:51:21.802Z", "started_at": "2015-12-24T17:54:27.722Z", @@ -450,6 +455,7 @@ Example of response "title": "Test the CI integration." }, "coverage": null, + "archived": false, "allow_failure": false, "created_at": "2015-12-24T15:51:21.880Z", "started_at": "2015-12-24T17:54:30.733Z", @@ -600,6 +606,7 @@ Example of response "title": "Test the CI integration." }, "coverage": null, + "archived": false, "allow_failure": false, "created_at": "2015-12-24T15:51:21.880Z", "started_at": "2015-12-24T17:54:30.733Z", @@ -705,6 +712,7 @@ Example of response "title": "Test the CI integration." }, "coverage": null, + "archived": false, "allow_failure": false, "created_at": "2016-01-11T10:13:33.506Z", "started_at": "2016-01-11T10:14:09.526Z", @@ -759,6 +767,7 @@ Example of response "title": "Test the CI integration." }, "coverage": null, + "archived": false, "allow_failure": false, "created_at": "2016-01-11T10:13:33.506Z", "started_at": null, @@ -817,6 +826,7 @@ Example of response "title": "Test the CI integration." }, "coverage": null, + "archived": false, "allow_failure": false, "download_url": null, "id": 1, @@ -899,6 +909,7 @@ Example response: "title": "Test the CI integration." }, "coverage": null, + "archived": false, "allow_failure": false, "created_at": "2016-01-11T10:13:33.506Z", "started_at": null, diff --git a/doc/development/pipelines/index.md b/doc/development/pipelines/index.md index cdcaae6a35e..072fb6382b4 100644 --- a/doc/development/pipelines/index.md +++ b/doc/development/pipelines/index.md @@ -750,28 +750,29 @@ graph LR ### Backend pipeline -[Reference pipeline](https://gitlab.com/gitlab-org/gitlab/-/pipelines/433316063). +[Reference pipeline](https://gitlab.com/gitlab-org/gitlab/-/pipelines/1118782302). ```mermaid graph RL; classDef criticalPath fill:#f66; - 1-3["compile-test-assets (5.5 minutes)"]; - class 1-3 criticalPath; + 1-1["clone-gitlab-repo (1 minute)"]; + 1-3["compile-test-assets (3 minutes)"]; click 1-3 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914317&udv=0" - 1-6["setup-test-env (3.6 minutes)"]; + 1-6["setup-test-env (4 minutes)"]; + class 1-6 criticalPath; click 1-6 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914315&udv=0" - 1-14["retrieve-tests-metadata"]; + 1-14["retrieve-tests-metadata (50 seconds)"]; click 1-14 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356697&udv=0" - 1-15["detect-tests"]; + 1-15["detect-tests (1 minute)"]; click 1-15 "https://app.periscopedata.com/app/gitlab/652085/EP---Jobs-Durations?widget=10113603&udv=1005715" - 2_5-1["rspec & db jobs (24 minutes)"]; + 2_5-1["rspec & db jobs (30~50 minutes)"]; class 2_5-1 criticalPath; click 2_5-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations" - 2_5-1 --> 1-3 & 1-6 & 1-14 & 1-15; + 2_5-1 --> 1-1 & 1-3 & 1-6 & 1-14 & 1-15; - ac-1["rspec:artifact-collector (2 minutes)<br/>(workaround for 'needs' limitation)"]; + ac-1["rspec:artifact-collector (30 seconds)<br/>(workaround for 'needs' limitation)"]; class ac-1 criticalPath; ac-1 --> 2_5-1; @@ -784,7 +785,6 @@ graph RL; class 4_3-1 criticalPath; click 4_3-1 "https://app.periscopedata.com/app/gitlab/652085/EP---Jobs-Durations?widget=13446492&udv=1005715" 4_3-1 --> 3_2-1; - ``` ### Review app pipeline diff --git a/doc/security/token_overview.md b/doc/security/token_overview.md index 4498ee893a7..9cd445ed47b 100644 --- a/doc/security/token_overview.md +++ b/doc/security/token_overview.md @@ -238,7 +238,7 @@ The following tables show the prefixes for each type of token where applicable. | Deploy key | Not applicable. | | Runner registration token | Not applicable. | | Runner authentication token | `glrt-` | -| Job token | Not applicable. | +| CI/CD Job token | `glcbt-` ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/426137) in GitLab 16.8 behind a feature flag named `prefix_ci_build_tokens`. Disabled by default.) | | Trigger token | `glptt-` | | Legacy runner registration token | GR1348941 | | Feed token | `glft-` | diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb index b5123ab49dc..f369fc5e183 100644 --- a/lib/api/ci/pipelines.rb +++ b/lib/api/ci/pipelines.rb @@ -183,7 +183,7 @@ module API .new(current_user: current_user, pipeline: pipeline, params: params) .execute - builds = builds.with_preloads + builds = builds.with_preloads.preload(:metadata) # rubocop:disable CodeReuse/ActiveRecord -- preload job.archived? present paginate(builds), with: Entities::Ci::Job end diff --git a/lib/api/entities/ci/job.rb b/lib/api/entities/ci/job.rb index d9e6b7eed75..2f748d28abf 100644 --- a/lib/api/entities/ci/job.rb +++ b/lib/api/entities/ci/job.rb @@ -12,6 +12,7 @@ module API expose :runner, with: ::API::Entities::Ci::Runner expose :artifacts_expire_at, documentation: { type: 'dateTime', example: '2016-01-19T09:05:50.355Z' } + expose :archived?, as: :archived, documentation: { type: 'boolean', example: false } expose( :tag_list, diff --git a/spec/fixtures/api/schemas/public_api/v4/job.json b/spec/fixtures/api/schemas/public_api/v4/job.json index 6265fbcff69..3a0c69786e9 100644 --- a/spec/fixtures/api/schemas/public_api/v4/job.json +++ b/spec/fixtures/api/schemas/public_api/v4/job.json @@ -22,7 +22,8 @@ "artifacts_expire_at", "tag_list", "runner", - "project" + "project", + "archived" ], "properties": { "id": { "type": "integer" }, @@ -70,7 +71,8 @@ }, "project": { "ci_job_token_scope_enabled": { "type": "boolean" } - } + }, + "archived": { "type": "boolean" } }, "additionalProperties":false } diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js index 8d59cb2692e..ad5804f6eb7 100644 --- a/spec/frontend/boards/board_list_spec.js +++ b/spec/frontend/boards/board_list_spec.js @@ -5,6 +5,7 @@ import { DraggableItemTypes, ListType } from 'ee_else_ce/boards/constants'; import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame'; import waitForPromises from 'helpers/wait_for_promises'; import createComponent from 'jest/boards/board_list_helper'; +import { ESC_KEY_CODE } from '~/lib/utils/keycodes'; import BoardCard from '~/boards/components/board_card.vue'; import eventHub from '~/boards/eventhub'; import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue'; @@ -203,9 +204,38 @@ describe('Board list component', () => { expect(document.body.classList.contains('is-dragging')).toBe(true); }); + + it('attaches `keyup` event listener on document', async () => { + jest.spyOn(document, 'addEventListener'); + findDraggable().vm.$emit('start', { + item: { + dataset: { + draggableItemType: DraggableItemTypes.card, + }, + }, + }); + await nextTick(); + + expect(document.addEventListener).toHaveBeenCalledWith('keyup', expect.any(Function)); + }); }); describe('handleDragOnEnd', () => { + const getDragEndParam = (draggableItemType) => ({ + oldIndex: 1, + newIndex: 0, + item: { + dataset: { + draggableItemType, + itemId: mockIssues[0].id, + itemIid: mockIssues[0].iid, + itemPath: mockIssues[0].referencePath, + }, + }, + to: { children: [], dataset: { listId: 'gid://gitlab/List/1' } }, + from: { dataset: { listId: 'gid://gitlab/List/2' } }, + }); + beforeEach(() => { startDrag(); }); @@ -213,42 +243,39 @@ describe('Board list component', () => { it('removes class `is-dragging` from document body', () => { document.body.classList.add('is-dragging'); - endDrag({ - oldIndex: 1, - newIndex: 0, - item: { - dataset: { - draggableItemType: DraggableItemTypes.card, - itemId: mockIssues[0].id, - itemIid: mockIssues[0].iid, - itemPath: mockIssues[0].referencePath, - }, - }, - to: { children: [], dataset: { listId: 'gid://gitlab/List/1' } }, - from: { dataset: { listId: 'gid://gitlab/List/2' } }, - }); + endDrag(getDragEndParam(DraggableItemTypes.card)); expect(document.body.classList.contains('is-dragging')).toBe(false); }); it(`should not handle the event if the dragged item is not a "${DraggableItemTypes.card}"`, () => { - endDrag({ - oldIndex: 1, - newIndex: 0, - item: { - dataset: { - draggableItemType: DraggableItemTypes.list, - itemId: mockIssues[0].id, - itemIid: mockIssues[0].iid, - itemPath: mockIssues[0].referencePath, - }, - }, - to: { children: [], dataset: { listId: 'gid://gitlab/List/1' } }, - from: { dataset: { listId: 'gid://gitlab/List/2' } }, - }); + endDrag(getDragEndParam(DraggableItemTypes.list)); expect(document.body.classList.contains('is-dragging')).toBe(true); }); + + it('detaches `keyup` event listener on document', async () => { + jest.spyOn(document, 'removeEventListener'); + + findDraggable().vm.$emit('end', getDragEndParam(DraggableItemTypes.card)); + await nextTick(); + + expect(document.removeEventListener).toHaveBeenCalledWith('keyup', expect.any(Function)); + }); + }); + + describe('handleKeyUp', () => { + it('dispatches `mouseup` event when Escape key is pressed', () => { + jest.spyOn(document, 'dispatchEvent'); + + document.dispatchEvent( + new Event('keyup', { + keyCode: ESC_KEY_CODE, + }), + ); + + expect(document.dispatchEvent).toHaveBeenCalledWith(new Event('mouseup')); + }); }); }); diff --git a/spec/frontend/lib/utils/secret_detection_spec.js b/spec/frontend/lib/utils/secret_detection_spec.js index 0d1bf1abbaa..b97827208d6 100644 --- a/spec/frontend/lib/utils/secret_detection_spec.js +++ b/spec/frontend/lib/utils/secret_detection_spec.js @@ -14,6 +14,8 @@ describe('containsSensitiveToken', () => { '1234567890', '!@#$%^&*()_+', 'https://example.com', + 'Some tokens are prefixed with glpat- or glcbt-, for example.', + 'glpat-FAKE', ]; it.each(nonSensitiveMessages)('returns false for message: %s', (message) => { @@ -33,6 +35,8 @@ describe('containsSensitiveToken', () => { 'glpat-1234567890 and feed_token=ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'token: gldt-cgyKc1k_AsnEpmP-5fRL', 'curl "https://gitlab.example.com/api/v4/groups/33/scim/identities" --header "PRIVATE-TOKEN: glsoat-cgyKc1k_AsnEpmP-5fRL', + 'CI_JOB_TOKEN=glcbt-FFFF_cgyKc1k_AsnEpmP-5fRL', + 'Use this secret job token: glcbt-1_cgyKc1k_AsnEpmP-5fRL', ]; it.each(sensitiveMessages)('returns true for message: %s', (message) => { diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 18c7e57d464..af3f54c5f0a 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -5507,10 +5507,11 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def stub_current_partition_id end - it 'includes partition_id as a token prefix' do - prefix = ci_build.token.split('_').first.to_i(16) + it 'includes partition_id in the token prefix' do + prefix = ci_build.token.match(/^glcbt-([\h]+)_/) + partition_prefix = prefix[1].to_i(16) - expect(prefix).to eq(ci_testing_partition_id) + expect(partition_prefix).to eq(ci_testing_partition_id) end end @@ -5648,7 +5649,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def it 'generates a token' do expect { ci_build.enqueue } - .to change { ci_build.token }.from(nil).to(a_string_starting_with(partition_id_prefix_in_16_bit_encode)) + .to change { ci_build.token }.from(nil).to(a_string_starting_with("glcbt-#{partition_id_prefix_in_16_bit_encode}")) end end @@ -5665,4 +5666,51 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def end end end + + describe '#prefix_and_partition_for_token' do + # 100.to_s(16) -> 64 + let(:ci_build) { described_class.new(partition_id: 100) } + + shared_examples 'partition prefix' do + it 'is prefixed with partition_id' do + ci_build.ensure_token + expect(ci_build.token).to match(/^64_[\w-]{20}$/) + end + end + + shared_examples 'static and partition prefixes' do + it 'is prefixed with static string and partition id' do + ci_build.ensure_token + expect(ci_build.token).to match(/^glcbt-64_[\w-]{20}$/) + end + end + + it_behaves_like 'static and partition prefixes' + + context 'when feature flag is globally disabled' do + before do + stub_feature_flags(prefix_ci_build_tokens: false) + end + + it_behaves_like 'partition prefix' + + context 'when enabled for a different project' do + let_it_be(:project) { create(:project) } + + before do + stub_feature_flags(prefix_ci_build_tokens: project) + end + + it_behaves_like 'partition prefix' + end + + context 'when enabled for the project' do + before do + stub_feature_flags(prefix_ci_build_tokens: ci_build.project) + end + + it_behaves_like 'static and partition prefixes' + end + end + end end diff --git a/spec/requests/api/ci/jobs_spec.rb b/spec/requests/api/ci/jobs_spec.rb index 382aabd45a1..941aa3734a3 100644 --- a/spec/requests/api/ci/jobs_spec.rb +++ b/spec/requests/api/ci/jobs_spec.rb @@ -70,6 +70,7 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do expect(json_response['artifacts']).to be_an Array expect(json_response['artifacts']).to be_empty expect(json_response['web_url']).to be_present + expect(json_response['archived']).to eq(jobx.archived?) end end |