Update from merge request

This commit is contained in:
root
2025-07-18 12:50:16 +00:00
parent ce2b1f9fcc
commit 68c14ae3d4
20 changed files with 296 additions and 94 deletions

View File

@ -1 +1 @@
2b6810dc5688ebdbf4024b4885fdc59686a3f1fa
47335750c521ec469a5074962fb28b48b9dc06a6

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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')

View File

@ -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'

View File

@ -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

View File

@ -0,0 +1 @@
243d4617b21078c793fae163d28ab38a74cded2dde6a1c72cf83ecc0a2150600

View File

@ -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)<sup>3</sup>](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**:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -426,7 +426,7 @@ RSpec.describe AvatarsHelper, feature_category: :source_code_management do
it 'displays group avatar' do
expected_pattern = %r{
<div\s+
alt="foo"\s+
aria-hidden="true"\s+
class="gl-avatar\s+
gl-avatar-s32\s+
gl-avatar-circle\s+

View File

@ -0,0 +1,107 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::RunnerProjectPolicy, feature_category: :runner do
using RSpec::Parameterized::TableSyntax
let_it_be(:project) { create(:project) }
let_it_be(:other_project) { create(:project) }
let_it_be(:developer_project) { create(:project) }
let_it_be(:runner) { create(:ci_runner, :project, projects: [project, other_project, developer_project]) }
let_it_be(:owner_runner_project) { project.runner_projects.first }
let_it_be(:member_runner_project) { other_project.runner_projects.first }
let_it_be(:developer_runner_project) { developer_project.runner_projects.first }
let_it_be(:owner) { create(:user, owner_of: [project, other_project]) }
let_it_be(:maintainer) { create(:user, maintainer_of: [project, other_project], developer_of: developer_project) }
let_it_be(:developer) { create(:user, developer_of: [project, other_project]) }
let_it_be(:reporter) { create(:user, reporter_of: [project, other_project]) }
let_it_be(:guest) { create(:user, guest_of: [project, other_project]) }
let_it_be(:non_member) { create(:user) }
let_it_be(:admin) { create(:admin) }
let_it_be(:anonymous) { nil }
let_it_be(:locked_runner) { create(:ci_runner, :project, :locked, projects: [project, other_project]) }
let_it_be(:locked_runner_project) { locked_runner.runner_projects.last }
let(:runner_project) { member_runner_project }
subject(:policy) { described_class.new(user, runner_project) }
describe 'ability :unassign_runner' do
shared_examples 'unassign_runner for user with project access' do |allowed:|
if allowed
it { is_expected.to be_allowed(:unassign_runner) }
else
it { is_expected.to be_disallowed(:unassign_runner) }
end
context 'when developer of project' do
let(:runner_project) { developer_runner_project }
it { is_expected.to be_disallowed(:unassign_runner) }
end
context 'with locked runner' do
let(:runner_project) { locked_runner_project }
it { is_expected.to be_disallowed(:unassign_runner) }
end
context 'with owner runner project (assigned to owner project)' do
let(:runner_project) { owner_runner_project }
it { is_expected.to be_disallowed(:unassign_runner) }
end
end
where(:user) do
[
ref(:anonymous),
ref(:non_member),
ref(:guest),
ref(:reporter),
ref(:developer)
]
end
with_them do
it { is_expected.to be_disallowed(:unassign_runner) }
end
context 'when user is maintainer' do
let(:user) { maintainer }
it_behaves_like 'unassign_runner for user with project access', allowed: true
end
context 'when user is owner' do
let(:user) { owner }
it_behaves_like 'unassign_runner for user with project access', allowed: true
end
context 'when user is admin' do
let(:user) { admin }
it { is_expected.to be_disallowed(:unassign_runner) }
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed(:unassign_runner) }
context 'with locked runner' do
let(:runner_project) { locked_runner_project }
it { is_expected.to be_allowed(:unassign_runner) }
end
context 'with owner runner project (assigned to owner project)' do
let(:runner_project) { owner_runner_project }
it { is_expected.to be_disallowed(:unassign_runner) }
end
end
end
end
end

View File

@ -31,7 +31,7 @@ RSpec.describe Mutations::Ci::Runner::UnassignFromProject, feature_category: :ru
let(:mutation_response) { graphql_mutation_response(:runner_unassign_from_project) }
specify { expect(described_class).to require_graphql_authorizations(:admin_runners) }
specify { expect(described_class).to require_graphql_authorizations(:unassign_runner) }
context 'with invalid parameters' do
context 'when project_path is missing' do
@ -85,7 +85,8 @@ RSpec.describe Mutations::Ci::Runner::UnassignFromProject, feature_category: :ru
it 'returns an error' do
post_graphql_mutation(mutation, current_user: group_owner)
expect_graphql_errors_to_include("Runner does not exist or is not assigned to this project")
expect_graphql_errors_to_include("The resource that you are attempting to access does not exist or you " \
"don't have permission to perform this action")
end
end
end
@ -94,8 +95,8 @@ RSpec.describe Mutations::Ci::Runner::UnassignFromProject, feature_category: :ru
context 'when user does not have necessary permissions' do
it 'does not allow non-accessible user to unassign a runner from a project' do
post_graphql_mutation(mutation, current_user: non_accessible_user)
expect_graphql_errors_to_include("The resource that you are attempting to access does not exist or you " \
"don't have permission to perform this action")
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['errors']).to include("User not allowed to manage project's runners")
expect(runner.reload.projects).to include(project)
end
end
@ -139,8 +140,9 @@ RSpec.describe Mutations::Ci::Runner::UnassignFromProject, feature_category: :ru
it 'returns an error' do
post_graphql_mutation(mutation, current_user: group_owner)
expect(response).to have_gitlab_http_status(:success)
expect_graphql_errors_to_include("Runner does not exist or is not assigned to this project")
expect_graphql_errors_to_include("The resource that you are attempting to access does not exist or you " \
"don't have permission to perform this action")
expect(runner.reload.projects).to include(project)
end
end

View File

@ -21,7 +21,7 @@ RSpec.describe ::Ci::Runners::UnassignRunnerService, '#execute', :aggregate_fail
expect { execute }.not_to change { runner.runner_projects.count }.from(2)
expect(execute).to be_error
expect(execute.message).to eq('User not allowed to unassign runner')
expect(execute.message).to eq("User not allowed to manage project's runners")
end
end
@ -32,7 +32,7 @@ RSpec.describe ::Ci::Runners::UnassignRunnerService, '#execute', :aggregate_fail
expect(runner_project).not_to receive(:destroy)
expect(execute).to be_error
expect(execute.message).to eq('User not allowed to unassign runner')
expect(execute.message).to eq("User not allowed to manage project's runners")
end
end