Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-09-05 09:11:57 +00:00
parent 8577a60062
commit f133142aee
55 changed files with 992 additions and 1571 deletions

View File

@ -52,6 +52,7 @@ docs-lint markdown:
stage: lint
needs: []
script:
- apk add libuuid
- source ./scripts/utils.sh
- yarn_install_script
- install_gitlab_gem

View File

@ -185,6 +185,51 @@ gitaly-transactions-selective-parallel:
variables:
QA_TESTS: ""
# ========== gitaly reftables backend ===========
# Verifies that E2E tests that interact with the gitaly backend work when the reftables backend is in use
# https://about.gitlab.com/blog/2024/05/30/a-beginners-guide-to-the-git-reftable-format/
# https://gitlab.com/groups/gitlab-org/-/epics/14946
gitaly-reftables-backend:
extends:
- .parallel
- .qa
parallel: 2
variables:
QA_SCENARIO: Test::Integration::Praefect
QA_CAN_TEST_PRAEFECT: "true"
KNAPSACK_TEST_FILE_PATTERN: "qa/specs/features/**/3_create/**/*_spec.rb"
QA_FEATURE_FLAGS: "gitaly_new_repo_reftable_backend=enabled"
rules:
- !reference [.rules:test:qa-parallel, rules]
- if: $QA_SUITES =~ /Test::Instance::All/
gitaly-reftables-backend-selective:
extends: .qa
variables:
QA_SCENARIO: Test::Integration::Praefect
QA_CAN_TEST_PRAEFECT: "true"
QA_FEATURE_FLAGS: "gitaly_new_repo_reftable_backend=enabled"
rules:
- !reference [.rules:test:qa-selective, rules]
- if: $QA_SUITES =~ /Test::Instance::All/
gitaly-reftables-enabled-backend-parallel:
extends:
- .qa
- .parallel
parallel: 2
variables:
QA_SCENARIO: Test::Integration::Praefect
QA_CAN_TEST_PRAEFECT: "true"
QA_GIT_DEFAULT_REF_FORMAT: 'reftable'
KNAPSACK_TEST_FILE_PATTERN: "qa/specs/features/**/3_create/**/*_spec.rb"
QA_FEATURE_FLAGS: "gitaly_new_repo_reftable_backend=enabled"
rules:
- !reference [.rules:test:qa-selective-parallel, rules]
- if: $QA_SUITES =~ /Test::Instance::All/
variables:
QA_TESTS: ""
# ========== git sha256 enabled ===========
git-sha256-repositories:
when: manual

View File

@ -1173,7 +1173,6 @@ RSpec/BeforeAllRoleAssignment:
- 'spec/requests/api/rubygem_packages_spec.rb'
- 'spec/requests/api/search_spec.rb'
- 'spec/requests/api/wikis_spec.rb'
- 'spec/requests/concerns/planning_hierarchy_spec.rb'
- 'spec/requests/groups/deploy_tokens_controller_spec.rb'
- 'spec/requests/groups/settings/access_tokens_controller_spec.rb'
- 'spec/requests/groups/settings/applications_controller_spec.rb'

View File

@ -41,7 +41,6 @@ Style/InlineDisableAnnotation:
- 'app/controllers/concerns/membership_actions.rb'
- 'app/controllers/concerns/milestone_actions.rb'
- 'app/controllers/concerns/notes_actions.rb'
- 'app/controllers/concerns/planning_hierarchy.rb'
- 'app/controllers/concerns/preferred_language_switcher.rb'
- 'app/controllers/concerns/preview_markdown.rb'
- 'app/controllers/concerns/registry/connection_errors_handler.rb'

View File

@ -1,6 +1,6 @@
<script>
import { debounce, uniq } from 'lodash';
import { GlDropdownDivider, GlDropdownItem, GlCollapsibleListbox, GlSprintf } from '@gitlab/ui';
import { GlDropdownDivider, GlDropdownItem, GlCollapsibleListbox } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __, s__, sprintf } from '~/locale';
import { convertEnvironmentScope } from './utils';
@ -29,7 +29,6 @@ export default {
GlCollapsibleListbox,
GlDropdownDivider,
GlDropdownItem,
GlSprintf,
},
mixins: [glFeatureFlagsMixin()],
props: {
@ -112,7 +111,8 @@ export default {
}
return (
this.searchTerm && ![...this.environments, this.customEnvScope].includes(this.searchTerm)
this.searchTerm?.includes('*') &&
![...this.environments, this.customEnvScope].includes(this.searchTerm)
);
},
shouldRenderDivider() {
@ -145,8 +145,8 @@ export default {
},
ENVIRONMENT_QUERY_LIMIT,
i18n: {
maxEnvsNote: s__(
'CiVariable|Maximum of %{limit} environments listed. For more environments, enter a search query.',
searchQueryNote: s__(
'CiVariable|Enter a search query to find more environments, or use * to create a wildcard.',
),
},
};
@ -167,15 +167,9 @@ export default {
>
<template #footer>
<gl-dropdown-divider v-if="shouldRenderDivider" />
<div data-testid="max-envs-notice">
<gl-dropdown-item class="gl-list-none" disabled>
<gl-sprintf :message="$options.i18n.maxEnvsNote" class="gl-text-sm">
<template #limit>
{{ $options.ENVIRONMENT_QUERY_LIMIT }}
</template>
</gl-sprintf>
</gl-dropdown-item>
</div>
<gl-dropdown-item class="gl-list-none" disabled data-testid="search-query-note">
{{ $options.i18n.searchQueryNote }}
</gl-dropdown-item>
<div v-if="shouldRenderCreateButton">
<!-- TODO: Rethink create wildcard button. https://gitlab.com/gitlab-org/gitlab/-/issues/396928 -->
<gl-dropdown-item

View File

@ -1,3 +0,0 @@
import { initWorkItemsHierarchy } from '~/work_items_hierarchy/work_items_hierarchy_bundle';
initWorkItemsHierarchy();

View File

@ -219,6 +219,7 @@ export default {
<work-item-link-child-metadata
:reference="displayReference"
:iid="childItem.iid"
:is-child-item-open="isChildItemOpen"
:metadata-widgets="metadataWidgets"
:show-weight="showWeight"
:work-item-type="childItemType"

View File

@ -1,101 +0,0 @@
<script>
import { GlBanner } from '@gitlab/ui';
import Cookies from '~/lib/utils/cookies';
import { parseBoolean } from '~/lib/utils/common_utils';
import RESPONSE from '../static_response';
import { WORK_ITEMS_SURVEY_COOKIE_NAME, workItemTypes } from '../constants';
import Hierarchy from './hierarchy.vue';
export default {
components: {
GlBanner,
Hierarchy,
},
inject: ['illustrationPath', 'licensePlan'],
data() {
return {
bannerVisible: !parseBoolean(Cookies.get(WORK_ITEMS_SURVEY_COOKIE_NAME)),
workItemHierarchy: RESPONSE[this.licensePlan],
};
},
computed: {
hasUnavailableStructure() {
return this.workItemTypes.unavailable.length > 0;
},
workItemTypes() {
return this.workItemHierarchy.reduce(
(itemTypes, item) => {
const skipItem = workItemTypes[item.type].isWorkItem;
if (skipItem) {
return itemTypes;
}
const key = item.available ? 'available' : 'unavailable';
const nestedTypes = item.nestedTypes?.map((type) => workItemTypes[type]);
itemTypes[key].push({
...item,
...workItemTypes[item.type],
nestedTypes,
});
return itemTypes;
},
{ available: [], unavailable: [] },
);
},
},
methods: {
handleClose() {
Cookies.set(WORK_ITEMS_SURVEY_COOKIE_NAME, 'true', { expires: 365 * 10 });
this.bannerVisible = false;
},
},
};
</script>
<template>
<div>
<gl-banner
v-if="bannerVisible"
class="gl-mt-4 !gl-px-5"
:title="s__('Hierarchy|Help us improve work items in GitLab!')"
:button-text="s__('Hierarchy|Take the work items survey')"
button-link="https://forms.gle/u1BmRp8rTbwj52iq5"
:svg-path="illustrationPath"
@close="handleClose"
>
<p>
{{
s__(
'Hierarchy|Is there a framework or type of work item you wish you had access to in GitLab? Give us your feedback and help us build the experiences valuable to you.',
)
}}
</p>
</gl-banner>
<h3 class="!gl-mt-5">{{ s__('Hierarchy|Planning hierarchy') }}</h3>
<p>
{{
s__(
'Hierarchy|Deliver value more efficiently by breaking down necessary work into a hierarchical structure. This structure helps teams understand scope, priorities, and how work cascades up toward larger goals.',
)
}}
</p>
<div class="gl-mb-2 gl-font-bold">{{ s__('Hierarchy|Current structure') }}</div>
<p class="!gl-mb-3">{{ s__('Hierarchy|You can start using these items now.') }}</p>
<hierarchy :work-item-types="workItemTypes.available" />
<div
v-if="hasUnavailableStructure"
data-testid="unavailable-structure"
class="gl-mb-2 gl-mt-5 gl-font-bold"
>
{{ s__('Hierarchy|Unavailable structure') }}
</div>
<p v-if="hasUnavailableStructure" class="!gl-mb-3">
{{ s__('Hierarchy|These items are unavailable in the current structure.') }}
</p>
<hierarchy :work-item-types="workItemTypes.unavailable" />
</div>
</template>

View File

@ -1,119 +0,0 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlIcon, GlBadge } from '@gitlab/ui';
export default {
components: {
GlIcon,
GlBadge,
},
props: {
workItemTypes: {
type: Array,
required: true,
},
},
methods: {
isLastItem(index, workItem) {
const hasMoreThanOneItem = workItem.nestedTypes.length > 1;
const isLastItemInArray = index === workItem.nestedTypes.length - 1;
return isLastItemInArray && hasMoreThanOneItem;
},
nestedWorkItemTypeMargin(index, workItem) {
const isLastItemInArray = index === workItem.nestedTypes.length - 1;
const hasMoreThanOneItem = workItem.nestedTypes.length > 1;
if (isLastItemInArray && hasMoreThanOneItem) {
return 'gl-ml-0';
}
return 'gl-ml-6';
},
},
};
</script>
<template>
<div>
<div
v-for="workItem in workItemTypes"
:key="workItem.id"
class="gl-mb-3"
:class="{ flex: !workItem.available }"
>
<span
class="gl-inline-flex gl-items-center gl-justify-center gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100 gl-pb-2 gl-pl-2 gl-pr-3 gl-pt-2 gl-leading-normal"
data-testid="work-item-wrapper"
>
<span
:style="{
backgroundColor: workItem.backgroundColor,
color: workItem.color,
}"
class="justify-content-center hierarchy-icon-wrapper gl-mr-2 gl-inline-flex gl-items-center gl-rounded-base"
>
<gl-icon :size="workItem.iconSize || 12" :name="workItem.icon" />
</span>
{{ workItem.title }}
</span>
<gl-badge
v-if="!workItem.available"
variant="info"
icon="license"
class="gl-ml-3 gl-self-center"
>{{ workItem.license }}</gl-badge
>
<div v-if="workItem.nestedTypes" :class="{ 'gl-relative': workItem.nestedTypes.length > 1 }">
<svg
v-if="workItem.nestedTypes.length > 1"
class="hierarchy-rounded-arrow-tail gl-text-gray-400"
data-testid="hierarchy-rounded-arrow-tail"
width="2"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="0.75"
y1="1"
x2="0.75"
y2="100%"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
/>
</svg>
<template v-for="(nestedWorkItem, index) in workItem.nestedTypes">
<div :key="nestedWorkItem.id" class="gl-ml-6 gl-mt-2 gl-block">
<gl-icon name="arrow-down" class="gl-text-gray-400" />
</div>
<gl-icon
v-if="isLastItem(index, workItem)"
:key="nestedWorkItem.id"
name="level-up"
class="hierarchy-rounded-arrow gl-ml-2 gl-text-gray-400"
/>
<span
:key="nestedWorkItem.id"
class="gl-mt-2 gl-inline-flex gl-items-center gl-justify-center gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100 gl-pb-2 gl-pl-2 gl-pr-3 gl-pt-2 gl-leading-normal"
:class="nestedWorkItemTypeMargin(index, workItem)"
>
<span
:style="{
backgroundColor: nestedWorkItem.backgroundColor,
color: nestedWorkItem.color,
}"
class="justify-content-center hierarchy-icon-wrapper gl-mr-2 gl-inline-flex gl-items-center gl-rounded-base"
>
<gl-icon :size="nestedWorkItem.iconSize || 12" :name="nestedWorkItem.icon" />
</span>
{{ nestedWorkItem.title }}
</span>
</template>
</div>
</div>
</div>
</template>

