diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index d1d4aa01747..ae6fb8f4b91 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -2b6810dc5688ebdbf4024b4885fdc59686a3f1fa +47335750c521ec469a5074962fb28b48b9dc06a6 diff --git a/app/components/pajamas/avatar_component.html.haml b/app/components/pajamas/avatar_component.html.haml index 502f673fe2c..c89121036e4 100644 --- a/app/components/pajamas/avatar_component.html.haml +++ b/app/components/pajamas/avatar_component.html.haml @@ -1,12 +1,11 @@ - if src = image_tag src, srcset: srcset, - alt: alt, class: avatar_classes, height: @size, width: @size, loading: "lazy", - **@avatar_options + **avatar_attributes - else - %div{ @avatar_options, alt: alt, class: avatar_classes } + %div{ avatar_attributes, class: avatar_classes } = initial diff --git a/app/components/pajamas/avatar_component.rb b/app/components/pajamas/avatar_component.rb index d37e0135b58..347374854b7 100644 --- a/app/components/pajamas/avatar_component.rb +++ b/app/components/pajamas/avatar_component.rb @@ -70,10 +70,26 @@ module Pajamas "#{src} 1x, #{retina_src} 2x" end + def aria_hide? + return true if !@alt && !src + + aria_hidden = @avatar_options[:aria]&.dig(:hidden) + aria_hidden.to_s == "true" + end + def alt + return if aria_hide? + @alt || @item.try(:name) end + def avatar_attributes + attributes = @avatar_options.dup + attributes[:alt] = alt if alt + attributes[:aria] = (attributes[:aria] || {}).merge(hidden: "true") if aria_hide? + attributes + end + def initial @item.name[0, 1].upcase end diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 165c8e76ec5..d33989c56e1 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -180,6 +180,8 @@ module Projects end def define_runners_variables + return if Feature.enabled?(:vue_project_runners_settings, @project) + @project_runners = @project.runners.ordered.page(params[:project_page]).per(NUMBER_OF_RUNNERS_PER_PAGE).with_tags @assignable_runners = current_user diff --git a/app/graphql/mutations/ci/runner/unassign_from_project.rb b/app/graphql/mutations/ci/runner/unassign_from_project.rb index 23c5d28e2eb..c8c643ba55f 100644 --- a/app/graphql/mutations/ci/runner/unassign_from_project.rb +++ b/app/graphql/mutations/ci/runner/unassign_from_project.rb @@ -6,9 +6,7 @@ module Mutations class UnassignFromProject < BaseMutation graphql_name 'RunnerUnassignFromProject' - include FindsProject - - authorize :admin_runners + authorize :unassign_runner argument :runner_id, ::Types::GlobalIDType[::Ci::Runner], required: true, @@ -19,18 +17,23 @@ module Mutations description: 'Full path of the project from which the runner will be unassigned.' def resolve(**args) - project = authorized_find!(args[:project_path]) - runner_id = GitlabSchema.parse_gid(args[:runner_id], expected_type: ::Ci::Runner).model_id - runner_project = project.runner_projects.find_by_runner_id(runner_id) - - unless runner_project&.runner - raise_resource_not_available_error! "Runner does not exist or is not assigned to this project" - end + runner_project = find_object(**args.slice(:runner_id, :project_path)) + raise_resource_not_available_error! unless runner_project result = ::Ci::Runners::UnassignRunnerService.new(runner_project, current_user).execute { errors: result.errors } end + + private + + def find_object(runner_id:, project_path:) + project = Project.find_by_full_path(project_path) + runner_id = GitlabSchema.parse_gid(runner_id, expected_type: ::Ci::Runner).model_id + return unless project + + project.runner_projects.find_by_runner_id(runner_id) + end end end end diff --git a/app/policies/ci/runner_project_policy.rb b/app/policies/ci/runner_project_policy.rb new file mode 100644 index 00000000000..325d4c73dbd --- /dev/null +++ b/app/policies/ci/runner_project_policy.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Ci + class RunnerProjectPolicy < BasePolicy + condition(:locked, scope: :subject) { @subject.runner.locked? } + + condition(:assigned_to_owner_project, scope: :subject) { @subject.project == @subject.runner.owner } + + condition(:can_admin_project_runners, score: 2) do + Ability.allowed?(@user, :admin_runners, @subject.project) + end + + rule { anonymous }.prevent_all + + rule { can_admin_project_runners }.enable :unassign_runner + + rule { ~admin & locked }.prevent :unassign_runner + + rule { assigned_to_owner_project }.prevent :unassign_runner + end +end + +Ci::RunnerProjectPolicy.prepend_mod diff --git a/app/services/ci/runners/unassign_runner_service.rb b/app/services/ci/runners/unassign_runner_service.rb index 065e0db3712..b7888cd753a 100644 --- a/app/services/ci/runners/unassign_runner_service.rb +++ b/app/services/ci/runners/unassign_runner_service.rb @@ -28,11 +28,9 @@ module Ci attr_reader :runner, :project, :user def authorize - unless user.present? && user.can?(:assign_runner, runner) - return ServiceResponse.error(message: 'User not allowed to unassign runner') - end + return ServiceResponse.success if Ability.allowed?(user, :unassign_runner, @runner_project) - unless user.can?(:admin_runners, project) + unless Ability.allowed?(user, :admin_runners, project) return ServiceResponse.error(message: "User not allowed to manage project's runners") end @@ -42,7 +40,9 @@ module Ci ) end - ServiceResponse.success + return ServiceResponse.error(message: 'Runner is locked') if runner.locked && !user&.can_admin_all_resources? + + ServiceResponse.error(message: 'User not allowed to unassign runner') end end end diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml index 7c7715da11f..2e3a08f9497 100644 --- a/app/views/groups/_home_panel.html.haml +++ b/app/views/groups/_home_panel.html.haml @@ -14,7 +14,7 @@ .gl-flex.gl-justify-between.gl-flex-wrap.gl-flex-col.sm:gl-flex-row.gl-gap-3.gl-my-5 .home-panel-title-row.gl-flex - = render Pajamas::AvatarComponent.new(@group, alt: @group.name, size: 48, class: 'float-none gl-self-start gl-shrink-0 gl-mr-3', avatar_options: { itemprop: 'logo' }) + = render Pajamas::AvatarComponent.new(@group, size: 48, class: 'float-none gl-self-start gl-shrink-0 gl-mr-3', avatar_options: { itemprop: 'logo' }) %h1.home-panel-title.gl-heading-1.gl-flex.gl-items-center.gl-flex-wrap.gl-gap-3.gl-break-anywhere.gl-mb-0{ itemprop: 'name' } = @group.name %span.visibility-icon.gl-text-subtle.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) } diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 0938b909141..670749129de 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -3,7 +3,7 @@ %header.project-home-panel.js-show-on-project-root.gl-mt-5{ class: [("empty-project" if empty_repo)] } .gl-flex.gl-justify-between.gl-flex-wrap.gl-flex-col.md:gl-flex-row.gl-gap-5 .home-panel-title-row.gl-flex.gl-items-center - = render Pajamas::AvatarComponent.new(@project, alt: @project.name, class: 'gl-self-start gl-shrink-0 gl-mr-4', size: 48, avatar_options: { itemprop: 'image' }) + = render Pajamas::AvatarComponent.new(@project, class: 'gl-self-start gl-shrink-0 gl-mr-4', size: 48, avatar_options: { itemprop: 'image' }) %h1.home-panel-title.gl-heading-1.gl-flex.gl-items-center.gl-flex-wrap.gl-gap-3.gl-break-anywhere.gl-mb-0{ data: { testid: 'project-name-content' }, itemprop: 'name' } = @project.name = visibility_level_content(@project, css_class: 'visibility-icon gl-inline-flex', icon_variant: 'subtle') diff --git a/db/docs/batched_background_migrations/backfill_snippet_repositories_snippet_organization_id.yml b/db/docs/batched_background_migrations/backfill_snippet_repositories_snippet_organization_id.yml index 121472bfeee..9991133d692 100644 --- a/db/docs/batched_background_migrations/backfill_snippet_repositories_snippet_organization_id.yml +++ b/db/docs/batched_background_migrations/backfill_snippet_repositories_snippet_organization_id.yml @@ -1,8 +1,9 @@ --- migration_job_name: BackfillSnippetRepositoriesSnippetOrganizationId -description: Backfills sharding key `snippet_repositories.snippet_organization_id` from `snippets`. +description: Backfills sharding key `snippet_repositories.snippet_organization_id` + from `snippets`. feature_category: source_code_management introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/175410 milestone: '17.10' queued_migration_version: 20241211134715 -finalized_by: # version of the migration that finalized this BBM +finalized_by: '20250717232204' diff --git a/db/post_migrate/20250717232204_finalize_hk_backfill_snippet_repositories_snippet_organization_id.rb b/db/post_migrate/20250717232204_finalize_hk_backfill_snippet_repositories_snippet_organization_id.rb new file mode 100644 index 00000000000..98ebe08bc81 --- /dev/null +++ b/db/post_migrate/20250717232204_finalize_hk_backfill_snippet_repositories_snippet_organization_id.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class FinalizeHkBackfillSnippetRepositoriesSnippetOrganizationId < Gitlab::Database::Migration[2.3] + milestone '18.3' + + disable_ddl_transaction! + + restrict_gitlab_migration gitlab_schema: :gitlab_main_cell + + def up + ensure_batched_background_migration_is_finished( + job_class_name: 'BackfillSnippetRepositoriesSnippetOrganizationId', + table_name: :snippet_repositories, + column_name: :snippet_id, + job_arguments: [:snippet_organization_id, :snippets, :organization_id, :snippet_id], + finalize: true + ) + end + + def down; end +end diff --git a/db/schema_migrations/20250717232204 b/db/schema_migrations/20250717232204 new file mode 100644 index 00000000000..c2f8c26af39 --- /dev/null +++ b/db/schema_migrations/20250717232204 @@ -0,0 +1 @@ +243d4617b21078c793fae163d28ab38a74cded2dde6a1c72cf83ecc0a2150600 \ No newline at end of file diff --git a/doc/administration/geo/replication/datatypes.md b/doc/administration/geo/replication/datatypes.md index 02c36d0a2ac..53e21aa00b3 100644 --- a/doc/administration/geo/replication/datatypes.md +++ b/doc/administration/geo/replication/datatypes.md @@ -235,6 +235,7 @@ successfully, you must replicate their data using some other means. | [Dependency Proxy Images](../../../user/packages/dependency_proxy/_index.md) | [**Yes** (15.7)](https://gitlab.com/groups/gitlab-org/-/epics/8833) | [**Yes** (15.7)](https://gitlab.com/groups/gitlab-org/-/epics/8833) | [**Yes** (15.7)](https://gitlab.com/groups/gitlab-org/-/epics/8833) | [**Yes** (16.4)3](https://gitlab.com/groups/gitlab-org/-/epics/8056) | | | [Vulnerability Export](../../../user/application_security/vulnerability_report/_index.md#exporting) | [Not planned](https://gitlab.com/groups/gitlab-org/-/epics/3111) | No | No | No | Not planned because they are ephemeral and sensitive information. They can be regenerated on demand. | | Packages NPM metadata cache | [Not planned](https://gitlab.com/gitlab-org/gitlab/-/issues/408278) | No | No | No | Not planned because it would not notably improve disaster recovery capabilities nor response times at secondary sites. | +| SBOM Vulnerability Scan Data | [Not planned](https://gitlab.com/gitlab-org/gitlab/-/issues/398199) | No | No | No | Not planned because data is temporary and has a short lifespan with limited impact on disaster recovery capabilities at secondary sites. | **Footnotes**: diff --git a/lib/api/ci/runners.rb b/lib/api/ci/runners.rb index d205c9c8fe8..66e4c037916 100644 --- a/lib/api/ci/runners.rb +++ b/lib/api/ci/runners.rb @@ -90,6 +90,15 @@ module API forbidden!("No access granted") unless can?(current_user, :assign_runner, runner) end + def authenticate_disable_runner!(runner_project) + not_found!('Runner') unless runner_project + + return if current_user.can_admin_all_resources? + + forbidden!("Runner is locked") if runner_project.runner.locked? + forbidden!("No access granted") unless can?(current_user, :unassign_runner, runner_project) + end + def authenticate_list_runners_jobs!(runner) return if current_user.can_read_all_resources? @@ -357,10 +366,8 @@ module API requires :runner_id, type: Integer, desc: 'The ID of a runner' end delete ':id/runners/:runner_id' do - authorize! :admin_runners, user_project - runner_project = user_project.runner_projects.find_by_runner_id(params[:runner_id]) - not_found!('Runner') unless runner_project + authenticate_disable_runner!(runner_project) destroy_conditionally!(runner_project) do response = ::Ci::Runners::UnassignRunnerService.new(runner_project, current_user).execute diff --git a/spec/components/pajamas/avatar_component_spec.rb b/spec/components/pajamas/avatar_component_spec.rb index bc4329d2b4a..c18849dbe61 100644 --- a/spec/components/pajamas/avatar_component_spec.rb +++ b/spec/components/pajamas/avatar_component_spec.rb @@ -97,6 +97,11 @@ RSpec.describe Pajamas::AvatarComponent, type: :component, feature_category: :de expect(page).to have_css "div.gl-avatar.gl-avatar-identicon", text: item.name[0].upcase end + it "automatically sets aria-hidden to true and omits alt text for accessibility" do + expect(page).to have_css "div.gl-avatar.gl-avatar-identicon[aria-hidden]" + expect(page).not_to have_css "div.gl-avatar.gl-avatar-identicon[alt]" + end + context "when the item has no id" do let(:item) { build :group } @@ -150,6 +155,14 @@ RSpec.describe Pajamas::AvatarComponent, type: :component, feature_category: :de expect(page).to have_css ".gl-avatar[alt='#{item.name}']" end end + + context "when aria-hidden is true" do + let(:options) { { avatar_options: { aria: { hidden: true } } } } + + it "sets aria-hidden to true" do + expect(page).to have_css ".gl-avatar[aria-hidden='true']" + end + end end describe "class" do diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb index 0e4c45f2af1..e1ef738b240 100644 --- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb +++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb @@ -39,77 +39,83 @@ RSpec.describe Projects::Settings::CiCdController, feature_category: :continuous end end - context 'with assignable project runners' do - let(:project_runner) { create(:ci_runner, :project, projects: [other_project]) } - + context 'when vue_project_runners_settings is disabled' do before do - group.add_maintainer(user) + stub_feature_flags(vue_project_runners_settings: false) end - it 'sets assignable project runners' do - request + context 'with assignable project runners' do + let(:project_runner) { create(:ci_runner, :project, projects: [other_project]) } - expect(assigns(:assignable_runners)).to contain_exactly(project_runner) - end - end + before do + group.add_maintainer(user) + end - context 'with project runners' do - let(:project_runner) { create(:ci_runner, :project, projects: [project]) } + it 'sets assignable project runners' do + request - it 'sets project runners' do - request - - expect(assigns(:project_runners)).to contain_exactly(project_runner) - end - end - - context 'with group runners' do - let(:project) { other_project } - let!(:group_runner) { create(:ci_runner, :group, groups: [group]) } - - it 'sets group runners' do - request - - expect(assigns(:group_runners_count)).to be(1) - expect(assigns(:group_runners)).to contain_exactly(group_runner) - end - end - - context 'with instance runners' do - let_it_be(:shared_runner) { create(:ci_runner, :instance) } - - it 'sets shared runners' do - request - - expect(assigns(:shared_runners_count)).to be(1) - expect(assigns(:shared_runners)).to contain_exactly(shared_runner) - end - end - - context 'prevents N+1 queries for tags' do - render_views - - def show - get :show, params: { namespace_id: project.namespace, project_id: project } + expect(assigns(:assignable_runners)).to contain_exactly(project_runner) + end end - it 'has the same number of queries with one tag or with many tags', :request_store do - group.add_maintainer(user) + context 'with project runners' do + let(:project_runner) { create(:ci_runner, :project, projects: [project]) } - show # warmup + it 'sets project runners' do + request - # with one tag - create(:ci_runner, :instance, tag_list: %w[shared_runner]) - create(:ci_runner, :project, projects: [other_project], tag_list: %w[project_runner]) - create(:ci_runner, :group, groups: [group], tag_list: %w[group_runner]) - control = ActiveRecord::QueryRecorder.new { show } + expect(assigns(:project_runners)).to contain_exactly(project_runner) + end + end - # with several tags - create(:ci_runner, :instance, tag_list: %w[shared_runner tag2 tag3]) - create(:ci_runner, :project, projects: [other_project], tag_list: %w[project_runner tag2 tag3]) - create(:ci_runner, :group, groups: [group], tag_list: %w[group_runner tag2 tag3]) + context 'with group runners' do + let(:project) { other_project } + let!(:group_runner) { create(:ci_runner, :group, groups: [group]) } - expect { show }.not_to exceed_query_limit(control) + it 'sets group runners' do + request + + expect(assigns(:group_runners_count)).to be(1) + expect(assigns(:group_runners)).to contain_exactly(group_runner) + end + end + + context 'with instance runners' do + let_it_be(:shared_runner) { create(:ci_runner, :instance) } + + it 'sets shared runners' do + request + + expect(assigns(:shared_runners_count)).to be(1) + expect(assigns(:shared_runners)).to contain_exactly(shared_runner) + end + end + + context 'prevents N+1 queries for tags' do + render_views + + def show + get :show, params: { namespace_id: project.namespace, project_id: project } + end + + it 'has the same number of queries with one tag or with many tags', :request_store do + group.add_maintainer(user) + + show # warmup + + # with one tag + create(:ci_runner, :instance, tag_list: %w[shared_runner]) + create(:ci_runner, :project, projects: [other_project], tag_list: %w[project_runner]) + create(:ci_runner, :group, groups: [group], tag_list: %w[group_runner]) + control = ActiveRecord::QueryRecorder.new { show } + + # with several tags + create(:ci_runner, :instance, tag_list: %w[shared_runner tag2 tag3]) + create(:ci_runner, :project, projects: [other_project], tag_list: %w[project_runner tag2 tag3]) + create(:ci_runner, :group, groups: [group], tag_list: %w[group_runner tag2 tag3]) + + expect { show }.not_to exceed_query_limit(control) + end end end diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb index 129a69c1d65..4e861486158 100644 --- a/spec/helpers/avatars_helper_spec.rb +++ b/spec/helpers/avatars_helper_spec.rb @@ -426,7 +426,7 @@ RSpec.describe AvatarsHelper, feature_category: :source_code_management do it 'displays group avatar' do expected_pattern = %r{