diff --git a/Gemfile b/Gemfile
index e72b9263c05..0213bc0af89 100644
--- a/Gemfile
+++ b/Gemfile
@@ -476,7 +476,7 @@ group :development, :test do
gem 'awesome_print', require: false # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'database_cleaner-active_record', '~> 2.1.0', feature_category: :database
+ gem 'database_cleaner-active_record', '~> 2.2.0', feature_category: :database
gem 'rspec-rails', '~> 6.1.1', feature_category: :shared
gem 'factory_bot_rails', '~> 6.4.3', feature_category: :tooling
@@ -515,7 +515,7 @@ group :development, :test do
# For now we only use vite in development / test, and not for production builds
# See: https://gitlab.com/gitlab-org/frontend/rfcs/-/issues/106
gem 'vite_rails', '~> 3.0.17', feature_category: :shared
- gem 'vite_ruby', '~> 3.5.0', feature_category: :shared
+ gem 'vite_ruby', '~> 3.7.0', feature_category: :shared
gem 'gitlab-housekeeper', path: 'gems/gitlab-housekeeper', feature_category: :tooling
end
@@ -569,7 +569,7 @@ group :test do
# Moved in `test` because https://gitlab.com/gitlab-org/gitlab/-/issues/217527
gem 'derailed_benchmarks', require: false # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'gitlab_quality-test_tooling', '~> 1.31.0', require: false, feature_category: :tooling
+ gem 'gitlab_quality-test_tooling', '~> 1.32.0', require: false, feature_category: :tooling
end
gem 'octokit', '~> 9.0', feature_category: :importers
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 3d55ff7cb69..10a8b5d6d18 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -101,7 +101,7 @@
{"name":"cvss-suite","version":"3.0.1","platform":"ruby","checksum":"b5ca9e9e94032a42fd0dc28c1e305378b62c949e35ed7111fc4a1d76f68ad3f9"},
{"name":"danger","version":"9.4.2","platform":"ruby","checksum":"43e552c6731030235a30fdeafe703d2e2ab9c30917154489cb0ecd9ad3259d80"},
{"name":"danger-gitlab","version":"8.0.0","platform":"ruby","checksum":"497dd7d0f6513913de651019223d8058cf494df10acbd17de92b175dfa04a3a8"},
-{"name":"database_cleaner-active_record","version":"2.1.0","platform":"ruby","checksum":"7384b973d67bcc1b5a850b876a4638aa83cca3bc88f9d87562fe25cd2dd60d8a"},
+{"name":"database_cleaner-active_record","version":"2.2.0","platform":"ruby","checksum":"3228d6d8ec1f2103fd6ab468dae923424318bcfabcf5dd5b02e5fcb0c486e1c7"},
{"name":"database_cleaner-core","version":"2.0.1","platform":"ruby","checksum":"8646574c32162e59ed7b5258a97a208d3c44551b854e510994f24683865d846c"},
{"name":"date","version":"3.3.3","platform":"java","checksum":"584e0a582d1eb2207b4eaac089d8a43f2ca10bea02682f286099642f15c56cce"},
{"name":"date","version":"3.3.3","platform":"ruby","checksum":"819792019d5712b748fb15f6dfaaedef14b0328723ef23583ea35f186774530f"},
@@ -227,7 +227,7 @@
{"name":"gitlab-styles","version":"12.0.1","platform":"ruby","checksum":"d8a302b0ab0e1f18e2d11501760f1b85c5e70b5e5ca628828a0786c7984ed133"},
{"name":"gitlab_chronic_duration","version":"0.12.0","platform":"ruby","checksum":"0d766944d415b5c831f176871ee8625783fc0c5bfbef2d79a3a616f207ffc16d"},
{"name":"gitlab_omniauth-ldap","version":"2.2.0","platform":"ruby","checksum":"bb4d20acb3b123ed654a8f6a47d3fac673ece7ed0b6992edb92dca14bad2838c"},
-{"name":"gitlab_quality-test_tooling","version":"1.31.0","platform":"ruby","checksum":"c13d38f2ba01469179db7211008722b1f4a55270cca561d832b1eee438124f52"},
+{"name":"gitlab_quality-test_tooling","version":"1.32.0","platform":"ruby","checksum":"72b7bb243d3e1f8006bef4bcf3585480a774f72bb9dd97bfcc52db4b32fd30fb"},
{"name":"globalid","version":"1.1.0","platform":"ruby","checksum":"b337e1746f0c8cb0a6c918234b03a1ddeb4966206ce288fbb57779f59b2d154f"},
{"name":"gon","version":"6.4.0","platform":"ruby","checksum":"e3a618d659392890f1aa7db420f17c75fd7d35aeb5f8fe003697d02c4b88d2f0"},
{"name":"google-apis-androidpublisher_v3","version":"0.34.0","platform":"ruby","checksum":"d7e1d7dd92f79c498fe2082222a1740d788e022e660c135564b3fd299cab5425"},
@@ -749,7 +749,7 @@
{"name":"view_component","version":"3.13.0","platform":"ruby","checksum":"316e6479f51387160e7eb372d33cbb97d586ac2ed9d9fe80495cec48b346187b"},
{"name":"virtus","version":"2.0.0","platform":"ruby","checksum":"8841dae4eb7fcc097320ba5ea516bf1839e5d056c61ee27138aa4bddd6e3d1c2"},
{"name":"vite_rails","version":"3.0.17","platform":"ruby","checksum":"b90e85a3e55802981cbdb43a4101d944b1e7055bfe85599d9cb7de0f1ea58bcc"},
-{"name":"vite_ruby","version":"3.5.0","platform":"ruby","checksum":"a3e5da3fdd816f831cb1530c4001a790aac862c89f74c09f48d5a3cfed3dea73"},
+{"name":"vite_ruby","version":"3.7.0","platform":"ruby","checksum":"4a34ada95e66821390896cf518c772146befc8f23d8954fa04416a5537e5f739"},
{"name":"vmstat","version":"2.3.0","platform":"ruby","checksum":"ab5446a3e3bd0a9cdb9d9ac69a0bbd119c4f161d945a0846a519dd7018af656d"},
{"name":"warden","version":"1.2.9","platform":"ruby","checksum":"46684f885d35a69dbb883deabf85a222c8e427a957804719e143005df7a1efd0"},
{"name":"warning","version":"1.3.0","platform":"ruby","checksum":"23695a5d8e50bd5c46068931b529bee0b28e4982cbcefbe77d867800dde8069e"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 12f9c12e397..450f742bff2 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -455,7 +455,7 @@ GEM
danger-gitlab (8.0.0)
danger
gitlab (~> 4.2, >= 4.2.0)
- database_cleaner-active_record (2.1.0)
+ database_cleaner-active_record (2.2.0)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
@@ -748,7 +748,7 @@ GEM
omniauth (>= 1.3, < 3)
pyu-ruby-sasl (>= 0.0.3.3, < 0.1)
rubyntlm (~> 0.5)
- gitlab_quality-test_tooling (1.31.0)
+ gitlab_quality-test_tooling (1.32.0)
activesupport (>= 7.0, < 7.2)
amatch (~> 0.4.1)
gitlab (~> 4.19)
@@ -1874,7 +1874,7 @@ GEM
vite_rails (3.0.17)
railties (>= 5.1, < 8)
vite_ruby (~> 3.0, >= 3.2.2)
- vite_ruby (3.5.0)
+ vite_ruby (3.7.0)
dry-cli (>= 0.7, < 2)
rack-proxy (~> 0.6, >= 0.6.1)
zeitwerk (~> 2.2)
@@ -1973,7 +1973,7 @@ DEPENDENCIES
cssbundling-rails (= 1.4.0)
csv_builder!
cvss-suite (~> 3.0.1)
- database_cleaner-active_record (~> 2.1.0)
+ database_cleaner-active_record (~> 2.2.0)
deckar01-task_list (= 2.3.4)
declarative_policy (~> 1.1.0)
deprecation_toolkit (~> 1.5.1)
@@ -2043,7 +2043,7 @@ DEPENDENCIES
gitlab-utils!
gitlab_chronic_duration (~> 0.12)
gitlab_omniauth-ldap (~> 2.2.0)
- gitlab_quality-test_tooling (~> 1.31.0)
+ gitlab_quality-test_tooling (~> 1.32.0)
gon (~> 6.4.0)
google-apis-androidpublisher_v3 (~> 0.34.0)
google-apis-cloudbilling_v1 (~> 0.21.0)
@@ -2273,7 +2273,7 @@ DEPENDENCIES
version_sorter (~> 2.3)
view_component (~> 3.13.0)
vite_rails (~> 3.0.17)
- vite_ruby (~> 3.5.0)
+ vite_ruby (~> 3.7.0)
vmstat (~> 2.3.0)
warning (~> 1.3.0)
webauthn (~> 3.0)
diff --git a/app/assets/javascripts/boards/graphql/sub_groups.query.graphql b/app/assets/javascripts/boards/graphql/sub_groups.query.graphql
new file mode 100644
index 00000000000..13f228e39b0
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/sub_groups.query.graphql
@@ -0,0 +1,22 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+
+fragment Group on Group {
+ id
+ name
+ fullName
+ fullPath
+}
+
+query getSubGroups($fullPath: ID!, $search: String, $after: String) {
+ group(fullPath: $fullPath) {
+ ...Group
+ descendantGroups(search: $search, after: $after, first: 100) {
+ nodes {
+ ...Group
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index 7aae0a325c3..5f56b3e700a 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -18,6 +18,7 @@ import {
TOKEN_TYPE_CONTACT,
TOKEN_TYPE_DRAFT,
TOKEN_TYPE_EPIC,
+ TOKEN_TYPE_GROUP,
TOKEN_TYPE_HEALTH,
TOKEN_TYPE_ITERATION,
TOKEN_TYPE_LABEL,
@@ -235,6 +236,16 @@ export const filtersMap = {
},
},
},
+ [TOKEN_TYPE_GROUP]: {
+ [API_PARAM]: {
+ [NORMAL_FILTER]: 'fullPath',
+ },
+ [URL_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'group_path',
+ },
+ },
+ },
[TOKEN_TYPE_REVIEWER]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'reviewerUsername',
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/group_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/group_token.vue
new file mode 100644
index 00000000000..8c58b5f723f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/group_token.vue
@@ -0,0 +1,109 @@
+
+
+
+
+
+ {{ activeTokenValue ? displayValue(activeTokenValue) : inputValue }}
+
+
+
+ {{ group.fullName }}
+
+
+
+
diff --git a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
index 0899d4073d8..9b656d771b3 100644
--- a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
+++ b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
@@ -12,6 +12,7 @@ import {
WORKSPACE_GROUP,
WORKSPACE_PROJECT,
} from '~/issues/constants';
+import { AutocompleteCache } from '~/issues/dashboard/utils';
import { defaultTypeTokenOptions } from '~/issues/list/constants';
import searchLabelsQuery from '~/issues/list/queries/search_labels.query.graphql';
import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
@@ -29,14 +30,20 @@ import {
OPERATORS_IS_NOT_OR,
TOKEN_TITLE_ASSIGNEE,
TOKEN_TITLE_AUTHOR,
+ TOKEN_TITLE_CONFIDENTIAL,
+ TOKEN_TITLE_GROUP,
TOKEN_TITLE_LABEL,
TOKEN_TITLE_MILESTONE,
+ TOKEN_TITLE_MY_REACTION,
TOKEN_TITLE_SEARCH_WITHIN,
TOKEN_TITLE_TYPE,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_CONFIDENTIAL,
+ TOKEN_TYPE_GROUP,
TOKEN_TYPE_LABEL,
TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
TOKEN_TYPE_SEARCH_WITHIN,
TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
@@ -47,11 +54,15 @@ import { STATE_CLOSED } from '../../constants';
import { sortOptions, urlSortParams } from '../constants';
import getWorkItemsQuery from '../queries/get_work_items.query.graphql';
-const UserToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/user_token.vue');
+const EmojiToken = () =>
+ import('~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue');
+const GroupToken = () =>
+ import('~/vue_shared/components/filtered_search_bar/tokens/group_token.vue');
const LabelToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/label_token.vue');
const MilestoneToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue');
+const UserToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/user_token.vue');
export default {
issuableListTabs,
@@ -63,7 +74,14 @@ export default {
IssueCardTimeInfo,
},
mixins: [glFeatureFlagMixin()],
- inject: ['fullPath', 'initialSort', 'isGroup', 'isSignedIn', 'workItemType'],
+ inject: [
+ 'autocompleteAwardEmojisPath',
+ 'fullPath',
+ 'initialSort',
+ 'isGroup',
+ 'isSignedIn',
+ 'workItemType',
+ ],
props: {
eeCreatedWorkItemsCount: {
type: Number,
@@ -97,6 +115,7 @@ export default {
search: this.searchQuery,
...this.apiFilterParams,
...this.pageParams,
+ includeDescendants: !this.apiFilterParams.fullPath,
types: this.apiFilterParams.types || [this.workItemType],
};
},
@@ -185,6 +204,15 @@ export default {
preloadedUsers,
multiSelect: this.glFeatures.groupMultiSelectTokens,
},
+ {
+ type: TOKEN_TYPE_GROUP,
+ icon: 'group',
+ title: TOKEN_TITLE_GROUP,
+ unique: true,
+ token: GroupToken,
+ operators: OPERATORS_IS,
+ fullPath: this.fullPath,
+ },
{
type: TOKEN_TYPE_LABEL,
title: TOKEN_TITLE_LABEL,
@@ -231,6 +259,31 @@ export default {
});
}
+ if (this.isSignedIn) {
+ tokens.push({
+ type: TOKEN_TYPE_CONFIDENTIAL,
+ title: TOKEN_TITLE_CONFIDENTIAL,
+ icon: 'eye-slash',
+ token: GlFilteredSearchToken,
+ unique: true,
+ operators: OPERATORS_IS,
+ options: [
+ { icon: 'eye-slash', value: 'yes', title: __('Yes') },
+ { icon: 'eye', value: 'no', title: __('No') },
+ ],
+ });
+
+ tokens.push({
+ type: TOKEN_TYPE_MY_REACTION,
+ title: TOKEN_TITLE_MY_REACTION,
+ icon: 'thumb-up',
+ token: EmojiToken,
+ unique: true,
+ fetchEmojis: this.fetchEmojis,
+ recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-my_reaction`,
+ });
+ }
+
tokens.sort((a, b) => a.title.localeCompare(b.title));
return tokens;
@@ -251,7 +304,18 @@ export default {
this.$apollo.queries.workItems.refetch();
},
},
+ created() {
+ this.autocompleteCache = new AutocompleteCache();
+ },
methods: {
+ fetchEmojis(search) {
+ return this.autocompleteCache.fetch({
+ url: this.autocompleteAwardEmojisPath,
+ cacheName: 'emojis',
+ searchProperty: 'name',
+ search,
+ });
+ },
fetchLabelsWithFetchPolicy(search, fetchPolicy = fetchPolicies.CACHE_FIRST) {
return this.$apollo
.query({
diff --git a/app/assets/javascripts/work_items/list/index.js b/app/assets/javascripts/work_items/list/index.js
index 0648347d6a1..8a41b8adbc5 100644
--- a/app/assets/javascripts/work_items/list/index.js
+++ b/app/assets/javascripts/work_items/list/index.js
@@ -14,6 +14,7 @@ export const mountWorkItemsListApp = () => {
Vue.use(VueApollo);
const {
+ autocompleteAwardEmojisPath,
fullPath,
hasEpicsFeature,
hasIssuableHealthStatusFeature,
@@ -34,6 +35,7 @@ export const mountWorkItemsListApp = () => {
name: 'WorkItemsListRoot',
apolloProvider,
provide: {
+ autocompleteAwardEmojisPath,
fullPath,
hasEpicsFeature: parseBoolean(hasEpicsFeature),
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
diff --git a/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql b/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql
index 2ac0762315c..59918d650bc 100644
--- a/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql
+++ b/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql
@@ -2,6 +2,7 @@
#import "ee_else_ce/work_items/list/queries/work_item_widgets.fragment.graphql"
query getWorkItems(
+ $includeDescendants: Boolean = true
$fullPath: ID!
$search: String
$sort: WorkItemSort
@@ -9,9 +10,11 @@ query getWorkItems(
$assigneeWildcardId: AssigneeWildcardId
$assigneeUsernames: [String!]
$authorUsername: String
+ $confidential: Boolean
$labelName: [String!]
$milestoneTitle: [String!]
$milestoneWildcardId: MilestoneWildcardId
+ $myReactionEmoji: String
$types: [IssueType!]
$in: [IssuableSearchableField!]
$not: NegatedWorkItemFilterInput
@@ -24,15 +27,17 @@ query getWorkItems(
group(fullPath: $fullPath) {
id
workItemStateCounts(
- includeDescendants: true
+ includeDescendants: $includeDescendants
sort: $sort
state: $state
assigneeUsernames: $assigneeUsernames
assigneeWildcardId: $assigneeWildcardId
authorUsername: $authorUsername
+ confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
types: $types
not: $not
or: $or
@@ -49,9 +54,11 @@ query getWorkItems(
assigneeUsernames: $assigneeUsernames
assigneeWildcardId: $assigneeWildcardId
authorUsername: $authorUsername
+ confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
types: $types
in: $in
not: $not
diff --git a/app/controllers/repositories/git_http_controller.rb b/app/controllers/repositories/git_http_controller.rb
index 85a996a8f52..995cfe26e6a 100644
--- a/app/controllers/repositories/git_http_controller.rb
+++ b/app/controllers/repositories/git_http_controller.rb
@@ -101,7 +101,7 @@ module Repositories
Onboarding::ProgressService.async(project.namespace_id).execute(action: :git_pull)
- return if Feature.enabled?(:disable_git_http_fetch_writes)
+ return if skip_fetch_statistics_increment?
Projects::FetchStatisticsIncrementService.new(project).execute
end
@@ -140,6 +140,14 @@ module Repositories
payload[:metadata] ||= {}
payload[:metadata][:repository_storage] = project&.repository_storage
end
+
+ def skip_fetch_statistics_increment?
+ # Since disable_git_http_fetch_writes FF does not define a feature flag actor,
+ # it is currently not possible to increment the project statistics without enabling
+ # or disabling it for all projects. The allow_git_http_fetch_writes FF allow us to control this.
+ Feature.enabled?(:disable_git_http_fetch_writes) &&
+ Feature.disabled?(:allow_git_http_fetch_writes, project, type: :beta)
+ end
end
end
diff --git a/app/helpers/work_items_helper.rb b/app/helpers/work_items_helper.rb
index ec9aed59171..1cbf872cb45 100644
--- a/app/helpers/work_items_helper.rb
+++ b/app/helpers/work_items_helper.rb
@@ -29,6 +29,7 @@ module WorkItemsHelper
def work_items_list_data(group, current_user)
{
+ autocomplete_award_emojis_path: autocomplete_award_emojis_path,
full_path: group.full_path,
initial_sort: current_user&.user_preference&.issues_sort,
is_signed_in: current_user.present?.to_s,
diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb
index f12c3767cf6..40e950b919e 100644
--- a/app/models/users/credit_card_validation.rb
+++ b/app/models/users/credit_card_validation.rb
@@ -5,6 +5,7 @@ module Users
include IgnorableColumns
RELEASE_DAY = Date.new(2021, 5, 17)
+ DAILY_VERIFICATION_LIMIT = 5
self.table_name = 'user_credit_card_validations'
@@ -82,5 +83,16 @@ module Users
def set_expiration_date_hash
self.expiration_date_hash = Gitlab::CryptoHelper.sha256(expiration_date.to_s)
end
+
+ def exceeded_daily_verification_limit?
+ return false unless Feature.enabled?(:credit_card_validation_daily_limit, user, type: :gitlab_com_derisk)
+
+ duplicate_record_count = self.class
+ .where(stripe_card_fingerprint: stripe_card_fingerprint)
+ .where('credit_card_validated_at > ?', 24.hours.ago)
+ .count
+
+ duplicate_record_count >= DAILY_VERIFICATION_LIMIT
+ end
end
end
diff --git a/app/services/users/upsert_credit_card_validation_service.rb b/app/services/users/upsert_credit_card_validation_service.rb
index 7e24bfcfee0..a49f8c10032 100644
--- a/app/services/users/upsert_credit_card_validation_service.rb
+++ b/app/services/users/upsert_credit_card_validation_service.rb
@@ -11,7 +11,7 @@ module Users
def execute
credit_card = Users::CreditCardValidation.find_or_initialize_by_user(user_id)
- credit_card_params = {
+ credit_card_attributes = {
credit_card_validated_at: credit_card_validated_at,
last_digits: last_digits,
holder_name: holder_name,
@@ -23,7 +23,11 @@ module Users
stripe_card_fingerprint: stripe_card_fingerprint
}
- credit_card.update!(credit_card_params)
+ credit_card.assign_attributes(credit_card_attributes)
+
+ return blocked if credit_card.exceeded_daily_verification_limit?
+
+ credit_card.save!
success
rescue ActiveRecord::InvalidForeignKey, ActiveRecord::NotNullViolation, ActiveRecord::RecordInvalid
@@ -85,5 +89,9 @@ module Users
def error
ServiceResponse.error(message: _('Error saving credit card validation record'))
end
+
+ def blocked
+ ServiceResponse.error(message: 'Credit card verification limit exceeded', reason: :rate_limited)
+ end
end
end
diff --git a/config/feature_flags/beta/allow_git_http_fetch_writes.yml b/config/feature_flags/beta/allow_git_http_fetch_writes.yml
new file mode 100644
index 00000000000..7fc7b492cd3
--- /dev/null
+++ b/config/feature_flags/beta/allow_git_http_fetch_writes.yml
@@ -0,0 +1,9 @@
+---
+name: allow_git_http_fetch_writes
+feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/426270
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/159835
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/473133
+milestone: '17.3'
+group: group::source code
+type: beta
+default_enabled: false
diff --git a/config/feature_flags/gitlab_com_derisk/credit_card_validation_daily_limit.yml b/config/feature_flags/gitlab_com_derisk/credit_card_validation_daily_limit.yml
new file mode 100644
index 00000000000..302f919c9a2
--- /dev/null
+++ b/config/feature_flags/gitlab_com_derisk/credit_card_validation_daily_limit.yml
@@ -0,0 +1,9 @@
+---
+name: credit_card_validation_daily_limit
+feature_issue_url: https://gitlab.com/gitlab-org/modelops/anti-abuse/team-tasks/-/issues/742
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/159151
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/472122
+milestone: '17.3'
+group: group::anti-abuse
+type: gitlab_com_derisk
+default_enabled: false
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 30301673f1b..00a6f5be0aa 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -1320,8 +1320,10 @@ module API
if service.success?
present user.credit_card_validation, with: Entities::UserCreditCardValidations
+ elsif service.reason == :rate_limited
+ render_api_error!(service.message, 400)
else
- render_api_error!('400 Bad Request', 400)
+ bad_request!
end
end
@@ -1342,13 +1344,13 @@ module API
attrs = declared_params(include_missing: false)
- render_api_error!('400 Bad Request', 400) unless attrs
+ bad_request! unless attrs
service = ::UserPreferences::UpdateService.new(current_user, attrs).execute
if service.success?
present preferences, with: Entities::UserPreferences
else
- render_api_error!('400 Bad Request', 400)
+ bad_request!
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 9ff526654f9..f092dfbe4b0 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8428,6 +8428,9 @@ msgstr ""
msgid "BillingPlan|Upgrade"
msgstr ""
+msgid "Billings|Credit card has exceeded the daily verification limit. Use a different card or try again later."
+msgstr ""
+
msgid "Billings|Error validating card details"
msgstr ""
diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb
index d3f639c639d..519a4772f96 100644
--- a/spec/controllers/repositories/git_http_controller_spec.rb
+++ b/spec/controllers/repositories/git_http_controller_spec.rb
@@ -26,6 +26,14 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m
end
end
+ shared_examples 'increments fetch statistics' do
+ it 'calls Projects::FetchStatisticsIncrementService service' do
+ expect(Projects::FetchStatisticsIncrementService).to receive(:new).with(project).and_call_original
+
+ send_request
+ end
+ end
+
context 'when repository container is a project' do
it_behaves_like described_class do
let(:container) { project }
@@ -111,10 +119,46 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m
stub_feature_flags(disable_git_http_fetch_writes: true)
end
- it 'does not increment statistics' do
- expect(Projects::FetchStatisticsIncrementService).not_to receive(:new)
+ context 'and allow_git_http_fetch_writes is disabled' do
+ before do
+ stub_feature_flags(allow_git_http_fetch_writes: false)
+ end
- send_request
+ it 'does not increment statistics' do
+ expect(Projects::FetchStatisticsIncrementService).not_to receive(:new)
+
+ send_request
+ end
+ end
+
+ context 'and allow_git_http_fetch_writes is enabled' do
+ before do
+ stub_feature_flags(allow_git_http_fetch_writes: true)
+ end
+
+ it_behaves_like 'increments fetch statistics'
+ end
+ end
+
+ context 'when disable_git_http_fetch_writes is disabled' do
+ before do
+ stub_feature_flags(disable_git_http_fetch_writes: false)
+ end
+
+ context 'and allow_git_http_fetch_writes is disabled' do
+ before do
+ stub_feature_flags(allow_git_http_fetch_writes: false)
+ end
+
+ it_behaves_like 'increments fetch statistics'
+ end
+
+ context 'and allow_git_http_fetch_writes is enabled' do
+ before do
+ stub_feature_flags(allow_git_http_fetch_writes: true)
+ end
+
+ it_behaves_like 'increments fetch statistics'
end
end
end
diff --git a/spec/features/projects/work_items/work_items_list_filters_spec.rb b/spec/features/projects/work_items/work_items_list_filters_spec.rb
index 64b409cc9e9..60a10aeb08e 100644
--- a/spec/features/projects/work_items/work_items_list_filters_spec.rb
+++ b/spec/features/projects/work_items/work_items_list_filters_spec.rb
@@ -9,7 +9,9 @@ RSpec.describe 'Work items list filters', :js, feature_category: :team_planning
let_it_be(:user2) { create(:user) }
let_it_be(:group) { create(:group) }
+ let_it_be(:sub_group) { create(:group, parent: group) }
let_it_be(:project) { create(:project, :public, group: group, developers: [user1, user2]) }
+ let_it_be(:sub_group_project) { create(:project, :public, group: sub_group, developers: [user1, user2]) }
let_it_be(:label1) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) }
@@ -18,24 +20,38 @@ RSpec.describe 'Work items list filters', :js, feature_category: :team_planning
let_it_be(:milestone2) { create(:milestone, group: group, start_date: 2.days.from_now, due_date: 9.days.from_now) }
let_it_be(:incident) do
- create(:incident, project: project, assignees: [user1], author: user1, labels: [label1], description: 'aaa')
+ create(:incident, project: project,
+ assignees: [user1],
+ author: user1,
+ description: 'aaa',
+ labels: [label1])
end
let_it_be(:issue) do
- create(:issue, project: project, author: user1, labels: [label1, label2], milestone: milestone1, title: 'eee')
+ create(:issue, project: project,
+ author: user1,
+ labels: [label1, label2],
+ milestone: milestone1,
+ title: 'eee')
end
let_it_be(:task) do
- create(:work_item, :task, project: project, assignees: [user2], author: user2, milestone: milestone2)
+ create(:work_item, :task, project: sub_group_project,
+ assignees: [user2],
+ author: user2,
+ confidential: true,
+ milestone: milestone2)
end
+ let_it_be(:award_emoji) { create(:award_emoji, :upvote, user: user1, awardable: issue) }
+
context 'for signed in user' do
before do
sign_in(user1)
visit group_work_items_path(group)
end
- describe 'assignees' do
+ describe 'assignee' do
it 'filters', :aggregate_failures do
select_tokens 'Assignee', '=', user1.username, submit: true
@@ -101,7 +117,33 @@ RSpec.describe 'Work items list filters', :js, feature_category: :team_planning
end
end
- describe 'labels' do
+ describe 'confidential' do
+ it 'filters', :aggregate_failures do
+ select_tokens 'Confidential', 'Yes', submit: true
+
+ expect(page).to have_css('.issue', count: 1)
+ expect(page).to have_link(task.title)
+
+ click_button 'Clear'
+
+ select_tokens 'Confidential', 'No', submit: true
+
+ expect(page).to have_css('.issue', count: 2)
+ expect(page).to have_link(incident.title)
+ expect(page).to have_link(issue.title)
+ end
+ end
+
+ describe 'group' do
+ it 'filters', :aggregate_failures do
+ select_tokens 'Group', sub_group.name, submit: true
+
+ expect(page).to have_css('.issue', count: 1)
+ expect(page).to have_link(task.title)
+ end
+ end
+
+ describe 'label' do
it 'filters', :aggregate_failures do
select_tokens 'Label', '=', label1.title, submit: true
@@ -141,7 +183,7 @@ RSpec.describe 'Work items list filters', :js, feature_category: :team_planning
end
end
- describe 'milestones' do
+ describe 'milestone' do
it 'filters', :aggregate_failures do
select_tokens 'Milestone', '=', milestone1.title, submit: true
@@ -187,6 +229,38 @@ RSpec.describe 'Work items list filters', :js, feature_category: :team_planning
end
end
+ describe 'my-reaction' do
+ it 'filters', :aggregate_failures do
+ select_tokens 'My-Reaction', '=', 'thumbsup', submit: true
+
+ expect(page).to have_css('.issue', count: 1)
+ expect(page).to have_link(issue.title)
+
+ click_button 'Clear'
+
+ select_tokens 'My-Reaction', '!=', 'thumbsup', submit: true
+
+ expect(page).to have_css('.issue', count: 2)
+ expect(page).to have_link(incident.title)
+ expect(page).to have_link(task.title)
+
+ click_button 'Clear'
+
+ select_tokens 'My-Reaction', '=', 'None', submit: true
+
+ expect(page).to have_css('.issue', count: 2)
+ expect(page).to have_link(incident.title)
+ expect(page).to have_link(task.title)
+
+ click_button 'Clear'
+
+ select_tokens 'My-Reaction', '=', 'Any', submit: true
+
+ expect(page).to have_css('.issue', count: 1)
+ expect(page).to have_link(issue.title)
+ end
+ end
+
describe 'search within' do
it 'filters', :aggregate_failures do
select_tokens 'Search Within', 'Titles'
diff --git a/spec/frontend/work_items/list/components/work_items_list_app_spec.js b/spec/frontend/work_items/list/components/work_items_list_app_spec.js
index 43c175ab104..c19f745f2d0 100644
--- a/spec/frontend/work_items/list/components/work_items_list_app_spec.js
+++ b/spec/frontend/work_items/list/components/work_items_list_app_spec.js
@@ -23,8 +23,11 @@ import {
OPERATOR_IS,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_CONFIDENTIAL,
+ TOKEN_TYPE_GROUP,
TOKEN_TYPE_LABEL,
TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
TOKEN_TYPE_SEARCH_WITHIN,
TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
@@ -60,6 +63,7 @@ describe('WorkItemsListApp component', () => {
[setSortPreferenceMutation, sortPreferenceMutationResponse],
]),
provide: {
+ autocompleteAwardEmojisPath: 'autocomplete/award/emojis/path',
fullPath: 'full/path',
initialSort: CREATED_DESC,
isGroup: true,
@@ -120,6 +124,7 @@ describe('WorkItemsListApp component', () => {
it('calls query to fetch work items', () => {
expect(defaultQueryHandler).toHaveBeenCalledWith({
fullPath: 'full/path',
+ includeDescendants: true,
sort: CREATED_DESC,
state: STATUS_OPEN,
firstPageSize: 20,
@@ -154,6 +159,7 @@ describe('WorkItemsListApp component', () => {
expect(defaultQueryHandler).toHaveBeenCalledWith({
fullPath: 'full/path',
+ includeDescendants: true,
sort: CREATED_DESC,
state: STATUS_OPEN,
firstPageSize: 20,
@@ -225,8 +231,11 @@ describe('WorkItemsListApp component', () => {
expect(findIssuableList().props('searchTokens')).toMatchObject([
{ type: TOKEN_TYPE_ASSIGNEE, preloadedUsers },
{ type: TOKEN_TYPE_AUTHOR, preloadedUsers },
+ { type: TOKEN_TYPE_CONFIDENTIAL },
+ { type: TOKEN_TYPE_GROUP },
{ type: TOKEN_TYPE_LABEL },
{ type: TOKEN_TYPE_MILESTONE },
+ { type: TOKEN_TYPE_MY_REACTION },
{ type: TOKEN_TYPE_SEARCH_WITHIN },
{ type: TOKEN_TYPE_TYPE },
]);
@@ -240,8 +249,11 @@ describe('WorkItemsListApp component', () => {
expect(findIssuableList().props('searchTokens')).toMatchObject([
{ type: TOKEN_TYPE_ASSIGNEE, preloadedUsers },
{ type: TOKEN_TYPE_AUTHOR, preloadedUsers },
+ { type: TOKEN_TYPE_CONFIDENTIAL },
+ { type: TOKEN_TYPE_GROUP },
{ type: TOKEN_TYPE_LABEL },
{ type: TOKEN_TYPE_MILESTONE },
+ { type: TOKEN_TYPE_MY_REACTION },
{ type: TOKEN_TYPE_SEARCH_WITHIN },
]);
});
@@ -276,6 +288,7 @@ describe('WorkItemsListApp component', () => {
expect(defaultQueryHandler).toHaveBeenCalledWith({
fullPath: 'full/path',
+ includeDescendants: true,
sort: CREATED_DESC,
state: STATUS_OPEN,
search: 'find issues',
diff --git a/spec/helpers/work_items_helper_spec.rb b/spec/helpers/work_items_helper_spec.rb
index e41eeb7a112..f88c2d17014 100644
--- a/spec/helpers/work_items_helper_spec.rb
+++ b/spec/helpers/work_items_helper_spec.rb
@@ -102,6 +102,7 @@ RSpec.describe WorkItemsHelper, feature_category: :team_planning do
it 'returns expected data' do
expect(work_items_list_data).to include(
{
+ autocomplete_award_emojis_path: autocomplete_award_emojis_path,
full_path: group.full_path,
initial_sort: current_user&.user_preference&.issues_sort,
is_signed_in: current_user.present?.to_s,
diff --git a/spec/models/users/credit_card_validation_spec.rb b/spec/models/users/credit_card_validation_spec.rb
index 9e86891acfc..0bf2fd6f48d 100644
--- a/spec/models/users/credit_card_validation_spec.rb
+++ b/spec/models/users/credit_card_validation_spec.rb
@@ -317,5 +317,45 @@ RSpec.describe Users::CreditCardValidation, feature_category: :user_profile do
end
end
end
+
+ describe '#exceeded_daily_verification_limit?' do
+ let(:credit_card_validation) { build(:credit_card_validation) }
+
+ subject(:exceeded_limit?) { credit_card_validation.exceeded_daily_verification_limit? }
+
+ before do
+ stub_const("#{described_class}::DAILY_VERIFICATION_LIMIT", 1)
+ end
+
+ it { is_expected.to eq(false) }
+
+ context 'when the limit has been exceeded' do
+ before do
+ create(:credit_card_validation, stripe_card_fingerprint: credit_card_validation.stripe_card_fingerprint)
+ end
+
+ it { is_expected.to eq(true) }
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(credit_card_validation_daily_limit: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ context 'when the limit is exceeded but records have credit_card_validated_at > 24 hours' do
+ before do
+ create(
+ :credit_card_validation,
+ stripe_card_fingerprint: credit_card_validation.stripe_card_fingerprint,
+ credit_card_validated_at: 25.hours.ago
+ )
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
end
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index df5800eca68..3a5fbe0a548 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -2205,6 +2205,35 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_manageme
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
end
+
+ context 'when the credit card daily verification limit has been exceeded' do
+ before do
+ stub_const("Users::CreditCardValidation::DAILY_VERIFICATION_LIMIT", 1)
+ create(:credit_card_validation, stripe_card_fingerprint: stripe_card_fingerprint)
+ end
+
+ it "returns a 400 error with the reason" do
+ put api(path, admin, admin_mode: true), params: params
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq('Credit card verification limit exceeded')
+ end
+ end
+
+ context 'when UpsertCreditCardValidationService returns an unexpected error' do
+ before do
+ allow_next_instance_of(::Users::UpsertCreditCardValidationService) do |instance|
+ allow(instance).to receive(:execute).and_return(ServiceResponse.error(message: 'upsert failed'))
+ end
+ end
+
+ it "returns a generic 400 error" do
+ put api(path, admin, admin_mode: true), params: params
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq('400 Bad request')
+ end
+ end
end
end
diff --git a/spec/services/users/upsert_credit_card_validation_service_spec.rb b/spec/services/users/upsert_credit_card_validation_service_spec.rb
index 02e8cad4011..60459396c7e 100644
--- a/spec/services/users/upsert_credit_card_validation_service_spec.rb
+++ b/spec/services/users/upsert_credit_card_validation_service_spec.rb
@@ -198,5 +198,21 @@ RSpec.describe Users::UpsertCreditCardValidationService, feature_category: :user
expect(service_result.message).to eq(_('Error saving credit card validation record'))
end
end
+
+ context 'when the credit card verification limit has been reached' do
+ before do
+ allow_next_instance_of(Users::CreditCardValidation) do |instance|
+ allow(instance).to receive(:exceeded_daily_verification_limit?).and_return(true)
+ end
+ end
+
+ it 'returns an error', :aggregate_failures do
+ service_result = service.execute
+
+ expect(service_result).to be_error
+ expect(service_result.message).to eq('Credit card verification limit exceeded')
+ expect(service_result.reason).to eq(:rate_limited)
+ end
+ end
end
end