View File

@ -1,62 +0,0 @@
import { __ } from '~/locale';
export const WORK_ITEMS_SURVEY_COOKIE_NAME = 'hide_work_items_hierarchy_survey';
/**
* Hard-coded strings since we're rendering hierarchy
* items from mock responses. Remove this when we
* have a real hierarchy endpoint.
*/
export const LICENSE_PLAN = {
FREE: 'free',
PREMIUM: 'premium',
ULTIMATE: 'ultimate',
};
export const workItemTypes = {
EPIC: {
title: __('Epic'),
icon: 'epic',
color: '#694CC0',
backgroundColor: '#E1D8F9',
},
ISSUE: {
title: __('Issue'),
icon: 'issues',
color: '#1068BF',
backgroundColor: '#CBE2F9',
},
TASK: {
title: __('Task'),
icon: 'todo-done',
color: '#217645',
backgroundColor: '#C3E6CD',
isWorkItem: true,
},
INCIDENT: {
title: __('Incident'),
icon: 'issue-type-incident',
backgroundColor: '#db2a0f',
color: '#FDD4CD',
iconSize: 16,
},
SUB_EPIC: {
title: __('Child epic'),
icon: 'epic',
color: '#AB6100',
backgroundColor: '#F5D9A8',
},
REQUIREMENT: {
title: __('Requirement'),
icon: 'requirements',
color: '#0068c5',
backgroundColor: '#c5e3fb',
},
TEST_CASE: {
title: __('Test case'),
icon: 'issue-type-test-case',
backgroundColor: '#007a3f',
color: '#bae8cb',
iconSize: 16,
},
};

View File

@ -1,11 +0,0 @@
import { LICENSE_PLAN } from './constants';
export function inferLicensePlan({ hasSubEpics, hasEpics }) {
if (hasSubEpics) {
return LICENSE_PLAN.ULTIMATE;
}
if (hasEpics) {
return LICENSE_PLAN.PREMIUM;
}
return LICENSE_PLAN.FREE;
}

View File

@ -1,142 +0,0 @@
const FREE_TIER = 'free';
const ULTIMATE_TIER = 'ultimate';
const PREMIUM_TIER = 'premium';
const RESPONSE = {
[FREE_TIER]: [
{
id: '1',
type: 'ISSUE',
available: true,
license: null,
nestedTypes: null,
},
{
id: '2',
type: 'TASK',
available: true,
license: null,
nestedTypes: null,
},
{
id: '3',
type: 'INCIDENT',
available: true,
license: null,
nestedTypes: null,
},
{
id: '4',
type: 'EPIC',
available: false,
license: 'Premium', // eslint-disable-line @gitlab/require-i18n-strings
nestedTypes: null,
},
{
id: '5',
type: 'SUB_EPIC',
available: false,
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
nestedTypes: null,
},
{
id: '6',
type: 'REQUIREMENT',
available: false,
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
nestedTypes: null,
},
{
id: '7',
type: 'TEST_CASE',
available: false,
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
nestedTypes: null,
},
],
[PREMIUM_TIER]: [
{
id: '1',
type: 'EPIC',
available: true,
license: null,
nestedTypes: ['ISSUE'],
},
{
id: '2',
type: 'TASK',
available: true,
license: null,
nestedTypes: null,
},
{
id: '3',
type: 'INCIDENT',
available: true,
license: null,
nestedTypes: null,
},
{
id: '5',
type: 'SUB_EPIC',
available: false,
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
nestedTypes: null,
},
{
id: '6',
type: 'REQUIREMENT',
available: false,
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
nestedTypes: null,
},
{
id: '7',
type: 'TEST_CASE',
available: false,
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
nestedTypes: null,
},
],
[ULTIMATE_TIER]: [
{
id: '1',
type: 'EPIC',
available: true,
license: null,
nestedTypes: ['SUB_EPIC', 'ISSUE'],
},
{
id: '2',
type: 'TASK',
available: true,
license: null,
nestedTypes: null,
},
{
id: '3',
type: 'INCIDENT',
available: true,
license: null,
nestedTypes: null,
},
{
id: '6',
type: 'REQUIREMENT',
available: true,
license: null,
nestedTypes: null,
},
{
id: '7',
type: 'TEST_CASE',
available: true,
license: null,
nestedTypes: null,
},
],
};
export default RESPONSE;

View File

@ -1,26 +0,0 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import App from './components/app.vue';
import { inferLicensePlan } from './hierarchy_util';
export const initWorkItemsHierarchy = () => {
const el = document.querySelector('#js-work-items-hierarchy');
const { illustrationPath, hasEpics, hasSubEpics } = el.dataset;
const licensePlan = inferLicensePlan({
hasEpics: parseBoolean(hasEpics),
hasSubEpics: parseBoolean(hasSubEpics),
});
return new Vue({
el,
provide: {
illustrationPath,
licensePlan,
},
render(createElement) {
return createElement(App);
},
});
};

View File

@ -2,7 +2,6 @@
@import './pages/commits';
@import './pages/events';
@import './pages/groups';
@import './pages/hierarchy';
@import './pages/issues';
@import './pages/note_form';
@import './pages/notes';

View File

@ -1,15 +0,0 @@
.hierarchy-rounded-arrow-tail {
position: absolute;
top: 4px;
left: 5px;
height: calc(100% - 20px);
}
.hierarchy-icon-wrapper {
height: $default-icon-size;
width: $default-icon-size;
}
.hierarchy-rounded-arrow {
transform: scale(1, -1) rotate(90deg);
}

View File

@ -1,15 +0,0 @@
# frozen_string_literal: true
module PlanningHierarchy
extend ActiveSupport::Concern
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def planning_hierarchy
return access_denied! unless can?(current_user, :read_planning_hierarchy, @project)
route_not_found
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
PlanningHierarchy.prepend_mod_with('PlanningHierarchy')

View File

@ -9,7 +9,6 @@ class ProjectsController < Projects::ApplicationController
include ImportUrlParams
include FiltersEvents
include SourcegraphDecorator
include PlanningHierarchy
REFS_LIMIT = 100
@ -66,7 +65,6 @@ class ProjectsController < Projects::ApplicationController
feature_category :team_planning, [:preview_markdown, :new_issuable_address]
feature_category :importers, [:export, :remove_export, :generate_new_export, :download_export]
feature_category :code_review_workflow, [:unfoldered_environment_names]
feature_category :portfolio_management, [:planning_hierarchy]
urgency :low, [:export, :remove_export, :generate_new_export, :download_export]
urgency :low, [:preview_markdown, :new_issuable_address]

View File

@ -80,9 +80,7 @@ class SessionsController < Devise::SessionsController
accept_pending_invitations
if Feature.enabled?(:new_broadcast_message_dismissal, current_user, type: :gitlab_com_derisk)
synchronize_broadcast_message_dismissals
end
synchronize_broadcast_message_dismissals
log_audit_event(current_user, resource, with: authentication_method)
log_user_activity(current_user)

View File

@ -362,7 +362,6 @@ class ProjectPolicy < BasePolicy
enable :read_wiki
enable :read_issue
enable :read_label
enable :read_planning_hierarchy
enable :read_milestone
enable :read_snippet
enable :read_project_member
@ -791,7 +790,6 @@ class ProjectPolicy < BasePolicy
enable :read_issue_board_list
enable :read_wiki
enable :read_label
enable :read_planning_hierarchy
enable :read_milestone
enable :read_snippet
enable :read_project_member

View File

@ -35,6 +35,7 @@ module Ml
)
add_metadata(model, @metadata)
audit_creation_event(model)
success(model)
end
@ -69,5 +70,17 @@ module Ml
def experiment_name
Ml::Model.prefixed_experiment(@name)
end
def audit_creation_event(model)
audit_context = {
name: 'ml_model_created',
author: @user,
scope: @project,
target: model,
message: "MlModel #{model.name} created"
}
::Gitlab::Audit::Auditor.audit(audit_context)
end
end
end

View File

@ -17,6 +17,8 @@ module Ml
return error unless @model.destroy
audit_destroy_event(@model)
success
end
@ -37,5 +39,17 @@ module Ml
def payload
{ model: @model }
end
def audit_destroy_event(model)
audit_context = {
name: 'ml_model_destroyed',
author: @user,
scope: model.project,
target: model,
message: "MlModel #{model.name} destroyed"
}
::Gitlab::Audit::Auditor.audit(audit_context)
end
end
end

View File

