Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-11-13 15:29:32 +00:00
parent d9b3085d45
commit d4a47f4e96
74 changed files with 1206 additions and 275 deletions

View File

@ -18,4 +18,4 @@ variables:
# Retry failed specs in separate process
QA_RETRY_FAILED_SPECS: "true"
# helm chart ref used by test-on-cng pipeline
GITLAB_HELM_CHART_REF: "38b68934edec88e8f8a816338d815cc2abdc8e03"
GITLAB_HELM_CHART_REF: "435313a4c06496fb81a1b00cb6970e9cdc536e41"

View File

@ -27,21 +27,6 @@
"rules": {
"gitlab/no-gl-class": null
}
},
{
"files": [
"app/assets/builds/tailwind.css"
],
"rules": {
"selector-class-pattern": null,
"gitlab/no-gl-class": null,
"length-zero-no-unit": null,
"property-no-vendor-prefix": null,
"value-no-vendor-prefix": null,
"scss/at-rule-no-unknown": null,
"tailwind/disallow-max-width-media-query": true
},
"reportNeedlessDisables": false
}
]
}

View File

@ -2,6 +2,29 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 17.5.2 (2024-11-12)
### Fixed (4 changes)
- [Fix group wiki activity events breaking the user feed](https://gitlab.com/gitlab-org/security/gitlab/-/commit/2c10d817d961bf6ae229fb436126713d0199aece)
- [Add param filtering to avoid error while saving project settings](https://gitlab.com/gitlab-org/security/gitlab/-/commit/7e1bf6aa4087c0789ecff48ca716b30d841a3140) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171554)) **GitLab Enterprise Edition**
- [Fix new project group templates pagination](https://gitlab.com/gitlab-org/security/gitlab/-/commit/3fed777c0e1f52816206b546f2063043febedd0b) **GitLab Enterprise Edition**
- [Update pdf worker file path in pdf viewer](https://gitlab.com/gitlab-org/security/gitlab/-/commit/406b66e9140b4ee4e79edc84e2870e0fbb90d149)
### Security (7 changes)
- [Add missing project_id for build_chat_data](https://gitlab.com/gitlab-org/security/gitlab/-/commit/5a4e1bd3443cc786ab7558b1d6fa77962318c173) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4602))
- [Use custom adapter for parsing FogBugz XML](https://gitlab.com/gitlab-org/security/gitlab/-/commit/f8c4b8942e6fca667c6a2b975d9fa792b0d559fa) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4592))
- [Removed id from authorize buttons and added specs](https://gitlab.com/gitlab-org/security/gitlab/-/commit/7e9ac80271a0c8a7ed73f1cb4a34f053652f07f6) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4573))
- [HTML injection in vulnerability Code flow leads to XSS on self hosted instances](https://gitlab.com/gitlab-org/security/gitlab/-/commit/fbff5c445ecc99f438ab56a0c5add0ff5cd1e2aa) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4564))
- [Remove is-unsafe-link from product analytics tables to prevent XSS](https://gitlab.com/gitlab-org/security/gitlab/-/commit/605d8bf88e03ec6f447141049952b623eab2200c) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4579))
- [Details of blocking merge request can be exposed via list](https://gitlab.com/gitlab-org/security/gitlab/-/commit/0fe3d3020954f79337b6138e7b1ee6baed346c3c) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4545))
- [Prevent agent access via unconfirmed or disallowed group members](https://gitlab.com/gitlab-org/security/gitlab/-/commit/fa41ba0bc926e7b0091e4fb1cb6298b0b86eace5) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4559))
### Performance (1 change)
- [Remove permissions JSONB column from the condition](https://gitlab.com/gitlab-org/security/gitlab/-/commit/a5b902c35e60e36f3e98db2af221976093fe2278)
## 17.5.1 (2024-10-22)
### Security (2 changes)
@ -733,6 +756,28 @@ entry.
- [Adjust signup page items for more clarity](https://gitlab.com/gitlab-org/gitlab/-/commit/e272c8a4c7b243758454d6f15363d0c13ca05c04) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/165202)) **GitLab Enterprise Edition**
- [Removes Unused CSS class](https://gitlab.com/gitlab-org/gitlab/-/commit/4e17154650ee4afc8b1ae4238d27efb908855a19) by @NIKU-SINGH ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/164637))
## 17.4.4 (2024-11-12)
### Fixed (4 changes)
- [Fix bug where car left after branch deletion](https://gitlab.com/gitlab-org/security/gitlab/-/commit/d88a8a2b0d5a864220e7ca612a73433fb61aa1e7) **GitLab Enterprise Edition**
- [Ensure auto_merge_enabled is set when validating merge trains](https://gitlab.com/gitlab-org/security/gitlab/-/commit/ec63d25c51b5e129ab9b8fea6c8bb5730ca1ff81) **GitLab Enterprise Edition**
- [Update pdf worker file path in pdf viewer](https://gitlab.com/gitlab-org/security/gitlab/-/commit/bd1436d5e7900ac7ca815302b5bbd8297e43c52d)
- [Security patch upgrade alert: Only expose to admins](https://gitlab.com/gitlab-org/security/gitlab/-/commit/6e852f3bde76486452977159f9597b1947ee84b3)
### Security (6 changes)
- [Use custom adapter for parsing FogBugz XML](https://gitlab.com/gitlab-org/security/gitlab/-/commit/d8cf278590e2f1b496fe7cec05bd58b8adf0703b) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4593))
- [Removed id from authorize buttons and added specs](https://gitlab.com/gitlab-org/security/gitlab/-/commit/577432b6e46b9cd6edd4e00a4667e249406f1026) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4574))
- [HTML injection in vulnerability Code flow leads to XSS on self hosted instances](https://gitlab.com/gitlab-org/security/gitlab/-/commit/24eaacb474ad08e0bcd41b6f5a1cdada51ca8d7f) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4565))
- [Remove is-unsafe-link from product analytics tables to prevent XSS](https://gitlab.com/gitlab-org/security/gitlab/-/commit/6ed52422fcfb1b5ab6702a57df0d564bb552472b) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4580))
- [Details of blocking merge request can be exposed via list](https://gitlab.com/gitlab-org/security/gitlab/-/commit/4d5b45a67287865c3e9a80f27755c05c46ae2bea) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4526))
- [Prevent agent access via unconfirmed or disallowed group members](https://gitlab.com/gitlab-org/security/gitlab/-/commit/e8fd87425e9c7d045986bc50b6f9e401eb695b95) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4560))
### Performance (1 change)
- [Remove permissions JSONB column from the condition](https://gitlab.com/gitlab-org/security/gitlab/-/commit/2f2ae57d46d3774cd483adcb8651c7bc52b2e67c)
## 17.4.3 (2024-10-22)
### Fixed (1 change)
@ -1635,6 +1680,17 @@ entry.
- [Update learn more link and docs formatting](https://gitlab.com/gitlab-org/gitlab/-/commit/6f536fdb20c2d2b96124afe693042c91483a32b2) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/164889))
## 17.3.7 (2024-11-12)
### Security (6 changes)
- [Use custom adapter for parsing FogBugz XML](https://gitlab.com/gitlab-org/security/gitlab/-/commit/8952776336f65ba2f7a182cb42e6714f4f17b97b) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4594))
- [Removed id from authorize buttons and added specs](https://gitlab.com/gitlab-org/security/gitlab/-/commit/5f2a1b9a8cd823901e1184177fa55d43f20a3200) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4575))
- [HTML injection in vulnerability Code flow leads to XSS on self hosted instances](https://gitlab.com/gitlab-org/security/gitlab/-/commit/59ac206c9475b5713e8aee79dffad95fda802384) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4566))
- [Remove is-unsafe-link from product analytics tables to prevent XSS](https://gitlab.com/gitlab-org/security/gitlab/-/commit/1420ca36c7c8fa50949d934ee9eb8a1a2dc3d6a5) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4581))
- [Details of blocking merge request can be exposed via list](https://gitlab.com/gitlab-org/security/gitlab/-/commit/aa81586dd7ca7fa7fc2d5c4b74b8d5971c573df7) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4527))
- [Prevent agent access via unconfirmed or disallowed group members](https://gitlab.com/gitlab-org/security/gitlab/-/commit/58ddb6195652c2d04fb90db5b53889273090c18c) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4561))
## 17.3.6 (2024-10-22)
### Security (2 changes)

View File

@ -1 +1 @@
7608c9704fb3dd503b9a7c18ea8d1a7ed6bdd02f
839d9cc922e06ce778a0b4b8569a42f5d7a4b9d1

View File

@ -1 +1 @@
59a9a4683512829b6ec9c9819b78d42e8e02f091
0bd021b3916f1cf7ab43fbb0e327292bb2e15510

View File

@ -12,7 +12,6 @@ import HelpPageLink from '~/vue_shared/components/help_page_link/help_page_link.
import createProtectionRuleMutation from '~/packages_and_registries/settings/project/graphql/mutations/create_container_protection_rule.mutation.graphql';
import { s__, __ } from '~/locale';
const GRAPHQL_ACCESS_LEVEL_VALUE_NULL = null;
const GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER = 'MAINTAINER';
const GRAPHQL_ACCESS_LEVEL_VALUE_OWNER = 'OWNER';
const GRAPHQL_ACCESS_LEVEL_VALUE_ADMIN = 'ADMIN';
@ -42,7 +41,6 @@ export default {
protectionRuleFormData: {
repositoryPathPattern: '',
minimumAccessLevelForPush: GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER,
minimumAccessLevelForDelete: GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER,
},
updateInProgress: false,
alertErrorMessages: [],
@ -66,12 +64,10 @@ export default {
projectPath: this.projectPath,
repositoryPathPattern: this.protectionRuleFormData.repositoryPathPattern,
minimumAccessLevelForPush: this.protectionRuleFormData.minimumAccessLevelForPush,
minimumAccessLevelForDelete: this.protectionRuleFormData.minimumAccessLevelForDelete,
};
},
minimumAccessLevelOptions() {
return [
{ value: GRAPHQL_ACCESS_LEVEL_VALUE_NULL, text: __('Developer (default)') },
{ value: GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER, text: __('Maintainer') },
{ value: GRAPHQL_ACCESS_LEVEL_VALUE_OWNER, text: __('Owner') },
{ value: GRAPHQL_ACCESS_LEVEL_VALUE_ADMIN, text: __('Admin') },
@ -170,19 +166,6 @@ export default {
/>
</gl-form-group>
<gl-form-group
:label="s__('ContainerRegistry|Minimum access level for delete')"
label-for="input-minimum-access-level-for-delete"
:disabled="isFieldDisabled"
>
<gl-form-select
id="input-minimum-access-level-for-delete"
v-model="protectionRuleFormData.minimumAccessLevelForDelete"
:options="minimumAccessLevelOptions"
:disabled="isFieldDisabled"
/>
</gl-form-group>
<div class="gl-flex gl-justify-start">
<gl-button
variant="confirm"

View File

@ -22,9 +22,6 @@ import { s__, __ } from '~/locale';
const PAGINATION_DEFAULT_PER_PAGE = 10;
const I18N_MINIMUM_ACCESS_LEVEL_FOR_PUSH = s__('ContainerRegistry|Minimum access level for push');
const I18N_MINIMUM_ACCESS_LEVEL_FOR_DELETE = s__(
'ContainerRegistry|Minimum access level for delete',
);
export default {
components: {
@ -48,7 +45,7 @@ export default {
i18n: {
settingBlockTitle: s__('ContainerRegistry|Protected containers'),
settingBlockDescription: s__(
'ContainerRegistry|When a container is protected, only certain user roles can push and delete the protected container image, which helps to avoid tampering with the container image.',
'ContainerRegistry|When a container is protected, only certain user roles can push the protected container image, which helps to avoid tampering with the container image.',
),
protectionRuleDeletionConfirmModal: {
title: s__('ContainerRegistry|Delete container protection rule?'),
@ -60,7 +57,6 @@ export default {
),
},
minimumAccessLevelForPush: I18N_MINIMUM_ACCESS_LEVEL_FOR_PUSH,
minimumAccessLevelForDelete: I18N_MINIMUM_ACCESS_LEVEL_FOR_DELETE,
},
apollo: {
protectionRulesQueryPayload: {
@ -94,7 +90,6 @@ export default {
return this.protectionRulesQueryResult.map((protectionRule) => {
return {
id: protectionRule.id,
minimumAccessLevelForDelete: protectionRule.minimumAccessLevelForDelete,
minimumAccessLevelForPush: protectionRule.minimumAccessLevelForPush,
repositoryPathPattern: protectionRule.repositoryPathPattern,
};
@ -210,11 +205,6 @@ export default {
minimumAccessLevelForPush: protectionRule.minimumAccessLevelForPush,
});
},
updateProtectionRuleMinimumAccessLevelForDelete(protectionRule) {
this.updateProtectionRule(protectionRule, {
minimumAccessLevelForDelete: protectionRule.minimumAccessLevelForDelete,
});
},
updateProtectionRule(protectionRule, updateData) {
this.clearAlertMessage();
@ -259,11 +249,6 @@ export default {
label: I18N_MINIMUM_ACCESS_LEVEL_FOR_PUSH,
tdClass: '!gl-align-middle',
},
{
key: 'minimumAccessLevelForDelete',
label: I18N_MINIMUM_ACCESS_LEVEL_FOR_DELETE,
tdClass: '!gl-align-middle',
},
{
key: 'rowActions',
label: __('Actions'),
@ -326,18 +311,6 @@ export default {
/>
</template>
<template #cell(minimumAccessLevelForDelete)="{ item }">
<gl-form-select
v-model="item.minimumAccessLevelForDelete"
class="gl-max-w-34"
required
:aria-label="$options.i18n.minimumAccessLevelForDelete"
:options="minimumAccessLevelOptions"
:disabled="isProtectionRuleMinimumAccessLevelForPushFormSelectDisabled(item)"
@change="updateProtectionRuleMinimumAccessLevelForDelete(item)"
/>
</template>
<template #cell(rowActions)="{ item }">
<gl-button
v-gl-tooltip

View File

@ -4,7 +4,6 @@ mutation createContainerProtectionRule($input: CreateContainerRegistryProtection
id
repositoryPathPattern
minimumAccessLevelForPush
minimumAccessLevelForDelete
}
errors
}

View File

@ -6,7 +6,6 @@ mutation deleteContainerRegistryProtectionRule(
id
repositoryPathPattern
minimumAccessLevelForPush
minimumAccessLevelForDelete
}
errors
}

View File

@ -6,7 +6,6 @@ mutation updateContainerRegistryProtectionRule(
id
repositoryPathPattern
minimumAccessLevelForPush
minimumAccessLevelForDelete
}
errors
}

View File

@ -14,7 +14,6 @@ query getProjectContainerProtectionRules(
id
repositoryPathPattern
minimumAccessLevelForPush
minimumAccessLevelForDelete
}
pageInfo {
...PageInfo

View File

@ -119,6 +119,12 @@ export default {
return this.currentTab === 0 && !this.showEmptyState;
},
},
mounted() {
document.addEventListener('visibilitychange', this.handleVisibilityChanged);
},
beforeDestroy() {
document.removeEventListener('visibilitychange', this.handleVisibilityChanged);
},
methods: {
nextPage(item) {
this.cursor = {
@ -152,6 +158,11 @@ export default {
this.alert?.dismiss();
this.queryFilterValues = { ...data };
},
handleVisibilityChanged() {
if (!document.hidden) {
this.updateAllQueries();
}
},
async handleItemChanged(id, markedAsDone) {
await this.updateAllQueries(false);
this.showUndoToast(id, markedAsDone);

View File

@ -697,6 +697,12 @@
color: $gl-text-color-secondary;
}
}
/* Vertically align badges with icons */
.gl-badge:has(> svg) {
position: relative;
bottom: -3px;
}
}
/**

View File

@ -4,6 +4,8 @@ class Import::FogbugzController < Import::BaseController
extend ::Gitlab::Utils::Override
before_action :verify_fogbugz_import_enabled
before_action -> { check_rate_limit!(:fogbugz_import, scope: current_user, redirect_back: true) }, only: :callback
before_action :user_map, only: [:new_user_map, :create_user_map]
before_action :verify_blocked_uri, only: :callback

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
module Resolvers
module WorkItems
class DescriptionTemplatesResolver < BaseResolver
include LooksAhead
type Types::WorkItems::TypeType.connection_type, null: true
argument :name, GraphQL::Types::String,
required: false,
description: "Fetches the specific DescriptionTemplate."
argument :search, GraphQL::Types::String,
required: false,
description: "Search for DescriptionTemplates by name."
def resolve_with_lookahead(**_args)
# TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/501097
[]
end
end
end
end

View File

@ -18,7 +18,7 @@ module Types
description: 'Human-readable display name for the access level.'
def human_access
::Gitlab::Access.human_access(object)
::Gitlab::Access.human_access_with_none(object)
end
end
end

View File

@ -9,11 +9,11 @@ module Types
Gitlab::Access.option_descriptions
end
value 'GUEST', value: Gitlab::Access::GUEST, description: descriptions[:guest]
value 'REPORTER', value: Gitlab::Access::REPORTER, description: descriptions[:reporter]
value 'DEVELOPER', value: Gitlab::Access::DEVELOPER, description: descriptions[:developer]
value 'MAINTAINER', value: Gitlab::Access::MAINTAINER, description: descriptions[:maintainer]
value 'OWNER', value: Gitlab::Access::OWNER, description: descriptions[:owner]
value 'GUEST', value: Gitlab::Access::GUEST, description: descriptions[Gitlab::Access::GUEST]
value 'REPORTER', value: Gitlab::Access::REPORTER, description: descriptions[Gitlab::Access::REPORTER]
value 'DEVELOPER', value: Gitlab::Access::DEVELOPER, description: descriptions[Gitlab::Access::DEVELOPER]
value 'MAINTAINER', value: Gitlab::Access::MAINTAINER, description: descriptions[Gitlab::Access::MAINTAINER]
value 'OWNER', value: Gitlab::Access::OWNER, description: descriptions[Gitlab::Access::OWNER]
end
end

View File

@ -114,6 +114,12 @@ module Types
method: :itself,
experiment: { milestone: '17.6' }
field :work_item_description_templates,
Types::WorkItems::DescriptionTemplateType.connection_type,
resolver: Resolvers::WorkItems::DescriptionTemplatesResolver,
null: true, experiment: { milestone: '17.6' },
description: 'Work item description templates available to the namespace.'
markdown_field :description_html, null: true
def achievements_path

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
module Types
module WorkItems
class DescriptionTemplateType < BaseObject
graphql_name 'WorkItemDescriptionTemplate'
authorize :read_work_item
field :content, GraphQL::Types::String,
description: 'Content of Description Template.', null: false
field :name, GraphQL::Types::String,
description: 'Name of Description Template.', null: false
end
end
end

View File

@ -17,6 +17,15 @@ module Ci
validates :component, :catalog_resource, :component_project, :used_by_project_id, presence: true
validates :last_used_date, uniqueness: { scope: [:component_id, :used_by_project_id] }, presence: true
def self.get_usage_for(component, used_by_project)
Ci::Catalog::Resources::Components::LastUsage.find_or_initialize_by(
component: component,
catalog_resource: component.catalog_resource,
component_project: component.project,
used_by_project_id: used_by_project.id
)
end
end
end
end

View File

@ -57,10 +57,10 @@ module Clusters
end
def groups_with_direct_membership_for(user)
::Group.joins("INNER JOIN members ON " \
"members.source_id = namespaces.id AND members.source_type = 'Namespace'")
.where(members: { user_id: user.id, access_level: Gitlab::Access::DEVELOPER.. })
.select('namespaces.id AS id, members.access_level AS access_level')
user
.groups_with_active_memberships
.merge(GroupMember.by_access_level(Gitlab::Access::DEVELOPER..))
.select('namespaces.id AS id, members.access_level AS access_level')
end
end
end

View File

@ -49,7 +49,9 @@ class MemberPresenter < Gitlab::View::Presenter::Delegated
raise NotImplementedError
end
def member_role_description; end
def member_role_description
member.role_description
end
private

View File

@ -40,7 +40,7 @@ class MemberEntity < Grape::Entity
end
expose :access_level do
expose :human_access, as: :string_value
expose :human_access_with_none, as: :string_value
expose :access_level, as: :integer_value
expose :member_role_id
expose :member_role_description, as: :description

View File

@ -19,12 +19,22 @@ module Ci
used_by_project_id: used_by_project.id
)
component_last_usage = Ci::Catalog::Resources::Components::LastUsage.get_usage_for(component, used_by_project)
if component_last_usage.new_record?
component_last_usage.last_used_date = Time.current.to_date
else
component_last_usage.touch(:last_used_date)
end
component_last_usage.save # Save last usage regardless of component_usage
if component_usage.save
ServiceResponse.success(message: 'Usage recorded')
else
errors = component_usage.errors
errors = component_usage.errors || component_last_usage.errors
if errors.size == 1 && errors.first.type == :taken # Only unique validation failed
if errors.size == 1 && errors.first.type == :taken
ServiceResponse.success(message: 'Usage already recorded for today')
else
exception = ValidationError.new(errors.full_messages.join(', '))

View File

@ -28,5 +28,5 @@
.div
= render Pajamas::ButtonComponent.new(type: :submit,
variant: :confirm,
button_options: { id: 'commit-changes', testid: 'authorization-button'}) do
button_options: {data: { testid: 'authorization-button'} }) do
= s_('DeviceAuth|Confirm')

View File

@ -11,5 +11,5 @@
.div
= render Pajamas::ButtonComponent.new(type: :submit,
variant: :confirm,
button_options: { id: 'commit-changes', testid: 'authorization-button'}) do
button_options: { data: { testid: 'authorization-button' }}) do
= s_('DeviceAuth|Authorize')

View File

@ -2,11 +2,11 @@
- page_title _('Group members')
- if can_admin_group_member?(@group)
= render_if_exists 'groups/group_members/link_to_pending_members'
= render ::Layouts::PageHeadingComponent.new(_('Group members')) do |c|
- c.with_description do
= group_member_header_subtext(@group)
- c.with_actions do
= render_if_exists 'groups/group_members/link_to_pending_members'
- if current_appearance&.member_guidelines?
.gl-w-full.order-md-1
= brand_member_guidelines

View File

@ -290,9 +290,6 @@ group_security_exclusions:
column: group_id
on_delete: async_delete
group_type_ci_runners_e59bb2812d:
- table: users
column: creator_id
on_delete: async_nullify
- table: namespaces
column: sharding_key_id
on_delete: async_delete
@ -303,10 +300,6 @@ groups_visits:
- table: users
column: user_id
on_delete: async_delete
instance_type_ci_runners_e59bb2812d:
- table: users
column: creator_id
on_delete: async_nullify
member_approvals:
- table: users
column: requested_by_id
@ -466,9 +459,6 @@ project_security_statistics:
column: project_id
on_delete: async_delete
project_type_ci_runners_e59bb2812d:
- table: users
column: creator_id
on_delete: async_nullify
- table: projects
column: sharding_key_id
on_delete: async_delete

View File

@ -5,4 +5,4 @@ feature_category: code_review_workflow
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/155492
milestone: '17.1'
queued_migration_version: 20240605193142
finalized_by: # version of the migration that finalized this BBM
finalized_by: '20241111232623'

View File

@ -1,7 +1,6 @@
---
table_name: users
classes:
- TmpUser
- User
feature_categories:
- user_profile

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class FinalizeBackfillMergeRequestAssigneesProjectId < Gitlab::Database::Migration[2.2]
milestone '17.6'
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_main_cell
def up
ensure_batched_background_migration_is_finished(
job_class_name: 'BackfillMergeRequestAssigneesProjectId',
table_name: :merge_request_assignees,
column_name: :id,
job_arguments: [:project_id, :merge_requests, :target_project_id, :merge_request_id],
finalize: true
)
end
def down; end
end

View File

@ -0,0 +1 @@
c6073984bfd3ae4a438324e9e448d6e0c925d3fe22e5395d49d77e4a6fac913f

View File

@ -29,9 +29,7 @@ Install one of the following GitLab-approved LLM models:
| Mistral Codestral | [Codestral 22B](https://huggingface.co/mistralai/Codestral-22B-v0.1) | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No |
| Mistral | [Mistral 7B](https://huggingface.co/mistralai/Mistral-7B-v0.1) | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
| Mistral | [Mistral 7B-it](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.3) | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
| Mistral | [Mixtral 8x7B](https://huggingface.co/mistralai/Mixtral-8x7B-v0.1) | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
| Mistral | [Mixtral 8x7B-it](https://huggingface.co/mistralai/Mixtral-8x7B-Instruct-v0.1) | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
| Mistral | [Mixtral 8x22B](https://huggingface.co/mistralai/Mixtral-8x22B-v0.1) | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
| Mistral | [Mixtral 8x22B-it](https://huggingface.co/mistralai/Mixtral-8x22B-Instruct-v0.1) | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
| Claude 3 | [Claude 3.5 Sonnet](https://www.anthropic.com/news/claude-3-5-sonnet) | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
| GPT | [GPT-3.5-Turbo](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models?tabs=python-secure#gpt-35) | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No |

View File

@ -17546,6 +17546,29 @@ The connection type for [`WorkItem`](#workitem).
| <a id="workitemconnectionnodes"></a>`nodes` | [`[WorkItem]`](#workitem) | A list of nodes. |
| <a id="workitemconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `WorkItemDescriptionTemplateConnection`
The connection type for [`WorkItemDescriptionTemplate`](#workitemdescriptiontemplate).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="workitemdescriptiontemplateconnectionedges"></a>`edges` | [`[WorkItemDescriptionTemplateEdge]`](#workitemdescriptiontemplateedge) | A list of edges. |
| <a id="workitemdescriptiontemplateconnectionnodes"></a>`nodes` | [`[WorkItemDescriptionTemplate]`](#workitemdescriptiontemplate) | A list of nodes. |
| <a id="workitemdescriptiontemplateconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `WorkItemDescriptionTemplateEdge`
The edge type for [`WorkItemDescriptionTemplate`](#workitemdescriptiontemplate).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="workitemdescriptiontemplateedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="workitemdescriptiontemplateedgenode"></a>`node` | [`WorkItemDescriptionTemplate`](#workitemdescriptiontemplate) | The item at the end of the edge. |
#### `WorkItemEdge`
The edge type for [`WorkItem`](#workitem).
@ -25519,6 +25542,27 @@ Returns [`WorkItem`](#workitem).
| ---- | ---- | ----------- |
| <a id="groupworkitemiid"></a>`iid` | [`String!`](#string) | IID of the work item. |
##### `Group.workItemDescriptionTemplates`
Work item description templates available to the namespace.
DETAILS:
**Introduced** in GitLab 17.6.
**Status**: Experiment.
Returns [`WorkItemDescriptionTemplateConnection`](#workitemdescriptiontemplateconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#pagination-arguments):
`before: String`, `after: String`, `first: Int`, and `last: Int`.
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="groupworkitemdescriptiontemplatesname"></a>`name` | [`String`](#string) | Fetches the specific DescriptionTemplate. |
| <a id="groupworkitemdescriptiontemplatessearch"></a>`search` | [`String`](#string) | Search for DescriptionTemplates by name. |
##### `Group.workItemStateCounts`
Counts of work items by state for the namespace. Returns `null` if the `namespace_level_work_items` feature flag is disabled.
@ -29286,6 +29330,27 @@ Returns [`WorkItem`](#workitem).
| ---- | ---- | ----------- |
| <a id="namespaceworkitemiid"></a>`iid` | [`String!`](#string) | IID of the work item. |
##### `Namespace.workItemDescriptionTemplates`
Work item description templates available to the namespace.
DETAILS:
**Introduced** in GitLab 17.6.
**Status**: Experiment.
Returns [`WorkItemDescriptionTemplateConnection`](#workitemdescriptiontemplateconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#pagination-arguments):
`before: String`, `after: String`, `first: Int`, and `last: Int`.
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="namespaceworkitemdescriptiontemplatesname"></a>`name` | [`String`](#string) | Fetches the specific DescriptionTemplate. |
| <a id="namespaceworkitemdescriptiontemplatessearch"></a>`search` | [`String`](#string) | Search for DescriptionTemplates by name. |
##### `Namespace.workItemTypes`
Work item types available to the namespace.
@ -36438,6 +36503,15 @@ Returns [`String!`](#string).
| <a id="workitemclosingmergerequestid"></a>`id` | [`MergeRequestsClosingIssuesID!`](#mergerequestsclosingissuesid) | Global ID of the closing merge request association. |
| <a id="workitemclosingmergerequestmergerequest"></a>`mergeRequest` | [`MergeRequest`](#mergerequest) | Related merge request. |
### `WorkItemDescriptionTemplate`
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="workitemdescriptiontemplatecontent"></a>`content` | [`String!`](#string) | Content of Description Template. |
| <a id="workitemdescriptiontemplatename"></a>`name` | [`String!`](#string) | Name of Description Template. |
### `WorkItemPermissions`
Check permissions for the current user on a work item.

View File

@ -1105,6 +1105,10 @@ Example response:
Shows information about the merge request dependencies that must be resolved before merging.
NOTE:
If the user does not have access to the blocking merge request, no `blocking_merge_request`
attribute is returned.
```plaintext
GET /projects/:id/merge_requests/:merge_request_iid/blocks
```

View File

@ -80,15 +80,16 @@ Configure pull mirroring settings.
Supported attributes:
| Attribute | Type | Required | Description |
|:---------------------------------|:--------|:---------|:------------|
| `enabled` | boolean | No | Enables pull mirroring on project when set to `true`. |
| `url` | string | No | URL of remote repository being mirrored. |
| `auth_user` | string | No | Username used for authentication of a project to pull mirror. |
| `auth_password` | string | No | Password used for authentication of a project to pull mirror. |
| `mirror_trigger_builds` | boolean | No | Trigger pipelines for mirror updates when set to `true`. |
| `only_mirror_protected_branches` | boolean | No | Limits mirroring to only protected branches when set to `true`. |
| `mirror_branch_regex` | String | No | Contains a regular expression. Only branches with names matching the regex are mirrored. Requires `only_mirror_protected_branches` to be disabled. |
| Attribute | Type | Required | Description |
|:----------|:-----|:---------|:------------|
| `enabled` | boolean | No | Enables pull mirroring on project when set to `true`. |
| `url` | string | No | URL of remote repository being mirrored. |
| `auth_user` | string | No | Username used for authentication of a project to pull mirror. |
| `auth_password` | string | No | Password used for authentication of a project to pull mirror. |
| `mirror_trigger_builds` | boolean | No | Trigger pipelines for mirror updates when set to `true`. |
| `only_mirror_protected_branches` | boolean | No | Limits mirroring to only protected branches when set to `true`. |
| `mirror_overwrites_diverged_branches` | boolean | No | Overwrite diverged branches. |
| `mirror_branch_regex` | String | No | Contains a regular expression. Only branches with names matching the regex are mirrored. Requires `only_mirror_protected_branches` to be disabled. |
Example request to add pull mirroring:

View File

@ -296,6 +296,62 @@ the entire line, each class that requires it should be applied on its own line.
This ensures that `!important` applies only where intended without affecting other classes in the same line.
## Responsive design
Our UI should work well on mobile and desktop. To accomplish this we use [CSS media queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries). In general we should take a mobile first approach to media queries. This means writing CSS for mobile, then using min-width media queries to override styles on desktop.
### Tailwind CSS classes
```html
<!-- Bad -->
<div class="gl-mt-5 max-lg:gl-mt-3"></div>
<!-- Good -->
<div class="gl-mt-3 md:gl-mt-5"></div>
<!-- Bad -->
<div class="gl-mt-3 sm:max-lg:gl-mt-5"></div>
<!-- Good -->
<div class="gl-mt-3 sm:gl-mt-5 lg:gl-mt-3"></div>
<!-- Okay if display property is dynamic (via Vue props or similar) and unknown -->
<!-- eslint-disable-next-line @gitlab/vue-tailwind-no-max-width-media-queries -->
<div class="max-lg:gl-hidden"></div>
```
### Component classes
```scss
// Bad
.class-name {
@apply gl-mt-5 max-lg:gl-mt-3;
}
// Good
.class-name {
@apply gl-mt-3 lg:gl-mt-5;
}
// Bad
.class-name {
display: block;
@include media-breakpoint-down(lg) {
display: flex;
}
}
// Good
.class-name {
display: flex;
@include media-breakpoint-up(lg) {
display: block;
}
}
```
## Naming
Filenames should use `snake_case`.

View File

@ -206,6 +206,14 @@ There is a rate limit for notification emails related to a project or group.
The **rate limit** is 1,000 notifications per 24 hours per project or group per user.
### FogBugz import
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/439101) in GitLab 17.6.
There is a rate limit for triggering project imports from FogBugz.
The **rate limit** is 1 triggered import per minute per user.
## Troubleshooting
### Rack Attack is denylisting the load balancer

View File

@ -57,6 +57,7 @@ First, create a step with:
inputs:
who:
default: world
type: string
```
- `spec` has one input called `who`.
@ -69,6 +70,7 @@ First, create a step with:
inputs:
who:
default: world
type: string
---
exec:
command:
@ -281,20 +283,22 @@ Add an output to your `hello` step.
inputs:
who:
default: world
type: string
outputs:
greeting: {}
greeting:
type: string
---
exec:
command:
- bash
- -c
- "echo greeting=hello ${{ inputs.who }} | tee ${{ output_file }}"
- "echo greeting=\"hello ${{ inputs.who }}\" | tee ${{ output_file }}"
```
- In this `spec`, you've defined a single output `greeting` without a default. Because
there is no default, the output `greeting` is required.
- Outputs are written to a file `${{ output_file }}` (provided at run time) in the form `key=value`.
- This step runs `echo greeting=hello ${{ inputs.name }}` and sends the output to the logs and the output file (`tee ${{ output_file }}`).
- This step runs `echo greeting=\"hello ${{ inputs.name }}\"` and sends the output to the logs and the output file (`tee ${{ output_file }}`).
1. In `step.yml`, add an output to the step:
@ -338,7 +342,7 @@ The echo step takes a single input `echo`, prints it to the logs, and outputs it
- name: hello_everybody
step: .
- name: all_my_greetings
step: gitlab.com/gitlab-org/ci-cd/runner-tools/echo-step@master
step: gitlab.com/gitlab-org/ci-cd/runner-tools/echo-step@main
inputs:
echo: "all my greetings say ${{ steps.hello_everybody.outputs.all_greetings }}"
image: registry.gitlab.com/gitlab-org/step-runner:v0

View File

@ -31,7 +31,6 @@ When a container repository is protected, the default behavior enforces these re
| Protect a container repository and its container images | The Maintainer role. |
| Push or create a new image in a container repository | The role set in the [**Minimum access level for push**](#protect-a-container-repository-and-create-a-protection-rule) setting. |
| Push or update an existing image in a container repository | The role set in the [**Minimum access level for push**](#protect-a-container-repository-and-create-a-protection-rule) setting |
| Delete an existing image from a container repository | The role set in the [**Minimum access level for delete**](#protect-a-container-repository-and-create-a-protection-rule) setting. |
You can use a wildcard (`*`) to protect multiple container repositories with the same container protection rule.
For example, you can protect different container repositories containing temporary container images built during a CI/CD pipeline.
@ -65,8 +64,6 @@ To protect a container repository:
The pattern can include a wildcard (`*`).
- **Minimum access level for push** describes the minimum access level required
to push (create or update) to the protected container repository path.
- **Minimum access level for delete** describes the minimum access level required
to delete from the protected container repository path.
1. Select **Protect**.
The container protection rule is created, and appears in the settings.

View File

@ -30,7 +30,7 @@ const extendConfigs = [
// rewrite.
let jhConfigs = [];
if (existsSync(path.resolve(dirname, 'jh'))) {
const pathToJhConfig = path.resolve(dirname, 'jh/eslint.config.js')
const pathToJhConfig = path.resolve(dirname, 'jh/eslint.config.js');
// eslint-disable-next-line import/no-dynamic-require, no-unsanitized/method
jhConfigs = (await import(pathToJhConfig)).default;
}
@ -161,6 +161,8 @@ export default [
'@gitlab/vue-no-undef-apollo-properties': 'error',
'@gitlab/tailwind-no-interpolation': 'error',
'@gitlab/vue-tailwind-no-interpolation': 'error',
'@gitlab/tailwind-no-max-width-media-queries': 'error',
'@gitlab/vue-tailwind-no-max-width-media-queries': 'error',
'no-param-reassign': [
'error',
@ -403,6 +405,8 @@ export default [
'@gitlab/no-runtime-template-compiler': 'off',
'@gitlab/tailwind-no-interpolation': 'off',
'@gitlab/vue-tailwind-no-interpolation': 'off',
'@gitlab/no-max-width-media-queries': 'off',
'@gitlab/vue-tailwind-no-max-width-media-queries': 'off',
'require-await': 'error',
'import/no-dynamic-require': 'off',
'no-import-assign': 'off',

View File

@ -0,0 +1,264 @@
# frozen_string_literal: true
require_relative 'helpers/file_helper'
require_relative 'helpers/milestones'
require_relative '../lib/generators/post_deployment_migration/post_deployment_migration_generator'
module Keeps
# This is an implementation of ::Gitlab::Housekeeper::Keep.
# This initializes the conversion of bigint columns for a given table.
#
# You can run it individually with:
#
# ```
# bundle exec gitlab-housekeeper -d -k Keeps::InitializeBigIntConversion
# ```
class InitializeBigIntConversion < ::Gitlab::Housekeeper::Keep
INTEGER_COLUMNS_FILE = 'db/integer_ids_not_yet_initialized_to_bigint.yml'
MIGRATION_TEMPLATE = 'generator_templates/active_record/migration/'
FALLBACK_REVIEWER_FEATURE_CATEGORY = 'database'
CLASS_WITH_NAMESPACE = /class\s+([A-Z][A-Za-z0-9]*(?:::[A-Z][A-Za-z0-9]*)+)\s*(?:<\s*[A-Z][A-Za-z0-9:]*\s*)?$/
CLASS_WITHOUT_NAMESPACE = /class\s+([A-Z][A-Za-z0-9]*)\s*(?:<\s*[A-Z][A-Za-z0-9:]*\s*)?$/
TABLE_INT_IDS_YAML_FILE_COMMENT = <<~MESSAGE
# -- DON'T MANUALLY EDIT --
# Contains the list of integer IDs which were converted to bigint for new installations in
# https://gitlab.com/gitlab-org/gitlab/-/issues/438124, but they are still integers for existing instances.
# On initialize_conversion_of_integer_to_bigint those integer IDs will be removed automatically from here.
MESSAGE
def initialize(...)
::PostDeploymentMigration::PostDeploymentMigrationGenerator.source_root(MIGRATION_TEMPLATE)
reset_db
migrate
super
end
def each_change
integer_columns_to_migrate.each do |table_name, columns|
change = build_change(table_name, columns)
change.changed_files = []
migration_file_1, migration_number_1 = generate_initialization_migration_file(table_name, columns)
migration_file_2, migration_number_2 = generate_backfill_migration_file(table_name, columns)
change.changed_files << migration_file_1
change.changed_files << migration_file_2
change.changed_files << Pathname.new('db').join('schema_migrations', migration_number_1).to_s
change.changed_files << Pathname.new('db').join('schema_migrations', migration_number_2).to_s
file_path = update_model(table_name, columns)
update_integer_columns_file(table_name)
migrate
change.changed_files << Pathname.new('db').join('structure.sql').to_s
change.changed_files << file_path
change.changed_files << INTEGER_COLUMNS_FILE
yield(change)
reset_db
end
end
private
def integer_columns_to_migrate
YAML.safe_load_file(INTEGER_COLUMNS_FILE)
end
def build_change(table_name, columns)
change = ::Gitlab::Housekeeper::Change.new
change.title = "Prepare conversion of #{table_name} to bigint".truncate(70, omission: '')
change.identifiers = [self.class.name.demodulize, table_name]
change.changelog_type = 'added'
change.labels = labels(table_name)
change.reviewers = pick_reviewers(table_name, change.identifiers).uniq
change.description = <<~MARKDOWN
Prepares conversion of `#{table_name}` to bigint for `#{columns.join(', ')}`
You can read more about the process for preparing bigint conversion in
https://docs.gitlab.com/ee/development/database/avoiding_downtime_in_migrations.html#migrating-integer-primary-keys-to-bigint.
As part of our process we want to ensure all ID columns are bigint to avoid the risk of overflowing while we continue our growth.
See https://gitlab.com/gitlab-org/gitlab/-/issues/465805+
Verify this MR as it was automatically created by `gitlab-housekeeper`.
Ensure that those columns are not being converted yet in the production database by checking Joe Bot through https://console.postgres.ai/gitlab.
If the columns were already converted in another merge request, consider closing this merge request
MARKDOWN
change
end
def labels(table_name)
table_info = Gitlab::Database::Dictionary.entries.find_by_table_name(table_name)
group_labels = table_info.feature_categories.flat_map do |feature_category|
groups_helper.labels_for_feature_category(feature_category)
end
group_labels << 'maintenance::scalability'
end
def pick_reviewers(table_name, identifiers)
table_info = Gitlab::Database::Dictionary.entries.find_by_table_name(table_name)
table_info.feature_categories.map do |feature_category|
groups_helper.pick_reviewer_for_feature_category(feature_category, identifiers,
fallback_feature_category: FALLBACK_REVIEWER_FEATURE_CATEGORY)
end
end
def generate_initialization_migration_file(table_name, columns)
migration_name = "initialize_conversion_of_#{table_name}_to_bigint".truncate(100, omission: '')
generator = ::PostDeploymentMigration::PostDeploymentMigrationGenerator.new([migration_name])
migration_content = <<~RUBY.strip
disable_ddl_transaction!
TABLE_NAME = :#{table_name}
COLUMNS = %i[#{columns.join(' ')}]
def up
initialize_conversion_of_integer_to_bigint(TABLE_NAME, COLUMNS, primary_key: :#{primary_key(table_name)})
end
def down
revert_initialize_conversion_of_integer_to_bigint(TABLE_NAME, COLUMNS)
end
RUBY
migration_file = generator.invoke_all.first
file_helper = ::Keeps::Helpers::FileHelper.new(migration_file)
file_helper.replace_method_content(:change, migration_content, strip_comments_from_file: true)
::Gitlab::Housekeeper::Shell.execute('rubocop', '-a', migration_file)
[migration_file, generator.migration_number]
end
def generate_backfill_migration_file(table_name, columns)
migration_name = "backfill_#{table_name}_for_bigint_conversion".truncate(100, omission: '')
generator = ::PostDeploymentMigration::PostDeploymentMigrationGenerator.new([migration_name])
gitlab_schema = Gitlab::Database::Dictionary.entries.find_by_table_name(table_name).gitlab_schema
migration_content = <<~RUBY.strip
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :#{gitlab_schema}
TABLE_NAME = :#{table_name}
COLUMNS = %i[#{columns.join(' ')}]
def up
backfill_conversion_of_integer_to_bigint(TABLE_NAME, COLUMNS, primary_key: :#{primary_key(table_name)})
end
def down
revert_backfill_conversion_of_integer_to_bigint(TABLE_NAME, COLUMNS)
end
RUBY
migration_file = generator.invoke_all.first
file_helper = ::Keeps::Helpers::FileHelper.new(migration_file)
file_helper.replace_method_content(:change, migration_content, strip_comments_from_file: true)
::Gitlab::Housekeeper::Shell.execute('rubocop', '-a', migration_file)
[migration_file, generator.migration_number]
end
def model_path(table_name)
model = Gitlab::Database::Dictionary.entries.find_by_table_name(table_name).classes.first
nested_model = model.underscore
file_path = Rails.root.join('app', 'models', "#{nested_model}.rb").to_s
if File.exist?(file_path)
file_path
else
Rails.root.join('ee', 'app', 'models', "#{nested_model}.rb").to_s
end
end
def update_model(table_name, columns)
class_name = Gitlab::Database::Dictionary.entries.find_by_table_name(table_name).classes.first.split("::").last
file_path = model_path(table_name)
ignore_columns = columns.map do |column|
"ignore_column :#{column}_convert_to_bigint, remove_with: '#{n_3_milestone.version}', " \
"remove_after: '#{n_2_milestone.date}'"
end
new_content = <<~RUBY
#{ignore_columns.join("\n")}
RUBY
insert_after_class_definition(file_path, class_name, new_content)
::Gitlab::Housekeeper::Shell.execute('rubocop', '-a', file_path)
file_path
end
def insert_after_class_definition(file_path, class_name, new_content)
content = File.read(file_path)
pattern = class_name.include?('::') ? CLASS_WITH_NAMESPACE : CLASS_WITHOUT_NAMESPACE
matches = content.scan(pattern)
return unless matches.flatten.include?(class_name)
updated_content = content.gsub(pattern) do |match|
if ::Regexp.last_match(1) == class_name
"#{match}\n#{new_content}\n"
else
match
end
end
File.write(file_path, updated_content)
end
def update_integer_columns_file(table_name)
file_path = Rails.root.join(INTEGER_COLUMNS_FILE)
data = YAML.safe_load_file(file_path)
data.delete(table_name)
File.open(INTEGER_COLUMNS_FILE, 'w') do |f|
f.write(TABLE_INT_IDS_YAML_FILE_COMMENT)
f.write(data.to_yaml)
end
end
def migrate
::Gitlab::Housekeeper::Shell.execute('rails', 'db:migrate', env: { 'RAILS_ENV' => 'test' })
end
def reset_db
ApplicationRecord.clear_all_connections!
::Gitlab::Housekeeper::Shell.execute('rails', 'db:reset', env: { 'RAILS_ENV' => 'test' })
end
def n_2_milestone
milestones_helper.upcoming_milestones[2]
end
def n_3_milestone
milestones_helper.upcoming_milestones[3]
end
def groups_helper
@groups_helper ||= ::Keeps::Helpers::Groups.new
end
def milestones_helper
@milestones_helper ||= ::Keeps::Helpers::Milestones.new
end
def primary_key(table_name)
Gitlab::Database::Dictionary.entries.find_by_table_name(table_name).classes.first.constantize.primary_key
end
end
end

View File

@ -106,76 +106,14 @@ module API
'campfire' => ::Integrations::Campfire.api_arguments,
'confluence' => ::Integrations::Confluence.api_arguments,
'custom-issue-tracker' => ::Integrations::CustomIssueTracker.api_arguments,
'datadog' => [
{
required: true,
name: :api_key,
type: String,
desc: 'API key used for authentication with Datadog'
},
{
required: false,
name: :datadog_site,
type: String,
desc: 'The Datadog site to send data to. To send data to the EU site, use datadoghq.eu'
},
{
required: false,
name: :api_url,
type: String,
desc: '(Advanced) The full URL for your Datadog site'
},
{
required: false,
name: :archive_trace_events,
type: ::Grape::API::Boolean,
desc: 'When enabled, job logs will be collected by Datadog and shown along pipeline execution traces'
},
{
required: false,
name: :datadog_service,
type: String,
desc: 'Tag all data from this GitLab instance in Datadog. Useful when managing several self-managed deployments'
},
{
required: false,
name: :datadog_env,
type: String,
desc: 'For self-managed deployments, set the env tag for all the data sent to Datadog'
},
{
required: false,
name: :datadog_tags,
type: String,
desc: 'Custom tags in Datadog. Specify one tag per line in the format: "key:value\nkey2:value2"'
}
],
'datadog' => ::Integrations::Datadog.api_arguments,
'diffblue-cover' => ::Integrations::DiffblueCover.api_arguments,
'discord' => [
::Integrations::Discord.api_arguments,
chat_notification_flags,
chat_notification_channels
].flatten,
'drone-ci' => [
{
required: true,
name: :token,
type: String,
desc: 'Drone CI token'
},
{
required: true,
name: :drone_url,
type: String,
desc: 'Drone CI URL'
},
{
required: false,
name: :enable_ssl_verification,
type: ::Grape::API::Boolean,
desc: 'Enable SSL verification'
}
],
'drone-ci' => ::Integrations::DroneCi.api_arguments,
'emails-on-push' => [
{
required: true,

View File

@ -65,11 +65,12 @@ module Gitlab
def option_descriptions
{
guest: s_('MemberRole|The Guest role is for users who need visibility into a project or group but should not have the ability to make changes, such as external stakeholders.'),
reporter: s_('MemberRole|The Reporter role is suitable for team members who need to stay informed about a project or group but do not actively contribute code.'),
developer: s_('MemberRole|The Developer role gives users access to contribute code while restricting sensitive administrative actions.'),
maintainer: s_('MemberRole|The Maintainer role is primarily used for managing code reviews, approvals, and administrative settings for projects. This role can also manage project memberships.'),
owner: s_('MemberRole|The Owner role is normally assigned to the individual or team responsible for managing and maintaining the group or creating the project. This role has the highest level of administrative control, and can manage all aspects of the group or project, including managing other Owners.')
NO_ACCESS => s_('MemberRole|The None role is assigned to the invited group users of a shared project when project sharing is disabled in group setting.'),
GUEST => s_('MemberRole|The Guest role is for users who need visibility into a project or group but should not have the ability to make changes, such as external stakeholders.'),
REPORTER => s_('MemberRole|The Reporter role is suitable for team members who need to stay informed about a project or group but do not actively contribute code.'),
DEVELOPER => s_('MemberRole|The Developer role gives users access to contribute code while restricting sensitive administrative actions.'),
MAINTAINER => s_('MemberRole|The Maintainer role is primarily used for managing code reviews, approvals, and administrative settings for projects. This role can also manage project memberships.'),
OWNER => s_('MemberRole|The Owner role is normally assigned to the individual or team responsible for managing and maintaining the group or creating the project. This role has the highest level of administrative control, and can manage all aspects of the group or project, including managing other Owners.')
}
end
@ -143,7 +144,11 @@ module Gitlab
options_with_owner.key(access)
end
def human_access_with_none(access)
def role_description(access)
option_descriptions[access]
end
def human_access_with_none(access, _member_role = nil)
options_with_none.key(access)
end
@ -204,6 +209,10 @@ module Gitlab
Gitlab::Access.human_access(access_field)
end
def role_description
Gitlab::Access.role_description(access_field)
end
def human_access_with_none
Gitlab::Access.human_access_with_none(access_field)
end

View File

@ -85,6 +85,7 @@ module Gitlab
vertex_embeddings_api: { threshold: 450, interval: 1.minute },
jobs_index: { threshold: -> { application_settings.project_jobs_api_rate_limit }, interval: 1.minute },
bulk_import: { threshold: 6, interval: 1.minute },
fogbugz_import: { threshold: 1, interval: 1.minute },
import_source_user_notification: { threshold: 1, interval: 8.hours },
projects_api_rate_limit_unauthenticated: {
threshold: -> { application_settings.projects_api_rate_limit_unauthenticated }, interval: 10.minutes

View File

@ -180,7 +180,7 @@ module Gitlab
last_analyzed_at = connection.select_value(
"SELECT pg_stat_get_last_analyze_time('#{table_to_query}'::regclass)"
)
last_analyzed_at.present? && last_analyzed_at >= Time.current - analyze_interval
last_analyzed_at.present? && last_analyzed_at >= ::Time.current - analyze_interval
end
end

View File

@ -0,0 +1,72 @@
# frozen_string_literal: true
module Gitlab
module Database
module Partitioning
module Time
class BaseStrategy
attr_reader :model, :partitioning_key, :retain_for, :retain_non_empty_partitions, :analyze_interval
delegate :table_name, to: :model
def initialize(
model, partitioning_key, retain_for: nil, retain_non_empty_partitions: false,
analyze_interval: nil)
@model = model
@partitioning_key = partitioning_key
@retain_for = retain_for
@retain_non_empty_partitions = retain_non_empty_partitions
@analyze_interval = analyze_interval
end
def current_partitions
raise NotImplementedError
end
# Check the currently existing partitions and determine which ones are missing
def missing_partitions
raise NotImplementedError
end
def extra_partitions
raise NotImplementedError
end
def desired_partitions
raise NotImplementedError
end
def relevant_range
raise NotImplementedError
end
def after_adding_partitions
# No-op, required by the partition manager
end
def validate_and_fix
# No-op, required by the partition manager
end
def oldest_active_date
raise NotImplementedError
end
def partition_name(lower_bound)
raise NotImplementedError
end
private
def partition_for(upper_bound:, lower_bound: nil)
TimePartition.new(table_name, lower_bound, upper_bound, partition_name: partition_name(lower_bound))
end
def pruning_old_partitions?
retain_for.present?
end
end
end
end
end
end

View File

@ -7,5 +7,8 @@ module Gitlab
# Custom adapter to validate the URL before each request
# This way we avoid DNS rebinds or other unsafe requests
::Fogbugz.adapter[:http] = HttpAdapter
# Custom adapter to validate size of incoming XML before
# attempting to parse it.
::Fogbugz.adapter[:xml] = XmlAdapter
end
end

View File

@ -0,0 +1,57 @@
# frozen_string_literal: true
module Gitlab
module FogbugzImport
module NokogiriBackendWithLimits
extend ActiveSupport::XmlMini_Nokogiri
module Conversions
module Document
def to_hash
objects = object_count(root)
if objects > XmlAdapter::MAX_ALLOWED_OBJECTS
raise XmlAdapter::ResponseTooLargeError,
"XML exceeds permitted complexity: #{objects}/#{XmlAdapter::MAX_ALLOWED_OBJECTS} objects"
end
super
end
private
def object_count(object)
return 0 if object.text? || object.cdata?
return 1 unless object.children.any?
1 + object.children.sum { |v| object_count(v) }
end
end
end
Nokogiri::XML::Document.include(Conversions::Document)
end
class XmlAdapter
ResponseTooLargeError = Class.new(StandardError)
MAX_ALLOWED_BYTES = 5.megabytes
MAX_ALLOWED_OBJECTS = 250_000
def self.parse(xml)
if xml.bytesize > MAX_ALLOWED_BYTES
raise ResponseTooLargeError, "XML exceeds permitted size: #{xml.bytesize}/#{MAX_ALLOWED_BYTES} bytes"
end
# We use ActiveSupport::XmlMini to get a simplified hash structure,
# aligned with what we were previously expecting from Crack, but we use
# Nokogiri for performance and security reasons.
ActiveSupport::XmlMini.with_backend(NokogiriBackendWithLimits) do
Hash.from_xml(xml)['response']
rescue Nokogiri::XML::SyntaxError
nil
end
end
end
end
end

View File

@ -3,8 +3,12 @@
module Gitlab
module Import
class PlaceholderUserCreator
LAMBDA_FOR_UNIQUE_USERNAME = ->(username) { User.username_exists?(username) }.freeze
LAMBDA_FOR_UNIQUE_EMAIL = ->(email) { User.find_by_email(email) || ::Email.find_by_email(email) }.freeze
LAMBDA_FOR_UNIQUE_USERNAME = ->(username) do
::Namespace.by_path(username) || User.username_exists?(username)
end.freeze
LAMBDA_FOR_UNIQUE_EMAIL = ->(email) do
User.find_by_email(email) || ::Email.find_by_email(email)
end.freeze
delegate :import_type, :namespace, :source_user_identifier, :source_name, :source_username, to: :source_user,
private: true

View File

@ -571,6 +571,7 @@ namespace :gitlab do
.reject { |c| c.name =~ /^(?:EE::)?Gitlab::(?:BackgroundMigration|DatabaseImporters)::/ }
.reject { |c| c.name =~ /^HABTM_/ }
.reject { |c| c < Gitlab::Database::Migration[1.0]::MigrationRecord }
.reject { |c| c.name == 'TmpUser' }
.each { |c| classes[c.table_name] << c.name if classes.has_key?(c.table_name) && c.name.present? }
sources.each do |source_name|

View File

@ -15269,9 +15269,6 @@ msgstr ""
msgid "ContainerRegistry|Manifest media type: %{mediaType}"
msgstr ""
msgid "ContainerRegistry|Minimum access level for delete"
msgstr ""
msgid "ContainerRegistry|Minimum access level for push"
msgstr ""
@ -15463,7 +15460,7 @@ msgstr ""
msgid "ContainerRegistry|We are having trouble connecting to the Container Registry. Please try refreshing the page. If this error persists, please review %{docLinkStart}the troubleshooting documentation%{docLinkEnd}."
msgstr ""
msgid "ContainerRegistry|When a container is protected, only certain user roles can push and delete the protected container image, which helps to avoid tampering with the container image."
msgid "ContainerRegistry|When a container is protected, only certain user roles can push the protected container image, which helps to avoid tampering with the container image."
msgstr ""
msgid "ContainerRegistry|While the rename is in progress, new uploads to the container registry are blocked. Ongoing uploads may fail and need to be retried."
@ -33716,6 +33713,9 @@ msgstr ""
msgid "MemberRole|The Minimal Access role is for users who need the least amount of access into groups and projects. You can assign this role as a default, before giving a user another role with more permissions."
msgstr ""
msgid "MemberRole|The None role is assigned to the invited group users of a shared project when project sharing is disabled in group setting."
msgstr ""
msgid "MemberRole|The Owner role is normally assigned to the individual or team responsible for managing and maintaining the group or creating the project. This role has the highest level of administrative control, and can manage all aspects of the group or project, including managing other Owners."
msgstr ""

View File

@ -9,7 +9,6 @@
"lint-docs": "scripts/lint-doc.sh",
"internal:eslint": "eslint --cache --max-warnings 0 --report-unused-disable-directives",
"internal:stylelint": "stylelint -q --rd '{ee/,}app/assets/{stylesheets/**/*.{css,scss},builds/tailwind.css}'",
"preinternal:stylelint": "yarn run tailwindcss:build",
"prejest": "yarn check-dependencies",
"build:css": "node scripts/frontend/build_css.mjs",
"tailwindcss:build": "node scripts/frontend/tailwindcss.mjs",

View File

@ -86,6 +86,14 @@ RSpec.describe Import::FogbugzController, feature_category: :importers do
include_examples 'denies local request', 'Only allowed schemes are http, https'
end
end
it_behaves_like 'rate limited endpoint', rate_limit_key: :fogbugz_import, with_redirect: true do
let(:current_user) { user }
def request
post :callback, params: { uri: uri, email: 'test@example.com', password: 'mypassword' }
end
end
end
describe 'POST #create_user_map' do

View File

@ -115,14 +115,16 @@ RSpec.describe 'Database schema',
p_ci_pipelines: %w[partition_id auto_canceled_by_partition_id auto_canceled_by_id],
p_ci_runner_machine_builds: %w[project_id],
ci_runners: %w[sharding_key_id], # This value is meant to populate the partitioned table, no other usage
ci_runners_e59bb2812d: %w[sharding_key_id], # This field is only used in the partitions, and has the appropriate FKs
instance_type_ci_runners_e59bb2812d: %w[creator_id sharding_key_id], # No need for LFKs on partition, already handled on ci_runners_e59bb2812d routing table.
group_type_ci_runners_e59bb2812d: %w[creator_id sharding_key_id], # No need for LFKs on partition, already handled on ci_runners_e59bb2812d routing table.
project_type_ci_runners_e59bb2812d: %w[creator_id sharding_key_id], # No need for LFKs on partition, already handled on ci_runners_e59bb2812d routing table.
ci_runner_machines: %w[sharding_key_id], # This value is meant to populate the partitioned table, no other usage
ci_runner_machines_687967fa8a: %w[runner_id sharding_key_id], # This field is only used in the partitions, and has the appropriate FKs. runner_id temporarily ignored due to incident 18792
instance_type_ci_runner_machines_687967fa8a: %w[runner_id sharding_key_id], # This field is always NULL in this partition. runner_id temporarily ignored due to incident 18792
group_type_ci_runner_machines_687967fa8a: %w[runner_id sharding_key_id], # No need for LFK, rows will be deleted by the FK to ci_runners. runner_id temporarily ignored due to incident 18792
project_type_ci_runner_machines_687967fa8a: %w[runner_id sharding_key_id], # No need for LFK, rows will be deleted by the FK to ci_runners. runner_id temporarily ignored due to incident 18792
ci_runner_projects: %w[runner_id],
ci_runners_e59bb2812d: %w[sharding_key_id], # This field is only used in the partitions, and has the appropriate FKs
instance_type_ci_runners_e59bb2812d: %w[sharding_key_id], # This field is always NULL in this partition
ci_sources_pipelines: %w[partition_id source_partition_id source_job_id],
ci_sources_projects: %w[partition_id],
ci_stages: %w[partition_id project_id pipeline_id],

View File

@ -13,6 +13,12 @@ RSpec.describe 'OAuth Provider', feature_category: :system_access do
)
end
def visit_oauth_device_authorization_path
visit oauth_device_authorizations_index_path(
user_code: user_code
)
end
before do
sign_in(user)
end
@ -27,6 +33,16 @@ RSpec.describe 'OAuth Provider', feature_category: :system_access do
it_behaves_like 'Secure OAuth Authorizations'
end
describe 'Device OAuth authorization' do
let(:user_code) { 'valid_user_code' }
before do
visit_oauth_device_authorization_path
end
it_behaves_like 'Secure Device OAuth Authorizations'
end
context 'when the OAuth application has HTML in the name' do
let(:client_name) { '<img src=x onerror=alert(1)>' }
let(:application) { create(:oauth_application, name: client_name, scopes: 'read_user') }
@ -42,6 +58,10 @@ RSpec.describe 'OAuth Provider', feature_category: :system_access do
end
end
it 'expects button not to have an id attribute' do
expect(find_by_testid('authorization-button')[:id].nil?).to be_truthy
end
# rubocop:disable Layout/LineLength -- It is a string
it 'sanitizes the HTML in the warning text' do
expect(page).to have_content(

View File

@ -27,8 +27,6 @@ describe('container Protection Rule Form', () => {
wrapper.findByRole('textbox', { name: /repository path pattern/i });
const findMinimumAccessLevelForPushSelect = () =>
wrapper.findByRole('combobox', { name: /minimum access level for push/i });
const findMinimumAccessLevelForDeleteSelect = () =>
wrapper.findByRole('combobox', { name: /minimum access level for delete/i });
const findSubmitButton = () => wrapper.findByRole('button', { name: /add rule/i });
const mountComponent = ({ config, provide = defaultProvidedValues } = {}) => {
@ -58,7 +56,7 @@ describe('container Protection Rule Form', () => {
.findAll('option')
.wrappers.map((option) => option.element.value);
it.each(['', 'MAINTAINER', 'OWNER', 'ADMIN'])(
it.each(['MAINTAINER', 'OWNER', 'ADMIN'])(
'includes the access level "%s" as an option',
(accessLevel) => {
mountComponent();
@ -80,7 +78,6 @@ describe('container Protection Rule Form', () => {
expect(findSubmitButton().props('disabled')).toBe(true);
expect(findRepositoryPathPatternInput().attributes('disabled')).toBe('disabled');
expect(findMinimumAccessLevelForPushSelect().attributes('disabled')).toBe('disabled');
expect(findMinimumAccessLevelForDeleteSelect().attributes('disabled')).toBe('disabled');
});
it('displays a loading spinner', () => {
@ -165,29 +162,6 @@ describe('container Protection Rule Form', () => {
});
});
it('dispatches correct apollo mutation when no minimumAccessLevelForPush is selected', async () => {
const mutationResolver = jest
.fn()
.mockResolvedValue(createContainerProtectionRuleMutationPayload());
mountComponentWithApollo({ mutationResolver });
await findRepositoryPathPatternInput().setValue(
createContainerProtectionRuleMutationInput.repositoryPathPattern,
);
await findMinimumAccessLevelForPushSelect().setValue('');
await submitForm();
expect(mutationResolver).toHaveBeenCalledWith({
input: {
projectPath: 'path',
...createContainerProtectionRuleMutationInput,
minimumAccessLevelForPush: null,
},
});
});
it('emits event "submit" when apollo mutation successful', async () => {
const mutationResolver = jest
.fn()

View File

@ -113,28 +113,10 @@ describe('Container protection rules project settings', () => {
(protectionRule, i) => {
expect(findTableRowCell(i, 0).text()).toBe(protectionRule.repositoryPathPattern);
expect(findTableRowCellComboboxSelectedOption(i, 1).text).toBe('Maintainer');
expect(findTableRowCellComboboxSelectedOption(i, 2).text).toBe('Maintainer');
},
);
});
it('renders table with container protection rule with blank minimumAccessLevelForDelete', async () => {
const containerProtectionRuleQueryResolver = jest.fn().mockResolvedValue(
containerProtectionRuleQueryPayload({
nodes: [{ ...containerProtectionRulesData[0], minimumAccessLevelForDelete: null }],
}),
);
createComponent({ containerProtectionRuleQueryResolver });
await waitForPromises();
expect(findTableRowCell(0, 0).text()).toBe(
containerProtectionRulesData[0].repositoryPathPattern,
);
expect(findTableRowCellComboboxSelectedOption(0, 1).text).toBe('Maintainer');
expect(findTableRowCellComboboxSelectedOption(0, 2).text).toBe('Developer (default)');
});
it('displays table in busy state and shows loading icon inside table', async () => {
createComponent();
@ -298,9 +280,8 @@ describe('Container protection rules project settings', () => {
});
describe.each`
comboboxName | minimumAccessLevelAttribute
${'Minimum access level for push'} | ${'minimumAccessLevelForPush'}
${'Minimum access level for delete'} | ${'minimumAccessLevelForDelete'}
comboboxName | minimumAccessLevelAttribute
${'Minimum access level for push'} | ${'minimumAccessLevelForPush'}
`(
'column "$comboboxName" with selectbox (combobox)',
({ comboboxName, minimumAccessLevelAttribute }) => {

View File

@ -171,13 +171,11 @@ export const containerProtectionRulesData = [
id: `gid://gitlab/ContainerRegistry::Protection::Rule/${i}`,
repositoryPathPattern: `@flight/flight/maintainer-${i}-*`,
minimumAccessLevelForPush: 'MAINTAINER',
minimumAccessLevelForDelete: 'MAINTAINER',
})),
{
id: 'gid://gitlab/ContainerRegistry::Protection::Rule/16',
repositoryPathPattern: '@flight/flight/owner-16-*',
minimumAccessLevelForPush: 'OWNER',
minimumAccessLevelForDelete: 'OWNER',
},
];
@ -218,7 +216,6 @@ export const createContainerProtectionRuleMutationPayload = ({ override, errors
export const createContainerProtectionRuleMutationInput = {
repositoryPathPattern: `@flight/flight-maintainer-14-*`,
minimumAccessLevelForPush: 'MAINTAINER',
minimumAccessLevelForDelete: 'MAINTAINER',
};
export const createContainerProtectionRuleMutationPayloadErrors = [

View File

@ -108,6 +108,29 @@ describe('TodosApp', () => {
);
});
it('refetches todos when page becomes visible again', async () => {
createComponent();
// Wait and account for initial query
await waitForPromises();
expect(todosQuerySuccessHandler).toHaveBeenCalledTimes(1);
expect(todosCountsQuerySuccessHandler).toHaveBeenCalledTimes(1);
// Make sure we don't refetch when document became hidden
jest.spyOn(document, 'hidden', 'get').mockReturnValue(true);
document.dispatchEvent(new Event('visibilitychange'));
await waitForPromises();
expect(todosQuerySuccessHandler).toHaveBeenCalledTimes(1);
expect(todosCountsQuerySuccessHandler).toHaveBeenCalledTimes(1);
// Expect refetch when document becomes visible
jest.spyOn(document, 'hidden', 'get').mockReturnValue(false);
document.dispatchEvent(new Event('visibilitychange'));
await waitForPromises();
expect(todosQuerySuccessHandler).toHaveBeenCalledTimes(2);
expect(todosCountsQuerySuccessHandler).toHaveBeenCalledTimes(2);
});
it('passes the default status to the filter bar', () => {
createComponent();

View File

@ -12,7 +12,7 @@ RSpec.describe GitlabSchema.types['Namespace'] do
id name path full_name full_path achievements_path description description_html visibility
lfs_enabled request_access_enabled projects root_storage_statistics shared_runners_setting
timelog_categories achievements work_item pages_deployments import_source_users work_item_types
sidebar
sidebar work_item_description_templates
]
expect(described_class).to include_graphql_fields(*expected_fields)

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::WorkItems::DescriptionTemplateType, feature_category: :portfolio_management do
include GraphqlHelpers
it 'exposes the expected fields' do
expected_fields = %i[content name]
expected_fields.each do |field|
expect(described_class).to have_graphql_field(field)
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Access, feature_category: :permissions do
let_it_be(:member) { create(:group_member, :developer) }
describe '#role_description' do
it 'returns the correct description of the access role' do
role = described_class.option_descriptions[described_class::DEVELOPER]
expect(member.role_description).to eq(role)
end
end
end

View File

@ -41,9 +41,23 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::ComponentUsage, feature_category: :p
it 'creates a component usage record' do
expect { perform }.to change { Ci::Catalog::Resources::Components::Usage.count }.by(1)
.and change { Ci::Catalog::Resources::Components::LastUsage.count }.by(1)
end
context 'when component usage has already been recorded', :freeze_time do
let!(:existing_last_usage) do
create(:catalog_resource_component_last_usage,
component: component, used_by_project_id: project.id, last_used_date: Time.current.to_date - 3.days)
end
it 'updates the last_used_date for the existing last_usage record' do
expect { step.perform! }.not_to change { Ci::Catalog::Resources::Components::LastUsage.count }
last_usage = Ci::Catalog::Resources::Components::LastUsage.find_by(component: component,
used_by_project_id: project.id)
expect(last_usage.last_used_date).to eq(Time.current.to_date)
end
it 'does not create a component usage record' do
step.perform!

View File

@ -0,0 +1,84 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::Partitioning::Time::BaseStrategy, feature_category: :database do
let(:model) { class_double(ApplicationRecord, table_name: table_name) }
let(:partitioning_key) { :created_at }
let(:table_name) { :_test_partitioned_test }
let(:base_strategy) { described_class.new(model, partitioning_key) }
describe '#current_partitions' do
subject(:current_partitions) { base_strategy.current_partitions }
it 'raises an error' do
expect { current_partitions }.to raise_error(NotImplementedError)
end
end
describe '#missing_partitions' do
subject(:missing_partitions) { base_strategy.missing_partitions }
it 'raises an error' do
expect { missing_partitions }.to raise_error(NotImplementedError)
end
end
describe '#extra_partitions' do
subject(:extra_partitions) { base_strategy.extra_partitions }
it 'raises an error' do
expect { extra_partitions }.to raise_error(NotImplementedError)
end
end
describe '#desired_partitions' do
subject(:desired_partitions) { base_strategy.desired_partitions }
it 'raises an error' do
expect { desired_partitions }.to raise_error(NotImplementedError)
end
end
describe '#relevant_range' do
subject(:relevant_range) { base_strategy.relevant_range }
it 'raises an error' do
expect { relevant_range }.to raise_error(NotImplementedError)
end
end
describe '#oldest_active_date' do
subject(:oldest_active_date) { base_strategy.oldest_active_date }
it 'raises an error' do
expect { oldest_active_date }.to raise_error(NotImplementedError)
end
end
describe '#partition_name' do
let(:from) { Date.current }
subject(:partition_name) { base_strategy.partition_name(from) }
it 'raises an error' do
expect { partition_name }.to raise_error(NotImplementedError)
end
end
describe '#after_adding_partitions' do
subject(:after_adding_partitions) { base_strategy.after_adding_partitions }
it 'does nothing' do
expect { after_adding_partitions }.not_to raise_error
end
end
describe '#validate_and_fix' do
subject(:validate_and_fix) { base_strategy.validate_and_fix }
it 'does nothing' do
expect { validate_and_fix }.not_to raise_error
end
end
end

View File

@ -0,0 +1,66 @@
# frozen_string_literal: true
require 'spec_helper'
# These specs are copying the requirements laid out by the original adapter
# spec: https://github.com/relatel/ruby-fogbugz/blob/master/spec/adapters/xml/crack_spec.rb
RSpec.describe Gitlab::FogbugzImport::XmlAdapter, feature_category: :importers do
let(:xml) { nil }
subject(:parsed_xml) { described_class.parse(xml) }
context 'when parsing an XML response' do
let(:xml) do
<<~XML
<?xml version="1.0" encoding="UTF-8"?>
<response>
<case>
<ixBug>1234</ixBug>
<sTitle>Sample Bug</sTitle>
</case>
</response>
XML
end
it { is_expected.to eq({ 'case' => { 'ixBug' => '1234', 'sTitle' => 'Sample Bug' } }) }
end
context 'when given an HTML response' do
let(:xml) do
<<~HTML
<html lang="en">
<head><title>Object moved</title></head>
<body><h2>Object moved to <a href="/new-location">here</a>.</h2></body>
</html>
HTML
end
it { is_expected.to be_nil }
end
context 'when parsing invalid XML' do
let(:xml) { "hold on, this isn't XML at all!" }
it { is_expected.to be_nil }
end
context 'when the XML body is too large' do
let(:xml) { instance_double(String, bytesize: described_class::MAX_ALLOWED_BYTES + 1) }
it 'raises a ResponseTooLargeError' do
expect { parsed_xml }.to raise_error(described_class::ResponseTooLargeError, /XML exceeds permitted size/)
end
end
context 'when the XML body is too complex' do
before do
stub_const("#{described_class}::MAX_ALLOWED_OBJECTS", 3)
end
let(:xml) { '<one><two><three><four></four></three></two></one>' }
it 'raises a ResponseTooLargeError' do
expect { parsed_xml }.to raise_error(described_class::ResponseTooLargeError, /XML exceeds permitted complexity/)
end
end
end

View File

@ -70,6 +70,18 @@ RSpec.describe Gitlab::Import::PlaceholderUserCreator, feature_category: :import
end
end
context 'when an existing namespace conflicts with the placeholder user namespace' do
before do
create(:group, path: 'aprycontributor_placeholder_user_1')
end
it 'creates a placeholder with a username that avoids the conflict' do
placeholder_user1 = service.execute
expect(placeholder_user1.username).to eq('aprycontributor_placeholder_user_2')
end
end
context 'when generating a unique email address' do
it 'validates against all stored email addresses' do
allow(Zlib).to receive(:crc32).and_return(123)

View File

@ -43,5 +43,38 @@ RSpec.describe Ci::Catalog::Resources::Components::LastUsage, type: :model, feat
)
end.to raise_error(ActiveRecord::RecordInvalid)
end
describe '.get_usage_for' do
let_it_be(:used_by_project) { create(:project) }
context 'when no record exists' do
it 'initializes a new record' do
last_usage = described_class.get_usage_for(component, used_by_project)
expect(last_usage).to be_a_new_record
expect(last_usage.component).to eq(component)
expect(last_usage.catalog_resource).to eq(component.catalog_resource)
expect(last_usage.component_project).to eq(component.project)
expect(last_usage.used_by_project_id).to eq(used_by_project.id)
end
end
context 'when a record exists' do
let!(:existing_record) do
create(:catalog_resource_component_last_usage,
component: component,
catalog_resource: component.catalog_resource,
component_project: component.project,
used_by_project_id: used_by_project.id)
end
it 'returns the existing record' do
last_usage = described_class.get_usage_for(component, used_by_project)
expect(last_usage).not_to be_a_new_record
expect(last_usage).to eq(existing_record)
end
end
end
end
end

View File

@ -19,32 +19,21 @@ RSpec.describe Clusters::Agents::Authorizations::UserAccess::GroupAuthorization,
subject { described_class.for_user(user) }
where(:user_role, :expected_access_level) do
:guest | nil
:reporter | nil
:developer | Gitlab::Access::DEVELOPER
:maintainer | Gitlab::Access::MAINTAINER
:owner | Gitlab::Access::OWNER
end
with_them do
before do
group.add_member(user, user_role)
context 'when user is member' do
where(:user_role, :expected_access_level) do
:guest | nil
:reporter | nil
:developer | Gitlab::Access::DEVELOPER
:maintainer | Gitlab::Access::MAINTAINER
:owner | Gitlab::Access::OWNER
end
it 'returns the expected result' do
if expected_access_level
expect(subject).to contain_exactly(authorization)
expect(subject.first.access_level).to eq(expected_access_level)
else
expect(subject).to be_empty
with_them do
before do
group.add_member(user, user_role)
end
end
context 'when authorization belongs to sub-group' do
let!(:authorization) { create(:agent_user_access_group_authorization, group: subgroup) }
it 'respects the role inheritance' do
it 'returns the expected result' do
if expected_access_level
expect(subject).to contain_exactly(authorization)
expect(subject.first.access_level).to eq(expected_access_level)
@ -53,14 +42,63 @@ RSpec.describe Clusters::Agents::Authorizations::UserAccess::GroupAuthorization,
end
end
it 'respects the role override' do
subgroup.add_member(user, :owner)
context 'when authorization belongs to sub-group' do
let!(:authorization) { create(:agent_user_access_group_authorization, group: subgroup) }
expect(subject).to contain_exactly(authorization)
expect(subject.first.access_level).to eq(Gitlab::Access::OWNER)
it 'respects the role inheritance' do
if expected_access_level
expect(subject).to contain_exactly(authorization)
expect(subject.first.access_level).to eq(expected_access_level)
else
expect(subject).to be_empty
end
end
it 'respects the role override' do
subgroup.add_member(user, :owner)
expect(subject).to contain_exactly(authorization)
expect(subject.first.access_level).to eq(Gitlab::Access::OWNER)
end
end
end
end
shared_examples 'does not yield an authorization' do
it { expect(subject).to be_empty }
end
context 'when user is blocked' do
let(:user) { create(:user, :blocked) }
before do
group.add_member(user, Gitlab::Access::MAINTAINER)
end
it_behaves_like 'does not yield an authorization'
end
context 'when user is banned' do
let(:user) { create(:user, :banned) }
before do
group.add_member(user, Gitlab::Access::MAINTAINER)
end
it_behaves_like 'does not yield an authorization'
end
context 'when user requested access' do
let!(:invited) { create(:group_member, :access_request, group: group, user: user) }
it_behaves_like 'does not yield an authorization'
end
context 'when user is awaiting' do
let!(:invited) { create(:group_member, :awaiting, group: group, user: user) }
it_behaves_like 'does not yield an authorization'
end
end
describe '#config_project' do

View File

@ -21,6 +21,14 @@ RSpec.describe MemberPresenter, feature_category: :groups_and_projects do
end
end
describe '#member_role_description' do
it 'returns the correct role description' do
description = Gitlab::Access.option_descriptions[Gitlab::Access::REPORTER]
expect(presenter.member_role_description).to eq(description)
end
end
describe '#role_type' do
it "returns 'default'" do
expect(presenter.role_type).to eq('default')

View File

@ -25,6 +25,12 @@ RSpec.describe MemberEntity, feature_category: :groups_and_projects do
expect(entity_hash[:can_remove]).to be(true)
end
describe '#access_level' do
it 'correctly exposes `string_value`' do
expect(entity_hash[:access_level][:string_value]).to eq(member.human_access_with_none)
end
end
context 'when is_source_accessible_to_current_user is true' do
before do
allow(member).to receive(:is_source_accessible_to_current_user).and_return(true)

View File

@ -11,19 +11,36 @@ RSpec.describe Ci::Components::Usages::CreateService, feature_category: :pipelin
describe '#execute' do
subject(:execute) { service.execute }
it 'creates a usage record', :aggregate_failures do
it 'creates a usage record and updates last_usage', :aggregate_failures do
expect { execute }.to change { Ci::Catalog::Resources::Components::Usage.count }.by(1)
.and change { Ci::Catalog::Resources::Components::LastUsage.count }.by(1)
expect(execute).to be_success
expect(execute.message).to eq('Usage recorded')
usage = Ci::Catalog::Resources::Components::Usage.find_by(component: component)
last_usage = Ci::Catalog::Resources::Components::LastUsage.find_by(component: component,
used_by_project_id: project.id)
expect(usage.catalog_resource).to eq(component.catalog_resource)
expect(usage.project).to eq(component.project)
expect(usage.used_by_project_id).to eq(project.id)
expect(last_usage.last_used_date).to be_present
end
context 'when usage has already been recorded', :freeze_time do
let!(:existing_last_usage) do
create(:catalog_resource_component_last_usage,
component: component, used_by_project_id: project.id, last_used_date: Time.current.to_date - 3.days)
end
it 'updates the last_used_date for the existing last_usage record' do
expect { execute }.not_to change { Ci::Catalog::Resources::Components::LastUsage.count }
last_usage = Ci::Catalog::Resources::Components::LastUsage.find_by(component: component,
used_by_project_id: project.id)
expect(last_usage.last_used_date).to eq(Time.current.to_date)
end
it 'does not create a usage record' do
service.execute

View File

@ -5,7 +5,7 @@
# - current_user
# - error_message # optional
RSpec.shared_examples 'rate limited endpoint' do |rate_limit_key:, graphql: false|
RSpec.shared_examples 'rate limited endpoint' do |rate_limit_key:, graphql: false, with_redirect: false|
let(:error_message) { _('This endpoint has been requested too many times. Try again later.') }
context 'when rate limiter enabled', :freeze_time, :clean_gitlab_redis_rate_limiting do
@ -39,6 +39,9 @@ RSpec.shared_examples 'rate limited endpoint' do |rate_limit_key:, graphql: fals
if graphql
expect_graphql_errors_to_include(error_message)
elsif with_redirect
expect(response).to be_redirect
expect(flash[:alert]).to eq(error_message)
else
expect(response).to have_gitlab_http_status(:too_many_requests)

View File

@ -17,3 +17,34 @@ RSpec.shared_examples 'Secure OAuth Authorizations' do
end
end
end
RSpec.shared_examples 'Secure Device OAuth Authorizations' do
let(:user) { create(:user) }
context 'when authorize page is rendered' do
it 'asks user to authorize the device' do
expect(page).to have_text "Authorize device to access to your GitLab account"
within_testid('authorization-button') do
expect(page).to have_content(format(_('Authorize')))
end
end
it 'does not render authorize button with id' do
expect(find_by_testid('authorization-button')[:id].nil?).to be_truthy
end
end
context 'when confirmation page is rendered' do
before do
find_by_testid('authorization-button').click
end
it 'renders confirmatoin button without id' do
within_testid('authorization-button') do
expect(page).to have_content(format(_('Confirm')))
end
expect(find_by_testid('authorization-button')[:id].nil?).to be_truthy
end
end
end