Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-05-05 18:17:47 +00:00
parent 6634288474
commit bc4cd6ffb9
72 changed files with 665 additions and 650 deletions

View File

@ -130,7 +130,8 @@ trigger-omnibus-env:
echo "OMNIBUS_GITLAB_BUILD_ON_ALL_OS=${OMNIBUS_GITLAB_BUILD_ON_ALL_OS:-false}" >> $BUILD_ENV
echo "GITLAB_ASSETS_TAG=$(assets_image_tag)" >> $BUILD_ENV
echo "EE=$([[ $FOSS_ONLY == '1' ]] && echo 'false' || echo 'true')" >> $BUILD_ENV
echo "TRIGGER_BRANCH=$([[ "$CI_COMMIT_REF_NAME" =~ ^[0-9-]+-stable(-ee)?$ ]] && echo ${CI_COMMIT_REF_NAME%-ee} || echo 'master')" >> $BUILD_ENV
target_branch_name="${CI_MERGE_REQUEST_TARGET_BRANCH_NAME:-${CI_COMMIT_REF_NAME}}"
echo "TRIGGER_BRANCH=$([[ "${target_branch_name}" =~ ^[0-9-]+-stable(-ee)?$ ]] && echo ${target_branch_name%-ee} || echo 'master')" >> $BUILD_ENV
echo "Built environment file for omnibus build:"
cat $BUILD_ENV
artifacts:

View File

@ -506,16 +506,6 @@ Layout/ArgumentAlignment:
- 'app/graphql/types/work_items/widgets/start_and_due_date_update_input_type.rb'
- 'app/graphql/types/x509_certificate_type.rb'
- 'app/graphql/types/x509_issuer_type.rb'
- 'app/models/bulk_imports/configuration.rb'
- 'app/models/bulk_imports/entity.rb'
- 'app/models/clusters/kubernetes_namespace.rb'
- 'app/models/container_repository.rb'
- 'app/models/cycle_analytics/project_level_stage_adapter.rb'
- 'app/models/deployment.rb'
- 'app/models/design_management/design.rb'
- 'app/models/design_management/version.rb'
- 'app/models/diff_discussion.rb'
- 'app/models/diff_viewer/base.rb'
- 'app/models/discussion.rb'
- 'app/models/environment.rb'
- 'app/models/generic_commit_status.rb'

View File