@ -33,16 +33,20 @@ module PagesDomains
api_order = ::Gitlab::LetsEncrypt::Client.new.load_order(acme_order.url)
# https://www.rfc-editor.org/rfc/rfc8555#section-7.1.6 - statuses diagram
case api_order.status
when 'ready'
api_order.request_certificate(private_key: acme_order.private_key, domain: pages_domain.domain)
PagesDomainSslRenewalWorker.perform_in(CERTIFICATE_PROCESSING_DELAY, pages_domain.id)
when 'valid'
save_certificate(acme_order.private_key, api_order)
acme_order.destroy!
when 'invalid'
save_order_error(acme_order, get_challenge_error(api_order))
begin
# https://www.rfc-editor.org/rfc/rfc8555#section-7.1.6 - statuses diagram
case api_order.status
when 'ready'
api_order.request_certificate(private_key: acme_order.private_key, domain: pages_domain.domain)
PagesDomainSslRenewalWorker.perform_in(CERTIFICATE_PROCESSING_DELAY, pages_domain.id)
when 'valid'
save_certificate(acme_order.private_key, api_order)
acme_order.destroy!
when 'invalid'
save_order_error(acme_order, get_challenge_error(api_order))
end
rescue Acme::Client::Error => e
save_order_error(acme_order, e.message)
end
end

View File

@ -1,5 +0,0 @@
- page_title _("Planning hierarchy")
- has_sub_epics = @project&.licensed_feature_available?(:subepics)
- has_epics = @project&.licensed_feature_available?(:epics)
#js-work-items-hierarchy{ data: { has_sub_epics: has_sub_epics.to_s, has_epics: has_epics.to_s, illustration_path: image_path('illustrations/rocket-launch-md.svg') } }

View File

@ -0,0 +1,9 @@
name: ml_model_created
description: ML model is created
introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/463215
introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/165011
feature_category: mlops
milestone: '17.4'
saved_to_database: true
scope: [Project]
streamed: true

View File

@ -0,0 +1,9 @@
name: ml_model_destroyed
description: ML model destroyed
introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/463215
introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/165011
feature_category: mlops
milestone: '17.4'
saved_to_database: true
scope: [Project]
streamed: true

View File

@ -1,9 +0,0 @@
---
name: new_broadcast_message_dismissal
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/438595
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/151056
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/461564
milestone: '17.1'
type: gitlab_com_derisk
group: group::activation
default_enabled: false

View File

@ -459,8 +459,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
get :planning_hierarchy
resources :badges, only: [] do
collection do
constraints format: /svg/ do

View File

