diff --git a/GITLAB_KAS_VERSION b/GITLAB_KAS_VERSION index 59cca1ab9c6..6e276af10bf 100644 --- a/GITLAB_KAS_VERSION +++ b/GITLAB_KAS_VERSION @@ -1 +1 @@ -c467a18d6d952c904fa033df2ea5b92aba98e8dc +b54405cd2d14a8a25aa2a5a464008ec9421b7aa6 diff --git a/Gemfile.checksum b/Gemfile.checksum index 0e2d1735653..875d5a41d89 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -224,13 +224,13 @@ {"name":"gitlab-dangerfiles","version":"4.8.1","platform":"ruby","checksum":"bbad321c9638152a643d27a20b35ba1e2d8eddcc6bdfc4493d7b96e816ecf300"}, {"name":"gitlab-experiment","version":"0.9.1","platform":"ruby","checksum":"f230ee742154805a755d5f2539dc44d93cdff08c5bbbb7656018d61f93d01f48"}, {"name":"gitlab-fog-azure-rm","version":"2.2.0","platform":"ruby","checksum":"31aa7c2170f57874053144e7f716ec9e15f32e71ffbd2c56753dce46e2e78ba9"}, -{"name":"gitlab-glfm-markdown","version":"0.0.27","platform":"aarch64-linux-gnu","checksum":"bb954cb5dc995f4720edb84caba2112aa54e3d48c18fc1cb552f4ea994a9d518"}, -{"name":"gitlab-glfm-markdown","version":"0.0.27","platform":"aarch64-linux-musl","checksum":"53cc1b44658860dae476c27ea2e78a36d5e8d39c51003d98267821685665162b"}, -{"name":"gitlab-glfm-markdown","version":"0.0.27","platform":"arm64-darwin","checksum":"ea05243121cc05ae41fce162be2fd0b86be06b6edc122ac0688cbb0e45341c97"}, -{"name":"gitlab-glfm-markdown","version":"0.0.27","platform":"ruby","checksum":"82cf8ffc22092cf1e17eb28a1dae8b63a4a141acc1b2864a0c11efc544b53c0f"}, -{"name":"gitlab-glfm-markdown","version":"0.0.27","platform":"x86_64-darwin","checksum":"ec6775054481b3e07a97d4be945fe41d043f89dc1fa1d95cdfc6a70b439ea0e4"}, -{"name":"gitlab-glfm-markdown","version":"0.0.27","platform":"x86_64-linux-gnu","checksum":"ebcccc617db4f669cd2de900e6d31fae5de67acedeb178d242063e338cb57050"}, -{"name":"gitlab-glfm-markdown","version":"0.0.27","platform":"x86_64-linux-musl","checksum":"77cf356f2a2fc496ec17206d68a6a1fe4f4d680bc1aac2206c32ee5393611a15"}, +{"name":"gitlab-glfm-markdown","version":"0.0.28","platform":"aarch64-linux-gnu","checksum":"67a2e7d2cd1208d22abb9c162e88aa725f0fa31ea7fa8a6f56481370a5d1ef2d"}, +{"name":"gitlab-glfm-markdown","version":"0.0.28","platform":"aarch64-linux-musl","checksum":"c4b8ce9061238f0cebdaeeceb64bf2e6f1d72f4bfe72b11ac191f45dce12e302"}, +{"name":"gitlab-glfm-markdown","version":"0.0.28","platform":"arm64-darwin","checksum":"fe71765e04305f5a34647b3338e5101f92547f627a76ddedab35631f98907420"}, +{"name":"gitlab-glfm-markdown","version":"0.0.28","platform":"ruby","checksum":"960e481c037bbe319ec73792a3fc0fa024a9c11ab16786f24591417a255514a1"}, +{"name":"gitlab-glfm-markdown","version":"0.0.28","platform":"x86_64-darwin","checksum":"1434de2be23464ac379e30a125213ce00e6c907f47362b50a2d50488bc1e73d2"}, +{"name":"gitlab-glfm-markdown","version":"0.0.28","platform":"x86_64-linux-gnu","checksum":"59c5a0c577cb9301253bce6c9c0e2b9585110a34c46197cedc16e1142fb2feaf"}, +{"name":"gitlab-glfm-markdown","version":"0.0.28","platform":"x86_64-linux-musl","checksum":"298757eb48905451285d8c2ef4ecbb1691ed2b30f522998bc65474be340def8a"}, {"name":"gitlab-kas-grpc","version":"17.9.1","platform":"ruby","checksum":"fd480c1669c741ceab8d5f86b7e5e32b71f4f25af8b523725382dae425aaa958"}, {"name":"gitlab-labkit","version":"0.37.0","platform":"ruby","checksum":"d2dd0a60db2149a9a8eebf2975dc23f54ac3ceb01bdba732eb1b26b86dfffa70"}, {"name":"gitlab-license","version":"2.6.0","platform":"ruby","checksum":"2c1f8ae73835640ec77bf758c1d0c9730635043c01cf77902f7976e826d7d016"}, diff --git a/Gemfile.lock b/Gemfile.lock index cf1031c9844..5e9f8650e68 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -750,7 +750,7 @@ GEM mime-types net-http-persistent (~> 4.0) nokogiri (~> 1, >= 1.10.8) - gitlab-glfm-markdown (0.0.27) + gitlab-glfm-markdown (0.0.28) rb_sys (~> 0.9.109) gitlab-kas-grpc (17.9.1) grpc (~> 1.0) diff --git a/Gemfile.next.checksum b/Gemfile.next.checksum index 0b85c33be9f..f7f4c13212a 100644 --- a/Gemfile.next.checksum +++ b/Gemfile.next.checksum @@ -224,13 +224,13 @@ {"name":"gitlab-dangerfiles","version":"4.8.1","platform":"ruby","checksum":"bbad321c9638152a643d27a20b35ba1e2d8eddcc6bdfc4493d7b96e816ecf300"}, {"name":"gitlab-experiment","version":"0.9.1","platform":"ruby","checksum":"f230ee742154805a755d5f2539dc44d93cdff08c5bbbb7656018d61f93d01f48"}, {"name":"gitlab-fog-azure-rm","version":"2.2.0","platform":"ruby","checksum":"31aa7c2170f57874053144e7f716ec9e15f32e71ffbd2c56753dce46e2e78ba9"}, -{"name":"gitlab-glfm-markdown","version":"0.0.27","platform":"aarch64-linux-gnu","checksum":"bb954cb5dc995f4720edb84caba2112aa54e3d48c18fc1cb552f4ea994a9d518"}, -{"name":"gitlab-glfm-markdown","version":"0.0.27","platform":"aarch64-linux-musl","checksum":"53cc1b44658860dae476c27ea2e78a36d5e8d39c51003d98267821685665162b"}, -{"name":"gitlab-glfm-markdown","version":"0.0.27","platform":"arm64-darwin","checksum":"ea05243121cc05ae41fce162be2fd0b86be06b6edc122ac0688cbb0e45341c97"}, -{"name":"gitlab-glfm-markdown","version":"0.0.27","platform":"ruby","checksum":"82cf8ffc22092cf1e17eb28a1dae8b63a4a141acc1b2864a0c11efc544b53c0f"}, -{"name":"gitlab-glfm-markdown","version":"0.0.27","platform":"x86_64-darwin","checksum":"ec6775054481b3e07a97d4be945fe41d043f89dc1fa1d95cdfc6a70b439ea0e4"}, -{"name":"gitlab-glfm-markdown","version":"0.0.27","platform":"x86_64-linux-gnu","checksum":"ebcccc617db4f669cd2de900e6d31fae5de67acedeb178d242063e338cb57050"}, -{"name":"gitlab-glfm-markdown","version":"0.0.27","platform":"x86_64-linux-musl","checksum":"77cf356f2a2fc496ec17206d68a6a1fe4f4d680bc1aac2206c32ee5393611a15"}, +{"name":"gitlab-glfm-markdown","version":"0.0.28","platform":"aarch64-linux-gnu","checksum":"67a2e7d2cd1208d22abb9c162e88aa725f0fa31ea7fa8a6f56481370a5d1ef2d"}, +{"name":"gitlab-glfm-markdown","version":"0.0.28","platform":"aarch64-linux-musl","checksum":"c4b8ce9061238f0cebdaeeceb64bf2e6f1d72f4bfe72b11ac191f45dce12e302"}, +{"name":"gitlab-glfm-markdown","version":"0.0.28","platform":"arm64-darwin","checksum":"fe71765e04305f5a34647b3338e5101f92547f627a76ddedab35631f98907420"}, +{"name":"gitlab-glfm-markdown","version":"0.0.28","platform":"ruby","checksum":"960e481c037bbe319ec73792a3fc0fa024a9c11ab16786f24591417a255514a1"}, +{"name":"gitlab-glfm-markdown","version":"0.0.28","platform":"x86_64-darwin","checksum":"1434de2be23464ac379e30a125213ce00e6c907f47362b50a2d50488bc1e73d2"}, +{"name":"gitlab-glfm-markdown","version":"0.0.28","platform":"x86_64-linux-gnu","checksum":"59c5a0c577cb9301253bce6c9c0e2b9585110a34c46197cedc16e1142fb2feaf"}, +{"name":"gitlab-glfm-markdown","version":"0.0.28","platform":"x86_64-linux-musl","checksum":"298757eb48905451285d8c2ef4ecbb1691ed2b30f522998bc65474be340def8a"}, {"name":"gitlab-kas-grpc","version":"17.9.1","platform":"ruby","checksum":"fd480c1669c741ceab8d5f86b7e5e32b71f4f25af8b523725382dae425aaa958"}, {"name":"gitlab-labkit","version":"0.37.0","platform":"ruby","checksum":"d2dd0a60db2149a9a8eebf2975dc23f54ac3ceb01bdba732eb1b26b86dfffa70"}, {"name":"gitlab-license","version":"2.6.0","platform":"ruby","checksum":"2c1f8ae73835640ec77bf758c1d0c9730635043c01cf77902f7976e826d7d016"}, diff --git a/Gemfile.next.lock b/Gemfile.next.lock index 3a4fcef2eb5..b67291f9e0f 100644 --- a/Gemfile.next.lock +++ b/Gemfile.next.lock @@ -762,7 +762,7 @@ GEM mime-types net-http-persistent (~> 4.0) nokogiri (~> 1, >= 1.10.8) - gitlab-glfm-markdown (0.0.27) + gitlab-glfm-markdown (0.0.28) rb_sys (~> 0.9.109) gitlab-kas-grpc (17.9.1) grpc (~> 1.0) diff --git a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue index 433ef8b666b..e2bc2ce202d 100644 --- a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue +++ b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue @@ -17,6 +17,7 @@ export default { i18n: { deleteLabel: __('Delete'), editLabel: __('Edit'), + editAriaLabel: (name) => sprintf(__('Edit %{name}'), { name }), toggleLabel: __('Feature flag status'), }, components: { @@ -207,7 +208,7 @@ export default { data-testid="feature-flag-edit-button" class="gl-flex-grow" icon="pencil" - :aria-label="$options.i18n.editLabel" + :aria-label="$options.i18n.editAriaLabel(item.name)" :href="item.edit_path" /> @@ -234,7 +235,7 @@ export default { data-testid="feature-flag-edit-button" class="gl-flex-grow" icon="pencil" - :aria-label="$options.i18n.editLabel" + :aria-label="$options.i18n.editAriaLabel(item.name)" :href="item.edit_path" /> diff --git a/app/models/ci/job_token/authorization.rb b/app/models/ci/job_token/authorization.rb index 458ef6330fa..89fc8d87e16 100644 --- a/app/models/ci/job_token/authorization.rb +++ b/app/models/ci/job_token/authorization.rb @@ -23,6 +23,11 @@ module Ci scope :for_project, ->(accessed_project) { where(accessed_project: accessed_project) } scope :preload_origin_project, -> { includes(origin_project: :route) } + attribute :job_token_policies, ::Gitlab::Database::Type::SymbolizedJsonb.new + validates :job_token_policies, json_schema: { + filename: 'ci_job_token_authorizations_policies', detail_errors: true + } + # Record in SafeRequestStore a cross-project access attempt def self.capture(origin_project:, accessed_project:) label = origin_project == accessed_project ? 'same-project' : 'cross-project' @@ -43,9 +48,11 @@ module Ci # completes. We will do that in a middleware. This is because the policy # rule about job token scope may be satisfied but a subsequent rule in # the Declarative Policies may block the authorization. - Gitlab::SafeRequestStore.fetch(REQUEST_CACHE_KEY) do - { accessed_project_id: accessed_project.id, origin_project_id: origin_project.id } - end + add_to_request_store_hash(accessed_project_id: accessed_project.id, origin_project_id: origin_project.id) + end + + def self.capture_job_token_policies(policies) + add_to_request_store_hash(policies: policies) end # Schedule logging of captured authorizations in a background worker. @@ -57,18 +64,38 @@ module Ci return unless authorizations accessed_project_id = authorizations[:accessed_project_id] + origin_project_id = authorizations[:origin_project_id] + return unless accessed_project_id && origin_project_id + Ci::JobToken::LogAuthorizationWorker # rubocop:disable CodeReuse/Worker -- This method is called from a middleware and it's better tested - .perform_in(CAPTURE_DELAY, accessed_project_id, authorizations[:origin_project_id]) + .perform_in(CAPTURE_DELAY, accessed_project_id, origin_project_id) end - def self.log_captures!(accessed_project_id:, origin_project_id:) - upsert({ + def self.log_captures!(accessed_project_id:, origin_project_id:, policies: []) + current_time = Time.current + attributes = { accessed_project_id: accessed_project_id, origin_project_id: origin_project_id, - last_authorized_at: Time.current - }, - unique_by: [:accessed_project_id, :origin_project_id], - on_duplicate: :update) + last_authorized_at: current_time + } + + transaction do + if policies.present? + auth_log = lock.find_by( + accessed_project_id: accessed_project_id, + origin_project_id: origin_project_id + ) + policies = policies.index_with(current_time) + attributes[:job_token_policies] = auth_log ? auth_log.job_token_policies.merge(policies) : policies + end + + upsert(attributes, unique_by: [:accessed_project_id, :origin_project_id], on_duplicate: :update) + end + end + + def self.add_to_request_store_hash(hash) + new_hash = captured_authorizations.present? ? captured_authorizations.merge(hash) : hash + Gitlab::SafeRequestStore[REQUEST_CACHE_KEY] = new_hash end def self.captured_authorizations diff --git a/app/models/ci/job_token/scope.rb b/app/models/ci/job_token/scope.rb index ac8af4b1411..622be5e7ef5 100644 --- a/app/models/ci/job_token/scope.rb +++ b/app/models/ci/job_token/scope.rb @@ -36,9 +36,14 @@ module Ci def policies_allowed?(accessed_project, policies) return true if self_referential?(accessed_project) - return true unless accessed_project.ci_inbound_job_token_scope_enabled? return false unless inbound_accessible?(accessed_project) + # We capture policies even if the inbound scopes are disabled or the feature flag is disabled + Ci::JobToken::Authorization.capture_job_token_policies(policies) if policies.present? + + return true unless accessed_project.ci_inbound_job_token_scope_enabled? + return true unless Feature.enabled?(:add_policies_to_ci_job_token, accessed_project) + policies_allowed_for_accessed_project?(accessed_project, policies) end diff --git a/app/validators/json_schemas/ci_job_token_authorizations_policies.json b/app/validators/json_schemas/ci_job_token_authorizations_policies.json new file mode 100644 index 00000000000..4bf9e101361 --- /dev/null +++ b/app/validators/json_schemas/ci_job_token_authorizations_policies.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Policies for CI job token authorization logs, captured with their last authorized time", + "type": "object", + "propertyNames": { + "$ref": "./ci_job_token_policies.json#/items" + }, + "additionalProperties": { + "type": "string", + "format": "date-time" + } +} diff --git a/app/workers/ci/job_token/log_authorization_worker.rb b/app/workers/ci/job_token/log_authorization_worker.rb index 31258badeb2..ba69717710c 100644 --- a/app/workers/ci/job_token/log_authorization_worker.rb +++ b/app/workers/ci/job_token/log_authorization_worker.rb @@ -12,10 +12,11 @@ module Ci idempotent! deduplicate :until_executed, including_scheduled: true, if_deduplicated: :reschedule_once - def perform(accessed_project_id, origin_project_id) + def perform(accessed_project_id, origin_project_id, policies = []) Ci::JobToken::Authorization.log_captures!( accessed_project_id: accessed_project_id, - origin_project_id: origin_project_id + origin_project_id: origin_project_id, + policies: policies.map(&:to_sym) ) end end diff --git a/db/docs/batched_background_migrations/backfill_onboarding_status_setup_for_company.yml b/db/docs/batched_background_migrations/backfill_onboarding_status_setup_for_company.yml new file mode 100644 index 00000000000..057d7fc558f --- /dev/null +++ b/db/docs/batched_background_migrations/backfill_onboarding_status_setup_for_company.yml @@ -0,0 +1,8 @@ +--- +migration_job_name: BackfillOnboardingStatusSetupForCompany +description: Moves data from user_preferences.setup_for_company to the new setup_for_company field in user_details.onboarding_status +feature_category: onboarding +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/180602 +milestone: '17.10' +queued_migration_version: 20250228155146 +finalized_by: # version of the migration that finalized this BBM diff --git a/db/migrate/20250225104252_add_policies_column_to_ci_job_token_authorizations.rb b/db/migrate/20250225104252_add_policies_column_to_ci_job_token_authorizations.rb new file mode 100644 index 00000000000..87ff670c655 --- /dev/null +++ b/db/migrate/20250225104252_add_policies_column_to_ci_job_token_authorizations.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddPoliciesColumnToCiJobTokenAuthorizations < Gitlab::Database::Migration[2.2] + milestone '17.10' + + def change + add_column :ci_job_token_authorizations, :job_token_policies, :jsonb, default: {}, null: false + end +end diff --git a/db/post_migrate/20250228155146_queue_backfill_onboarding_status_setup_for_company.rb b/db/post_migrate/20250228155146_queue_backfill_onboarding_status_setup_for_company.rb new file mode 100644 index 00000000000..9039eb57b6a --- /dev/null +++ b/db/post_migrate/20250228155146_queue_backfill_onboarding_status_setup_for_company.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class QueueBackfillOnboardingStatusSetupForCompany < Gitlab::Database::Migration[2.2] + milestone '17.10' + + restrict_gitlab_migration gitlab_schema: :gitlab_main + + MIGRATION = "BackfillOnboardingStatusSetupForCompany" + DELAY_INTERVAL = 2.minutes + BATCH_SIZE = 1000 + SUB_BATCH_SIZE = 100 + + def up + queue_batched_background_migration( + MIGRATION, + :user_details, + :user_id, + job_interval: DELAY_INTERVAL, + batch_size: BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE + ) + end + + def down + delete_batched_background_migration(MIGRATION, :user_details, :user_id, []) + end +end diff --git a/db/schema_migrations/20250225104252 b/db/schema_migrations/20250225104252 new file mode 100644 index 00000000000..3fdba9d7949 --- /dev/null +++ b/db/schema_migrations/20250225104252 @@ -0,0 +1 @@ +885e811ba46d6fde0e865d44e89241ce7f777ec65887e59ce7507bf830de333c \ No newline at end of file diff --git a/db/schema_migrations/20250228155146 b/db/schema_migrations/20250228155146 new file mode 100644 index 00000000000..87c2e33f979 --- /dev/null +++ b/db/schema_migrations/20250228155146 @@ -0,0 +1 @@ +9ce991961668a1e306c2348cf338a353966c35f9c2f11b105380f0a51df7f4d4 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 392818cfb0d..a8e4850f148 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -10554,7 +10554,8 @@ CREATE TABLE ci_job_token_authorizations ( id bigint NOT NULL, accessed_project_id bigint NOT NULL, origin_project_id bigint NOT NULL, - last_authorized_at timestamp with time zone NOT NULL + last_authorized_at timestamp with time zone NOT NULL, + job_token_policies jsonb DEFAULT '{}'::jsonb NOT NULL ); CREATE SEQUENCE ci_job_token_authorizations_id_seq diff --git a/doc/administration/auth/crowd.md b/doc/administration/auth/crowd.md index e044afba909..82d0b414509 100644 --- a/doc/administration/auth/crowd.md +++ b/doc/administration/auth/crowd.md @@ -17,11 +17,9 @@ this provider also allows Crowd authentication for Git-over-https requests. ## Configure a new Crowd application -1. Choose 'Applications' in the top menu, then 'Add application'. -1. Go through the 'Add application' steps, entering the appropriate details. - The screenshot below shows an example configuration. - - ![Final confirmation page in Crowd for application configuration](img/crowd_application_v9_0.png) +1. On the top menu, select **Applications > Add application**. +1. Go through the **Add application** steps, entering the appropriate details. +1. When complete, select **Add application**. ## Configure GitLab diff --git a/doc/administration/auth/img/crowd_application_v9_0.png b/doc/administration/auth/img/crowd_application_v9_0.png deleted file mode 100644 index 5029a005667..00000000000 Binary files a/doc/administration/auth/img/crowd_application_v9_0.png and /dev/null differ diff --git a/doc/administration/packages/container_registry_troubleshooting.md b/doc/administration/packages/container_registry_troubleshooting.md index 5de3e0f3f1f..5c65bf2beca 100644 --- a/doc/administration/packages/container_registry_troubleshooting.md +++ b/doc/administration/packages/container_registry_troubleshooting.md @@ -499,24 +499,37 @@ Now that we have mitmproxy and Docker running, we can attempt to sign in and push a container image. You may need to run as root to do this. For example: ```shell -docker login s3-testing.myregistry.com:5050 -docker push s3-testing.myregistry.com:5050/root/docker-test/docker-image +docker login example.s3.amazonaws.com:5050 +docker push example.s3.amazonaws.com:5050/root/docker-test/docker-image ``` In the example above, we see the following trace on the mitmproxy window: -![mitmproxy output from Docker](img/mitmproxy_docker_v8_11.png) +```plaintext +PUT https://example.s3.amazonaws.com:4567/v2/root/docker-test/blobs/uploads/(UUID)/(QUERYSTRING) + ← 201 text/plain [no content] 661ms +HEAD https://example.s3.amazonaws.com:4567/v2/root/docker-test/blobs/sha256:(SHA) + ← 307 application/octet-stream [no content] 93ms +HEAD https://example.s3.amazonaws.com:4567/v2/root/docker-test/blobs/sha256:(SHA) + ← 307 application/octet-stream [no content] 101ms +HEAD https://example.s3.amazonaws.com:4567/v2/root/docker-test/blobs/sha256:(SHA) + ← 307 application/octet-stream [no content] 87ms +HEAD https://amazonaws.example.com/docker/registry/vs/blobs/sha256/dd/(UUID)/data(QUERYSTRING) + ← 403 application/xml [no content] 80ms +HEAD https://amazonaws.example.com/docker/registry/vs/blobs/sha256/dd/(UUID)/data(QUERYSTRING) + ← 403 application/xml [no content] 62ms +``` -The above image shows: +This output shows: -- The initial PUT requests went through fine with a 201 status code. -- The 201 redirected the client to the S3 bucket. -- The HEAD request to the AWS bucket reported a 403 Unauthorized. +- The initial PUT requests went through fine with a `201` status code. +- The `201` redirected the client to the Amazon S3 bucket. +- The HEAD request to the AWS bucket reported a `403 Unauthorized`. What does this mean? This strongly suggests that the S3 user does not have the right [permissions to perform a HEAD request](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html). The solution: check the [IAM permissions again](https://distribution.github.io/distribution/storage-drivers/s3/). -Once the right permissions were set, the error goes away. +After the right permissions were set, the error went away. ## Missing `gitlab-registry.key` prevents container repository deletion diff --git a/doc/administration/packages/img/mitmproxy_docker_v8_11.png b/doc/administration/packages/img/mitmproxy_docker_v8_11.png deleted file mode 100644 index aa3b6a0b830..00000000000 Binary files a/doc/administration/packages/img/mitmproxy_docker_v8_11.png and /dev/null differ diff --git a/doc/development/feature_flags/_index.md b/doc/development/feature_flags/_index.md index 0e0643c7629..20e2fec6373 100644 --- a/doc/development/feature_flags/_index.md +++ b/doc/development/feature_flags/_index.md @@ -2,16 +2,14 @@ stage: none group: unassigned info: 'See the Technical Writers assigned to Development Guidelines: https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-development-guidelines' +description: Developer documentation about GitLab feature flags. title: Feature flags in the development of GitLab --- -{{< alert type="note" >}} - -This document explains how to contribute to the development and operations of the GitLab product. -If you want to use feature flags to show and hide functionality in your own applications, -view [this feature flags information](../../operations/feature_flags.md) instead. - -{{< /alert >}} +This page explains how developers contribute to the development and operations of the GitLab product +through feature flags. To create custom feature flags to show and hide features in your own applications, +see [Create a feature flag](../../operations/feature_flags.md#create-a-feature-flag). +A [complete list of feature flags](../../user/feature_flags.md) in GitLab is also available. {{< alert type="warning" >}} diff --git a/doc/operations/feature_flags.md b/doc/operations/feature_flags.md index 7f052793f12..862465943e5 100644 --- a/doc/operations/feature_flags.md +++ b/doc/operations/feature_flags.md @@ -2,6 +2,7 @@ stage: Deploy group: Environments info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments +description: Create and maintain a custom feature flag for your GitLab application. title: Feature flags --- @@ -17,6 +18,8 @@ You can toggle a feature on and off to subsets of users, helping you achieve Con Feature flags help reduce risk, allowing you to do controlled testing, and separate feature delivery from customer launch. +A [complete list of feature flags](../user/feature_flags.md) in GitLab is also available. + For an example of feature flags in action, see [Eliminating risk with feature flags](https://www.youtube.com/watch?v=U9WqoK9froI). diff --git a/doc/security/two_factor_authentication.md b/doc/security/two_factor_authentication.md index e3719a33921..83aa51adcc9 100644 --- a/doc/security/two_factor_authentication.md +++ b/doc/security/two_factor_authentication.md @@ -115,7 +115,7 @@ You can enforce 2FA for all users in a group or subgroup. Prerequisites: -- You must have the Maintainer or Owner role for the group. +- You must have the Owner role for the group. To enforce 2FA for a group: diff --git a/doc/subscriptions/gitlab_dedicated/_index.md b/doc/subscriptions/gitlab_dedicated/_index.md index cd3dc9ce119..9efe12f0465 100644 --- a/doc/subscriptions/gitlab_dedicated/_index.md +++ b/doc/subscriptions/gitlab_dedicated/_index.md @@ -246,9 +246,17 @@ The following operational features are not available: ### Feature flags -[Feature flags](../../administration/feature_flags.md) are not available for GitLab Dedicated. +GitLab uses [feature flags](../../user/feature_flags.md) to support the development and rollout of new or experimental features. +In GitLab Dedicated: -Feature flags support the development and rollout of new or experimental features on GitLab.com. Features behind feature flags are not considered ready for production use, are experimental and therefore unsafe for GitLab Dedicated. Stability and SLAs may be affected by changing default settings. +- Features using feature flags that are **enabled by default** are available. +- Features using feature flags that are **disabled by default** are not available and cannot be enabled by administrators. + +Features behind flags that are disabled by default are not ready for production use and therefore unsafe for GitLab Dedicated. + +When a feature becomes generally available and the flag is enabled or removed, the feature becomes available in +GitLab Dedicated in the same GitLab version. GitLab Dedicated follows its own +[release schedule](../gitlab_dedicated/maintenance.md) for version deployments. ## Migrate to GitLab Dedicated diff --git a/doc/user/feature_flags.md b/doc/user/feature_flags.md index 84117980a34..2619e995520 100644 --- a/doc/user/feature_flags.md +++ b/doc/user/feature_flags.md @@ -2,14 +2,17 @@ stage: none group: unassigned info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments -description: Complete list of flags. +description: Complete list of all feature flags in GitLab. title: All feature flags in GitLab layout: feature_flags --- -The following feature flags exist in GitLab. These flags determine the availability of each feature. +GitLab provides feature flags to turn specific features on or off. +This page contains a list of all feature flags provided by GitLab. In GitLab Self-Managed, +GitLab administrators can [change the state of these feature flags](../administration/feature_flags.md). -For self-managed instances, [GitLab administrators can change the state of the flag](../administration/feature_flags.md). +For help developing custom feature flags, see +[Create a feature flag](../operations/feature_flags.md#create-a-feature-flag). diff --git a/doc/user/project/members/_index.md b/doc/user/project/members/_index.md index 14757d6a329..36db081d57b 100644 --- a/doc/user/project/members/_index.md +++ b/doc/user/project/members/_index.md @@ -12,9 +12,12 @@ title: Members of a project {{< /details >}} -Members are the users and groups who have access to your project. +Members are the users and groups who have access to your project. Members can be added directly to your +project or inherit access through groups. -Each member gets a role, which determines what they can do in the project. +Each member has a role that determines what they can do in the project. Project members with the +appropriate role can add users to projects, remove users from projects, and manage access requests to +control access to project resources. ## Membership types diff --git a/doc/user/project/members/sharing_projects_groups.md b/doc/user/project/members/sharing_projects_groups.md index 7a4903d908b..85ae594c1de 100644 --- a/doc/user/project/members/sharing_projects_groups.md +++ b/doc/user/project/members/sharing_projects_groups.md @@ -221,19 +221,37 @@ After you [specify a user cap for the group](../../group/manage.md#specify-a-use ## Sharing groups -When you want a group to have access to your group, -you can invite another [group](../../group/_index.md) to the group. -The invited group's direct members get access to the group. +When you want another group's members to have access to your group, +you can invite the [group](../../group/_index.md) to your group. +The group's direct members get access to the group, which becomes a **shared group**. + +Only direct members of the invited group get access to the shared group, not inherited or subgroup members. To grant subgroup members access, invite the subgroup directly. + +The following table provides an overview of the group members that get access to a shared group: + +| Group member source | Access to shared group | +|--------------------------------------------------------------|------------------------| +| Direct member of the group that is invited | {{< icon name="check-circle" >}} Yes | +| Inherited member of the group that is invited | {{< icon name="dotted-circle" >}} No | +| Member of a subgroup, but not of the group that is invited | {{< icon name="dotted-circle" >}} No | + +### Member access and roles + +Each member's access is based on the: + +- Role they're assigned in the invited group. +- Maximum role you choose when you invite the group. + +If a group member has a role for the invited group with fewer permissions than the maximum role for your group, +the member keeps the permissions of their invited group role. +The least access is granted between the access in the invited group and the access in the inviting group. After you invite a group to your group: -- The **Groups** tab of the group's **Members** page lists the invited group. This list includes both public and private groups. -- The **Members** tab of the group's **Members** page lists the members of the invited group. -- All direct members of the invited group have access to the inviting group. - The least access is granted between the access in the invited group and the access in the inviting group. -- Inherited members of the invited group do not gain access to the inviting group. -- On the group's usage quota page, direct members of the invited group who have the **Group Invite** badge - next to their profile count towards the billable members of the inviting group. +- On the group's overview page, groups that this group has been shared with are listed on the **Shared groups** tab. +- On the group's **Members** page, the invited group is listed on the **Groups** tab. This list includes both public and private groups. +- On the group's **Members** page, the members of the invited group are listed on the **Members** tab. +- On the group's usage quota page, direct members of the invited group who have the **Group Invite** badge next to their profile count towards the billable members of the inviting group. [In GitLab 16.11 and later](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/144638), the invited group's name and membership source are masked on the **Members** and the **Groups** tabs, diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/_index.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/_index.md index 622b539bf8c..ed72e2dcac9 100644 --- a/doc/user/project/pages/custom_domains_ssl_tls_certification/_index.md +++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/_index.md @@ -134,8 +134,6 @@ Whether it's a user or a project website, the DNS record should point to your Pages domain (`namespace.gitlab.io`), without any path. -![DNS `CNAME` record pointing to GitLab.com project](img/dns_cname_record_example_v9_0.png) - ##### For both root and subdomains There are a few cases where you need to point both the subdomain and root diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/img/dns_cname_record_example_v9_0.png b/doc/user/project/pages/custom_domains_ssl_tls_certification/img/dns_cname_record_example_v9_0.png deleted file mode 100644 index 43d1a838544..00000000000 Binary files a/doc/user/project/pages/custom_domains_ssl_tls_certification/img/dns_cname_record_example_v9_0.png and /dev/null differ diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index d72d9cf8f99..b0e4ac230f8 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -1036,7 +1036,6 @@ module API def job_token_policies_authorized?(project) return true unless current_user&.from_ci_job_token? - return true unless Feature.enabled?(:add_policies_to_ci_job_token, project) return true if skip_job_token_policies? return true if publicly_accessible_feature?(project) diff --git a/lib/gitlab/background_migration/backfill_onboarding_status_setup_for_company.rb b/lib/gitlab/background_migration/backfill_onboarding_status_setup_for_company.rb new file mode 100644 index 00000000000..a98193173e2 --- /dev/null +++ b/lib/gitlab/background_migration/backfill_onboarding_status_setup_for_company.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + class BackfillOnboardingStatusSetupForCompany < BatchedMigrationJob + operation_name :backfill_onboarding_status_setup_for_company + feature_category :onboarding + + def perform + each_sub_batch do |sub_batch| + UserDetail + .where(user_id: sub_batch.select(:user_id)) + .where("(user_details.onboarding_status->'setup_for_company') IS NULL") + .joins("INNER JOIN user_preferences ON user_details.user_id = user_preferences.user_id") + .where.not(user_preferences: { setup_for_company: nil }) + .update_all( + "onboarding_status = jsonb_set( + COALESCE(onboarding_status, '{}'::jsonb), + '{setup_for_company}', + (SELECT to_jsonb(user_preferences.setup_for_company) + FROM user_preferences + WHERE user_details.user_id = user_preferences.user_id) + )" + ) + end + end + end + end +end diff --git a/spec/frontend/feature_flags/components/feature_flags_table_spec.js b/spec/frontend/feature_flags/components/feature_flags_table_spec.js index 4c8959e6b0b..987bde04da9 100644 --- a/spec/frontend/feature_flags/components/feature_flags_table_spec.js +++ b/spec/frontend/feature_flags/components/feature_flags_table_spec.js @@ -126,9 +126,10 @@ describe('Feature flag table', () => { expect(wrapper.findByTestId('flags-table-action-buttons').exists()).toBe(true); expect(wrapper.findByTestId('feature-flag-delete-button').exists()).toBe(true); expect(wrapper.findByTestId('feature-flag-edit-button').exists()).toBe(true); - expect(wrapper.findByTestId('feature-flag-edit-button').attributes('href')).toEqual( - 'edit/path', - ); + expect(wrapper.findByTestId('feature-flag-edit-button').attributes()).toMatchObject({ + 'aria-label': 'Edit flag name', + href: 'edit/path', + }); }); }); diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb index 375240a0ab8..2498cc76e65 100644 --- a/spec/lib/api/helpers_spec.rb +++ b/spec/lib/api/helpers_spec.rb @@ -327,14 +327,6 @@ RSpec.describe API::Helpers, feature_category: :shared do it { is_expected.to eq project } end - context 'when the `add_policies_to_ci_job_token` feature flag is disabled' do - before do - stub_feature_flags(add_policies_to_ci_job_token: false) - end - - it { is_expected.to eq project } - end - context 'when the project feature is publicly accessible' do let_it_be(:project) { create(:project, :public, :builds_enabled) } diff --git a/spec/lib/gitlab/background_migration/backfill_onboarding_status_setup_for_company_spec.rb b/spec/lib/gitlab/background_migration/backfill_onboarding_status_setup_for_company_spec.rb new file mode 100644 index 00000000000..bf30dfe21f2 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_onboarding_status_setup_for_company_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillOnboardingStatusSetupForCompany, feature_category: :onboarding do + let(:users) { table(:users) } + let(:user_details) { table(:user_details) } + let(:user_preferences) { table(:user_preferences) } + let(:first_user) { users.create!(projects_limit: 0, email: 'user1@example.com') } + let!(:user_preference) do + user_preferences.create!( + user_id: first_user.id, + setup_for_company: true + ) + end + + let!(:user_detail) do + user_details.create!( + user_id: first_user.id, + onboarding_status: { 'setup_for_company' => true } + ) + end + + let(:second_user) { users.create!(projects_limit: 0, email: 'user2@example.com') } + let!(:user_preference_to_change) do + user_preferences.create!( + user_id: second_user.id, + setup_for_company: false + ) + end + + let!(:user_detail_to_change) do + user_details.create!( + user_id: second_user.id, + onboarding_status: {} + ) + end + + let(:last_user) { users.create!(projects_limit: 0, email: 'user3@example.com') } + let!(:user_preference_not_to_be_set) do + user_preferences.create!( + user_id: last_user.id, + setup_for_company: nil + ) + end + + let!(:user_detail_not_to_be_set) do + user_details.create!( + user_id: last_user.id, + onboarding_status: {} + ) + end + + subject(:migration) do + described_class.new( + start_id: user_detail.user_id, + end_id: user_detail_not_to_be_set.user_id, + batch_table: :user_details, + batch_column: :user_id, + sub_batch_size: 100, + pause_ms: 0, + connection: ApplicationRecord.connection + ) + end + + describe '#perform' do + it 'updates the correct data' do + migration.perform + + expect(user_detail.reload.onboarding_status).to eq({ 'setup_for_company' => true }) + expect(user_detail_to_change.reload.onboarding_status).to eq({ 'setup_for_company' => false }) + expect(user_detail_not_to_be_set.reload.onboarding_status).to eq({}) + end + end +end diff --git a/spec/migrations/20250228155146_queue_backfill_onboarding_status_setup_for_company_spec.rb b/spec/migrations/20250228155146_queue_backfill_onboarding_status_setup_for_company_spec.rb new file mode 100644 index 00000000000..7eabfa4b777 --- /dev/null +++ b/spec/migrations/20250228155146_queue_backfill_onboarding_status_setup_for_company_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe QueueBackfillOnboardingStatusSetupForCompany, migration: :gitlab_main, feature_category: :onboarding do + let!(:batched_migration) { described_class::MIGRATION } + + it 'schedules a new batched migration' do + reversible_migration do |migration| + migration.before -> { + expect(batched_migration).not_to have_scheduled_batched_migration + } + + migration.after -> { + expect(batched_migration).to have_scheduled_batched_migration( + table_name: :user_details, + column_name: :user_id, + interval: described_class::DELAY_INTERVAL, + batch_size: described_class::BATCH_SIZE, + sub_batch_size: described_class::SUB_BATCH_SIZE + ) + } + end + end +end diff --git a/spec/models/ci/job_token/authorization_spec.rb b/spec/models/ci/job_token/authorization_spec.rb index 3bfa5f09b5b..8d54aed0342 100644 --- a/spec/models/ci/job_token/authorization_spec.rb +++ b/spec/models/ci/job_token/authorization_spec.rb @@ -6,12 +6,68 @@ RSpec.describe Ci::JobToken::Authorization, feature_category: :secrets_managemen let_it_be(:origin_project) { create(:project) } let_it_be(:accessed_project) { create(:project) } let_it_be(:another_project) { create(:project) } + let_it_be(:policies) { [:read_jobs, :admin_environments] } describe 'associations' do it { is_expected.to belong_to(:origin_project).class_name('Project') } it { is_expected.to belong_to(:accessed_project).class_name('Project') } end + describe 'validating job_token_policies' do + subject(:auth_log) { described_class.new(job_token_policies: policies) } + + let(:valid_policies) do + schema = 'app/validators/json_schemas/ci_job_token_policies.json' + Gitlab::Json.parse(File.read(schema)).dig('items', 'enum') + end + + context 'when not assigning any policies' do + let(:policies) { {} } + + it { is_expected.to be_valid } + end + + context 'when assigning all valid policies with valid values' do + let(:policies) { valid_policies.index_with(Time.current) } + + it { is_expected.to be_valid } + end + + context 'when assigning an invalid policy with a valid value' do + let(:policies) { { invalid_policy: Time.current } } + + it { is_expected.to be_invalid } + + it 'contains a descriptive error' do + auth_log.valid? + expect(auth_log.errors[:job_token_policies]).to include("value at root is not one of: #{valid_policies}") + end + end + + context 'when assigning a valid policy with an invalid value' do + let(:policies) { { read_jobs: true } } + + it { is_expected.to be_invalid } + + it 'contains a descriptive error' do + auth_log.valid? + expect(auth_log.errors[:job_token_policies]).to include('value at `/read_jobs` is not a string') + end + end + + context 'when assigning a valid policy with a string that is not a datetime' do + let(:policies) { { read_jobs: 'not a date time string' } } + + it { is_expected.to be_invalid } + + it 'contains a descriptive error' do + auth_log.valid? + expect(auth_log.errors[:job_token_policies]) + .to include('value at `/read_jobs` does not match format: date-time') + end + end + end + describe '.capture', :request_store do subject(:capture) do described_class.capture(origin_project: origin_project, accessed_project: accessed_project) @@ -50,6 +106,41 @@ RSpec.describe Ci::JobToken::Authorization, feature_category: :secrets_managemen end end + describe '.capture_job_token_policies', :request_store do + subject(:capture_job_token_policies) { described_class.capture_job_token_policies(policies) } + + let(:policies) { [:read_environments, :read_jobs] } + + it 'captures the policies in the RequestStore' do + capture_job_token_policies + expect(described_class.captured_authorizations).to eq(policies: policies) + end + end + + describe '.add_to_request_store_hash', :request_store do + subject(:captured_authorizations) { described_class.captured_authorizations } + + let(:old_hash) { { old_value: 1 } } + let(:new_hash) { { new_value: 2 } } + + context 'when no values have been captured in the RequestStore yet' do + before do + described_class.add_to_request_store_hash(old_hash) + end + + it { is_expected.to eq(old_hash) } + end + + context 'when previous values have been captured in the RequestStore' do + before do + described_class.add_to_request_store_hash(old_hash) + described_class.add_to_request_store_hash(new_hash) + end + + it { is_expected.to eq(old_hash.merge(new_hash)) } + end + end + describe '.log_captures_async', :request_store do subject(:log_captures_async) do described_class.log_captures_async @@ -65,9 +156,9 @@ RSpec.describe Ci::JobToken::Authorization, feature_category: :secrets_managemen context 'when authorizations have been captured during the request' do before do - described_class.capture( - origin_project: origin_project, - accessed_project: accessed_project) + described_class.add_to_request_store_hash( + origin_project_id: origin_project&.id, + accessed_project_id: accessed_project&.id) end context 'when authorization is cross project' do @@ -79,8 +170,14 @@ RSpec.describe Ci::JobToken::Authorization, feature_category: :secrets_managemen end end - context 'when authorization is self-referential' do - let(:accessed_project) { origin_project } + context 'when the origin project is missing' do + let(:origin_project) { nil } + + it_behaves_like 'does not log the authorization' + end + + context 'when the accessed project is missing' do + let(:accessed_project) { nil } it_behaves_like 'does not log the authorization' end @@ -93,16 +190,23 @@ RSpec.describe Ci::JobToken::Authorization, feature_category: :secrets_managemen describe '.log_captures!' do subject(:log_captures) do - described_class.log_captures!(origin_project_id: origin_project.id, accessed_project_id: accessed_project.id) + described_class.log_captures!( + origin_project_id: origin_project.id, + accessed_project_id: accessed_project.id, + policies: policies) end context 'when authorization does not exist in the database' do - it 'creates a new authorization' do + it 'creates a new authorization', :freeze_time do expect { log_captures }.to change { described_class.count }.by(1) expect(described_class.last).to have_attributes( origin_project: origin_project, - accessed_project: accessed_project) + accessed_project: accessed_project, + job_token_policies: { + read_jobs: Time.current, + admin_environments: Time.current + }) end end @@ -111,12 +215,15 @@ RSpec.describe Ci::JobToken::Authorization, feature_category: :secrets_managemen create(:ci_job_token_authorization, origin_project: origin_project, accessed_project: accessed_project, - last_authorized_at: 1.day.ago) + last_authorized_at: 1.day.ago, + job_token_policies: { read_jobs: 1.day.ago }) end - it 'updates the timestamp instead of creating a new record' do + it 'updates the policies and the last_authorized_at timestamp instead of creating a new record', :freeze_time do expect { log_captures } - .to change { existing_authorization.reload.last_authorized_at } + .to change { existing_authorization.reload.last_authorized_at }.to(Time.current) + .and change { existing_authorization.job_token_policies.keys }.to(policies) + .and change { existing_authorization.job_token_policies[:read_jobs] }.to(Time.current) .and not_change { described_class.count } end end diff --git a/spec/models/ci/job_token/scope_spec.rb b/spec/models/ci/job_token/scope_spec.rb index 57666b50c6f..70a951df46c 100644 --- a/spec/models/ci/job_token/scope_spec.rb +++ b/spec/models/ci/job_token/scope_spec.rb @@ -230,17 +230,35 @@ RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, f end describe '#policies_allowed?' do - subject { scope.policies_allowed?(accessed_project, policies) } + subject(:policies_allowed?) { scope.policies_allowed?(accessed_project, policies) } let(:scope) { described_class.new(target_project) } let_it_be(:target_project) { create(:project) } let_it_be(:allowed_policy) { ::Ci::JobToken::Policies::POLICIES.first } let(:accessed_project) { create_inbound_accessible_project_for_policies(target_project, [allowed_policy]) } + shared_examples 'capturing job token policies' do + it 'captures job token policies' do + expect(::Ci::JobToken::Authorization).to receive(:capture_job_token_policies).with(policies) + + policies_allowed? + end + end + + shared_examples 'not capturing job token policies' do + it 'does not capture job token policies' do + expect(::Ci::JobToken::Authorization).not_to receive(:capture_job_token_policies) + + policies_allowed? + end + end + context 'when no policies are given' do let_it_be(:policies) { [] } it { is_expected.to be(false) } + + it_behaves_like 'not capturing job token policies' end context 'when the policies are defined in the scope' do @@ -248,6 +266,8 @@ RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, f it { is_expected.to be(true) } + it_behaves_like 'capturing job token policies' + context 'when an admin policy is defined in the scope' do let_it_be(:allowed_policy) { 'admin_jobs' } let_it_be(:policies) { [:admin_jobs, :read_jobs] } @@ -261,18 +281,24 @@ RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, f let(:accessed_project) { create(:project) } it { is_expected.to be(false) } + + it_behaves_like 'not capturing job token policies' end end - context 'when the policy are not defined in the scope' do + context 'when the policies are not defined in the scope' do let_it_be(:policies) { [:not_allowed_policy] } it { is_expected.to be(false) } + it_behaves_like 'capturing job token policies' + context 'when the accessed project is the target project' do let(:accessed_project) { target_project } it { is_expected.to be(true) } + + it_behaves_like 'not capturing job token policies' end context 'when the accessed project does not have ci_inbound_job_token_scope_enabled set to true' do @@ -281,12 +307,26 @@ RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, f end it { is_expected.to be(true) } + + it_behaves_like 'capturing job token policies' + end + + context 'when the `add_policies_to_ci_job_token` flag is disabled' do + before do + stub_feature_flags(add_policies_to_ci_job_token: false) + end + + it { is_expected.to be(true) } + + it_behaves_like 'capturing job token policies' end context 'when the accessed project has not enabled fine grained permissions' do let(:accessed_project) { create_inbound_accessible_project(target_project) } it { is_expected.to be(true) } + + it_behaves_like 'capturing job token policies' end end end diff --git a/spec/workers/ci/job_token/log_authorization_worker_spec.rb b/spec/workers/ci/job_token/log_authorization_worker_spec.rb index 8ef536de39b..3aa3d1e3086 100644 --- a/spec/workers/ci/job_token/log_authorization_worker_spec.rb +++ b/spec/workers/ci/job_token/log_authorization_worker_spec.rb @@ -7,15 +7,15 @@ RSpec.describe Ci::JobToken::LogAuthorizationWorker, feature_category: :secrets_ let_it_be(:origin_project) { build_stubbed(:project) } let_it_be(:accessed_project) { build_stubbed(:project) } - subject(:perform) { described_class.new.perform(accessed_project.id, origin_project.id) } + subject(:perform) { described_class.new.perform(*job_args) } it_behaves_like 'an idempotent worker' do - let(:job_args) { [accessed_project.id, origin_project.id] } + let(:job_args) { [accessed_project.id, origin_project.id, ['read_jobs']] } it 'calls Ci::JobToken::Authorization#log_captures!' do expect(Ci::JobToken::Authorization) .to receive(:log_captures!) - .with(accessed_project_id: accessed_project.id, origin_project_id: origin_project.id) + .with(accessed_project_id: accessed_project.id, origin_project_id: origin_project.id, policies: [:read_jobs]) .and_call_original perform