@ -94,7 +94,7 @@ export const initPage = async () => {
router,
provide: {
projectPath: dataSet.projectFullPath,
graphqlEtagKey: dataSet.graphqlEtagPath,
graphqlEtagKey: dataSet.graphqlEtagKey,
},
render(createElement) {
return createElement('router-view');

View File

@ -16,7 +16,10 @@ export const isGid = (id) => {
return false;
};
const parseGid = (gid) => parseInt(`${gid}`.replace(/gid:\/\/gitlab\/.*\//g, ''), 10);
const parseGid = (gid) => {
const [type, id] = `${gid}`.replace(/gid:\/\/gitlab\//g, '').split('/');
return { type, id };
};
/**
* Ids generated by GraphQL endpoints are usually in the format
@ -27,8 +30,24 @@ const parseGid = (gid) => parseInt(`${gid}`.replace(/gid:\/\/gitlab\/.*\//g, '')
* @returns {Number}
*/
export const getIdFromGraphQLId = (gid = '') => {
const parsedGid = parseGid(gid);
return Number.isInteger(parsedGid) ? parsedGid : null;
const rawId = isGid(gid) ? parseGid(gid).id : gid;
const id = parseInt(rawId, 10);
return Number.isInteger(id) ? id : null;
};
/**
* Ids generated by GraphQL endpoints are usually in the format
* gid://gitlab/Environments/123. This method extracts Type string
* from the Id path
*
* @param {String} gid GraphQL global ID
* @returns {String}
*/
export const getTypeFromGraphQLId = (gid = '') => {
if (!isGid(gid)) return null;
const { type } = parseGid(gid);
return type || null;
};
export const MutationOperationMode = {

View File

@ -12,7 +12,6 @@ import FeatureCard from './feature_card.vue';
import TrainingProviderList from './training_provider_list.vue';
export const i18n = {
compliance: s__('SecurityConfiguration|Compliance'),
configurationHistory: s__('SecurityConfiguration|Configuration history'),
securityTesting: s__('SecurityConfiguration|Security testing'),
latestPipelineDescription: s__(
@ -59,10 +58,6 @@ export default {
type: Array,
required: true,
},
augmentedComplianceFeatures: {
type: Array,
required: true,
},
gitlabCiPresent: {
type: Boolean,
required: false,
@ -101,9 +96,7 @@ export default {
},
computed: {
canUpgrade() {
return [...this.augmentedSecurityFeatures, ...this.augmentedComplianceFeatures].some(
({ available }) => !available,
);
return [...this.augmentedSecurityFeatures].some(({ available }) => !available);
},
canViewCiHistory() {
return Boolean(this.gitlabCiPresent && this.gitlabCiHistoryPath);
@ -225,44 +218,6 @@ export default {
</template>
</section-layout>
</gl-tab>
<gl-tab
data-testid="compliance-testing-tab"
:title="$options.i18n.compliance"
query-param-value="compliance-testing"
>
<section-layout :heading="$options.i18n.compliance">
<template #description>
<p>
<span data-testid="latest-pipeline-info-compliance">
<gl-sprintf
v-if="latestPipelinePath"
:message="$options.i18n.latestPipelineDescription"
>
<template #link="{ content }">
<gl-link :href="latestPipelinePath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</span>
{{ $options.i18n.description }}
</p>
<p v-if="canViewCiHistory">
<gl-link data-testid="compliance-view-history-link" :href="gitlabCiHistoryPath">{{
$options.i18n.configurationHistory
}}</gl-link>
</p>
</template>
<template #features>
<feature-card
v-for="feature in augmentedComplianceFeatures"
:key="feature.type"
:feature="feature"
class="gl-mb-6"
@error="onError"
/>
</template>
</section-layout>
</gl-tab>
<gl-tab
data-testid="vulnerability-management-tab"
:title="$options.i18n.vulnerabilityManagement"

View File

@ -13,7 +13,6 @@ import {
REPORT_TYPE_COVERAGE_FUZZING,
REPORT_TYPE_CORPUS_MANAGEMENT,
REPORT_TYPE_API_FUZZING,
REPORT_TYPE_LICENSE_COMPLIANCE,
} from '~/vue_shared/security_reports/constants';
import kontraLogo from 'images/vulnerability/kontra-logo.svg';
@ -150,14 +149,6 @@ export const API_FUZZING_NAME = __('API Fuzzing');
export const API_FUZZING_DESCRIPTION = __('Find bugs in your code with API fuzzing.');
export const API_FUZZING_HELP_PATH = helpPagePath('user/application_security/api_fuzzing/index');
export const LICENSE_COMPLIANCE_NAME = __('License Compliance');
export const LICENSE_COMPLIANCE_DESCRIPTION = __(
'Search your project dependencies for their licenses and apply policies.',
);
export const LICENSE_COMPLIANCE_HELP_PATH = helpPagePath(
'user/compliance/license_compliance/index',
);
export const CLUSTER_IMAGE_SCANNING_NAME = s__('ciReport|Cluster Image Scanning');
export const SCANNER_NAMES_MAP = {
@ -273,15 +264,6 @@ export const securityFeatures = [
},
];
export const complianceFeatures = [
{
name: LICENSE_COMPLIANCE_NAME,
description: LICENSE_COMPLIANCE_DESCRIPTION,
helpPath: LICENSE_COMPLIANCE_HELP_PATH,
type: REPORT_TYPE_LICENSE_COMPLIANCE,
},
];
export const featureToMutationMap = {
[REPORT_TYPE_SAST]: {
mutationId: 'configureSast',

View File

@ -3,7 +3,7 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils';
import SecurityConfigurationApp from './components/app.vue';
import { securityFeatures, complianceFeatures } from './components/constants';
import { securityFeatures } from './components/constants';
import { augmentFeatures } from './utils';
export const initSecurityConfiguration = (el) => {
@ -28,9 +28,8 @@ export const initSecurityConfiguration = (el) => {
vulnerabilityTrainingDocsPath,
} = el.dataset;
const { augmentedSecurityFeatures, augmentedComplianceFeatures } = augmentFeatures(
const { augmentedSecurityFeatures } = augmentFeatures(
securityFeatures,
complianceFeatures,
features ? JSON.parse(features) : [],
);
@ -48,7 +47,6 @@ export const initSecurityConfiguration = (el) => {
render(createElement) {
return createElement(SecurityConfigurationApp, {
props: {
augmentedComplianceFeatures,
augmentedSecurityFeatures,
latestPipelinePath,
gitlabCiHistoryPath,

View File

@ -2,19 +2,18 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { SCANNER_NAMES_MAP } from '~/security_configuration/components/constants';
/**
* This function takes in 3 arrays of objects, securityFeatures, complianceFeatures and features.
* securityFeatures and complianceFeatures are static arrays living in the constants.
* This function takes in 3 arrays of objects, securityFeatures and features.
* securityFeatures are static arrays living in the constants.
* features is dynamic and coming from the backend.
* This function builds a superset of those arrays.
* It looks for matching keys within the dynamic and the static arrays
* and will enrich the objects with the available static data.
* @param [{}] securityFeatures
* @param [{}] complianceFeatures
* @param [{}] features
* @returns {Object} Object with enriched features from constants divided into Security and Compliance Features
*/
export const augmentFeatures = (securityFeatures, complianceFeatures, features = []) => {
export const augmentFeatures = (securityFeatures, features = []) => {
const featuresByType = features.reduce((acc, feature) => {
acc[feature.type] = convertObjectPropsToCamelCase(feature, { deep: true });
return acc;
@ -39,7 +38,6 @@ export const augmentFeatures = (securityFeatures, complianceFeatures, features =
return {
augmentedSecurityFeatures: securityFeatures.map((feature) => augmentFeature(feature)),
augmentedComplianceFeatures: complianceFeatures.map((feature) => augmentFeature(feature)),
};
};

View File

@ -27,7 +27,6 @@ export const REPORT_TYPE_CONTAINER_SCANNING = 'container_scanning';
export const REPORT_TYPE_CLUSTER_IMAGE_SCANNING = 'cluster_image_scanning';
export const REPORT_TYPE_COVERAGE_FUZZING = 'coverage_fuzzing';
export const REPORT_TYPE_CORPUS_MANAGEMENT = 'corpus_management';
export const REPORT_TYPE_LICENSE_COMPLIANCE = 'license_scanning';
export const REPORT_TYPE_API_FUZZING = 'api_fuzzing';
export const REPORT_TYPE_MANUALLY_ADDED = 'generic';

View File

@ -46,7 +46,8 @@ import workItemAssigneesSubscription from '../graphql/work_item_assignees.subscr
import workItemMilestoneSubscription from '../graphql/work_item_milestone.subscription.graphql';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql';
import { findHierarchyWidgetChildren, getWorkItemQuery } from '../utils';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import { findHierarchyWidgetChildren } from '../utils';
import WorkItemTree from './work_item_links/work_item_tree.vue';
import WorkItemActions from './work_item_actions.vue';
@ -137,18 +138,15 @@ export default {
},
apollo: {
workItem: {
query() {
return getWorkItemQuery(this.fetchByIid);
},
query: workItemByIidQuery,
variables() {
return this.queryVariables;
},
skip() {
return !this.workItemId && !this.workItemIid;
return !this.workItemIid;
},
update(data) {
const workItem = this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem;
return workItem ?? {};
return data.workspace.workItems.nodes[0] ?? {};
},
error() {
this.setEmptyState();
@ -316,18 +314,11 @@ export default {
workItemNotes() {
return this.isWidgetPresent(WIDGET_TYPE_NOTES);
},
fetchByIid() {
return true;
},
queryVariables() {
return this.fetchByIid
? {
fullPath: this.fullPath,
iid: this.workItemIid,
}
: {
id: this.workItemId,
};
return {
fullPath: this.fullPath,
iid: this.workItemIid,
};
},
children() {
return this.workItem ? findHierarchyWidgetChildren(this.workItem) : [];
@ -408,14 +399,12 @@ export default {
},
toggleChildFromCache(workItem, childId, store) {
const sourceData = store.readQuery({
query: getWorkItemQuery(this.fetchByIid),
query: workItemByIidQuery,
variables: this.queryVariables,
});
const newData = produce(sourceData, (draftState) => {
const widgets = this.fetchByIid
? draftState.workspace.workItems.nodes[0].widgets
: draftState.workItem.widgets;
const { widgets } = draftState.workspace.workItems.nodes[0];
const widgetHierarchy = widgets.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY);
const index = widgetHierarchy.children.nodes.findIndex((child) => child.id === childId);
@ -428,7 +417,7 @@ export default {
});
store.writeQuery({
query: getWorkItemQuery(this.fetchByIid),
query: workItemByIidQuery,
variables: this.queryVariables,
data: newData,
});
@ -475,12 +464,8 @@ export default {
this.$emit('has-notes');
},
updateUrl(modalWorkItem) {
const params = this.fetchByIid
? { work_item_iid: modalWorkItem?.iid }
: { work_item_id: getIdFromGraphQLId(modalWorkItem?.id) };
updateHistory({
url: setUrlParams(params),
url: setUrlParams({ work_item_iid: modalWorkItem?.iid }),
replace: true,
});
},
@ -722,7 +707,6 @@ export default {
:can-update="canUpdate"
:project-path="fullPath"
:confidential="workItem.confidential"
:fetch-by-iid="fetchByIid"
@addWorkItemChild="addChild"
@removeChild="removeChild"
@show-modal="openInModal"

View File

@ -61,11 +61,6 @@ export default {
type: String,
required: true,
},
fetchByIid: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
@ -174,7 +169,7 @@ export default {
:work-item-id="workItemId"
:work-item-iid="workItemIid"
:work-item-type="workItemType"
:fetch-by-iid="fetchByIid"
fetch-by-iid
@removeChild="$emit('removeChild', $event)"
@show-modal="showModal"
/>

View File

@ -219,10 +219,3 @@
width: calc(100% + 24px);
margin: -28px -12px 0;
}
.ai-genie-chat-message {
pre,
code {
@include gl-font-sm;
}
}

View File

@ -305,3 +305,8 @@ body.gl-dark {
// lightens chat bubble in darkmode as $gray-50 matches drawer background. See tanuki_bot_chat.scss
background-color: $gray-100;
}
.ai-genie-chat,
.ai-genie-chat .gl-form-input {
background-color: $gray-10;
}

View File

@ -51,7 +51,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:code_quality_inline_drawer, project)
push_frontend_feature_flag(:hide_create_issue_resolve_all, project)
push_frontend_feature_flag(:auto_merge_labels_mr_widget, project)
push_frontend_feature_flag(:summarize_my_code_review, current_user)
push_force_frontend_feature_flag(:summarize_my_code_review, summarize_my_code_review_enabled?)
push_frontend_feature_flag(:mr_activity_filters, current_user)
end
@ -603,6 +603,18 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
Date.strptime(date, "%Y-%m-%d")&.to_time&.to_i if date
rescue Date::Error, TypeError
end
def summarize_my_code_review_enabled?
namespace = project&.group&.root_ancestor
return false if namespace.nil?
Feature.enabled?(:summarize_my_code_review, current_user) &&
namespace.group_namespace? &&
namespace.licensed_feature_available?(:summarize_my_mr_code_review) &&
namespace.experiment_features_enabled &&
namespace.third_party_ai_features_enabled &&
merge_request.send_to_ai?
end
end
Projects::MergeRequestsController.prepend_mod_with('Projects::MergeRequestsController')

View File

@ -48,4 +48,8 @@ module SessionsHelper
# Moved to Gitlab::Utils::Email in 15.9
Gitlab::Utils::Email.obfuscated_email(email)
end
def remember_me_enabled?
Gitlab::CurrentSettings.remember_me_enabled?
end
end

View File

@ -38,10 +38,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
add_authentication_token_field :static_objects_external_storage_auth_token, encrypted: :required
add_authentication_token_field :error_tracking_access_token, encrypted: :required
belongs_to :self_monitoring_project, class_name: "Project", foreign_key: :instance_administration_project_id,
inverse_of: :application_setting
belongs_to :push_rule
alias_attribute :self_monitoring_project_id, :instance_administration_project_id
belongs_to :instance_group, class_name: "Group", foreign_key: :instance_administrators_group_id,
inverse_of: :application_setting

View File

@ -9,7 +9,7 @@ class BulkImports::Configuration < ApplicationRecord
validates :url, :access_token, length: { maximum: 255 }, presence: true
validates :url, public_url: { schemes: %w[http https], enforce_sanitization: true, ascii_only: true },
allow_nil: true
allow_nil: true
attr_encrypted :url,
key: Settings.attr_encrypted_db_key_base_32,

View File

@ -41,22 +41,14 @@ class BulkImports::Entity < ApplicationRecord
validates :project, absence: true, if: :group
validates :group, absence: true, if: :project
validates :source_type, presence: true
validates :source_full_path,
presence: true,
format: { with: Gitlab::Regex.bulk_import_source_full_path_regex,
message: Gitlab::Regex.bulk_import_source_full_path_regex_message }
validates :source_full_path, presence: true, format: {
with: Gitlab::Regex.bulk_import_source_full_path_regex,
message: Gitlab::Regex.bulk_import_source_full_path_regex_message
}
validates :destination_name,
presence: true,
if: -> { group || project }
validates :destination_namespace,
exclusion: [nil],
if: :group
validates :destination_namespace,
presence: true,
if: :project?
validates :destination_name, presence: true, if: -> { group || project }
validates :destination_namespace, exclusion: [nil], if: :group
validates :destination_namespace, presence: true, if: :project?
validate :validate_parent_is_a_group, if: :parent
validate :validate_imported_entity_type
@ -72,9 +64,8 @@ class BulkImports::Entity < ApplicationRecord
alias_attribute :destination_slug, :destination_name
delegate :default_project_visibility,
:default_group_visibility,
to: :'Gitlab::CurrentSettings.current_application_settings'
delegate :default_project_visibility, :default_group_visibility,
to: :'Gitlab::CurrentSettings.current_application_settings'
state_machine :status, initial: :created do
state :created, value: 0

View File

@ -22,9 +22,9 @@ module Clusters
delegate :api_url, to: :platform_kubernetes, allow_nil: true
attr_encrypted :service_account_token,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc'
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc'
scope :has_service_account_token, -> { where.not(encrypted_service_account_token: nil) }
scope :with_environment_name, -> (name) { joins(:environment).where(environments: { name: name }) }

View File

@ -38,8 +38,8 @@ class ContainerRepository < ApplicationRecord
validates :migration_aborted_in_state, inclusion: { in: ABORTABLE_MIGRATION_STATES }, allow_nil: true
validates :migration_retries_count, presence: true,
numericality: { greater_than_or_equal_to: 0 },
allow_nil: false
numericality: { greater_than_or_equal_to: 0 },
allow_nil: false
enum status: { delete_scheduled: 0, delete_failed: 1, delete_ongoing: 2 }
enum expiration_policy_cleanup_status: { cleanup_unscheduled: 0, cleanup_scheduled: 1, cleanup_unfinished: 2, cleanup_ongoing: 3 }
@ -124,9 +124,7 @@ class ContainerRepository < ApplicationRecord
state :import_done
state :import_skipped do
validates :migration_skipped_reason,
:migration_skipped_at,
presence: true
validates :migration_skipped_reason, :migration_skipped_at, presence: true
end
state :import_aborted do
@ -603,8 +601,7 @@ class ContainerRepository < ApplicationRecord
end
def self.build_from_path(path)
self.new(project: path.repository_project,
name: path.repository_name)
self.new(project: path.repository_project, name: path.repository_name)
end
def self.find_or_create_from_path(path)
@ -622,13 +619,11 @@ class ContainerRepository < ApplicationRecord
end
def self.find_by_path!(path)
self.find_by!(project: path.repository_project,
name: path.repository_name)
self.find_by!(project: path.repository_project, name: path.repository_name)
end
def self.find_by_path(path)
self.find_by(project: path.repository_project,
name: path.repository_name)
self.find_by(project: path.repository_project, name: path.repository_name)
end
private

View File

@ -16,12 +16,12 @@ module CycleAnalytics
presenter = Analytics::CycleAnalytics::StagePresenter.new(stage)
serializer.new.represent(ProjectLevelStage.new(
title: presenter.title,
description: presenter.description,
legend: presenter.legend,
name: stage.name,
project_median: median
))
title: presenter.title,
description: presenter.description,
legend: presenter.legend,
name: stage.name,
project_median: median
))
end
# rubocop: enable CodeReuse/Presenter

View File

@ -372,9 +372,11 @@ class Deployment < ApplicationRecord
# i.e.:
# MergeRequest.select(1, 2).to_sql #=> SELECT 1, 2 FROM "merge_requests"
# MergeRequest.select(1, 1).to_sql #=> SELECT 1 FROM "merge_requests"
select = relation.select('merge_requests.id',
"#{id} as deployment_id",
"#{environment_id} as environment_id").to_sql
select = relation.select(
'merge_requests.id',
"#{id} as deployment_id",
"#{environment_id} as environment_id"
).to_sql
# We don't use `ApplicationRecord.legacy_bulk_insert` here so that we don't need to
# first pluck lots of IDs into memory.

View File

@ -31,8 +31,8 @@ module DesignManagement
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_internal_id :iid, scope: :project, presence: true,
hook_names: %i[create update], # Deal with old records
track_if: -> { !importing? }
hook_names: %i[create update], # Deal with old records
track_if: -> { !importing? }
validates :project, :filename, presence: true
validates :issue, presence: true, unless: :importing?

View File

@ -36,10 +36,10 @@ module DesignManagement
belongs_to :author, class_name: 'User'
has_many :actions
has_many :designs,
through: :actions,
class_name: "DesignManagement::Design",
source: :design,
inverse_of: :versions
through: :actions,
class_name: "DesignManagement::Design",
source: :design,
inverse_of: :versions
validates :designs, presence: true, unless: :importing?
validates :sha, presence: true

View File

@ -10,13 +10,13 @@ class DiffDiscussion < Discussion
DiffNote
end
delegate :position,
:original_position,
:change_position,
:diff_note_positions,
:on_text?,
:on_image?,
to: :first_note
delegate :position,
:original_position,
:change_position,
:diff_note_positions,
:on_text?,
:on_image?,
to: :first_note
def legacy_diff_discussion?
false

View File

@ -101,8 +101,9 @@ module DiffViewer
def render_error_options
options = []
blob_url = Gitlab::Routing.url_helpers.project_blob_path(diff_file.repository.project,
File.join(diff_file.content_sha, diff_file.file_path))
blob_url = Gitlab::Routing.url_helpers.project_blob_path(
diff_file.repository.project, File.join(diff_file.content_sha, diff_file.file_path)
)
options << ActionController::Base.helpers.link_to(_('view the blob'), blob_url)
options

View File

@ -99,8 +99,7 @@ module Integrations
end
def allow_local_api_url?
allow_local_requests_from_web_hooks_and_services? ||
(self_monitoring_project? && internal_prometheus_url?)
allow_local_requests_from_web_hooks_and_services? || internal_prometheus_url?
end
def configured?
@ -127,10 +126,6 @@ module Integrations
delegate :allow_local_requests_from_web_hooks_and_services?, to: :current_settings, private: true
def self_monitoring_project?
project && project.id == current_settings.self_monitoring_project_id
end
def internal_prometheus_url?
api_url.present? && api_url == ::Gitlab::Prometheus::Internal.uri
end

View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
# rubocop: disable Gitlab/NamespacedClass
class Organization < ApplicationRecord
end
# rubocop: enable Gitlab/NamespacedClass

View File

@ -173,7 +173,7 @@ class Project < ApplicationRecord
has_one :last_event, -> { order 'events.created_at DESC' }, class_name: 'Event'
has_many :boards
has_many :application_setting, inverse_of: :self_monitoring_project
has_many :application_setting
def self.integration_association_name(name)
"#{name}_integration"
@ -2857,10 +2857,6 @@ class Project < ApplicationRecord
Feature.enabled?(:group_protected_branches, group) || Feature.enabled?(:allow_protected_branches_for_group, group)
end
def self_monitoring?
Gitlab::CurrentSettings.self_monitoring_project_id == id
end
def deploy_token_create_url(opts = {})
Gitlab::Routing.url_helpers.create_deploy_token_project_settings_repository_path(self, opts)
end

View File

@ -1077,6 +1077,13 @@ class User < ApplicationRecord
update(otp_backup_codes: nil)
end
# Returns true if the user is allowed to sign in with either otp or recovery codes.
def sign_in_with_codes_allowed?
return two_factor_otp_enabled? unless Feature.enabled?(:webauthn_without_totp)
two_factor_enabled?
end
def two_factor_enabled?
two_factor_otp_enabled? || two_factor_webauthn_enabled?
end

View File

@ -15,18 +15,39 @@ module Members
@skip_auth = skip_authorization
if a_group_owner?(member)
process_destroy_of_group_owner_member(member, skip_subresources, unassign_issuables)
process_destroy_of_group_owner_member(member, skip_subresources)
else
destroy_member(member)
destroy_data_related_to_member(member, skip_subresources, unassign_issuables)
destroy_data_related_to_member(member, skip_subresources)
end
enqueue_jobs_that_needs_to_be_run_only_once_per_hierarchy(member, unassign_issuables)
member
end
# We use this to mark recursive calls made to this service from within the same service.
# We do this so as to help us run some tasks that needs to be run only once per hierarchy, and not recursively.
def mark_as_recursive_call
@recursive_call = true
end
private
def process_destroy_of_group_owner_member(member, skip_subresources, unassign_issuables)
# These actions need to be executed only once per hierarchy because the underlying services
# apply these actions to the entire hierarchy anyway, so there is no need to execute them recursively.
def enqueue_jobs_that_needs_to_be_run_only_once_per_hierarchy(member, unassign_issuables)
return if recursive_call?
enqueue_delete_todos(member)
enqueue_unassign_issuables(member) if unassign_issuables
end
def recursive_call?
@recursive_call == true
end
def process_destroy_of_group_owner_member(member, skip_subresources)
# Deleting 2 different group owners via the API in quick succession could lead to
# wrong results for the `last_owner?` check due to race conditions. To prevent this
# we wrap both the last_owner? check and the deletes of owners within a lock.
@ -40,23 +61,23 @@ module Members
end
# deletion of related data does not have to be within the lock.
destroy_data_related_to_member(member, skip_subresources, unassign_issuables) unless last_group_owner
destroy_data_related_to_member(member, skip_subresources) unless last_group_owner
end
def destroy_member(member)
member.destroy
end
def destroy_data_related_to_member(member, skip_subresources, unassign_issuables)
def destroy_data_related_to_member(member, skip_subresources)
member.user&.invalidate_cache_counts
delete_member_associations(member, skip_subresources, unassign_issuables)
delete_member_associations(member, skip_subresources)
end
def a_group_owner?(member)
member.is_a?(GroupMember) && member.owner?
end
def delete_member_associations(member, skip_subresources, unassign_issuables)
def delete_member_associations(member, skip_subresources)
if member.request? && member.user != current_user
notification_service.decline_access_request(member)
end
@ -64,8 +85,6 @@ module Members
delete_subresources(member) unless skip_subresources
delete_project_invitations_by(member) unless skip_subresources
resolve_access_request_todos(current_user, member)
enqueue_delete_todos(member)
enqueue_unassign_issuables(member) if unassign_issuables
after_execute(member: member)
end
@ -110,13 +129,17 @@ module Members
def destroy_project_members(members)
members.each do |project_member|
self.class.new(current_user).execute(project_member, skip_authorization: @skip_auth)
service = self.class.new(current_user)
service.mark_as_recursive_call
service.execute(project_member, skip_authorization: @skip_auth)
end
end
def destroy_group_members(members)
members.each do |group_member|
self.class.new(current_user).execute(group_member, skip_authorization: @skip_auth, skip_subresources: true)
service = self.class.new(current_user)
service.mark_as_recursive_call
service.execute(group_member, skip_authorization: @skip_auth, skip_subresources: true)
end
end

View File

@ -7,7 +7,7 @@
- ldap_servers.each_with_index do |server, i|
.login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i == 0 && form_based_auth_provider_has_active_class?(:ldapmain)) }
.login-body
= render 'devise/sessions/new_ldap', server: server, hide_remember_me: true, submit_message: _('Enter Admin Mode')
= render 'devise/sessions/new_ldap', server: server, render_remember_me: false, submit_message: _('Enter Admin Mode')
= render_if_exists 'devise/sessions/new_smartcard'

View File

@ -20,4 +20,4 @@
- if omniauth_enabled? && button_based_providers_enabled?
.clearfix
= render 'devise/shared/omniauth_box', hide_remember_me: true
= render 'devise/shared/omniauth_box', render_remember_me: false

View File

@ -9,7 +9,7 @@
.tab-content
.login-box.tab-pane.gl-p-5.active{ id: 'login-pane', role: 'tabpanel' }
.login-body
- if current_user.two_factor_otp_enabled?
- if current_user.sign_in_with_codes_allowed?
= render 'admin/sessions/two_factor_otp'
- if current_user.two_factor_webauthn_enabled?
= render 'authentication/authenticate', render_remember_me: false, target_path: admin_session_path

View File

@ -21,7 +21,7 @@
%div
%p= _("We heard back from your device. You have been authenticated.")
= form_tag(target_path, method: :post, id: 'js-login-token-2fa-form') do |f|
- if render_remember_me
- if remember_me_enabled? && render_remember_me
- resource_params = params[resource_name].presence || params
= hidden_field_tag 'user[remember_me]', resource_params.fetch(:remember_me, 0)
= hidden_field_tag 'user[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"

View File

@ -5,15 +5,15 @@
.form-group.gl-px-5
= f.label :password, class: "label-bold #{'gl-mb-1' if Feature.enabled?(:restyle_login_page, @project)}"
= f.password_field :password, class: 'form-control gl-form-input bottom', autocomplete: 'current-password', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field', testid: 'password-field' }
- if devise_mapping.rememberable?
.gl-px-5
.gl-display-inline-block
.gl-px-5
.gl-display-inline-block
- if remember_me_enabled?
= f.gitlab_ui_checkbox_component :remember_me, _('Remember me')
.gl-float-right
- if unconfirmed_email?
= link_to _('Resend confirmation email'), new_user_confirmation_path
- else
= link_to _('Forgot your password?'), new_password_path(:user)
.gl-float-right
- if unconfirmed_email?
= link_to _('Resend confirmation email'), new_user_confirmation_path
- else
= link_to _('Forgot your password?'), new_password_path(:user)
%div
- if Feature.enabled?(:arkose_labs_login_challenge)
= render_if_exists 'devise/sessions/arkose_labs'

View File

@ -5,7 +5,7 @@
.form-group.gl-px-5
= label_tag :password
= password_field_tag :password, nil, { autocomplete: 'current-password', class: "form-control bottom", title: _("This field is required."), required: true }
- if devise_mapping.rememberable?
- if remember_me_enabled?
.remember-me.gl-px-5
%label{ for: "remember_me" }
= check_box_tag :remember_me, '1', false, id: 'remember_me'

View File

@ -1,5 +1,5 @@
- server = local_assigns.fetch(:server)
- hide_remember_me = local_assigns.fetch(:hide_remember_me, false)
- render_remember_me = remember_me_enabled? && local_assigns.fetch(:render_remember_me, true)
- submit_message = local_assigns.fetch(:submit_message, _('Sign in'))
= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "gl-show-field-errors") do
@ -9,7 +9,7 @@
.form-group.gl-px-5
= label_tag :password
= password_field_tag :password, nil, { autocomplete: 'current-password', class: "form-control gl-form-input bottom", title: _("This field is required."), data: { qa_selector: 'password_field' }, required: true }
- if !hide_remember_me && devise_mapping.rememberable?
- if render_remember_me
.gl-px-5
= render Pajamas::CheckboxTagComponent.new(name: 'remember_me') do |c|
= c.label do

View File

@ -2,10 +2,11 @@
= render 'devise/shared/tab_single', tab_title: _('Two-Factor Authentication') if Feature.disabled?(:restyle_login_page, @project)
.login-box.gl-p-5
.login-body
- if @user.two_factor_otp_enabled? || (Feature.enabled?(:webauthn_without_totp) && @user.two_factor_enabled?)
- if @user.sign_in_with_codes_allowed?
= gitlab_ui_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: "edit_user gl-show-field-errors js-2fa-form #{'hidden' if @user.two_factor_webauthn_enabled?}" }) do |f|
- resource_params = params[resource_name].presence || params
= f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
- if remember_me_enabled?
= f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
%div
= f.label _('Enter verification code'), name: :otp_attempt, class: Feature.enabled?(:restyle_login_page, @project) ? 'gl-mb-1' : ''
= f.text_field :otp_attempt, class: 'form-control gl-form-input', required: true, autofocus: true, autocomplete: 'off', inputmode: 'numeric', title: _('This field is required.'), data: { qa_selector: 'two_fa_code_field' }

View File

@ -1,4 +1,4 @@
- hide_remember_me = local_assigns.fetch(:hide_remember_me, false)
- render_remember_me = remember_me_enabled? && local_assigns.fetch(:render_remember_me, true)
- restyle_login_page_enabled = Feature.enabled?(:restyle_login_page, @project)
%div{ class: restyle_login_page_enabled ? 'omniauth-container gl-mt-5 gl-p-5 gl-text-center gl-w-90p gl-ml-auto gl-mr-auto' : 'omniauth-container gl-mt-5 gl-p-5' }
%label{ class: restyle_login_page_enabled ? 'gl-font-weight-normal' : 'gl-font-weight-bold' }
@ -12,7 +12,7 @@
= provider_image_tag(provider)
%span.gl-button-text
= label_for_provider(provider)
- unless hide_remember_me
- if render_remember_me
= render Pajamas::CheckboxTagComponent.new(name: 'remember_me_omniauth', value: nil) do |c|
= c.label do
= _('Remember me')

View File

@ -1,13 +0,0 @@
- return unless project.self_monitoring?
= content_for :page_level_alert do
.flash-container.flash-container-page.sticky
%div{ class: [container_class, 'limit-container-width', 'gl-pt-5!'] }
= render Pajamas::AlertComponent.new(title: _('Deprecation notice'),
variant: :danger,
alert_options: { class: 'gl-mb-3 gl-sticky' }) do |c|
= c.body do
- deprecation_link = '<a href="%{url}">'.html_safe % { url: help_page_path('update/deprecations', anchor: 'gitlab-self-monitoring-project') }
- removal_link = '<a href="%{url}">'.html_safe % { url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/348909' }
- opstrace_link = '<a href="%{url}">'.html_safe % { url: 'https://gitlab.com/groups/gitlab-org/-/epics/6976' }
= _("Self-monitoring was %{deprecation}deprecated%{link_end} in GitLab 14.9, and is %{removal}scheduled for removal%{link_end} in GitLab 16.0. For information on a possible replacement, %{opstrace}learn more about Opstrace%{link_end}.").html_safe % { deprecation: deprecation_link, removal: removal_link, opstrace: opstrace_link, link_end: '</a>'.html_safe }

View File

@ -7,7 +7,6 @@
= render "home_panel"
= render "archived_notice", project: @project
= render "self_monitoring_deprecation_notice", project: @project
= render "invite_members_empty_project" if can_admin_project_member?(@project)

View File

@ -8,7 +8,6 @@
= render_if_exists 'shared/ultimate_feature_removal_banner', project: @project
= render partial: 'flash_messages', locals: { project: @project }
= render "self_monitoring_deprecation_notice", project: @project
= render 'clusters_deprecation_alert'

10
db/docs/organizations.yml Normal file
View File

@ -0,0 +1,10 @@
---
table_name: organizations
classes:
- Organization
feature_categories:
- cell
description: Define ownership of namespaces, projects, and users by organizations
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119421
milestone: "16.0"
gitlab_schema: gitlab_main

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class CreateOrganizations < Gitlab::Database::Migration[2.1]
def change
create_table :organizations do |t|
t.timestamps_with_timezone null: false
end
end
end

View File

@ -0,0 +1 @@
c6897ef9e8c57b2b0dc8c94c0b2b9311996528b8f88bbf9b6a955de5d5c5120f

View File

@ -19253,6 +19253,21 @@ CREATE SEQUENCE operations_user_lists_id_seq
ALTER SEQUENCE operations_user_lists_id_seq OWNED BY operations_user_lists.id;
CREATE TABLE organizations (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL
);
CREATE SEQUENCE organizations_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE organizations_id_seq OWNED BY organizations.id;
CREATE TABLE packages_build_infos (
id bigint NOT NULL,
package_id integer NOT NULL,
@ -25413,6 +25428,8 @@ ALTER TABLE ONLY operations_strategies_user_lists ALTER COLUMN id SET DEFAULT ne
ALTER TABLE ONLY operations_user_lists ALTER COLUMN id SET DEFAULT nextval('operations_user_lists_id_seq'::regclass);
ALTER TABLE ONLY organizations ALTER COLUMN id SET DEFAULT nextval('organizations_id_seq'::regclass);
ALTER TABLE ONLY p_ci_builds_metadata ALTER COLUMN id SET DEFAULT nextval('ci_builds_metadata_id_seq'::regclass);
ALTER TABLE ONLY packages_build_infos ALTER COLUMN id SET DEFAULT nextval('packages_build_infos_id_seq'::regclass);
@ -27639,6 +27656,9 @@ ALTER TABLE ONLY operations_strategies_user_lists
ALTER TABLE ONLY operations_user_lists
ADD CONSTRAINT operations_user_lists_pkey PRIMARY KEY (id);
ALTER TABLE ONLY organizations
ADD CONSTRAINT organizations_pkey PRIMARY KEY (id);
ALTER TABLE ONLY p_ci_runner_machine_builds
ADD CONSTRAINT p_ci_runner_machine_builds_pkey PRIMARY KEY (build_id, partition_id);

View File

@ -7,7 +7,7 @@ type: index, reference
# AI/ML powered features
GitLab is creating AI-assisted features across our DevSecOps platform. These features aim to help increase velocity and solve key painpoints across the software development lifecycle.
GitLab is creating AI-assisted features across our DevSecOps platform. These features aim to help increase velocity and solve key pain points across the software development lifecycle.
## Enable AI/ML features
@ -32,13 +32,13 @@ When a feature is [Generally Available](../policy/alpha-beta-support.md#generall
## Beta AI features
[Beta features](../policy/alpha-beta-support.md#beta) do not require the [group-level experiment features setting](group/manage.md#group-experiment-features-setting) to be enabled.
[Beta features](../policy/alpha-beta-support.md#beta) do not require the [group-level experiment features setting](group/manage.md#group-experiment-features-setting) to be enabled.
- [Code Suggestions](project/repository/code_suggestions.md)
## Experiment AI features
## Experiment AI features
[Experiment features](../policy/alpha-beta-support.md#experiment) will soon require the [group-level Experiment features setting](group/manage.md#group-experiment-features-setting) to be enabled.
[Experiment features](../policy/alpha-beta-support.md#experiment) will soon require the [group-level Experiment features setting](group/manage.md#group-experiment-features-setting) to be enabled.
## Third-party AI features
@ -50,25 +50,32 @@ Third-party AI features require the [group-level third-party AI features setting
This feature is an [Experiment](../policy/alpha-beta-support.md) on GitLab.com that is powered by OpenAI's GPT-3.
If you spend a lot of time trying to understand pieces of code that others have created, or
are struggling to understand code written in a language that you are not familiar with, GitLab can help you get up to speed faster. By using a large language model, GitLab can explain the code in natural language.
GitLab can help you get up to speed faster if you:
- Spend a lot of time trying to understand pieces of code that others have created, or
- Struggle to understand code written in a language that you are not familiar with.
By using a large language model, GitLab can explain the code in natural language.
Prerequisites:
- The project must be a public project on GitLab.com.
- You must have the Ultimate subscription tier.
- You must have the GitLab Ultimate subscription tier.
- You must be a member of the project.
To explain your code:
1. Navigate to the file and select the lines that you want to have explained.
1. On the left side, select the question mark. You might have to scroll to the first line of your selection to view it. This sends the selected code together with a prompt to provide an explanation to the large language model referenced above.
1. A drawer is displayed. Wait a moment for the explanation to be generated.
1. Provide feedback about how satisfied you are with the explanation so we can improve the results.
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, select **Merge requests**, then select your merge request.
1. On the secondary menu, select **Changes**.
1. Go to the file, and select the lines that you want to have explained.
1. On the left side, select the question mark (**{question}**). You might have to scroll to the first line of your selection to view it. This sends the selected code, together with a prompt, to provide an explanation to the large language model.
1. A drawer is displayed on the right side of the page. Wait a moment for the explanation to be generated.
1. Provide feedback about how satisfied you are with the explanation, so we can improve the results.
![How to use the Explain Code Experiment](img/explain_code_experiment.png)
Please be aware we cannot guarantee that the large language model will produce results that are correct. Use the explanation with caution.
We cannot guarantee that the large language model produces results that are correct. Use the explanation with caution.
### GitLab Chat **(ULTIMATE SAAS)**
@ -76,14 +83,74 @@ Please be aware we cannot guarantee that the large language model will produce r
This feature is an [Experiment](../policy/alpha-beta-support.md) on GitLab.com that is powered by OpenAI's GPT-3. It requires the [group-level third-party AI features setting](group/manage.md#group-third-party-ai-features-setting) to be enabled.
Getting help has never been easier. If you have a question about how the GitLab product works, you can ask product how-to questions and get AI generated support from GitLab Chat.
Getting help has never been easier. If you have a question about how the GitLab product works, you can ask product how-to questions and get AI generated support from GitLab Chat.
1. In the lower-left corner, select the Help icon.
1. Select **Ask in GitLab Chat**. A drawer opens on the right side of your screen.
1. Enter your question in the chat input box and press **Enter** or select **Send**. It may take a few seconds for the interactive AI chat to search the product docs and produce an answer.
1. Enter your question in the chat input box and press **Enter** or select **Send**. It may take a few seconds for the interactive AI chat to search the product documentation and produce an answer.
To give feedback, select the **Give Feedback** link.
### Summarize merge request changes **(ULTIMATE SAAS)**
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10400) in GitLab 16.0 as an [Experiment](../policy/alpha-beta-support.md#experiment).
This feature is an [Experiment](../policy/alpha-beta-support.md) on GitLab.com that is powered by OpenAI's GPT-3. It requires the [group-level third-party AI features setting](group/manage.md#group-third-party-ai-features-setting) to be enabled.
Merge request summaries can be generated by using the `/summarize_diff` quick action in a merge request comment. This posts a comment from a GitLab bot that provides a summary of the changes and the related SHA for when that summary was generated.
Feedback on this experimental feature can be provided in [issue 408726](https://gitlab.com/gitlab-org/gitlab/-/issues/408726).
#### Data usage
This data is sent to the large language model referenced above when you use the `/summarize_diff` quick action:
1. The diff of changes between the head of the source branch and the target branch
### Summarize my merge request review **(ULTIMATE SAAS)**
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10466) in GitLab 16.0 as an [Experiment](../policy/alpha-beta-support.md#experiment).
This feature is an [Experiment](../policy/alpha-beta-support.md) on GitLab.com that is powered by OpenAI's GPT-3. It requires the [group-level third-party AI features setting](group/manage.md#group-third-party-ai-features-setting) to be enabled.
When you've completed your review of a merge request and are ready to [submit your review](project/merge_requests/reviews/index.md#submit-a-review) you can choose to have summary generated for you. To generate the summary:
1. Select the AI Actions dropdown list.
1. Select **Summarize my code review**.
The summary is generated and entered in to the comment box where you can edit and refine prior to submitting with your review.
Feedback on this experimental feature can be provided in [issue 408991](https://gitlab.com/gitlab-org/gitlab/-/issues/408991).
#### Data usage
This data is sent to the large language model referenced above when you click on **Summarize my code review**:
1. Draft comment's text
1. File path of the commented file(s)
### Generate suggested tests in merge requests **(ULTIMATE SAAS)**
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10366) in GitLab 16.0 as an [Experiment](../policy/alpha-beta-support.md#experiment).
This feature is an [Experiment](../policy/alpha-beta-support.md) on GitLab.com that is powered by OpenAI's GPT-3. It requires the [group-level third-party AI features setting](group/manage.md#group-third-party-ai-features-setting) to be enabled.
When in a merge request you can choose to have GitLab suggest tests for the file you are reviewing. This can help to determine if appropriate test coverage has been provided or help with writing tests to provide more coverage for your project. To generate a test suggestion:
1. Select the menu icon on the header of a file.
1. Select **Generate test with AI**.
A sidebar opens where the test suggestion is generated. From there you can choose to copy that suggestion in to your editor as the start of your tests.
Feedback on this experimental feature can be provided in [issue 408995](https://gitlab.com/gitlab-org/gitlab/-/issues/408995).
#### Data usage
This data is sent to the large language model referenced above when you click on **Generate test with AI**:
1. Contents of the file
1. The file name
## Data Usage
GitLab AI features leverage generative AI to help increase velocity and aim to help make you more productive. Each feature operates independently of other features and is not required for other features to function.
@ -102,13 +169,13 @@ These features are in a variety of [feature support levels](../policy/alpha-beta
### Data privacy
Some AI features require the use of third-party AI services models and APIs from: Google AI and OpenAI. The processing of any personal data is in accordance with our [Privacy Statement](https://about.gitlab.com/privacy/). You may also visit the [Sub-Processors page](https://about.gitlab.com/privacy/subprocessors/#third-party-sub-processors) to see the list of our Sub-Processors that we use in order to provide these features.
Some AI features require the use of third-party AI services models and APIs from: Google AI and OpenAI. The processing of any personal data is in accordance with our [Privacy Statement](https://about.gitlab.com/privacy/). You may also visit the [Sub-Processors page](https://about.gitlab.com/privacy/subprocessors/#third-party-sub-processors) to see the list of our Sub-Processors that we use in order to provide these features.
Group owners can control which top-level groups have access to third-party AI features by using the [group level third-party AI features setting](group/manage.md#group-third-party-ai-features-setting).
### Model accuracy and quality
Generative AI may produce unexpected results that may be:
Generative AI may produce unexpected results that may be:
- Low-quality
- Incoherent

View File

@ -340,11 +340,7 @@ namespace :gitlab do
exit 1
end
# A list of projects that GitLab creates automatically on install/upgrade
# gc = Gitlab::CurrentSettings.current_application_settings
seed_projects = [Gitlab::CurrentSettings.current_application_settings.self_monitoring_project]
if (Project.count - seed_projects.count { |x| !x.nil? }).eql?(0)
if Project.count.eql?(0)
puts "No user created projects. Database not active"
exit 1
end

View File

@ -15146,9 +15146,6 @@ msgstr ""
msgid "Deprecated API rate limits"
msgstr ""
msgid "Deprecation notice"
msgstr ""
msgid "Deprecations|For information on a possible replacement %{epicStart} learn more about Opstrace %{epicEnd}."
msgstr ""
@ -39766,9 +39763,6 @@ msgstr ""
msgid "Search users or groups"
msgstr ""
msgid "Search your project dependencies for their licenses and apply policies."
msgstr ""
msgid "Search your projects"
msgstr ""
@ -40007,9 +40001,6 @@ msgstr ""
msgid "SecurityConfiguration|By default, all analyzers are applied in order to cover all languages across your project, and only run if the language is detected in the merge request."
msgstr ""
msgid "SecurityConfiguration|Compliance"
msgstr ""
msgid "SecurityConfiguration|Configuration guide"
msgstr ""
@ -40076,7 +40067,7 @@ msgstr ""
msgid "SecurityConfiguration|Manage profiles for use by DAST scans."
msgstr ""
msgid "SecurityConfiguration|More scan types, including DAST, Dependency Scanning, Fuzzing, and Licence Compliance"
msgid "SecurityConfiguration|More scan types, including DAST, Dependency Scanning, Fuzzing"
msgstr ""
msgid "SecurityConfiguration|Not enabled"
@ -41198,9 +41189,6 @@ msgstr ""
msgid "Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. \"By %{link_open}@johnsmith%{link_close}\"). It will also associate and/or assign these issues and comments with the selected user."
msgstr ""
msgid "Self-monitoring was %{deprecation}deprecated%{link_end} in GitLab 14.9, and is %{removal}scheduled for removal%{link_end} in GitLab 16.0. For information on a possible replacement, %{opstrace}learn more about Opstrace%{link_end}."
msgstr ""
msgid "Send"
msgstr ""

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
FactoryBot.define do
factory :organization
end

View File

@ -394,6 +394,24 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
providers: [mock_saml_config_with_upstream_two_factor_authn_contexts])
end
it 'displays the remember me checkbox' do
visit new_user_session_path
expect(page).to have_field('remember_me_omniauth')
end
context 'when remember me is not enabled' do
before do
stub_application_setting(remember_me_enabled: false)
end
it 'does not display the remember me checkbox' do
visit new_user_session_path
expect(page).not_to have_field('remember_me_omniauth')
end
end
context 'when authn_context is worth two factors' do
let(:mock_saml_response) do
File.read('spec/fixtures/authentication/saml_response.xml')
@ -444,6 +462,24 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
describe 'without two-factor authentication' do
it 'displays the remember me checkbox' do
visit new_user_session_path
expect(page).to have_content(_('Remember me'))
end
context 'when remember me is not enabled' do
before do
stub_application_setting(remember_me_enabled: false)
end
it 'does not display the remember me checkbox' do
visit new_user_session_path
expect(page).not_to have_content(_('Remember me'))
end
end
context 'with correct username and password' do
let(:user) { create(:user) }
@ -750,17 +786,37 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
allow(instance).to receive(:"user_#{provider}_omniauth_callback_path")
.and_return("/users/auth/#{provider}/callback")
end
visit new_user_session_path
end
it 'correctly renders tabs and panes' do
visit new_user_session_path
ensure_tab_pane_correctness(['Main LDAP', 'Standard'])
end
it 'renders link to sign up path' do
visit new_user_session_path
expect(page.body).to have_link('Register now', href: new_user_registration_path)
end
it 'displays the remember me checkbox' do
visit new_user_session_path
ensure_remember_me_in_tab(ldap_server_config['label'])
end
context 'when remember me is not enabled' do
before do
stub_application_setting(remember_me_enabled: false)
end
it 'does not display the remember me checkbox' do
visit new_user_session_path
ensure_remember_me_not_in_tab(ldap_server_config['label'])
end
end
end
context 'when crowd is enabled' do
@ -775,13 +831,31 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
allow(instance).to receive(:user_crowd_omniauth_authorize_path)
.and_return("/users/auth/crowd/callback")
end
visit new_user_session_path
end
it 'correctly renders tabs and panes' do
visit new_user_session_path
ensure_tab_pane_correctness(%w(Crowd Standard))
end
it 'displays the remember me checkbox' do
visit new_user_session_path
ensure_remember_me_in_tab(_('Crowd'))
end
context 'when remember me is not enabled' do
before do
stub_application_setting(remember_me_enabled: false)
end
it 'does not display the remember me checkbox' do
visit new_user_session_path
ensure_remember_me_not_in_tab(_('Crowd'))
end
end
end
end

View File

@ -3,6 +3,7 @@ import Visibility from 'visibilityjs';
import {
isGid,
getIdFromGraphQLId,
getTypeFromGraphQLId,
convertToGraphQLId,
convertToGraphQLIds,
convertFromGraphQLIds,
@ -26,52 +27,30 @@ describe('isGid', () => {
});
});
describe('getIdFromGraphQLId', () => {
[
{
input: '',
output: null,
},
{
input: null,
output: null,
},
{
input: 2,
output: 2,
},
{
input: 'gid://',
output: null,
},
{
input: 'gid://gitlab/',
output: null,
},
{
input: 'gid://gitlab/Environments',
output: null,
},
{
input: 'gid://gitlab/Environments/',
output: null,
},
{
input: 'gid://gitlab/Environments/0',
output: 0,
},
{
input: 'gid://gitlab/Environments/123',
output: 123,
},
{
input: 'gid://gitlab/DesignManagement::Version/2',
output: 2,
},
].forEach(({ input, output }) => {
it(`getIdFromGraphQLId returns ${output} when passed ${input}`, () => {
expect(getIdFromGraphQLId(input)).toBe(output);
});
describe.each`
input | id | type
${''} | ${null} | ${null}
${null} | ${null} | ${null}
${0} | ${0} | ${null}
${'0'} | ${0} | ${null}
${2} | ${2} | ${null}
${'2'} | ${2} | ${null}
${'gid://'} | ${null} | ${null}
${'gid://gitlab'} | ${null} | ${null}
${'gid://gitlab/'} | ${null} | ${null}
${'gid://gitlab/Environments'} | ${null} | ${'Environments'}
${'gid://gitlab/Environments/'} | ${null} | ${'Environments'}
${'gid://gitlab/Environments/0'} | ${0} | ${'Environments'}
${'gid://gitlab/Environments/123'} | ${123} | ${'Environments'}
${'gid://gitlab/Environments/123/test'} | ${123} | ${'Environments'}
${'gid://gitlab/DesignManagement::Version/123'} | ${123} | ${'DesignManagement::Version'}
`('parses GraphQL ID `$input`', ({ input, id, type }) => {
it(`getIdFromGraphQLId returns ${id}`, () => {
expect(getIdFromGraphQLId(input)).toBe(id);
});
it(`getTypeFromGraphQLId returns ${type}`, () => {
expect(getTypeFromGraphQLId(input)).toBe(type);
});
});

View File

@ -8,7 +8,6 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createAlert } from '~/alert';
import Description from '~/issues/show/components/description.vue';
import eventHub from '~/issues/show/event_hub';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
import workItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import TaskList from '~/task_list';
@ -35,13 +34,6 @@ const $toast = {
};
const issueDetailsResponse = getIssueDetailsResponse();
const workItemQueryResponse = {
data: {
workItem: null,
},
};
const queryHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
const workItemTypesQueryHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
describe('Description component', () => {
@ -72,7 +64,6 @@ describe('Description component', () => {
...provide,
},
apolloProvider: createMockApollo([
[workItemQuery, queryHandler],
[workItemTypesQuery, workItemTypesQueryHandler],
[getIssueDetailsQuery, issueDetailsQueryHandler],
[createWorkItemMutation, createWorkItemMutationHandler],

View File

@ -11,7 +11,7 @@ import AutoDevopsEnabledAlert from '~/security_configuration/components/auto_dev
import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from '~/security_configuration/components/constants';
import FeatureCard from '~/security_configuration/components/feature_card.vue';
import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
import { complianceFeaturesMock, securityFeaturesMock, provideMock } from '../mock_data';
import { securityFeaturesMock, provideMock } from '../mock_data';
const gitlabCiHistoryPath = 'test/historyPath';
const { vulnerabilityTrainingDocsPath, projectFullPath } = provideMock;
@ -29,7 +29,6 @@ describe('~/security_configuration/components/app', () => {
wrapper = mountExtended(SecurityConfigurationApp, {
propsData: {
augmentedSecurityFeatures: securityFeaturesMock,
augmentedComplianceFeatures: complianceFeaturesMock,
securityTrainingEnabled: true,
...propsData,
},
@ -72,12 +71,7 @@ describe('~/security_configuration/components/app', () => {
text: i18n.configurationHistory,
container: findByTestId('security-testing-tab'),
});
const findComplianceViewHistoryLink = () =>
findLink({
href: gitlabCiHistoryPath,
text: i18n.configurationHistory,
container: findByTestId('compliance-testing-tab'),
});
const findAutoDevopsAlert = () => wrapper.findComponent(AutoDevopsAlert);
const findAutoDevopsEnabledAlert = () => wrapper.findComponent(AutoDevopsEnabledAlert);
const findVulnerabilityManagementTab = () => wrapper.findByTestId('vulnerability-management-tab');
@ -94,7 +88,7 @@ describe('~/security_configuration/components/app', () => {
});
describe('tabs', () => {
const expectedTabs = ['security-testing', 'compliance-testing', 'vulnerability-management'];
const expectedTabs = ['security-testing', 'vulnerability-management'];
it('renders GlTab Component', () => {
expect(findTab().exists()).toBe(true);
@ -123,9 +117,8 @@ describe('~/security_configuration/components/app', () => {
it('renders right amount of feature cards for given props with correct props', () => {
const cards = findFeatureCards();
expect(cards).toHaveLength(2);
expect(cards).toHaveLength(1);
expect(cards.at(0).props()).toEqual({ feature: securityFeaturesMock[0] });
expect(cards.at(1).props()).toEqual({ feature: complianceFeaturesMock[0] });
});
it('renders a basic description', () => {
@ -137,7 +130,6 @@ describe('~/security_configuration/components/app', () => {
});
it('should not show configuration History Link when gitlabCiPresent & gitlabCiHistoryPath are not defined', () => {
expect(findComplianceViewHistoryLink().exists()).toBe(false);
expect(findSecurityViewHistoryLink().exists()).toBe(false);
});
});
@ -158,7 +150,7 @@ describe('~/security_configuration/components/app', () => {
it('should show Alert with error Message', async () => {
expect(findManageViaMRErrorAlert().exists()).toBe(false);
findFeatureCards().at(1).vm.$emit('error', errorMessage);
findFeatureCards().at(0).vm.$emit('error', errorMessage);
await nextTick();
expect(findManageViaMRErrorAlert().exists()).toBe(true);
@ -166,7 +158,7 @@ describe('~/security_configuration/components/app', () => {
});
it('should hide Alert when it is dismissed', async () => {
findFeatureCards().at(1).vm.$emit('error', errorMessage);
findFeatureCards().at(0).vm.$emit('error', errorMessage);
await nextTick();
expect(findManageViaMRErrorAlert().exists()).toBe(true);
@ -257,7 +249,6 @@ describe('~/security_configuration/components/app', () => {
createComponent({
augmentedSecurityFeatures: securityFeaturesMock,
augmentedComplianceFeatures: complianceFeaturesMock,
autoDevopsEnabled: true,
});
@ -285,24 +276,6 @@ describe('~/security_configuration/components/app', () => {
latestPipelinePath: 'test/path',
});
});
it('should show latest pipeline info on the security tab with correct link when latestPipelinePath is defined', () => {
const latestPipelineInfoSecurity = findByTestId('latest-pipeline-info-security');
expect(latestPipelineInfoSecurity.text()).toMatchInterpolatedText(
i18n.latestPipelineDescription,
);
expect(latestPipelineInfoSecurity.find('a').attributes('href')).toBe('test/path');
});
it('should show latest pipeline info on the compliance tab with correct link when latestPipelinePath is defined', () => {
const latestPipelineInfoCompliance = findByTestId('latest-pipeline-info-compliance');
expect(latestPipelineInfoCompliance.text()).toMatchInterpolatedText(
i18n.latestPipelineDescription,
);
expect(latestPipelineInfoCompliance.find('a').attributes('href')).toBe('test/path');
});
});
describe('given gitlabCiPresent & gitlabCiHistoryPath props', () => {
@ -314,10 +287,8 @@ describe('~/security_configuration/components/app', () => {
});
it('should show configuration History Link', () => {
expect(findComplianceViewHistoryLink().exists()).toBe(true);
expect(findSecurityViewHistoryLink().exists()).toBe(true);
expect(findComplianceViewHistoryLink().attributes('href')).toBe('test/historyPath');
expect(findSecurityViewHistoryLink().attributes('href')).toBe('test/historyPath');
});
});

View File

@ -4,14 +4,8 @@ import {
SAST_DESCRIPTION,
SAST_HELP_PATH,
SAST_CONFIG_HELP_PATH,
LICENSE_COMPLIANCE_NAME,
LICENSE_COMPLIANCE_DESCRIPTION,
LICENSE_COMPLIANCE_HELP_PATH,
} from '~/security_configuration/components/constants';
import {
REPORT_TYPE_LICENSE_COMPLIANCE,
REPORT_TYPE_SAST,
} from '~/vue_shared/security_reports/constants';
import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants';
export const testProjectPath = 'foo/bar';
export const testProviderIds = [101, 102, 103];
@ -128,16 +122,6 @@ export const securityFeaturesMock = [
},
];
export const complianceFeaturesMock = [
{
name: LICENSE_COMPLIANCE_NAME,
description: LICENSE_COMPLIANCE_DESCRIPTION,
helpPath: LICENSE_COMPLIANCE_HELP_PATH,
type: REPORT_TYPE_LICENSE_COMPLIANCE,
configurationHelpPath: LICENSE_COMPLIANCE_HELP_PATH,
},
];
export const provideMock = {
upgradePath: '/upgrade',
autoDevopsHelpPagePath: '/autoDevopsHelpPagePath',

View File

@ -9,13 +9,6 @@ describe('augmentFeatures', () => {
},
];
const mockComplianceFeatures = [
{
name: 'LICENSE_COMPLIANCE',
type: 'LICENSE_COMPLIANCE',
},
];
const mockFeaturesWithSecondary = [
{
name: 'DAST',
@ -51,30 +44,25 @@ describe('augmentFeatures', () => {
const expectedOutputDefault = {
augmentedSecurityFeatures: mockSecurityFeatures,
augmentedComplianceFeatures: mockComplianceFeatures,
};
const expectedOutputSecondary = {
augmentedSecurityFeatures: mockSecurityFeatures,
augmentedComplianceFeatures: mockFeaturesWithSecondary,
};
const expectedOutputCustomFeature = {
augmentedSecurityFeatures: mockValidCustomFeature,
augmentedComplianceFeatures: mockComplianceFeatures,
};
describe('returns an object with augmentedSecurityFeatures and augmentedComplianceFeatures when', () => {
describe('returns an object with augmentedSecurityFeatures when', () => {
it('given an empty array', () => {
expect(augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, [])).toEqual(
expectedOutputDefault,
);
expect(augmentFeatures(mockSecurityFeatures, [])).toEqual(expectedOutputDefault);
});
it('given an invalid populated array', () => {
expect(
augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, mockInvalidCustomFeature),
).toEqual(expectedOutputDefault);
expect(augmentFeatures(mockSecurityFeatures, mockInvalidCustomFeature)).toEqual(
expectedOutputDefault,
);
});
it('features have secondary key', () => {
@ -84,21 +72,17 @@ describe('augmentFeatures', () => {
});
it('given a valid populated array', () => {
expect(
augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, mockValidCustomFeature),
).toEqual(expectedOutputCustomFeature);
expect(augmentFeatures(mockSecurityFeatures, mockValidCustomFeature)).toEqual(
expectedOutputCustomFeature,
);
});
});
describe('returns an object with camelcased keys', () => {
it('given a customfeature in snakecase', () => {
expect(
augmentFeatures(
mockSecurityFeatures,
mockComplianceFeatures,
mockValidCustomFeatureSnakeCase,
),
).toEqual(expectedOutputCustomFeature);
expect(augmentFeatures(mockSecurityFeatures, mockValidCustomFeatureSnakeCase)).toEqual(
expectedOutputCustomFeature,
);
});
});
});

View File

@ -18,10 +18,10 @@ import updateWorkItemNotificationsMutation from '~/work_items/graphql/update_wor
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import convertWorkItemMutation from '~/work_items/graphql/work_item_convert.mutation.graphql';
import {
workItemResponseFactory,
convertWorkItemMutationResponse,
projectWorkItemTypesQueryResponse,
convertWorkItemMutationErrorResponse,
workItemByIidResponseFactory,
} from '../mock_data';
jest.mock('~/lib/utils/common_utils');
@ -211,45 +211,45 @@ describe('WorkItemActions component', () => {
describe('notifications action', () => {
const errorMessage = 'Failed to subscribe';
const id = 'gid://gitlab/WorkItem/1';
const notificationToggledOffMessage = 'Notifications turned off.';
const notificationToggledOnMessage = 'Notifications turned on.';
const workItemQueryResponse = workItemResponseFactory({ canUpdate: true, canDelete: true });
const inputVariablesOff = {
id: workItemQueryResponse.data.workItem.id,
id,
notificationsWidget: {
subscribed: false,
},
};
const inputVariablesOn = {
id: workItemQueryResponse.data.workItem.id,
id,
notificationsWidget: {
subscribed: true,
},
};
const notificationsOffExpectedResponse = workItemResponseFactory({
const notificationsOffExpectedResponse = workItemByIidResponseFactory({
subscribed: false,
});
const toggleNotificationsOffHandler = jest.fn().mockResolvedValue({
data: {
workItemUpdate: {
workItem: notificationsOffExpectedResponse.data.workItem,
workItem: notificationsOffExpectedResponse.data.workspace.workItems.nodes[0],
errors: [],
},
},
});
const notificationsOnExpectedResponse = workItemResponseFactory({
const notificationsOnExpectedResponse = workItemByIidResponseFactory({
subscribed: true,
});
const toggleNotificationsOnHandler = jest.fn().mockResolvedValue({
data: {
workItemUpdate: {
workItem: notificationsOnExpectedResponse.data.workItem,
workItem: notificationsOnExpectedResponse.data.workspace.workItems.nodes[0],
errors: [],
},
},

View File

@ -8,9 +8,7 @@ import { mockTracking } from 'helpers/tracking_helper';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
import { config } from '~/graphql_shared/issuable_client';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
import {
@ -22,7 +20,6 @@ import {
import {
projectMembersResponseWithCurrentUser,
mockAssignees,
workItemQueryResponse,
currentUserResponse,
currentUserNullResponse,
projectMembersResponseWithoutCurrentUser,
@ -78,25 +75,11 @@ describe('WorkItemAssignees component', () => {
canInviteMembers = false,
canUpdate = true,
} = {}) => {
const apolloProvider = createMockApollo(
[
[userSearchQuery, searchQueryHandler],
[currentUserQuery, currentUserQueryHandler],
[updateWorkItemMutation, updateWorkItemMutationHandler],
],
{},
{
typePolicies: config.cacheConfig.typePolicies,
},
);
apolloProvider.clients.defaultClient.writeQuery({
query: workItemQuery,
variables: {
id: workItemId,
},
data: workItemQueryResponse.data,
});
const apolloProvider = createMockApollo([
[userSearchQuery, searchQueryHandler],
[currentUserQuery, currentUserQueryHandler],
[updateWorkItemMutation, updateWorkItemMutationHandler],
]);
wrapper = mountExtended(WorkItemAssignees, {
propsData: {

View File

@ -39,7 +39,7 @@ import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_ta
import {
mockParent,
workItemDatesSubscriptionResponse,
workItemByIidResponseFactory as workItemResponseFactory,
workItemByIidResponseFactory,
workItemTitleSubscriptionResponse,
workItemAssigneesSubscriptionResponse,
workItemMilestoneSubscriptionResponse,
@ -52,8 +52,8 @@ describe('WorkItemDetail component', () => {
Vue.use(VueApollo);
const workItemQueryResponse = workItemResponseFactory({ canUpdate: true, canDelete: true });
const workItemQueryResponseWithoutParent = workItemResponseFactory({
const workItemQueryResponse = workItemByIidResponseFactory({ canUpdate: true, canDelete: true });
const workItemQueryResponseWithoutParent = workItemByIidResponseFactory({
parent: null,
canUpdate: true,
canDelete: true,
@ -221,7 +221,7 @@ describe('WorkItemDetail component', () => {
describe('confidentiality', () => {
const errorMessage = 'Mutation failed';
const confidentialWorkItem = workItemResponseFactory({
const confidentialWorkItem = workItemByIidResponseFactory({
confidential: true,
});
const workItem = confidentialWorkItem.data.workspace.workItems.nodes[0];
@ -398,7 +398,7 @@ describe('WorkItemDetail component', () => {
describe('with parent', () => {
beforeEach(() => {
const parentResponse = workItemResponseFactory(mockParent);
const parentResponse = workItemByIidResponseFactory(mockParent);
createComponent({ handler: jest.fn().mockResolvedValue(parentResponse) });
return waitForPromises();
@ -437,7 +437,7 @@ describe('WorkItemDetail component', () => {
},
},
};
const parentResponse = workItemResponseFactory(mockParentObjective);
const parentResponse = workItemByIidResponseFactory(mockParentObjective);
createComponent({ handler: jest.fn().mockResolvedValue(parentResponse) });
await waitForPromises();
@ -492,7 +492,7 @@ describe('WorkItemDetail component', () => {
describe('when the assignees widget does not exist', () => {
it('does not call the assignees subscription', async () => {
const response = workItemResponseFactory({ assigneesWidgetPresent: false });
const response = workItemByIidResponseFactory({ assigneesWidgetPresent: false });
const handler = jest.fn().mockResolvedValue(response);
createComponent({ handler });
await waitForPromises();
@ -514,7 +514,7 @@ describe('WorkItemDetail component', () => {
describe('when the due date widget does not exist', () => {
it('does not call the dates subscription', async () => {
const response = workItemResponseFactory({ datesWidgetPresent: false });
const response = workItemByIidResponseFactory({ datesWidgetPresent: false });
const handler = jest.fn().mockResolvedValue(response);
createComponent({ handler });
await waitForPromises();
@ -537,7 +537,7 @@ describe('WorkItemDetail component', () => {
createComponent({
handler: jest
.fn()
.mockResolvedValue(workItemResponseFactory({ assigneesWidgetPresent: false })),
.mockResolvedValue(workItemByIidResponseFactory({ assigneesWidgetPresent: false })),
});
await waitForPromises();
@ -551,7 +551,7 @@ describe('WorkItemDetail component', () => {
${'renders when widget is returned from API'} | ${true} | ${true}
${'does not render when widget is not returned from API'} | ${false} | ${false}
`('$description', async ({ labelsWidgetPresent, exists }) => {
const response = workItemResponseFactory({ labelsWidgetPresent });
const response = workItemByIidResponseFactory({ labelsWidgetPresent });
const handler = jest.fn().mockResolvedValue(response);
createComponent({ handler });
await waitForPromises();
@ -567,7 +567,7 @@ describe('WorkItemDetail component', () => {
${'when widget is not returned from API'} | ${false} | ${false}
`('$description', ({ datesWidgetPresent, exists }) => {
it(`${datesWidgetPresent ? 'renders' : 'does not render'} due date component`, async () => {
const response = workItemResponseFactory({ datesWidgetPresent });
const response = workItemByIidResponseFactory({ datesWidgetPresent });
const handler = jest.fn().mockResolvedValue(response);
createComponent({ handler });
await waitForPromises();
@ -594,7 +594,7 @@ describe('WorkItemDetail component', () => {
${'renders when widget is returned from API'} | ${true} | ${true}
${'does not render when widget is not returned from API'} | ${false} | ${false}
`('$description', async ({ milestoneWidgetPresent, exists }) => {
const response = workItemResponseFactory({ milestoneWidgetPresent });
const response = workItemByIidResponseFactory({ milestoneWidgetPresent });
const handler = jest.fn().mockResolvedValue(response);
createComponent({ handler });
await waitForPromises();
@ -614,7 +614,7 @@ describe('WorkItemDetail component', () => {
describe('when the assignees widget does not exist', () => {
it('does not call the milestone subscription', async () => {
const response = workItemResponseFactory({ milestoneWidgetPresent: false });
const response = workItemByIidResponseFactory({ milestoneWidgetPresent: false });
const handler = jest.fn().mockResolvedValue(response);
createComponent({ handler });
await waitForPromises();
@ -632,6 +632,13 @@ describe('WorkItemDetail component', () => {
expect(successHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' });
});
it('skips the work item query when there is no workItemIid', async () => {
createComponent({ workItemIid: null });
await waitForPromises();
expect(successHandler).not.toHaveBeenCalled();
});
it('calls the work item query when isModal=true', async () => {
createComponent({ isModal: true });
await waitForPromises();
@ -648,7 +655,7 @@ describe('WorkItemDetail component', () => {
});
describe('work item has children', () => {
const objectiveWorkItem = workItemResponseFactory({
const objectiveWorkItem = workItemByIidResponseFactory({
workItemType: objectiveType,
confidential: true,
});

View File

@ -684,87 +684,6 @@ export const createWorkItemMutationErrorResponse = {
},
};
export const createWorkItemFromTaskMutationResponse = {
data: {
workItemCreateFromTask: {
__typename: 'WorkItemCreateFromTaskPayload',
errors: [],
workItem: {
__typename: 'WorkItem',
description: 'New description',
id: 'gid://gitlab/WorkItem/1',
iid: '1',
title: 'Updated title',
state: 'OPEN',
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
project: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
archived: false,
},
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
name: 'Task',
iconName: 'issue-type-task',
},
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
setWorkItemMetadata: false,
__typename: 'WorkItemPermissions',
},
widgets: [
{
__typename: 'WorkItemWidgetDescription',
type: 'DESCRIPTION',
description: 'New description',
descriptionHtml: '<p>New description</p>',
lastEditedAt: '2022-09-21T06:18:42Z',
lastEditedBy: {
name: 'Administrator',
webPath: '/root',
},
},
],
},
newWorkItem: {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1000000',
iid: '100',
title: 'Updated title',
state: 'OPEN',
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
description: '',
confidential: false,
project: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
archived: false,
},
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
name: 'Task',
iconName: 'issue-type-task',
},
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
setWorkItemMetadata: false,
__typename: 'WorkItemPermissions',
},
widgets: [],
},
},
},
};
export const deleteWorkItemResponse = {
data: { workItemDelete: { errors: [], __typename: 'WorkItemDeletePayload' } },
};
@ -1831,18 +1750,6 @@ export const projectMilestonesResponseWithNoMilestones = {
},
};
export const projectWorkItemResponse = {
data: {
workspace: {
id: 'gid://gitlab/Project/1',
workItems: {
nodes: [workItemQueryResponse.data.workItem],
},
__typename: 'Project',
},
},
};
export const mockWorkItemNotesResponse = {
data: {
workItem: {

View File

@ -6,7 +6,7 @@ import {
currentUserResponse,
workItemAssigneesSubscriptionResponse,
workItemDatesSubscriptionResponse,
workItemByIidResponseFactory as workItemResponseFactory,
workItemByIidResponseFactory,
workItemTitleSubscriptionResponse,
workItemLabelsSubscriptionResponse,
workItemMilestoneSubscriptionResponse,
@ -32,7 +32,7 @@ describe('Work items router', () => {
Vue.use(VueApollo);
const workItemQueryHandler = jest.fn().mockResolvedValue(workItemResponseFactory());
const workItemQueryHandler = jest.fn().mockResolvedValue(workItemByIidResponseFactory());
const currentUserQueryHandler = jest.fn().mockResolvedValue(currentUserResponse);
const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);

View File

@ -87,4 +87,24 @@ RSpec.describe SessionsHelper do
expect(subject).to eq('ma**@e******.com')
end
end
describe '#remember_me_enabled?' do
subject { helper.remember_me_enabled? }
context 'when application setting is enabled' do
before do
stub_application_setting(remember_me_enabled: true)
end
it { is_expected.to be true }
end
context 'when application setting is disabled' do
before do
stub_application_setting(remember_me_enabled: false)
end
it { is_expected.to be false }
end
end
end

View File

@ -639,8 +639,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
create(:alert_management_alert, project: project, created_at: n.days.ago)
end
stub_application_setting(self_monitoring_project: project)
for_defined_days_back do
create(:product_analytics_event, project: project, se_category: 'epics', se_action: 'promote')
end

View File

@ -26,12 +26,6 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
end
describe 'associations' do
it do
is_expected.to belong_to(:self_monitoring_project).class_name('Project')
.with_foreign_key(:instance_administration_project_id)
.inverse_of(:application_setting)
end
it do
is_expected.to belong_to(:instance_group).class_name('Group')
.with_foreign_key(:instance_administrators_group_id)

View File

@ -90,37 +90,6 @@ RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching,
end
end
end
context 'with self-monitoring project and internal Prometheus' do
before do
integration.api_url = 'http://localhost:9090'
stub_application_setting(self_monitoring_project_id: project.id)
stub_config(prometheus: { enable: true, server_address: 'localhost:9090' })
end
it 'allows self-monitoring project to connect to internal Prometheus' do
aggregate_failures do
['127.0.0.1', '192.168.2.3'].each do |url|
allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([Addrinfo.tcp(url, 80)])
expect(integration.can_query?).to be true
end
end
end
it 'does not allow self-monitoring project to connect to other local URLs' do
integration.api_url = 'http://localhost:8000'
aggregate_failures do
['127.0.0.1', '192.168.2.3'].each do |url|
allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([Addrinfo.tcp(url, 80)])
expect(integration.can_query?).to be false
end
end
end
end
end
end
@ -218,23 +187,6 @@ RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching,
it 'blocks local requests' do
expect(integration.prometheus_client).to be_nil
end
context 'with self-monitoring project and internal Prometheus URL' do
before do
stub_application_setting(allow_local_requests_from_web_hooks_and_services: false)
stub_application_setting(self_monitoring_project_id: project.id)
stub_config(prometheus: {
enable: true,
server_address: api_url
})
end
it 'allows local requests' do
expect(integration.prometheus_client).not_to be_nil
expect { integration.prometheus_client.ping }.not_to raise_error
end
end
end
context 'behind IAP' do

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
require 'spec_helper'
# rubocop: disable Lint/EmptyBlock
# rubocop: disable RSpec/EmptyExampleGroup
RSpec.describe Organization, type: :model, feature_category: :cell do
end
# rubocop: enable RSpec/EmptyExampleGroup
# rubocop: enable Lint/EmptyBlock

View File

@ -7551,24 +7551,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
describe '#self_monitoring?' do
let_it_be(:project) { create(:project) }
subject { project.self_monitoring? }
context 'when the project is instance self-monitoring' do
before do
stub_application_setting(self_monitoring_project_id: project.id)
end
it { is_expected.to be true }
end
context 'when the project is not self-monitoring' do
it { is_expected.to be false }
end
end
describe '#add_export_job' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }

View File

@ -2174,6 +2174,20 @@ RSpec.describe User, feature_category: :user_profile do
end
end
describe '#sign_in_with_codes_allowed?' do
let_it_be(:user) { create(:user, :two_factor_via_webauthn) }
context 'when `webauthn_without_totp` disabled' do
before do
stub_feature_flags(webauthn_without_totp: false)
end
it { expect(user.sign_in_with_codes_allowed?).to eq(false) }
end
it { expect(user.sign_in_with_codes_allowed?).to eq(true) }
end
describe '#two_factor_otp_enabled?' do
let_it_be(:user) { create(:user) }

View File

@ -463,16 +463,26 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do
end
context 'subresources' do
let(:user) { create(:user) }
let(:member_user) { create(:user) }
let_it_be_with_reload(:user) { create(:user) }
let_it_be_with_reload(:member_user) { create(:user) }
let(:group) { create(:group, :public) }
let(:subgroup) { create(:group, parent: group) }
let(:subsubgroup) { create(:group, parent: subgroup) }
let(:subsubproject) { create(:project, group: subsubgroup) }
let_it_be_with_reload(:group) { create(:group, :public) }
let_it_be_with_reload(:subgroup) { create(:group, parent: group) }
let_it_be(:private_subgroup) { create(:group, :private, parent: group, name: 'private_subgroup') }
let_it_be(:private_subgroup_with_direct_membership) { create(:group, :private, parent: group) }
let_it_be_with_reload(:subsubgroup) { create(:group, parent: subgroup) }
let(:group_project) { create(:project, :public, group: group) }
let(:control_project) { create(:project, group: subsubgroup) }
let_it_be_with_reload(:group_project) { create(:project, :public, group: group) }
let_it_be_with_reload(:control_project) { create(:project, :private, group: subsubgroup) }
let_it_be_with_reload(:subsubproject) { create(:project, :public, group: subsubgroup) }
let_it_be(:private_subgroup_project) do
create(:project, :private, group: private_subgroup, name: 'private_subgroup_project')
end
let_it_be(:private_subgroup_with_direct_membership_project) do
create(:project, :private, group: private_subgroup_with_direct_membership, name: 'private_subgroup_project')
end
context 'with memberships' do
before do
@ -481,14 +491,68 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do
subsubproject.add_developer(member_user)
group_project.add_developer(member_user)
control_project.add_maintainer(user)
private_subgroup_with_direct_membership.add_developer(member_user)
group.add_owner(user)
@group_member = create(:group_member, :developer, group: group, user: member_user)
end
let_it_be(:todo_in_public_group_project) do
create(:todo, :pending,
project: group_project,
user: member_user,
target: create(:issue, project: group_project)
)
end
let_it_be(:mr_in_public_group_project) do
create(:merge_request, source_project: group_project, assignees: [member_user])
end
let_it_be(:todo_in_private_subgroup_project) do
create(:todo, :pending,
project: private_subgroup_project,
user: member_user,
target: create(:issue, project: private_subgroup_project)
)
end
let_it_be(:mr_in_private_subgroup_project) do
create(:merge_request, source_project: private_subgroup_project, assignees: [member_user])
end
let_it_be(:todo_in_public_subsubgroup_project) do
create(:todo, :pending,
project: subsubproject,
user: member_user,
target: create(:issue, project: subsubproject)
)
end
let_it_be(:mr_in_public_subsubgroup_project) do
create(:merge_request, source_project: subsubproject, assignees: [member_user])
end
let_it_be(:todo_in_private_subgroup_with_direct_membership_project) do
create(:todo, :pending,
project: private_subgroup_with_direct_membership_project,
user: member_user,
target: create(:issue, project: private_subgroup_with_direct_membership_project)
)
end
let_it_be(:mr_in_private_subgroup_with_direct_membership_project) do
create(:merge_request,
source_project: private_subgroup_with_direct_membership_project,
assignees: [member_user]
)
end
context 'with skipping of subresources' do
subject(:execute_service) { described_class.new(user).execute(@group_member, skip_subresources: true) }
before do
described_class.new(user).execute(@group_member, skip_subresources: true)
execute_service
end
it 'removes the group membership' do
@ -514,11 +578,35 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do
it 'does not remove the user from the control project' do
expect(control_project.members.map(&:user)).to include(user)
end
context 'todos', :sidekiq_inline do
it 'removes todos for which the user no longer has access' do
expect(member_user.todos).to include(
todo_in_public_group_project,
todo_in_public_subsubgroup_project,
todo_in_private_subgroup_with_direct_membership_project
)
expect(member_user.todos).not_to include(todo_in_private_subgroup_project)
end
end
context 'issuables', :sidekiq_inline do
subject(:execute_service) do
described_class.new(user).execute(@group_member, skip_subresources: true, unassign_issuables: true)
end
it 'removes assigned issuables, even in subresources' do
expect(member_user.assigned_merge_requests).to be_empty
end
end
end
context 'without skipping of subresources' do
subject(:execute_service) { described_class.new(user).execute(@group_member, skip_subresources: false) }
before do
described_class.new(user).execute(@group_member, skip_subresources: false)
execute_service
end
it 'removes the project membership' do
@ -544,6 +632,30 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do
it 'does not remove the user from the control project' do
expect(control_project.members.map(&:user)).to include(user)
end
context 'todos', :sidekiq_inline do
it 'removes todos for which the user no longer has access' do
expect(member_user.todos).to include(
todo_in_public_group_project,
todo_in_public_subsubgroup_project
)
expect(member_user.todos).not_to include(
todo_in_private_subgroup_project,
todo_in_private_subgroup_with_direct_membership_project
)
end
end
context 'issuables', :sidekiq_inline do
subject(:execute_service) do
described_class.new(user).execute(@group_member, skip_subresources: false, unassign_issuables: true)
end
it 'removes assigned issuables' do
expect(member_user.assigned_merge_requests).to be_empty
end
end
end
end
@ -626,4 +738,13 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do
expect(project.members.not_accepted_invitations_by_user(member_user)).to be_empty
end
end
describe '#mark_as_recursive_call' do
it 'marks the instance as recursive' do
service = described_class.new(current_user)
service.mark_as_recursive_call
expect(service.send(:recursive_call?)).to eq(true)
end
end
end

View File

@ -30,4 +30,20 @@ module UserLoginHelper
def ensure_one_active_pane
expect(page).to have_selector('.tab-pane.active', count: 1)
end
def ensure_remember_me_in_tab(tab_name)
find_link(tab_name).click
within '.tab-pane.active' do
expect(page).to have_content _('Remember me')
end
end
def ensure_remember_me_not_in_tab(tab_name)
find_link(tab_name).click
within '.tab-pane.active' do
expect(page).not_to have_content _('Remember me')
end
end
end

View File

@ -961,19 +961,17 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
using RSpec::Parameterized::TableSyntax
let(:task) { 'gitlab:db:active' }
let(:self_monitoring) { double('self_monitoring') }
where(:needs_migration, :self_monitoring_project, :project_count, :exit_status, :exit_code) do
true | nil | nil | 1 | false
false | :self_monitoring | 1 | 1 | false
false | nil | 0 | 1 | false
false | :self_monitoring | 2 | 0 | true
where(:needs_migration, :project_count, :exit_status, :exit_code) do
true | nil | 1 | false
false | 1 | 0 | true
false | 0 | 1 | false
false | 2 | 0 | true
end
with_them do
it 'exits 0 or 1 depending on user modifications to the database' do
allow_any_instance_of(ActiveRecord::MigrationContext).to receive(:needs_migration?).and_return(needs_migration)
allow_any_instance_of(ApplicationSetting).to receive(:self_monitoring_project).and_return(self_monitoring_project)
allow(Project).to receive(:count).and_return(project_count)
expect { run_rake_task(task) }.to raise_error do |error|