@ -1,319 +1,11 @@
---
stage: Verify
group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
redirect_to: '../../secrets/hashicorp_vault.md'
remove_date: '2024-12-05'
---
# Authenticating and reading secrets with HashiCorp Vault
This document was moved to [another location](../../secrets/hashicorp_vault.md).
DETAILS:
**Tier:** Premium, Ultimate
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
WARNING:
Authenticating with `CI_JOB_JWT` was [deprecated in GitLab 15.9](../../../update/deprecations.md#old-versions-of-json-web-tokens-are-deprecated)
and the token is scheduled to be removed in GitLab 17.0. Use
[ID tokens to authenticate with HashiCorp Vault](../../secrets/id_token_authentication.md#automatic-id-token-authentication-with-hashicorp-vault)
instead, as demonstrated on this page.
NOTE:
Starting in Vault 1.17, [JWT auth login requires bound audiences on the role](https://developer.hashicorp.com/vault/docs/upgrading/upgrade-to-1.17.x#jwt-auth-login-requires-bound-audiences-on-the-role)
when the JWT contains an `aud` claim. The `aud` claim can be a single string or a list of strings.
This tutorial demonstrates how to authenticate, configure, and read secrets with HashiCorp's Vault from GitLab CI/CD.
## Prerequisites
This tutorial assumes you are familiar with GitLab CI/CD and Vault.
To follow along, you must have:
- An account on GitLab.
- Access to a running Vault server (at least v1.2.0) to configure authentication and to create roles and policies. For HashiCorp Vaults, this can be the Open Source or Enterprise version.
NOTE:
You must replace the `vault.example.com` URL below with the URL of your Vault server, and `gitlab.example.com` with the URL of your GitLab instance.
## How it works
ID tokens are JSON Web Tokens (JWTs) used for OIDC authentication with third-party services. If a job has at least one ID token defined, the `secrets` keyword automatically uses that token to authenticate with Vault.
The following fields are included in the JWT:
| Field | When | Description |
|-------------------------|------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `jti` | Always | Unique identifier for this token |
| `iss` | Always | Issuer, the domain of your GitLab instance |
| `iat` | Always | Issued at |
| `nbf` | Always | Not valid before |
| `exp` | Always | Expires at |
| `sub` | Always | Subject (job ID) |
| `namespace_id` | Always | Use this to scope to group or user level namespace by ID |
| `namespace_path` | Always | Use this to scope to group or user level namespace by path |
| `project_id` | Always | Use this to scope to project by ID |
| `project_path` | Always | Use this to scope to project by path |
| `user_id` | Always | ID of the user executing the job |
| `user_login` | Always | Username of the user executing the job |
| `user_email` | Always | Email of the user executing the job |
| `pipeline_id` | Always | ID of this pipeline |
| `pipeline_source` | Always | [Pipeline source](../../jobs/job_rules.md#common-if-clauses-with-predefined-variables) |
| `job_id` | Always | ID of this job |
| `ref` | Always | Git ref for this job |
| `ref_type` | Always | Git ref type, either `branch` or `tag` |
| `ref_path` | Always | Fully qualified ref for the job. For example, `refs/heads/main`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119075) in GitLab 16.0. |
| `ref_protected` | Always | `true` if this Git ref is protected, `false` otherwise |
| `environment` | Job specifies an environment | Environment this job specifies |
| `groups_direct` | User is a direct member of 0 to 200 groups | The paths of the user's direct membership groups. Omitted if the user is a direct member of more than 200 groups. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/435848) in GitLab 16.11). |
| `environment_protected` | Job specifies an environment | `true` if specified environment is protected, `false` otherwise |
| `deployment_tier` | Job specifies an environment | [Deployment tier](../../environments/index.md#deployment-tier-of-environments) of environment this job specifies ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/363590) in GitLab 15.2) |
| `environment_action` | Job specifies an environment | [Environment action (`environment:action`)](../../environments/index.md) specified in the job. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/) in GitLab 16.5) |
Example JWT payload:
```json
{
"jti": "c82eeb0c-5c6f-4a33-abf5-4c474b92b558",
"iss": "gitlab.example.com",
"iat": 1585710286,
"nbf": 1585798372,
"exp": 1585713886,
"sub": "job_1212",
"namespace_id": "1",
"namespace_path": "mygroup",
"project_id": "22",
"project_path": "mygroup/myproject",
"user_id": "42",
"user_login": "myuser",
"user_email": "myuser@example.com",
"pipeline_id": "1212",
"pipeline_source": "web",
"job_id": "1212",
"ref": "auto-deploy-2020-04-01",
"ref_type": "branch",
"ref_path": "refs/heads/auto-deploy-2020-04-01",
"ref_protected": "true",
"groups_direct": ["mygroup/mysubgroup", "myothergroup/myothersubgroup"],
"environment": "production",
"environment_protected": "true",
"environment_action": "start"
}
```
The JWT is encoded by using RS256 and signed with a dedicated private key. The expire time for the token is set to job's timeout, if specified, or 5 minutes if it is not. The key used to sign this token may change without any notice. In such case retrying the job generates new JWT using the current signing key.
You can use this JWT for authentication with a Vault server that is configured to allow
the JWT authentication method. Provide your GitLab instance's base URL
(for example `https://gitlab.example.com`) to your Vault server as the `oidc_discovery_url`.
The server can then retrieve the keys for validating the token from your instance.
When configuring roles in Vault, you can use [bound claims](https://developer.hashicorp.com/vault/docs/auth/jwt#bound-claims) to match against the JWT claims and restrict which secrets each CI/CD job has access to.
To communicate with Vault, you can use either its CLI client or perform API requests (using `curl` or another client).
## Example
WARNING:
JWTs are credentials, which can grant access to resources. Be careful where you paste them!
Let's say you have the passwords for your staging and production databases stored in a Vault server that is running on `http://vault.example.com:8200`. Your staging password is `pa$$w0rd` and your production password is `real-pa$$w0rd`.
```shell
$ vault kv get -field=password secret/myproject/staging/db
pa$$w0rd
$ vault kv get -field=password secret/myproject/production/db
real-pa$$w0rd
```
To configure your Vault server, start by enabling the [JWT Auth](https://developer.hashicorp.com/vault/docs/auth/jwt) method:
```shell
$ vault auth enable jwt
Success! Enabled jwt auth method at: jwt/
```
Then create policies that allow you to read these secrets (one for each secret):
```shell
$ vault policy write myproject-staging - <<EOF
# Policy name: myproject-staging
#
# Read-only permission on 'secret/myproject/staging/*' path
path "secret/myproject/staging/*" {
capabilities = [ "read" ]
}
EOF
Success! Uploaded policy: myproject-staging
$ vault policy write myproject-production - <<EOF
# Policy name: myproject-production
#
# Read-only permission on 'secret/myproject/production/*' path
path "secret/myproject/production/*" {
capabilities = [ "read" ]
}
EOF
Success! Uploaded policy: myproject-production
```
You also need roles that link the JWT with these policies.
One for staging named `myproject-staging`:
```shell
$ vault write auth/jwt/role/myproject-staging - <<EOF
{
"role_type": "jwt",
"policies": ["myproject-staging"],
"token_explicit_max_ttl": 60,
"user_claim": "user_email",
"bound_audiences": "https://vault.example.com",
"bound_claims": {
"project_id": "22",
"ref": "master",
"ref_type": "branch"
}
}
EOF
```
And one for production named `myproject-production`:
```shell
$ vault write auth/jwt/role/myproject-production - <<EOF
{
"role_type": "jwt",
"policies": ["myproject-production"],
"token_explicit_max_ttl": 60,
"user_claim": "user_email",
"bound_audiences": "https://vault.example.com",
"bound_claims_type": "glob",
"bound_claims": {
"project_id": "22",
"ref_protected": "true",
"ref_type": "branch",
"ref": "auto-deploy-*"
}
}
EOF
```
This example uses [bound claims](https://developer.hashicorp.com/vault/api-docs/auth/jwt#bound_claims) to specify that only a JWT with matching values for the specified claims is allowed to authenticate.
Combined with [protected branches](../../../user/project/protected_branches.md), you can restrict who is able to authenticate and read the secrets.
Any of the claims [included in the JWT](#how-it-works) can be matched against a list of values
in the bound claims. For example:
```json
"bound_claims": {
"user_login": ["alice", "bob", "mallory"]
}
"bound_claims": {
"ref": ["main", "develop", "test"]
}
"bound_claims": {
"namespace_id": ["10", "20", "30"]
}
"bound_claims": {
"project_id": ["12", "22", "37"]
}
```
- If only `namespace_id` is used, all projects in the namespace are allowed. Nested projects are not included, so their namespace IDs must also be added to the list if needed.
- If both `namespace_id` and `project_id` are used, Vault first checks if the project's namespace is in `namespace_id` then checks if the project is in `project_id`.
[`token_explicit_max_ttl`](https://developer.hashicorp.com/vault/api-docs/auth/jwt#token_explicit_max_ttl) specifies that the token issued by Vault, upon successful authentication, has a hard lifetime limit of 60 seconds.
[`user_claim`](https://developer.hashicorp.com/vault/api-docs/auth/jwt#user_claim) specifies the name for the Identity alias created by Vault upon a successful login.
[`bound_claims_type`](https://developer.hashicorp.com/vault/api-docs/auth/jwt#bound_claims_type) configures the interpretation of the `bound_claims` values. If set to `glob`, the values are interpreted as globs, with `*` matching any number of characters.
The claim fields listed in [the table above](#how-it-works) can also be accessed for [Vault's policy path templating](https://developer.hashicorp.com/vault/tutorials/policies/policy-templating?in=vault%2Fpolicies) purposes by using the accessor name of the JWT auth within Vault. The [mount accessor name](https://developer.hashicorp.com/vault/tutorials/auth-methods/identity#step-1-create-an-entity-with-alias) (`ACCESSOR_NAME` in the example below) can be retrieved by running `vault auth list`.
Policy template example making use of a named metadata field named `project_path`:
```plaintext
path "secret/data/{{identity.entity.aliases.ACCESSOR_NAME.metadata.project_path}}/staging/*" {
capabilities = [ "read" ]
}
```
Role example to support the templated policy above, mapping the claim field `project_path` as a metadata field through use of [`claim_mappings`](https://developer.hashicorp.com/vault/api-docs/auth/jwt#claim_mappings) configuration:
```plaintext
{
"role_type": "jwt",
...
"claim_mappings": {
"project_path": "project_path"
}
}
```
For the full list of options, see Vault's [Create Role documentation](https://developer.hashicorp.com/vault/api-docs/auth/jwt#create-role).
WARNING:
Always restrict your roles to project or namespace by using one of the provided claims (for example, `project_id` or `namespace_id`). Otherwise any JWT generated by this instance may be allowed to authenticate using this role.
Now, configure the JWT Authentication method:
```shell
$ vault write auth/jwt/config \
oidc_discovery_url="https://gitlab.example.com" \
bound_issuer="https://gitlab.example.com"
```
[`bound_issuer`](https://developer.hashicorp.com/vault/api-docs/auth/jwt#bound_issuer) specifies that only a JWT with the issuer (that is, the `iss` claim) set to `gitlab.example.com` can use this method to authenticate, and that the `oidc_discovery_url` (`https://gitlab.example.com`) should be used to validate the token.
For the full list of available configuration options, see Vault's [API documentation](https://developer.hashicorp.com/vault/api-docs/auth/jwt#configure).
In GitLab, create the following [CI/CD variables](../../variables/index.md#for-a-project) to provide details about your Vault server:
- `VAULT_SERVER_URL` - The URL of your Vault server, for example `https://vault.example.com:8200`.
- `VAULT_AUTH_ROLE` - Optional. The role to use when attempting to authenticate. If no role is specified, Vault uses the [default role](https://developer.hashicorp.com/vault/api-docs/auth/jwt#default_role) specified when the authentication method was configured.
- `VAULT_AUTH_PATH` - Optional. The path where the authentication method is mounted. Default is `jwt`.
- `VAULT_NAMESPACE` - Optional. The [Vault Enterprise namespace](https://developer.hashicorp.com/vault/docs/enterprise/namespaces) to use for reading secrets and authentication. If no namespace is specified, Vault uses the root (`/`) namespace. The setting is ignored by Vault Open Source.
The following job, when run for the default branch, can read secrets under `secret/myproject/staging/`, but not the secrets under `secret/myproject/production/`:
```yaml
job_with_secrets:
id_tokens:
VAULT_ID_TOKEN:
aud: https://vault.example.com
secrets:
STAGING_DB_PASSWORD:
vault: secret/myproject/staging/db/password@secrets # authenticates using $VAULT_ID_TOKEN
script:
- access-staging-db.sh --token $STAGING_DB_PASSWORD
```
In this example:
- `id_tokens` - The JSON Web Token (JWT) used for OIDC authentication. The `aud` claim
is set to match the `bound_audiences` parameter of the Vault JWT authentication method.
- `@secrets` - The vault name, where your Secrets Engines are enabled.
- `secret/myproject/staging/db` - The path location of the secret in Vault.
- `password` The field to be fetched within the referenced secret.
### Limit token access to Vault secrets
You can control ID token access to Vault secrets by using Vault protections
and GitLab features. For example, restrict the token by:
- Using Vault [bound audiences](https://developer.hashicorp.com/vault/docs/auth/jwt#bound-audiences)
for specific ID token `aud` claims.
- Using Vault [bound claims](https://developer.hashicorp.com/vault/docs/auth/jwt#bound-claims)
for specific groups using `group_claim`.
- Hard coding values for Vault bound claims based on the `user_login` and `user_email`
of specific users.
- Setting Vault time limits for TTL of the token as specified in [`token_explicit_max_ttl`](https://developer.hashicorp.com/vault/api-docs/auth/jwt#token_explicit_max_ttl),
where the token expires after authentication.
- Scoping the JWT to [GitLab protected branches](../../../user/project/protected_branches.md)
that are restricted to a subset of project users.
- Scoping the JWT to [GitLab protected tags](../../../user/project/protected_tags.md),
that are restricted to a subset of project users.
<!-- This redirect file can be deleted after <2024-12-05>. -->
<!-- Redirects that point to other docs in the same project expire in three months. -->
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->

View File

@ -0,0 +1,355 @@
---
stage: Verify
group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# Use HashiCorp Vault secrets in GitLab CI/CD
DETAILS:
**Tier:** Premium, Ultimate
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
WARNING:
Authenticating with `CI_JOB_JWT` was [deprecated in GitLab 15.9](../../update/deprecations.md#old-versions-of-json-web-tokens-are-deprecated)
and the token is scheduled to be removed in GitLab 18.0. Use
[ID tokens to authenticate with HashiCorp Vault](id_token_authentication.md#automatic-id-token-authentication-with-hashicorp-vault)
instead, as demonstrated on this page.
NOTE:
Starting in Vault 1.17, [JWT auth login requires bound audiences on the role](https://developer.hashicorp.com/vault/docs/upgrading/upgrade-to-1.17.x#jwt-auth-login-requires-bound-audiences-on-the-role)
when the JWT contains an `aud` claim. The `aud` claim can be a single string or a list of strings.
This tutorial demonstrates how to authenticate, configure, and read secrets with HashiCorp's Vault from GitLab CI/CD.
## Prerequisites
This tutorial assumes you are familiar with GitLab CI/CD and Vault.
To follow along, you must have:
- An account on GitLab.
- Access to a running Vault server (at least v1.2.0) to configure authentication and to create roles and policies.
For HashiCorp Vaults, this can be the Open Source or Enterprise version.
NOTE:
You must replace the `vault.example.com` URL below with the URL of your Vault server,
and `gitlab.example.com` with the URL of your GitLab instance.
## How it works
ID tokens are JSON Web Tokens (JWTs) used for OIDC authentication with third-party services.
If a job has at least one ID token defined, the `secrets` keyword automatically uses that token
to authenticate with Vault.
The following fields are included in the JWT:
| Field | When | Description |
|-------------------------|--------------------------------------------|-------------|
| `jti` | Always | Unique identifier for this token |
| `iss` | Always | Issuer, the domain of your GitLab instance |
| `iat` | Always | Issued at |
| `nbf` | Always | Not valid before |
| `exp` | Always | Expires at |
| `sub` | Always | Subject (job ID) |
| `namespace_id` | Always | Use this to scope to group or user level namespace by ID |
| `namespace_path` | Always | Use this to scope to group or user level namespace by path |
| `project_id` | Always | Use this to scope to project by ID |
| `project_path` | Always | Use this to scope to project by path |
| `user_id` | Always | ID of the user executing the job |
| `user_login` | Always | Username of the user executing the job |
| `user_email` | Always | Email of the user executing the job |
| `pipeline_id` | Always | ID of this pipeline |
| `pipeline_source` | Always | [Pipeline source](../jobs/job_rules.md#common-if-clauses-with-predefined-variables) |
| `job_id` | Always | ID of this job |
| `ref` | Always | Git ref for this job |
| `ref_type` | Always | Git ref type, either `branch` or `tag` |
| `ref_path` | Always | Fully qualified ref for the job. For example, `refs/heads/main`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119075) in GitLab 16.0. |
| `ref_protected` | Always | `true` if this Git ref is protected, `false` otherwise |
| `environment` | Job specifies an environment | Environment this job specifies |
| `groups_direct` | User is a direct member of 0 to 200 groups | The paths of the user's direct membership groups. Omitted if the user is a direct member of more than 200 groups. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/435848) in GitLab 16.11). |
| `environment_protected` | Job specifies an environment | `true` if specified environment is protected, `false` otherwise |
| `deployment_tier` | Job specifies an environment | [Deployment tier](../environments/index.md#deployment-tier-of-environments) of environment this job specifies ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/363590) in GitLab 15.2) |
| `environment_action` | Job specifies an environment | [Environment action (`environment:action`)](../environments/index.md) specified in the job. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/) in GitLab 16.5) |
Example JWT payload:
```json
{
"jti": "c82eeb0c-5c6f-4a33-abf5-4c474b92b558",
"iss": "gitlab.example.com",
"iat": 1585710286,
"nbf": 1585798372,
"exp": 1585713886,
"sub": "job_1212",
"namespace_id": "1",
"namespace_path": "mygroup",
"project_id": "22",
"project_path": "mygroup/myproject",
"user_id": "42",
"user_login": "myuser",
"user_email": "myuser@example.com",
"pipeline_id": "1212",
"pipeline_source": "web",
"job_id": "1212",
"ref": "auto-deploy-2020-04-01",
"ref_type": "branch",
"ref_path": "refs/heads/auto-deploy-2020-04-01",
"ref_protected": "true",
"groups_direct": ["mygroup/mysubgroup", "myothergroup/myothersubgroup"],
"environment": "production",
"environment_protected": "true",
"environment_action": "start"
}
```
The JWT is encoded by using RS256 and signed with a dedicated private key. The expire time
for the token is set to job's timeout, if specified, or 5 minutes if it is not.
The key used to sign this token may change without any notice. In such case retrying the job
generates new JWT using the current signing key.
You can use this JWT for authentication with a Vault server that is configured to allow
the JWT authentication method. Provide your GitLab instance's base URL
(for example `https://gitlab.example.com`) to your Vault server as the `oidc_discovery_url`.
The server can then retrieve the keys for validating the token from your instance.
When configuring roles in Vault, you can use [bound claims](https://developer.hashicorp.com/vault/docs/auth/jwt#bound-claims)
to match against the JWT claims and restrict which secrets each CI/CD job has access to.
To communicate with Vault, you can use either its CLI client or perform API requests (using `curl` or another client).
## Example
WARNING:
JWTs are credentials, which can grant access to resources. Be careful where you paste them!
Let's say you have the passwords for your staging and production databases stored in a Vault server
that is running on `http://vault.example.com:8200`. Your staging password is `pa$$w0rd`
and your production password is `real-pa$$w0rd`.
```shell
$ vault kv get -field=password secret/myproject/staging/db
pa$$w0rd
$ vault kv get -field=password secret/myproject/production/db
real-pa$$w0rd
```
To configure your Vault server, start by enabling the [JWT Auth](https://developer.hashicorp.com/vault/docs/auth/jwt) method:
```shell
$ vault auth enable jwt
Success! Enabled jwt auth method at: jwt/
```
Then create policies that allow you to read these secrets (one for each secret):
```shell
$ vault policy write myproject-staging - <<EOF
# Policy name: myproject-staging
#
# Read-only permission on 'secret/myproject/staging/*' path
path "secret/myproject/staging/*" {
capabilities = [ "read" ]
}
EOF
Success! Uploaded policy: myproject-staging
$ vault policy write myproject-production - <<EOF
# Policy name: myproject-production
#
# Read-only permission on 'secret/myproject/production/*' path
path "secret/myproject/production/*" {
capabilities = [ "read" ]
}
EOF
Success! Uploaded policy: myproject-production
```
You also need roles that link the JWT with these policies.
One for staging named `myproject-staging`:
```shell
$ vault write auth/jwt/role/myproject-staging - <<EOF
{
"role_type": "jwt",
"policies": ["myproject-staging"],
"token_explicit_max_ttl": 60,
"user_claim": "user_email",
"bound_audiences": "https://vault.example.com",
"bound_claims": {
"project_id": "22",
"ref": "master",
"ref_type": "branch"
}
}
EOF
```
And one for production named `myproject-production`:
```shell
$ vault write auth/jwt/role/myproject-production - <<EOF
{
"role_type": "jwt",
"policies": ["myproject-production"],
"token_explicit_max_ttl": 60,
"user_claim": "user_email",
"bound_audiences": "https://vault.example.com",
"bound_claims_type": "glob",
"bound_claims": {
"project_id": "22",
"ref_protected": "true",
"ref_type": "branch",
"ref": "auto-deploy-*"
}
}
EOF
```
This example uses [bound claims](https://developer.hashicorp.com/vault/api-docs/auth/jwt#bound_claims)
to specify that only a JWT with matching values for the specified claims is allowed to authenticate.
Combined with [protected branches](../../user/project/protected_branches.md),
you can restrict who is able to authenticate and read the secrets.
Any of the claims [included in the JWT](#how-it-works) can be matched against a list of values
in the bound claims. For example:
```json
"bound_claims": {
"user_login": ["alice", "bob", "mallory"]
}
"bound_claims": {
"ref": ["main", "develop", "test"]
}
"bound_claims": {
"namespace_id": ["10", "20", "30"]
}
"bound_claims": {
"project_id": ["12", "22", "37"]
}
```
- If only `namespace_id` is used, all projects in the namespace are allowed. Nested projects are not included,
so their namespace IDs must also be added to the list if needed.
- If both `namespace_id` and `project_id` are used, Vault first checks if the project's namespace
is in `namespace_id` then checks if the project is in `project_id`.
[`token_explicit_max_ttl`](https://developer.hashicorp.com/vault/api-docs/auth/jwt#token_explicit_max_ttl)
specifies that the token issued by Vault, upon successful authentication, has a hard lifetime limit of 60 seconds.
[`user_claim`](https://developer.hashicorp.com/vault/api-docs/auth/jwt#user_claim)
specifies the name for the Identity alias created by Vault upon a successful login.
[`bound_claims_type`](https://developer.hashicorp.com/vault/api-docs/auth/jwt#bound_claims_type)
configures the interpretation of the `bound_claims` values. If set to `glob`, the values are interpreted as globs,
with `*` matching any number of characters.
The claim fields listed in [the table above](#how-it-works) can also be accessed for
[Vault's policy path templating](https://developer.hashicorp.com/vault/tutorials/policies/policy-templating?in=vault%2Fpolicies)
purposes by using the accessor name of the JWT auth in Vault.
The [mount accessor name](https://developer.hashicorp.com/vault/tutorials/auth-methods/identity#step-1-create-an-entity-with-alias)
(`ACCESSOR_NAME` in the example below) can be retrieved by running `vault auth list`.
Policy template example making use of a named metadata field named `project_path`:
```plaintext
path "secret/data/{{identity.entity.aliases.ACCESSOR_NAME.metadata.project_path}}/staging/*" {
capabilities = [ "read" ]
}
```
Role example to support the templated policy above, mapping the claim field `project_path`
as a metadata field through use of [`claim_mappings`](https://developer.hashicorp.com/vault/api-docs/auth/jwt#claim_mappings)
configuration:
```plaintext
{
"role_type": "jwt",
...
"claim_mappings": {
"project_path": "project_path"
}
}
```
For the full list of options, see Vault's [Create Role documentation](https://developer.hashicorp.com/vault/api-docs/auth/jwt#create-role).
WARNING:
Always restrict your roles to project or namespace by using one of the provided claims
(for example, `project_id` or `namespace_id`). Otherwise any JWT generated by this instance
may be allowed to authenticate using this role.
Now, configure the JWT Authentication method:
```shell
$ vault write auth/jwt/config \
oidc_discovery_url="https://gitlab.example.com" \
bound_issuer="https://gitlab.example.com"
```
[`bound_issuer`](https://developer.hashicorp.com/vault/api-docs/auth/jwt#bound_issuer)
specifies that only a JWT with the issuer (that is, the `iss` claim) set to `gitlab.example.com`
can use this method to authenticate, and that the `oidc_discovery_url` (`https://gitlab.example.com`)
should be used to validate the token.
For the full list of available configuration options, see Vault's [API documentation](https://developer.hashicorp.com/vault/api-docs/auth/jwt#configure).
In GitLab, create the following [CI/CD variables](../variables/index.md#for-a-project)
to provide details about your Vault server:
- `VAULT_SERVER_URL` - The URL of your Vault server, for example `https://vault.example.com:8200`.
- `VAULT_AUTH_ROLE` - Optional. The role to use when attempting to authenticate. If no role is specified,
Vault uses the [default role](https://developer.hashicorp.com/vault/api-docs/auth/jwt#default_role)
specified when the authentication method was configured.
- `VAULT_AUTH_PATH` - Optional. The path where the authentication method is mounted.
Default is `jwt`.
- `VAULT_NAMESPACE` - Optional. The [Vault Enterprise namespace](https://developer.hashicorp.com/vault/docs/enterprise/namespaces)
to use for reading secrets and authentication. If no namespace is specified, Vault uses the root (`/`) namespace.
The setting is ignored by Vault Open Source.
The following job, when run for the default branch, can read secrets under `secret/myproject/staging/`,
but not the secrets under `secret/myproject/production/`:
```yaml
job_with_secrets:
id_tokens:
VAULT_ID_TOKEN:
aud: https://vault.example.com
secrets:
STAGING_DB_PASSWORD:
vault: secret/myproject/staging/db/password@secrets # authenticates using $VAULT_ID_TOKEN
script:
- access-staging-db.sh --token $STAGING_DB_PASSWORD
```
In this example:
- `id_tokens` - The JSON Web Token (JWT) used for OIDC authentication. The `aud` claim
is set to match the `bound_audiences` parameter of the Vault JWT authentication method.
- `@secrets` - The vault name, where your Secrets Engines are enabled.
- `secret/myproject/staging/db` - The path location of the secret in Vault.
- `password` The field to be fetched in the referenced secret.
### Limit token access to Vault secrets
You can control ID token access to Vault secrets by using Vault protections
and GitLab features. For example, restrict the token by:
- Using Vault [bound audiences](https://developer.hashicorp.com/vault/docs/auth/jwt#bound-audiences)
for specific ID token `aud` claims.
- Using Vault [bound claims](https://developer.hashicorp.com/vault/docs/auth/jwt#bound-claims)
for specific groups using `group_claim`.
- Hard coding values for Vault bound claims based on the `user_login` and `user_email`
of specific users.
- Setting Vault time limits for TTL of the token as specified in [`token_explicit_max_ttl`](https://developer.hashicorp.com/vault/api-docs/auth/jwt#token_explicit_max_ttl),
where the token expires after authentication.
- Scoping the JWT to [GitLab protected branches](../../user/project/protected_branches.md)
that are restricted to a subset of project users.
- Scoping the JWT to [GitLab protected tags](../../user/project/protected_tags.md),
that are restricted to a subset of project users.

View File

@ -29,7 +29,7 @@ first supported provider, and [KV-V2](https://developer.hashicorp.com/vault/docs
as the first supported secrets engine.
Use [ID tokens](../yaml/index.md#id_tokens) to [authenticate with Vault](https://developer.hashicorp.com/vault/docs/auth/jwt#jwt-authentication).
The [Authenticating and Reading Secrets With HashiCorp Vault](../examples/authenticating-with-hashicorp-vault/index.md)
The [Authenticating and Reading Secrets With HashiCorp Vault](hashicorp_vault.md)
tutorial has more details about authenticating with ID tokens.
You must [configure your Vault server](#configure-your-vault-server) before you
@ -49,7 +49,7 @@ is summarized by this diagram:
1. Runner reads secrets from the HashiCorp Vault.
NOTE:
Read the [Authenticating and Reading Secrets With HashiCorp Vault](../examples/authenticating-with-hashicorp-vault/index.md)
Read the [Authenticating and Reading Secrets With HashiCorp Vault](hashicorp_vault.md)
tutorial for a version of this feature. It's available to all
subscription levels, supports writing secrets to and deleting secrets from Vault,
and supports multiple secrets engines.
@ -236,8 +236,8 @@ claims like `project_id` or `namespace_id`. Without these restrictions, any JWT
generated by this GitLab instance may be allowed to authenticate using this role.
For a full list of ID token JWT claims, read the
[How It Works](../examples/authenticating-with-hashicorp-vault/index.md#how-it-works) section of the
[Authenticating and Reading Secrets With HashiCorp Vault](../examples/authenticating-with-hashicorp-vault/index.md) tutorial.
[How It Works](hashicorp_vault.md) section of the
[Authenticating and Reading Secrets With HashiCorp Vault](hashicorp_vault.md) tutorial.
You can also specify some attributes for the resulting Vault tokens, such as time-to-live,
IP address range, and number of uses. The full list of options is available in

View File

@ -506,7 +506,7 @@ about the renewal.
renew for any other reason, the email tells you to contact our Sales team or
[manually renew in the Customers Portal](#renew-subscription-manually).
- If there are no issues, the email specifies the:
- Names and quantity of the products being renewed
- Names and quantity of the products being renewed.
- Total amount you owe. If your usage increases before renewal, this amount will change.
#### Enable or disable automatic subscription renewal

View File

@ -29,6 +29,5 @@ with [Scaled Agile Framework (SAFe)](https://handbook.gitlab.com/handbook/market
| [**Issues**](../user/project/issues/index.md)<br>Tasks, bug reports, feature requests, tracking. | [**Issue boards**](../user/project/issue_board.md)<br>Visualization, workflow, Kanban, prioritization. | [**Comments and threads**](../user/discussions/index.md)<br> Mentions, locked discussions, internal notes, thread resolution. |
| [**Tasks**](../user/tasks.md)<br>Task labels, confidential tasks, linked items, task weights. | [**Requirements**](../user/project/requirements/index.md)<br>Acceptance criteria, requirements test reports, CSV import. | [**Time tracking**](../user/project/time_tracking.md)<br>Estimates, time spent, reporting. |
| [**CRM**](../user/crm/index.md)<br>Customer management, organizations, contacts, permissions. | [**Wikis**](../user/project/wiki/index.md)<br>Documentation, external wikis, wiki events, history. | [**Epics**](../user/group/epics/index.md)<br>Roadmaps, hierarchies, planning, issue progress. |
| [**Roadmaps**](../user/group/roadmap/index.md)<br>Epic progress, timelines, milestones, goals. | [**Planning hierarchies**](../user/group/planning_hierarchy/index.md)<br>Organization, structure, multi-level epics, nesting. | [**Objectives and key results**](../user/okrs.md)<br>Goal setting, performance tracking, child objectives, health status. |
| [**Roadmaps**](../user/group/roadmap/index.md)<br>Epic progress, timelines, milestones, goals. | [**Objectives and key results**](../user/okrs.md)<br>Goal setting, performance tracking, child objectives, health status. | [**To-Do List**](../user/todos.md)<br>Task management, actions, access changes. |
| [**Keyboard shortcuts**](../user/shortcuts.md)<br>Global shortcuts, navigation, quick access. | [**Quick actions**](../user/project/quick_actions.md)<br>Commands, shortcuts, inline actions. | [**Markdown**](../user/markdown.md)<br>Formatting, inline HTML, GitLab-specific references, diagrams and flowcharts. |
| [**To-Do List**](../user/todos.md)<br>Task management, actions, access changes. | | |

View File

@ -65,7 +65,11 @@ Note the following:
### Job naming best practice
> - Naming conflict handling was introduced in GitLab 17.4 with a flag named `pipeline_execution_policy_suffix`. Disabled by default.
> - Naming conflict handling [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/473189) in GitLab 17.4 with a flag named `pipeline_execution_policy_suffix`. Disabled by default.
FLAG:
The availability of job naming conflict handling is controlled by a feature flag.
For more information, see the history.
There is no visible indicator for jobs coming from a security policy. Adding a unique prefix or suffix to job names makes it easier to identify them and avoid job name collisions.

View File

@ -379,6 +379,8 @@ Audit event types belong to the following product categories.
| Name | Description | Saved to database | Streamed | Introduced in | Scope |
|:------------|:------------|:------------------|:---------|:--------------|:--------------|
| [`project_feature_model_experiments_access_level_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121027) | Model experiments access level was updated | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.1](https://gitlab.com/gitlab-org/gitlab/-/issues/412384) | Project |
| [`ml_model_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/165011) | ML model is created | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.4](https://gitlab.com/gitlab-org/gitlab/-/issues/463215) | Project |
| [`ml_model_destroyed`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/165011) | ML model destroyed | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.4](https://gitlab.com/gitlab-org/gitlab/-/issues/463215) | Project |
| [`project_feature_model_registry_access_level_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/138399) | Model registry access level was updated | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.7](https://gitlab.com/gitlab-org/gitlab/-/issues/412734) | Project |
### Not categorized

View File

@ -64,8 +64,6 @@ accDescr: How issues and child epics relate to parent epics
Child_epic --> Issue2
```
Also, read more about possible [planning hierarchies](../planning_hierarchy/index.md).
### Child issues from different group hierarchies
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/371081) in GitLab 15.5 [with a flag](../../../administration/feature_flags.md) named `epic_issues_from_different_hierarchies`. Disabled by default.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -1,78 +1,11 @@
---
stage: Plan
group: Product Planning
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
redirect_to: '../../../topics/plan_and_track.md'
remove_date: '2024-11-26'
---
# Planning hierarchies
This document was moved to [another location](../../../topics/plan_and_track.md).
DETAILS:
**Tier:** Premium, Ultimate
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
Planning hierarchies are an integral part of breaking down your work in GitLab.
To understand how you can use epics and issues together in hierarchies, remember the following:
- [Epics](../epics/index.md) exist in groups.
- [Issues](../../project/issues/index.md) exist in projects.
GitLab is not opinionated on how you structure your work and the hierarchy you can build with multi-level
epics. For example, you can use the hierarchy as a folder of issues for bigger initiatives.
To learn about hierarchies in general, common frameworks, and using GitLab for
portfolio management, see
[How to use GitLab for Agile portfolio planning and project management](https://about.gitlab.com/blog/2020/11/11/gitlab-for-agile-portfolio-planning-project-management/).
## Hierarchies with epics
With epics, you can achieve the following hierarchy:
```mermaid
%%{init: { "fontFamily": "GitLab Sans" }}%%
graph TD
accTitle: Hierarchies with epics
accDescr: Use epics to link projects, issues, and groups
Group_epic --> Project1_Issue1
Group_epic --> Project1_Issue2
Group_epic --> Project2_Issue1
```
### Hierarchies with multi-level epics
DETAILS:
**Tier:** Ultimate
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
With the addition of [multi-level epics](../epics/manage_epics.md#multi-level-child-epics) and up to
seven levels of nested epics, you can achieve the following hierarchy:
<!--
Image below was generated with the following Mermaid code.
Attached as an image because a rendered diagram doesn't look clear on the docs page.
```mermaid
%%{init: { "fontFamily": "GitLab Sans" }}%%
classDiagram
direction TD
class Epic
class Issue
Epic *-- "0..7" Epic
Epic "1"*-- "0..*" Issue
```
-->
![Diagram showing possible relationships of multi-level epics](img/hierarchy_with_multi_level_epics.png)
## View ancestry of an issue
In an issue, you can view the parented epic above the issue in the right sidebar under **Epic**.
![epics state dropdown list](img/issue-view-parent-epic-in-sidebar_v14_6.png)
## View ancestry of an epic
In an epic, you can view the ancestors as parents in the right sidebar under **Ancestors**.
![epics state dropdown list](img/epic-view-ancestors-in-sidebar_v14_6.png)
<!-- This redirect file can be deleted after <2024-11-26>. -->
<!-- Redirects that point to other docs in the same project expire in three months. -->
<!-- Redirects that point to docs in a different project or site (link is not relative and starts with `https:`) expire in one year. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->

View File

@ -9,8 +9,8 @@ module Gitlab
}.freeze
DOC_TYPE_ONLY_SORT = {
popularity_asc: %w[issue],
popularity_desc: %w[issue]
popularity_asc: %w[issue work_item],
popularity_desc: %w[issue work_item]
}.freeze
def sort_and_direction(order_by, sort)

View File

@ -11408,9 +11408,6 @@ msgstr ""
msgid "Child"
msgstr ""
msgid "Child epic"
msgstr ""
msgid "Child issues and epics"
msgstr ""
@ -11935,10 +11932,10 @@ msgstr ""
msgid "CiVariable|Define a CI/CD variable in the UI"
msgstr ""
msgid "CiVariable|GitLab CI/CD supports OpenID Connect (OIDC) to give your build and deployment jobs access to cloud credentials and services. %{linkStart}How do I configure OIDC for my cloud provider?%{linkEnd}"
msgid "CiVariable|Enter a search query to find more environments, or use * to create a wildcard."
msgstr ""
msgid "CiVariable|Maximum of %{limit} environments listed. For more environments, enter a search query."
msgid "CiVariable|GitLab CI/CD supports OpenID Connect (OIDC) to give your build and deployment jobs access to cloud credentials and services. %{linkStart}How do I configure OIDC for my cloud provider?%{linkEnd}"
msgstr ""
msgid "CiVariable|New environment"
@ -20937,6 +20934,9 @@ msgstr ""
msgid "Enter a number from 0 to 100."
msgstr ""
msgid "Enter a search query to find more branches, or use * to create a wildcard."
msgstr ""
msgid "Enter a valid URL"
msgstr ""
@ -27075,39 +27075,12 @@ msgstr ""
msgid "Hide values"
msgstr ""
msgid "Hierarchy|Current structure"
msgstr ""
msgid "Hierarchy|Deliver value more efficiently by breaking down necessary work into a hierarchical structure. This structure helps teams understand scope, priorities, and how work cascades up toward larger goals."
msgstr ""
msgid "Hierarchy|Help us improve work items in GitLab!"
msgstr ""
msgid "Hierarchy|Is there a framework or type of work item you wish you had access to in GitLab? Give us your feedback and help us build the experiences valuable to you."
msgstr ""
msgid "Hierarchy|Planning hierarchy"
msgstr ""
msgid "Hierarchy|Something went wrong while fetching ancestors."
msgstr ""
msgid "Hierarchy|Something went wrong while fetching children."
msgstr ""
msgid "Hierarchy|Take the work items survey"
msgstr ""
msgid "Hierarchy|These items are unavailable in the current structure."
msgstr ""
msgid "Hierarchy|Unavailable structure"
msgstr ""
msgid "Hierarchy|You can start using these items now."
msgstr ""
msgid "High - S2"
msgstr ""
@ -32908,9 +32881,6 @@ msgstr ""
msgid "Maximum number of variables loaded (2000)"
msgstr ""
msgid "Maximum of %{limit} branches listed. For more branches, enter a search query."
msgstr ""
msgid "Maximum of 255 characters"
msgstr ""
@ -40557,9 +40527,6 @@ msgstr ""
msgid "Plan:"
msgstr ""
msgid "Planning hierarchy"
msgstr ""
msgid "PlantUML"
msgstr ""
@ -49582,6 +49549,9 @@ msgstr ""
msgid "SecurityReports|%{count} Selected"
msgstr ""
msgid "SecurityReports|%{count}+"
msgstr ""
msgid "SecurityReports|%{count}+ projects"
msgstr ""

View File

@ -276,8 +276,8 @@
"custom-jquery-matchers": "^2.1.0",
"eslint": "8.57.0",
"eslint-import-resolver-jest": "3.0.2",
"eslint-import-resolver-webpack": "0.13.8",
"eslint-plugin-import": "^2.29.1",
"eslint-import-resolver-webpack": "0.13.9",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-local-rules": "^3.0.2",
"eslint-plugin-no-jquery": "2.7.0",
"eslint-plugin-no-unsanitized": "^4.1.0",

View File

@ -246,56 +246,31 @@ RSpec.describe SessionsController, feature_category: :system_access do
create(:broadcast_message_dismissal, broadcast_message: other_message, user: build(:user))
end
context 'when new_broadcast_message_dismissal feature flag is not enabled' do
before do
stub_feature_flags(new_broadcast_message_dismissal: false)
end
it 'creates dismissed cookies based on db records' do
expect(cookies["hide_broadcast_message_#{message_banner.id}"]).to be_nil
expect(cookies["hide_broadcast_message_#{message_notification.id}"]).to be_nil
expect(cookies["hide_broadcast_message_#{other_message.id}"]).to be_nil
it 'does not create dismissed cookies based on db records' do
expect(cookies["hide_broadcast_message_#{message_banner.id}"]).to be_nil
expect(cookies["hide_broadcast_message_#{message_notification.id}"]).to be_nil
expect(cookies["hide_broadcast_message_#{other_message.id}"]).to be_nil
post_action
post_action
expect(cookies["hide_broadcast_message_#{message_banner.id}"]).to be_nil
expect(cookies["hide_broadcast_message_#{message_notification.id}"]).to be_nil
expect(cookies["hide_broadcast_message_#{other_message.id}"]).to be_nil
end
expect(cookies["hide_broadcast_message_#{message_banner.id}"]).to be(true)
expect(cookies["hide_broadcast_message_#{message_notification.id}"]).to be(true)
expect(cookies["hide_broadcast_message_#{other_message.id}"]).to be_nil
end
context 'when new_broadcast_message_dismissal feature flag is enabled' do
context 'when dismissal is expired' do
let_it_be(:message) { create(:broadcast_message, broadcast_type: :banner, message: 'banner') }
before do
allow(Gitlab::AppLogger).to receive(:info).and_call_original
stub_feature_flags(new_broadcast_message_dismissal: true)
create(:broadcast_message_dismissal, :expired, broadcast_message: message, user: user)
end
it 'creates dismissed cookies based on db records' do
expect(cookies["hide_broadcast_message_#{message_banner.id}"]).to be_nil
expect(cookies["hide_broadcast_message_#{message_notification.id}"]).to be_nil
expect(cookies["hide_broadcast_message_#{other_message.id}"]).to be_nil
it 'does not create cookie' do
expect(cookies["hide_broadcast_message_#{message.id}"]).to be_nil
post_action
expect(cookies["hide_broadcast_message_#{message_banner.id}"]).to be(true)
expect(cookies["hide_broadcast_message_#{message_notification.id}"]).to be(true)
expect(cookies["hide_broadcast_message_#{other_message.id}"]).to be_nil
end
context 'when dismissal is expired' do
let_it_be(:message) { create(:broadcast_message, broadcast_type: :banner, message: 'banner') }
before do
create(:broadcast_message_dismissal, :expired, broadcast_message: message, user: user)
end
it 'does not create cookie' do
expect(cookies["hide_broadcast_message_#{message.id}"]).to be_nil
post_action
expect(cookies["hide_broadcast_message_#{message.id}"]).to be_nil
end
expect(cookies["hide_broadcast_message_#{message.id}"]).to be_nil
end
end
end

View File

@ -21,7 +21,7 @@ describe('Ci environments dropdown', () => {
const findListboxText = () => findListbox().props('toggleText');
const findCreateWildcardButton = () => wrapper.findByTestId('create-wildcard-button');
const findDropdownDivider = () => wrapper.findComponent(GlDropdownDivider);
const findMaxEnvNote = () => wrapper.findByTestId('max-envs-notice');
const findSearchQueryNote = () => wrapper.findByTestId('search-query-note');
const createComponent = ({ props = {}, searchTerm = '' } = {}) => {
wrapper = mountExtended(CiEnvironmentsDropdown, {
@ -34,10 +34,10 @@ describe('Ci environments dropdown', () => {
findListbox().vm.$emit('search', searchTerm);
};
describe('create wildcard button', () => {
describe('when canCreateWildcard is true', () => {
describe('create wildcard buttons', () => {
describe('when canCreateWildcard is true and search has wildcard character', () => {
beforeEach(() => {
createComponent({ props: { canCreateWildcard: true }, searchTerm: 'stable' });
createComponent({ props: { canCreateWildcard: true }, searchTerm: 'stable/*' });
});
it('renders create button during search', () => {
@ -45,9 +45,19 @@ describe('Ci environments dropdown', () => {
});
});
describe('when canCreateWildcard is true and wildcard character is missing from search', () => {
beforeEach(() => {
createComponent({ props: { canCreateWildcard: true }, searchTerm: 'stable/' });
});
it('does not render create button during search', () => {
expect(findCreateWildcardButton().exists()).toBe(false);
});
});
describe('when canCreateWildcard is false', () => {
beforeEach(() => {
createComponent({ props: { canCreateWildcard: false }, searchTerm: 'stable' });
createComponent({ props: { canCreateWildcard: false }, searchTerm: 'stable/*' });
});
it('does not render create button during search', () => {
@ -59,7 +69,7 @@ describe('Ci environments dropdown', () => {
describe('No environments found', () => {
describe('default behavior', () => {
beforeEach(() => {
createComponent({ searchTerm: 'stable' });
createComponent({ searchTerm: 'stable/*' });
});
it('renders dropdown divider', () => {
@ -69,7 +79,7 @@ describe('Ci environments dropdown', () => {
it('renders create button with search term if environments do not contain search term', () => {
const button = findCreateWildcardButton();
expect(button.exists()).toBe(true);
expect(button.text()).toBe('Create wildcard: stable');
expect(button.text()).toBe('Create wildcard: stable/*');
});
});
});
@ -170,8 +180,9 @@ describe('Ci environments dropdown', () => {
});
it('displays note about max environments', () => {
expect(findMaxEnvNote().exists()).toBe(true);
expect(findMaxEnvNote().text()).toContain('30');
expect(findSearchQueryNote().text()).toBe(
'Enter a search query to find more environments, or use * to create a wildcard.',
);
});
});
@ -191,7 +202,7 @@ describe('Ci environments dropdown', () => {
});
describe('when creating a new environment scope from a search term', () => {
const searchTerm = 'new-env';
const searchTerm = 'new-env-*';
beforeEach(() => {
createComponent({ searchTerm });
});

View File

@ -1,57 +0,0 @@
import Vue, { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { GlBanner } from '@gitlab/ui';
import App from '~/work_items_hierarchy/components/app.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
Vue.use(VueApollo);
describe('WorkItemsHierarchy App', () => {
let wrapper;
const createComponent = (props = {}, data = {}) => {
wrapper = extendedWrapper(
mount(App, {
provide: {
illustrationPath: '/foo.svg',
licensePlan: 'free',
...props,
},
data() {
return data;
},
}),
);
};
describe('survey banner', () => {
it('shows when the banner is visible', () => {
createComponent({}, { bannerVisible: true });
expect(wrapper.findComponent(GlBanner).exists()).toBe(true);
});
it('hide when close is called', async () => {
createComponent({}, { bannerVisible: true });
wrapper.findByTestId('close-icon').trigger('click');
await nextTick();
expect(wrapper.findComponent(GlBanner).exists()).toBe(false);
});
});
describe('Unavailable structure', () => {
it.each`
licensePlan | visible
${'free'} | ${true}
${'premium'} | ${true}
${'ultimate'} | ${false}
`('visibility is $visible when plan is $licensePlan', ({ licensePlan, visible }) => {
createComponent({ licensePlan });
expect(wrapper.findByTestId('unavailable-structure').exists()).toBe(visible);
});
});
});

View File

@ -1,113 +0,0 @@
import { mount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlBadge } from '@gitlab/ui';
import Hierarchy from '~/work_items_hierarchy/components/hierarchy.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import RESPONSE from '~/work_items_hierarchy/static_response';
import { workItemTypes } from '~/work_items_hierarchy/constants';
Vue.use(VueApollo);
describe('WorkItemsHierarchy Hierarchy', () => {
let wrapper;
const workItemsFromResponse = (response) => {
return response.reduce(
(itemTypes, item) => {
const key = item.available ? 'available' : 'unavailable';
itemTypes[key].push({
...item,
...workItemTypes[item.type],
nestedTypes: item.nestedTypes
? item.nestedTypes.map((type) => workItemTypes[type])
: null,
});
return itemTypes;
},
{ available: [], unavailable: [] },
);
};
const createComponent = (props = {}) => {
wrapper = extendedWrapper(
mount(Hierarchy, {
propsData: {
workItemTypes: props.workItemTypes,
...props,
},
}),
);
};
describe('available structure', () => {
let items = [];
beforeEach(() => {
items = workItemsFromResponse(RESPONSE.ultimate).available;
createComponent({ workItemTypes: items });
});
it('renders all work items', () => {
expect(wrapper.findAllByTestId('work-item-wrapper')).toHaveLength(items.length);
});
it('does not render badges', () => {
expect(wrapper.findComponent(GlBadge).exists()).toBe(false);
});
});
describe('unavailable structure', () => {
let items = [];
beforeEach(() => {
items = workItemsFromResponse(RESPONSE.premium).unavailable;
createComponent({ workItemTypes: items });
});
it('renders all work items', () => {
expect(wrapper.findAllByTestId('work-item-wrapper')).toHaveLength(items.length);
});
it('renders license badges for all work items', () => {
expect(wrapper.findAllComponents(GlBadge)).toHaveLength(items.length);
});
it('does not render svg icon for linking', () => {
expect(wrapper.findByTestId('hierarchy-rounded-arrow-tail').exists()).toBe(false);
expect(wrapper.findByTestId('level-up-icon').exists()).toBe(false);
});
});
describe('nested work items', () => {
describe.each`
licensePlan | arrowTailVisible | levelUpIconVisible | arrowDownIconVisible
${'ultimate'} | ${true} | ${true} | ${true}
${'premium'} | ${false} | ${false} | ${true}
${'free'} | ${false} | ${false} | ${false}
`(
'when $licensePlan license',
({ licensePlan, arrowTailVisible, levelUpIconVisible, arrowDownIconVisible }) => {
let items = [];
beforeEach(() => {
items = workItemsFromResponse(RESPONSE[licensePlan]).available;
createComponent({ workItemTypes: items });
});
it(`${arrowTailVisible ? 'render' : 'does not render'} arrow tail svg`, () => {
expect(wrapper.findByTestId('hierarchy-rounded-arrow-tail').exists()).toBe(
arrowTailVisible,
);
});
it(`${levelUpIconVisible ? 'render' : 'does not render'} arrow tail svg`, () => {
expect(wrapper.findByTestId('level-up-icon').exists()).toBe(levelUpIconVisible);
});
it(`${arrowDownIconVisible ? 'render' : 'does not render'} arrow tail svg`, () => {
expect(wrapper.findByTestId('arrow-down-icon').exists()).toBe(arrowDownIconVisible);
});
},
);
});
});

View File

@ -1,16 +0,0 @@
import { inferLicensePlan } from '~/work_items_hierarchy/hierarchy_util';
import { LICENSE_PLAN } from '~/work_items_hierarchy/constants';
describe('inferLicensePlan', () => {
it.each`
epics | subEpics | licensePlan
${true} | ${true} | ${LICENSE_PLAN.ULTIMATE}
${true} | ${false} | ${LICENSE_PLAN.PREMIUM}
${false} | ${false} | ${LICENSE_PLAN.FREE}
`(
'returns $licensePlan when epic is $epics and sub-epic is $subEpics',
({ epics, subEpics, licensePlan }) => {
expect(inferLicensePlan({ hasEpics: epics, hasSubEpics: subEpics })).toBe(licensePlan);
},
);
});

View File

@ -1,22 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe PlanningHierarchy, type: :request, feature_category: :groups_and_projects do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
before do
project.add_maintainer(user)
sign_in(user)
end
describe 'GET #planning_hierarchy' do
it 'renders planning hierarchy' do
get project_planning_hierarchy_path(project)
expect(response).to have_gitlab_http_status(:not_found)
end
end
end

View File

@ -9,24 +9,38 @@ RSpec.describe ::Ml::CreateModelService, feature_category: :mlops do
let_it_be(:description) { 'description' }
let_it_be(:metadata) { [] }
let(:audit_event) do
{
name: 'ml_model_created',
author: user,
scope: project
}
end
before do
allow(Gitlab::InternalEvents).to receive(:track_event)
allow(Gitlab::Audit::Auditor).to receive(:audit).and_call_original
end
subject(:create_model) { described_class.new(project, name, user, description, metadata).execute }
describe '#execute' do
describe '#execute', :aggregate_failures do
subject(:model_payload) { create_model.payload }
let(:audit_context) do
audit_event.merge(target: model_payload, message: "MlModel #{name} created")
end
context 'when model name is not supplied' do
let(:name) { nil }
let(:project) { existing_model.project }
it 'returns a model with errors', :aggregate_failures do
it 'returns a model with errors' do
expect { create_model }.not_to change { Ml::Model.count }
expect(create_model).to be_error
expect(Gitlab::InternalEvents).not_to have_received(:track_event)
expect(create_model.message).to include("Name can't be blank")
expect(Gitlab::Audit::Auditor).not_to have_received(:audit)
end
end
@ -34,13 +48,15 @@ RSpec.describe ::Ml::CreateModelService, feature_category: :mlops do
let(:name) { 'new_model' }
let(:project) { existing_model.project }
it 'creates a model', :aggregate_failures do
it 'creates a model' do
expect { create_model }.to change { Ml::Model.count }.by(1)
expect(Gitlab::InternalEvents).to have_received(:track_event).with(
'model_registry_ml_model_created',
{ project: project, user: user }
)
expect(Gitlab::Audit::Auditor).to have_received(:audit).with(audit_context)
expect(model_payload.name).to eq('new_model')
expect(model_payload.default_experiment.name).to eq('[model]new_model')
end
@ -50,13 +66,14 @@ RSpec.describe ::Ml::CreateModelService, feature_category: :mlops do
let(:name) { existing_model.name }
let(:project) { another_project }
it 'creates a model', :aggregate_failures do
it 'creates a model' do
expect { create_model }.to change { Ml::Model.count }.by(1)
expect(Gitlab::InternalEvents).to have_received(:track_event).with(
'model_registry_ml_model_created',
{ project: project, user: user }
)
expect(Gitlab::Audit::Auditor).to have_received(:audit).with(audit_context)
expect(model_payload.name).to eq(name)
end
end
@ -65,11 +82,12 @@ RSpec.describe ::Ml::CreateModelService, feature_category: :mlops do
let(:name) { existing_model.name }
let(:project) { existing_model.project }
it 'returns a model with errors', :aggregate_failures do
it 'returns a model with errors' do
expect { create_model }.not_to change { Ml::Model.count }
expect(create_model).to be_error
expect(Gitlab::InternalEvents).not_to have_received(:track_event)
expect(create_model.message).to eq(["Name should be unique in the project"])
expect(Gitlab::Audit::Auditor).not_to have_received(:audit)
end
end
@ -78,10 +96,11 @@ RSpec.describe ::Ml::CreateModelService, feature_category: :mlops do
let(:project) { existing_model.project }
let(:metadata) { [{ key: 'key1', value: 'value1' }, { key: 'key2', value: 'value2' }] }
it 'creates metadata records', :aggregate_failures do
it 'creates metadata records' do
expect { create_model }.to change { Ml::Model.count }.by(1)
expect(model_payload.name).to eq(name)
expect(Gitlab::Audit::Auditor).to have_received(:audit).with(audit_context)
expect(model_payload.metadata.count).to be 2
end
end
@ -92,8 +111,9 @@ RSpec.describe ::Ml::CreateModelService, feature_category: :mlops do
let(:project) { existing_model.project }
let(:metadata) { [{ key: 'key1', value: 'value1' }, { key: 'key1', value: 'value2' }] }
it 'raises an error', :aggregate_failures do
it 'raises an error' do
expect { create_model }.to raise_error(ActiveRecord::RecordInvalid)
expect(Gitlab::Audit::Auditor).not_to have_received(:audit)
end
end
@ -103,8 +123,9 @@ RSpec.describe ::Ml::CreateModelService, feature_category: :mlops do
let(:project) { existing_model.project }
let(:metadata) { [{ key: 'key1', value: 'value1' }, { key: '', value: 'value2' }] }
it 'raises an error', :aggregate_failures do
it 'raises an error' do
expect { create_model }.to raise_error(ActiveRecord::RecordInvalid)
expect(Gitlab::Audit::Auditor).not_to have_received(:audit)
end
end
end

View File

@ -9,8 +9,21 @@ RSpec.describe ::Ml::DestroyModelService, feature_category: :mlops do
let(:model) { model0 }
let(:service) { described_class.new(model, user) }
let(:audit_event) do
{
name: 'ml_model_destroyed',
author: user,
scope: model.project,
message: "MlModel #{model.name} destroyed",
target: model
}
end
describe '#execute' do
before do
allow(Gitlab::Audit::Auditor).to receive(:audit).and_call_original
end
describe '#execute', :aggregate_failures do
subject(:service_result) { service.execute }
context 'when model fails to delete' do
@ -18,17 +31,19 @@ RSpec.describe ::Ml::DestroyModelService, feature_category: :mlops do
allow(model).to receive(:destroy).and_return(false)
expect(service_result).to be_error
expect(Gitlab::Audit::Auditor).not_to have_received(:audit)
end
end
context 'when a model exists' do
it 'destroys the model', :aggregate_failures do
it 'destroys the model' do
allow_next_instance_of(Packages::MarkPackagesForDestructionService) do |instance|
allow(instance).to receive(:execute).and_return ServiceResponse.success(message: "")
end
expect { service_result }.to change { Ml::Model.count }.by(-1).and change { Ml::ModelVersion.count }.by(-1)
expect(service_result).to be_success
expect(Gitlab::Audit::Auditor).to have_received(:audit).with(audit_event)
end
context 'when a package cannot be marked for destruction' do
@ -40,10 +55,11 @@ RSpec.describe ::Ml::DestroyModelService, feature_category: :mlops do
end
end
it 'returns success with warning', :aggregate_failures do
it 'returns success with warning' do
expect { service_result }.not_to change { Ml::Model.count }
expect(service_result).to be_error
expect(service_result.message).to eq("An error")
expect(Gitlab::Audit::Auditor).not_to have_received(:audit)
end
end
end

View File

@ -130,6 +130,17 @@ RSpec.describe PagesDomains::ObtainLetsEncryptCertificateService, feature_catego
service.execute
end
describe 'when #request_certificate returns a client error' do
before do
allow(api_order).to receive(:request_certificate).and_raise(
Acme::Client::Error::BadCSR,
'Error finalizing order :: CN was longer than 64 bytes'
)
end
it_behaves_like 'saves error and sends notification'
end
end
context 'when order is valid' do

View File

@ -7206,7 +7206,6 @@
- './spec/requests/api/users_preferences_spec.rb'
- './spec/requests/api/users_spec.rb'
- './spec/requests/api/wikis_spec.rb'
- './spec/requests/concerns/planning_hierarchy_spec.rb'
- './spec/requests/dashboard_controller_spec.rb'
- './spec/requests/dashboard/projects_controller_spec.rb'
- './spec/requests/git_http_spec.rb'

View File

@ -26,7 +26,7 @@ RSpec.shared_context 'ProjectPolicy context' do
%i[
award_emoji create_issue create_note
create_project read_issue_board read_issue read_issue_iid read_issue_link
read_label read_planning_hierarchy read_issue_board_list read_milestone read_note read_project
read_label read_issue_board_list read_milestone read_note read_project
read_project_for_iids read_project_member read_release read_snippet
read_wiki upload_file
]

View File

@ -1,14 +1,14 @@
# frozen_string_literal: true
RSpec.shared_examples 'access restricted confidential issues' do
RSpec.shared_examples 'access restricted confidential issues' do |document_type: :issue|
let(:query) { 'issue' }
let(:author) { create(:user) }
let(:assignee) { create(:user) }
let(:project) { create(:project, :internal) }
let!(:issue) { create(:issue, project: project, title: 'Issue 1') }
let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignees: [assignee]) }
let!(:issue) { create(document_type, project: project, title: 'Issue 1') }
let!(:security_issue_1) { create(document_type, :confidential, project: project, title: 'Security issue 1', author: author) }
let!(:security_issue_2) { create(document_type, :confidential, title: 'Security issue 2', project: project, assignees: [assignee]) }
subject(:objects) do
described_class.new(user, query, project: project).objects('issues')

694
yarn.lock

File diff suppressed because it is too large Load Diff