Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue149
-rw-r--r--app/assets/javascripts/issues/show/components/task_list_item_actions.vue4
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue9
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js12
-rw-r--r--app/graphql/mutations/achievements/award.rb38
-rw-r--r--app/graphql/resolvers/achievements/achievements_resolver.rb27
-rw-r--r--app/graphql/resolvers/achievements/user_achievements_resolver.rb33
-rw-r--r--app/graphql/types/achievements/achievement_type.rb6
-rw-r--r--app/graphql/types/achievements/user_achievement_type.rb51
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/graphql/types/namespace_type.rb8
-rw-r--r--app/graphql/types/user_interface.rb9
-rw-r--r--app/helpers/application_settings_helper.rb3
-rw-r--r--app/helpers/plan_limits_helper.rb28
-rw-r--r--app/models/achievements/user_achievement.rb2
-rw-r--r--app/models/application_setting.rb14
-rw-r--r--app/models/application_setting_implementation.rb3
-rw-r--r--app/policies/achievements/user_achievement_policy.rb7
-rw-r--r--app/policies/group_policy.rb14
-rw-r--r--app/services/achievements/award_service.rb48
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml18
-rw-r--r--app/views/admin/application_settings/_projects_api_limits.html.haml21
-rw-r--r--app/views/admin/application_settings/network.html.haml3
-rw-r--r--app/views/help/instance_configuration/_ci_cd_limits.html.haml16
-rw-r--r--app/views/projects/branch_rules/_show.html.haml5
-rw-r--r--config/feature_flags/development/rate_limit_for_unauthenticated_projects_api_access.yml8
-rw-r--r--db/fixtures/development/36_achievements.rb48
-rw-r--r--db/migrate/20230217065736_add_projects_api_rate_limit_unauthenticated_to_application_settings.rb7
-rw-r--r--db/schema_migrations/202302170657361
-rw-r--r--db/structure.sql1
-rw-r--r--doc/administration/geo/replication/configuration.md13
-rw-r--r--doc/administration/geo/replication/img/adding_a_secondary_v15_8.pngbin0 -> 40126 bytes
-rw-r--r--doc/administration/job_artifacts.md2
-rw-r--r--doc/api/graphql/reference/index.md75
-rw-r--r--doc/api/group_level_variables.md2
-rw-r--r--doc/api/instance_level_ci_variables.md2
-rw-r--r--doc/api/job_artifacts.md2
-rw-r--r--doc/api/project_level_variables.md2
-rw-r--r--doc/api/secure_files.md2
-rw-r--r--doc/api/settings.md4
-rw-r--r--doc/api/visual_review_discussions.md2
-rw-r--r--doc/architecture/blueprints/ci_data_decay/pipeline_partitioning.md14
-rw-r--r--doc/ci/cloud_services/aws/index.md2
-rw-r--r--doc/ci/cloud_services/azure/index.md2
-rw-r--r--doc/ci/cloud_services/google_cloud/index.md2
-rw-r--r--doc/ci/cloud_services/index.md2
-rw-r--r--doc/ci/examples/authenticating-with-hashicorp-vault/index.md2
-rw-r--r--doc/ci/examples/end_to_end_testing_webdriverio/index.md2
-rw-r--r--doc/ci/introduction/index.md2
-rw-r--r--doc/ci/jobs/ci_job_token.md2
-rw-r--r--doc/ci/jobs/index.md2
-rw-r--r--doc/ci/pipelines/job_artifacts.md2
-rw-r--r--doc/ci/pipelines/pipeline_architectures.md2
-rw-r--r--doc/ci/pipelines/pipeline_artifacts.md2
-rw-r--r--doc/ci/review_apps/index.md2
-rw-r--r--doc/ci/secrets/id_token_authentication.md2
-rw-r--r--doc/ci/secrets/index.md2
-rw-r--r--doc/ci/secure_files/index.md2
-rw-r--r--doc/ci/ssh_keys/index.md2
-rw-r--r--doc/ci/testing/accessibility_testing.md2
-rw-r--r--doc/ci/testing/browser_performance_testing.md2
-rw-r--r--doc/ci/testing/fail_fast_testing.md2
-rw-r--r--doc/ci/testing/index.md2
-rw-r--r--doc/ci/testing/load_performance_testing.md2
-rw-r--r--doc/ci/testing/metrics_reports.md2
-rw-r--r--doc/ci/testing/test_coverage_visualization.md2
-rw-r--r--doc/ci/testing/unit_test_report_examples.md2
-rw-r--r--doc/ci/testing/unit_test_reports.md2
-rw-r--r--doc/ci/triggers/index.md2
-rw-r--r--doc/ci/troubleshooting.md2
-rw-r--r--doc/ci/variables/index.md2
-rw-r--r--doc/ci/variables/predefined_variables.md2
-rw-r--r--doc/ci/variables/where_variables_can_be_used.md2
-rw-r--r--doc/ci/yaml/artifacts_reports.md4
-rw-r--r--doc/user/admin_area/appearance.md21
-rw-r--r--doc/user/admin_area/settings/rate_limit_on_projects_api.md33
-rw-r--r--doc/user/application_security/vulnerabilities/index.md6
-rw-r--r--doc/user/group/repositories_analytics/index.md2
-rw-r--r--lib/api/projects.rb8
-rw-r--r--lib/gitlab/application_rate_limiter.rb5
-rw-r--r--locale/gitlab.pot12
-rw-r--r--spec/factories/achievements/user_achievements.rb14
-rw-r--r--spec/features/admin/admin_settings_spec.rb12
-rw-r--r--spec/frontend/issues/show/components/description_spec.js146
-rw-r--r--spec/frontend/issues/show/components/task_list_item_actions_spec.js2
-rw-r--r--spec/frontend/issues/show/mock_data/mock_data.js30
-rw-r--r--spec/graphql/mutations/achievements/award_spec.rb53
-rw-r--r--spec/graphql/resolvers/achievements/achievements_resolver_spec.rb34
-rw-r--r--spec/graphql/types/achievements/achievement_type_spec.rb1
-rw-r--r--spec/graphql/types/achievements/user_achievement_type_spec.rb24
-rw-r--r--spec/graphql/types/user_type_spec.rb1
-rw-r--r--spec/helpers/plan_limits_helper_spec.rb29
-rw-r--r--spec/models/achievements/user_achievement_spec.rb2
-rw-r--r--spec/models/application_setting_spec.rb3
-rw-r--r--spec/policies/group_policy_spec.rb18
-rw-r--r--spec/requests/api/graphql/achievements/user_achievements_query_spec.rb83
-rw-r--r--spec/requests/api/graphql/mutations/achievements/award_spec.rb106
-rw-r--r--spec/requests/api/graphql/user/user_achievements_query_spec.rb91
-rw-r--r--spec/requests/api/projects_spec.rb60
-rw-r--r--spec/requests/api/settings_spec.rb5
-rw-r--r--spec/services/achievements/award_service_spec.rb73
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb1
-rw-r--r--spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb1
-rw-r--r--spec/views/admin/application_settings/network.html.haml_spec.rb33
104 files changed, 1277 insertions, 412 deletions
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index 5682f85bc97..b36a456f41b 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -1,35 +1,26 @@
<script>
-import { GlModalDirective, GlToast } from '@gitlab/ui';
+import { GlToast } from '@gitlab/ui';
import $ from 'jquery';
-import { uniqueId } from 'lodash';
import Sortable from 'sortablejs';
import Vue from 'vue';
import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import SafeHtml from '~/vue_shared/directives/safe_html';
-import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_ISSUE, TYPENAME_WORK_ITEM } from '~/graphql_shared/constants';
import { createAlert } from '~/flash';
import { TYPE_ISSUE } from '~/issues/constants';
-import { isMetaKey } from '~/lib/utils/common_utils';
-import { isPositiveInteger } from '~/lib/utils/number_utils';
-import { getParameterByName, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
import { __, s__, sprintf } from '~/locale';
import { getSortableDefaultOptions, isDragging } from '~/sortable/utils';
import TaskList from '~/task_list';
-import Tracking from '~/tracking';
import addHierarchyChildMutation from '~/work_items/graphql/add_hierarchy_child.mutation.graphql';
import removeHierarchyChildMutation from '~/work_items/graphql/remove_hierarchy_child.mutation.graphql';
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
-import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import {
sprintfWorkItem,
I18N_WORK_ITEM_ERROR_CREATING,
I18N_WORK_ITEM_ERROR_DELETING,
- TRACKING_CATEGORY_SHOW,
TASK_TYPE_NAME,
} from '~/work_items/constants';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
@@ -51,12 +42,8 @@ const workItemTypes = {
export default {
directives: {
SafeHtml,
- GlModal: GlModalDirective,
},
- components: {
- WorkItemDetailModal,
- },
- mixins: [animateMixin, glFeatureFlagMixin(), Tracking.mixin()],
+ mixins: [animateMixin],
inject: ['fullPath', 'hasIterationsFeature'],
props: {
canUpdate: {
@@ -104,18 +91,12 @@ export default {
},
},
data() {
- const workItemId = getParameterByName('work_item_id');
-
return {
hasTaskListItemActions: false,
preAnimation: false,
pulseAnimation: false,
initialUpdate: true,
issueDetails: {},
- activeTask: {},
- workItemId: isPositiveInteger(workItemId)
- ? convertToGraphQLId(TYPENAME_WORK_ITEM, workItemId)
- : undefined,
workItemTypes: [],
};
},
@@ -129,18 +110,7 @@ export default {
},
update: (data) => data.issue,
skip() {
- return !this.issueId;
- },
- },
- workItem: {
- query: workItemQuery,
- variables() {
- return {
- id: this.workItemId,
- };
- },
- skip() {
- return !this.workItemId;
+ return !this.canUpdate || !this.issueId;
},
},
workItemTypes: {
@@ -153,10 +123,13 @@ export default {
update(data) {
return data.workspace?.workItemTypes?.nodes;
},
+ skip() {
+ return !this.canUpdate;
+ },
},
},
computed: {
- taskWorkItemType() {
+ taskWorkItemTypeId() {
return this.workItemTypes.find((type) => type.name === TASK_TYPE_NAME)?.id;
},
issueGid() {
@@ -185,12 +158,6 @@ export default {
this.renderGFM();
this.updateTaskStatusText();
- if (this.workItemId) {
- const taskLink = this.$el.querySelector(
- `.gfm-issue[data-issue="${getIdFromGraphQLId(this.workItemId)}"]`,
- );
- this.openWorkItemDetailModal(taskLink);
- }
},
beforeDestroy() {
eventHub.$off('convert-task-list-item', this.convertTaskListItem);
@@ -223,10 +190,10 @@ export default {
},
renderSortableLists() {
// We exclude GLFM table of contents which have a `section-nav` class on the root `ul`.
- const lists = document.querySelectorAll(
+ const lists = this.$el.querySelectorAll?.(
'.description .md > ul:not(.section-nav), .description .md > ul:not(.section-nav) ul, .description ol',
);
- lists.forEach((list) => {
+ lists?.forEach((list) => {
if (list.children.length <= 1) {
return;
}
@@ -355,59 +322,15 @@ export default {
this.$emit('saveDescription', newDescription);
},
renderTaskListItemActions() {
- if (!this.$el?.querySelectorAll) {
- return;
- }
-
- const taskListFields = this.$el.querySelectorAll('.task-list-item:not(.inapplicable)');
-
- taskListFields.forEach((item) => {
- const taskLink = item.querySelector('.gfm-issue');
- if (taskLink) {
- const { issue, referenceType, issueType } = taskLink.dataset;
- if (issueType !== workItemTypes.TASK) {
- return;
- }
- const workItemId = convertToGraphQLId(TYPENAME_WORK_ITEM, issue);
- this.addHoverListeners(taskLink, workItemId);
- taskLink.classList.add('gl-link');
- taskLink.addEventListener('click', (e) => {
- if (isMetaKey(e)) {
- return;
- }
- e.preventDefault();
- this.openWorkItemDetailModal(taskLink);
- this.workItemId = workItemId;
- this.updateWorkItemIdUrlQuery(issue);
- this.track('viewed_work_item_from_modal', {
- category: TRACKING_CATEGORY_SHOW,
- label: 'work_item_view',
- property: `type_${referenceType}`,
- });
- });
- return;
- }
+ const taskListItems = this.$el.querySelectorAll?.('.task-list-item:not(.inapplicable)');
- const toggleClass = uniqueId('task-list-item-actions-');
- const dropdown = this.createTaskListItemActions({ canUpdate: this.canUpdate, toggleClass });
- this.addPointerEventListeners(item, `.${toggleClass}`);
+ taskListItems?.forEach((item) => {
+ const dropdown = this.createTaskListItemActions({ canUpdate: this.canUpdate });
this.insertNextToTaskListItemText(dropdown, item);
+ this.addPointerEventListeners(item, '.task-list-item-actions');
this.hasTaskListItemActions = true;
});
},
- addHoverListeners(taskLink, id) {
- let workItemPrefetch;
- taskLink.addEventListener('mouseover', () => {
- workItemPrefetch = setTimeout(() => {
- this.workItemId = id;
- }, 150);
- });
- taskLink.addEventListener('mouseout', () => {
- if (workItemPrefetch) {
- clearTimeout(workItemPrefetch);
- }
- });
- },
insertNextToTaskListItemText(element, listItem) {
const children = Array.from(listItem.children);
const paragraph = children.find((el) => el.tagName === 'P');
@@ -424,27 +347,6 @@ export default {
listItem.append(element);
}
},
- setActiveTask(el) {
- const { parentElement } = el;
- const lineNumbers = parentElement.dataset.sourcepos.match(/\b\d+(?=:)/g);
- this.activeTask = {
- title: parentElement.innerText,
- lineNumberStart: lineNumbers[0],
- lineNumberEnd: lineNumbers[1],
- };
- },
- openWorkItemDetailModal(el) {
- if (!el) {
- return;
- }
-
- this.setActiveTask(el);
- this.$refs.detailsModal.show();
- },
- closeWorkItemDetailModal() {
- this.workItemId = undefined;
- this.updateWorkItemIdUrlQuery(undefined);
- },
async createTask({ taskTitle, taskDescription, oldDescription }) {
try {
const { title, description } = extractTaskTitleAndDescription(taskTitle, taskDescription);
@@ -465,7 +367,7 @@ export default {
},
projectPath: this.fullPath,
title,
- workItemTypeId: this.taskWorkItemType,
+ workItemTypeId: this.taskWorkItemTypeId,
};
const { data } = await this.$apollo.mutate({
@@ -529,16 +431,6 @@ export default {
captureError: true,
});
},
- handleDeleteTask(description) {
- this.$emit('updateDescription', description);
- this.$toast.show(s__('WorkItem|Task deleted'));
- },
- updateWorkItemIdUrlQuery(workItemId) {
- updateHistory({
- url: setUrlParams({ work_item_id: workItemId }),
- replace: true,
- });
- },
},
safeHtmlConfig: { ADD_TAGS: ['gl-emoji', 'copy-code'] },
};
@@ -566,16 +458,5 @@ export default {
data-testid="textarea"
>
</textarea>
- <work-item-detail-modal
- ref="detailsModal"
- :can-update="canUpdate"
- :work-item-id="workItemId"
- :issue-gid="issueGid"
- :lock-version="lockVersion"
- :line-number-start="activeTask.lineNumberStart"
- :line-number-end="activeTask.lineNumberEnd"
- @workItemDeleted="handleDeleteTask"
- @close="closeWorkItemDetailModal"
- />
</div>
</template>
diff --git a/app/assets/javascripts/issues/show/components/task_list_item_actions.vue b/app/assets/javascripts/issues/show/components/task_list_item_actions.vue
index d0beb0f39b3..03d298e0ddf 100644
--- a/app/assets/javascripts/issues/show/components/task_list_item_actions.vue
+++ b/app/assets/javascripts/issues/show/components/task_list_item_actions.vue
@@ -13,7 +13,7 @@ export default {
GlDropdown,
GlDropdownItem,
},
- inject: ['canUpdate', 'toggleClass'],
+ inject: ['canUpdate'],
methods: {
convertToTask() {
eventHub.$emit('convert-task-list-item', this.$el.closest('li').dataset.sourcepos);
@@ -35,7 +35,7 @@ export default {
right
:text="$options.i18n.taskActions"
text-sr-only
- :toggle-class="`task-list-item-actions gl-opacity-0 gl-p-2! ${toggleClass}`"
+ toggle-class="task-list-item-actions gl-opacity-0 gl-p-2!"
>
<gl-dropdown-item v-if="canUpdate" @click="convertToTask">
{{ $options.i18n.convertToTask }}
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
index 35e65698cd2..b565bda247d 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
@@ -27,6 +27,9 @@ export default {
branchRulesPath: {
default: '',
},
+ showCodeOwners: { default: false },
+ showStatusChecks: { default: false },
+ showApprovers: { default: false },
},
props: {
name: {
@@ -112,13 +115,13 @@ export default {
if (this.branchProtection?.allowForcePush) {
approvalDetails.push(this.$options.i18n.allowForcePush);
}
- if (this.branchProtection?.codeOwnerApprovalRequired) {
+ if (this.showCodeOwners && this.branchProtection?.codeOwnerApprovalRequired) {
approvalDetails.push(this.$options.i18n.codeOwnerApprovalRequired);
}
- if (this.statusChecksTotal) {
+ if (this.showStatusChecks && this.statusChecksTotal) {
approvalDetails.push(this.statusChecksText);
}
- if (this.approvalRulesTotal) {
+ if (this.showApprovers && this.approvalRulesTotal) {
approvalDetails.push(this.approvalRulesText);
}
if (this.mergeAccessLevels.total > 0) {
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js b/app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js
index 042be089e09..a8736c87e22 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import BranchRulesApp from '~/projects/settings/repository/branch_rules/app.vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
Vue.use(VueApollo);
@@ -12,7 +13,13 @@ const apolloProvider = new VueApollo({
export default function mountBranchRules(el) {
if (!el) return null;
- const { projectPath, branchRulesPath } = el.dataset;
+ const {
+ projectPath,
+ branchRulesPath,
+ showCodeOwners,
+ showStatusChecks,
+ showApprovers,
+ } = el.dataset;
return new Vue({
el,
@@ -20,6 +27,9 @@ export default function mountBranchRules(el) {
provide: {
projectPath,
branchRulesPath,
+ showCodeOwners: parseBoolean(showCodeOwners),
+ showStatusChecks: parseBoolean(showStatusChecks),
+ showApprovers: parseBoolean(showApprovers),
},
render(createElement) {
return createElement(BranchRulesApp);
diff --git a/app/graphql/mutations/achievements/award.rb b/app/graphql/mutations/achievements/award.rb
new file mode 100644
index 00000000000..b486049594d
--- /dev/null
+++ b/app/graphql/mutations/achievements/award.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Achievements
+ class Award < BaseMutation
+ graphql_name 'AchievementsAward'
+
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ field :user_achievement,
+ ::Types::Achievements::UserAchievementType,
+ null: true,
+ description: 'Achievement award.'
+
+ argument :achievement_id, ::Types::GlobalIDType[::Achievements::Achievement],
+ required: true,
+ description: 'Global ID of the achievement being awarded.'
+
+ argument :user_id, ::Types::GlobalIDType[::User],
+ required: true,
+ description: 'Global ID of the user being awarded the achievement.'
+
+ authorize :award_achievement
+
+ def resolve(args)
+ achievement = authorized_find!(id: args[:achievement_id])
+
+ recipient_id = args[:user_id].model_id
+ result = ::Achievements::AwardService.new(current_user, achievement.id, recipient_id).execute
+ { user_achievement: result.payload, errors: result.errors }
+ end
+
+ def find_object(id:)
+ GitlabSchema.object_from_id(id, expected_type: ::Achievements::Achievement)
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/achievements/achievements_resolver.rb b/app/graphql/resolvers/achievements/achievements_resolver.rb
new file mode 100644
index 00000000000..1d71fa1d9c1
--- /dev/null
+++ b/app/graphql/resolvers/achievements/achievements_resolver.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Achievements
+ class AchievementsResolver < BaseResolver
+ include LooksAhead
+
+ type ::Types::Achievements::AchievementType.connection_type, null: true
+
+ alias_method :namespace, :object
+
+ def resolve_with_lookahead
+ return ::Achievements::Achievement.none if Feature.disabled?(:achievements, namespace)
+
+ apply_lookahead(namespace.achievements)
+ end
+
+ private
+
+ def preloads
+ {
+ user_achievements: [{ user_achievements: [:user, :awarded_by_user, :revoked_by_user] }]
+ }
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/achievements/user_achievements_resolver.rb b/app/graphql/resolvers/achievements/user_achievements_resolver.rb
new file mode 100644
index 00000000000..594fb3419d6
--- /dev/null
+++ b/app/graphql/resolvers/achievements/user_achievements_resolver.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Achievements
+ class UserAchievementsResolver < BaseResolver
+ include LooksAhead
+
+ type ::Types::Achievements::UserAchievementType.connection_type, null: true
+
+ def resolve_with_lookahead
+ user_achievements = object.user_achievements
+
+ apply_lookahead(user_achievements)
+ end
+
+ private
+
+ def unconditional_includes
+ [
+ { achievement: [:namespace] }
+ ]
+ end
+
+ def preloads
+ {
+ user: [:user],
+ awarded_by_user: [:awarded_by_user],
+ revoked_by_user: [:revoked_by_user]
+ }
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/achievements/achievement_type.rb b/app/graphql/types/achievements/achievement_type.rb
index 67cc9778797..71f51b9b741 100644
--- a/app/graphql/types/achievements/achievement_type.rb
+++ b/app/graphql/types/achievements/achievement_type.rb
@@ -42,6 +42,12 @@ module Types
null: false,
description: 'Timestamp the achievement was last updated.'
+ field :user_achievements,
+ Types::Achievements::UserAchievementType.connection_type,
+ null: true,
+ alpha: { milestone: '15.10' },
+ description: "Recipients for the achievement."
+
def avatar_url
object.avatar_url(only_path: false)
end
diff --git a/app/graphql/types/achievements/user_achievement_type.rb b/app/graphql/types/achievements/user_achievement_type.rb
new file mode 100644
index 00000000000..d2146807445
--- /dev/null
+++ b/app/graphql/types/achievements/user_achievement_type.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Types
+ module Achievements
+ class UserAchievementType < BaseObject
+ graphql_name 'UserAchievement'
+
+ authorize :read_achievement
+
+ field :id,
+ ::Types::GlobalIDType[::Achievements::UserAchievement],
+ null: false,
+ description: 'ID of the user achievement.'
+
+ field :achievement,
+ ::Types::Achievements::AchievementType,
+ null: false,
+ description: 'Achievement awarded.'
+
+ field :user,
+ ::Types::UserType,
+ null: false,
+ description: 'Achievement recipient.'
+
+ field :awarded_by_user,
+ ::Types::UserType,
+ null: false,
+ description: 'Awarded by.'
+
+ field :revoked_by_user,
+ ::Types::UserType,
+ null: true,
+ description: 'Revoked by.'
+
+ field :created_at,
+ Types::TimeType,
+ null: false,
+ description: 'Timestamp the achievement was created.'
+
+ field :updated_at,
+ Types::TimeType,
+ null: false,
+ description: 'Timestamp the achievement was last updated.'
+
+ field :revoked_at,
+ Types::TimeType,
+ null: true,
+ description: 'Timestamp the achievement was revoked.'
+ end
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 4b15b1f2ad8..f72ad183fb0 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -6,6 +6,7 @@ module Types
include Gitlab::Graphql::MountMutation
+ mount_mutation Mutations::Achievements::Award, alpha: { milestone: '15.10' }
mount_mutation Mutations::Achievements::Create
mount_mutation Mutations::Admin::SidekiqQueues::DeleteJobs
mount_mutation Mutations::AlertManagement::CreateAlertIssue
diff --git a/app/graphql/types/namespace_type.rb b/app/graphql/types/namespace_type.rb
index fc55ff512b6..3420f16213f 100644
--- a/app/graphql/types/namespace_type.rb
+++ b/app/graphql/types/namespace_type.rb
@@ -68,7 +68,9 @@ module Types
null: true,
alpha: { milestone: '15.8' },
description: "Achievements for the namespace. " \
- "Returns `null` if the `achievements` feature flag is disabled."
+ "Returns `null` if the `achievements` feature flag is disabled.",
+ extras: [:lookahead],
+ resolver: ::Resolvers::Achievements::AchievementsResolver
markdown_field :description_html, null: true
@@ -83,10 +85,6 @@ module Types
def root_storage_statistics
Gitlab::Graphql::Loaders::BatchRootStorageStatisticsLoader.new(object.id).find
end
-
- def achievements
- object.achievements if Feature.enabled?(:achievements, object)
- end
end
end
diff --git a/app/graphql/types/user_interface.rb b/app/graphql/types/user_interface.rb
index 9115b5a4760..83d2f3f830a 100644
--- a/app/graphql/types/user_interface.rb
+++ b/app/graphql/types/user_interface.rb
@@ -153,6 +153,15 @@ module Types
field :profile_enable_gitpod_path, GraphQL::Types::String, null: true,
description: 'Web path to enable Gitpod for the user.'
+ field :user_achievements,
+ Types::Achievements::UserAchievementType.connection_type,
+ null: true,
+ alpha: { milestone: '15.10' },
+ description: "Achievements for the user. " \
+ "Only returns for namespaces where the `achievements` feature flag is enabled.",
+ extras: [:lookahead],
+ resolver: ::Resolvers::Achievements::UserAchievementsResolver
+
definition_methods do
def resolve_type(object, context)
# in the absense of other information, we cannot tell - just default to
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 59ba5fa65d3..adb99a523c2 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -479,7 +479,8 @@ module ApplicationSettingsHelper
:bulk_import_enabled,
:allow_runner_registration_token,
:user_defaults_to_private_profile,
- :deactivation_email_additional_text
+ :deactivation_email_additional_text,
+ :projects_api_rate_limit_unauthenticated
].tap do |settings|
next if Gitlab.com?
diff --git a/app/helpers/plan_limits_helper.rb b/app/helpers/plan_limits_helper.rb
new file mode 100644
index 00000000000..71869b3ba30
--- /dev/null
+++ b/app/helpers/plan_limits_helper.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module PlanLimitsHelper
+ def plan_limit_setting_description(limit_name)
+ case limit_name
+ when :ci_pipeline_size
+ s_('AdminSettings|Maximum number of jobs in a single pipeline')
+ when :ci_active_jobs
+ s_('AdminSettings|Total number of jobs in currently active pipelines')
+ when :ci_active_pipelines
+ s_('AdminSettings|Maximum number of active pipelines per project')
+ when :ci_project_subscriptions
+ s_('AdminSettings|Maximum number of pipeline subscriptions to and from a project')
+ when :ci_pipeline_schedules
+ s_('AdminSettings|Maximum number of pipeline schedules')
+ when :ci_needs_size_limit
+ s_('AdminSettings|Maximum number of DAG dependencies that a job can have')
+ when :ci_registered_group_runners
+ s_('AdminSettings|Maximum number of runners registered per group')
+ when :ci_registered_project_runners
+ s_('AdminSettings|Maximum number of runners registered per project')
+ when :pipeline_hierarchy_size
+ s_("AdminSettings|Maximum number of downstream pipelines in a pipeline's hierarchy tree")
+ else
+ raise ArgumentError, "No description available for plan limit #{limit_name}"
+ end
+ end
+end
diff --git a/app/models/achievements/user_achievement.rb b/app/models/achievements/user_achievement.rb
index 885ec660cc9..293dad7aa24 100644
--- a/app/models/achievements/user_achievement.rb
+++ b/app/models/achievements/user_achievement.rb
@@ -8,7 +8,7 @@ module Achievements
belongs_to :awarded_by_user,
class_name: 'User',
inverse_of: :awarded_user_achievements,
- optional: true
+ optional: false
belongs_to :revoked_by_user,
class_name: 'User',
inverse_of: :revoked_user_achievements,
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 5319f4aee77..c80ff359a69 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -563,14 +563,12 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
validates :throttle_protected_paths_period_in_seconds
end
- validates :notes_create_limit,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
-
- validates :search_rate_limit,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
-
- validates :search_rate_limit_unauthenticated,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ with_options(numericality: { only_integer: true, greater_than_or_equal_to: 0 }) do
+ validates :notes_create_limit
+ validates :search_rate_limit
+ validates :search_rate_limit_unauthenticated
+ validates :projects_api_rate_limit_unauthenticated
+ end
validates :notes_create_limit_allowlist,
length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') },
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index a5f262f2e1e..4e2ec0d4ecd 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -249,7 +249,8 @@ module ApplicationSettingImplementation
can_create_group: true,
bulk_import_enabled: false,
allow_runner_registration_token: true,
- user_defaults_to_private_profile: false
+ user_defaults_to_private_profile: false,
+ projects_api_rate_limit_unauthenticated: 400
}.tap do |hsh|
hsh.merge!(non_production_defaults) unless Rails.env.production?
end
diff --git a/app/policies/achievements/user_achievement_policy.rb b/app/policies/achievements/user_achievement_policy.rb
new file mode 100644
index 00000000000..b500d0a25c8
--- /dev/null
+++ b/app/policies/achievements/user_achievement_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Achievements
+ class UserAchievementPolicy < ::BasePolicy
+ delegate { @subject.achievement.namespace }
+ end
+end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index b560a5d914a..ae736865e17 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -88,6 +88,10 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
Feature.enabled?(:create_runner_workflow)
end
+ condition(:achievements_enabled, scope: :subject) do
+ Feature.enabled?(:achievements, @subject)
+ end
+
condition(:group_runner_registration_allowed, scope: :subject) do
Gitlab::CurrentSettings.valid_runner_registrars.include?('group') && @subject.runner_registration_enabled?
end
@@ -131,9 +135,17 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :read_group_member
enable :read_custom_emoji
enable :read_counts
+ end
+
+ rule { can?(:read_group) & achievements_enabled }.policy do
enable :read_achievement
end
+ rule { can?(:maintainer_access) & achievements_enabled }.policy do
+ enable :admin_achievement
+ enable :award_achievement
+ end
+
rule { ~public_group & ~has_access }.prevent :read_counts
rule { ~can_read_group_member }.policy do
@@ -156,7 +168,6 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :admin_crm_organization
enable :admin_crm_contact
enable :read_cluster
-
enable :read_group_all_available_runners
end
@@ -191,7 +202,6 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :maintainer_access
enable :read_upload
enable :destroy_upload
- enable :admin_achievement
end
rule { owner }.policy do
diff --git a/app/services/achievements/award_service.rb b/app/services/achievements/award_service.rb
new file mode 100644
index 00000000000..674bb8837fb
--- /dev/null
+++ b/app/services/achievements/award_service.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Achievements
+ class AwardService
+ attr_reader :current_user, :achievement_id, :recipient_id
+
+ def initialize(current_user, achievement_id, recipient_id)
+ @current_user = current_user
+ @achievement_id = achievement_id
+ @recipient_id = recipient_id
+ end
+
+ def execute
+ achievement = Achievements::Achievement.find(achievement_id)
+ return error_no_permissions unless allowed?(achievement)
+
+ recipient = User.find(recipient_id)
+
+ user_achievement = Achievements::UserAchievement.create(
+ achievement: achievement,
+ user: recipient,
+ awarded_by_user: current_user)
+ return error_awarding(user_achievement) unless user_achievement.persisted?
+
+ ServiceResponse.success(payload: user_achievement)
+ rescue ActiveRecord::RecordNotFound => e
+ error(e.message)
+ end
+
+ private
+
+ def allowed?(achievement)
+ current_user&.can?(:award_achievement, achievement)
+ end
+
+ def error_no_permissions
+ error('You have insufficient permissions to award this achievement')
+ end
+
+ def error_awarding(user_achievement)
+ error(user_achievement&.errors&.full_messages || 'Failed to award achievement')
+ end
+
+ def error(message)
+ ServiceResponse.error(message: Array(message))
+ end
+ end
+end
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index 8fafa52cd4c..fd671f72238 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -77,31 +77,31 @@
%fieldset
= f.hidden_field(:plan_id, value: plan.id)
.form-group
- = f.label :ci_pipeline_size, s_('AdminSettings|Maximum number of jobs in a single pipeline')
+ = f.label :ci_pipeline_size, plan_limit_setting_description(:ci_pipeline_size)
= f.number_field :ci_pipeline_size, class: 'form-control gl-form-input'
.form-group
- = f.label :ci_active_jobs, s_('AdminSettings|Total number of jobs in currently active pipelines')
+ = f.label :ci_active_jobs, plan_limit_setting_description(:ci_active_jobs)
= f.number_field :ci_active_jobs, class: 'form-control gl-form-input'
.form-group
- = f.label :ci_active_pipelines, s_('AdminSettings|Maximum number of active pipelines per project')
+ = f.label :ci_active_pipelines, plan_limit_setting_description(:ci_active_pipelines)
= f.number_field :ci_active_pipelines, class: 'form-control gl-form-input'
.form-group
- = f.label :ci_project_subscriptions, s_('AdminSettings|Maximum number of pipeline subscriptions to and from a project')
+ = f.label :ci_project_subscriptions, plan_limit_setting_description(:ci_project_subscriptions)
= f.number_field :ci_project_subscriptions, class: 'form-control gl-form-input'
.form-group
- = f.label :ci_pipeline_schedules, s_('AdminSettings|Maximum number of pipeline schedules')
+ = f.label :ci_pipeline_schedules, plan_limit_setting_description(:ci_pipeline_schedules)
= f.number_field :ci_pipeline_schedules, class: 'form-control gl-form-input'
.form-group
- = f.label :ci_needs_size_limit, s_('AdminSettings|Maximum number of DAG dependencies that a job can have')
+ = f.label :ci_needs_size_limit, plan_limit_setting_description(:ci_needs_size_limit)
= f.number_field :ci_needs_size_limit, class: 'form-control gl-form-input'
.form-text.text-muted= s_('AdminSettings|This limit cannot be disabled. Set to 0 to block all DAG dependencies.')
.form-group
- = f.label :ci_registered_group_runners, s_('AdminSettings|Maximum number of runners registered per group')
+ = f.label :ci_registered_group_runners, plan_limit_setting_description(:ci_registered_group_runners)
= f.number_field :ci_registered_group_runners, class: 'form-control gl-form-input'
.form-group
- = f.label :ci_registered_project_runners, s_('AdminSettings|Maximum number of runners registered per project')
+ = f.label :ci_registered_project_runners, plan_limit_setting_description(:ci_registered_project_runners)
= f.number_field :ci_registered_project_runners, class: 'form-control gl-form-input'
.form-group
- = f.label :pipeline_hierarchy_size, s_("AdminSettings|Maximum number of downstream pipelines in a pipeline's hierarchy tree")
+ = f.label :pipeline_hierarchy_size, plan_limit_setting_description(:pipeline_hierarchy_size)
= f.number_field :pipeline_hierarchy_size, class: 'form-control gl-form-input'
= f.submit s_('AdminSettings|Save %{name} limits').html_safe % { name: plan.name.capitalize }, pajamas_button: true
diff --git a/app/views/admin/application_settings/_projects_api_limits.html.haml b/app/views/admin/application_settings/_projects_api_limits.html.haml
new file mode 100644
index 00000000000..4efab4d77a9
--- /dev/null
+++ b/app/views/admin/application_settings/_projects_api_limits.html.haml
@@ -0,0 +1,21 @@
+%section.settings.as-projects-api-limits.no-animate#js-projects-api-limits-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
+ = _('Projects API rate limit')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Set the per-IP address rate limit applicable to unauthenticated requests for getting a list of projects via the API.')
+ = link_to _('Learn more.'), help_page_path('user/admin_area/settings/rate_limit_on_projects_api.md'), target: '_blank', rel: 'noopener noreferrer'
+ .settings-content
+ = gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-projects-api-limits-settings'), html: { class: 'fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :projects_api_rate_limit_unauthenticated, _('Maximum requests per 10 minutes per IP address'), class: 'label-bold'
+ = f.number_field :projects_api_rate_limit_unauthenticated, class: 'form-control gl-form-input'
+ .form-text.gl-text-gray-600
+ = _("Set this number to 0 to disable the limit.")
+
+ = f.submit _('Save changes'), data: { qa_selector: 'save_changes_button' }, pajamas_button: true
diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml
index 779263b439f..fe5f0960d01 100644
--- a/app/views/admin/application_settings/network.html.haml
+++ b/app/views/admin/application_settings/network.html.haml
@@ -146,6 +146,9 @@
.settings-content
= render 'users_api_limits'
+- if Feature.enabled?(:rate_limit_for_unauthenticated_projects_api_access)
+ = render 'projects_api_limits'
+
%section.settings.as-import-export-limits.no-animate#js-import-export-limits-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
diff --git a/app/views/help/instance_configuration/_ci_cd_limits.html.haml b/app/views/help/instance_configuration/_ci_cd_limits.html.haml
index bd5b8a6f10d..0a5cbb710e3 100644
--- a/app/views/help/instance_configuration/_ci_cd_limits.html.haml
+++ b/app/views/help/instance_configuration/_ci_cd_limits.html.haml
@@ -19,34 +19,34 @@
%th= title.to_s.humanize
%tbody
%tr
- %td= s_('AdminSettings|Maximum number of jobs in a single pipeline')
+ %td= plan_limit_setting_description(:ci_pipeline_size)
- ci_cd_limits.each_value do |limits|
%td= instance_configuration_disabled_cell_html(limits[:ci_pipeline_size])
%tr
- %td= s_('AdminSettings|Total number of jobs in currently active pipelines')
+ %td= plan_limit_setting_description(:ci_active_jobs)
- ci_cd_limits.each_value do |limits|
%td= instance_configuration_disabled_cell_html(limits[:ci_active_jobs])
%tr
- %td= s_('AdminSettings|Maximum number of active pipelines per project')
+ %td= plan_limit_setting_description(:ci_active_pipelines)
- ci_cd_limits.each_value do |limits|
%td= instance_configuration_disabled_cell_html(limits[:ci_active_pipelines])
%tr
- %td= s_('AdminSettings|Maximum number of pipeline subscriptions to and from a project')
+ %td= plan_limit_setting_description(:ci_project_subscriptions)
- ci_cd_limits.each_value do |limits|
%td= instance_configuration_disabled_cell_html(limits[:ci_project_subscriptions])
%tr
- %td= s_('AdminSettings|Maximum number of pipeline schedules')
+ %td= plan_limit_setting_description(:ci_pipeline_schedules)
- ci_cd_limits.each_value do |limits|
%td= instance_configuration_disabled_cell_html(limits[:ci_pipeline_schedules])
%tr
- %td= s_('AdminSettings|Maximum number of DAG dependencies that a job can have')
+ %td= plan_limit_setting_description(:ci_needs_size_limit)
- ci_cd_limits.each_value do |limits|
%td= instance_configuration_disabled_cell_html(limits[:ci_needs_size_limit])
%tr
- %td= s_('AdminSettings|Maximum number of runners registered per group')
+ %td= plan_limit_setting_description(:ci_registered_group_runners)
- ci_cd_limits.each_value do |limits|
%td= instance_configuration_disabled_cell_html(limits[:ci_registered_group_runners])
%tr
- %td= s_('AdminSettings|Maximum number of runners registered per project')
+ %td= plan_limit_setting_description(:ci_registered_project_runners)
- ci_cd_limits.each_value do |limits|
%td= instance_configuration_disabled_cell_html(limits[:ci_registered_project_runners])
diff --git a/app/views/projects/branch_rules/_show.html.haml b/app/views/projects/branch_rules/_show.html.haml
index 27525b441ab..32b093bb95c 100644
--- a/app/views/projects/branch_rules/_show.html.haml
+++ b/app/views/projects/branch_rules/_show.html.haml
@@ -1,4 +1,7 @@
- expanded = expanded_by_default?
+- show_code_owners = @project.licensed_feature_available?(:code_owner_approval_required)
+- show_status_checks = @project.licensed_feature_available?(:external_status_checks)
+- show_approvers = @project.licensed_feature_available?(:merge_request_approvers)
%section.settings.no-animate#branch-rules{ class: ('expanded' if expanded) }
.settings-header
@@ -9,4 +12,4 @@
= _('Define rules for who can push, merge, and the required approvals for each branch.')
.settings-content.gl-pr-0
- #js-branch-rules{ data: { project_path: @project.full_path, branch_rules_path: project_settings_repository_branch_rules_path(@project) } }
+ #js-branch-rules{ data: { project_path: @project.full_path, branch_rules_path: project_settings_repository_branch_rules_path(@project), show_code_owners: show_code_owners.to_s, show_status_checks: show_status_checks.to_s, show_approvers: show_approvers.to_s } }
diff --git a/config/feature_flags/development/rate_limit_for_unauthenticated_projects_api_access.yml b/config/feature_flags/development/rate_limit_for_unauthenticated_projects_api_access.yml
new file mode 100644
index 00000000000..8707cba0676
--- /dev/null
+++ b/config/feature_flags/development/rate_limit_for_unauthenticated_projects_api_access.yml
@@ -0,0 +1,8 @@
+---
+name: rate_limit_for_unauthenticated_projects_api_access
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112283
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/391922
+milestone: '15.10'
+type: development
+group: group::organization
+default_enabled: false
diff --git a/db/fixtures/development/36_achievements.rb b/db/fixtures/development/36_achievements.rb
new file mode 100644
index 00000000000..bde854f34b3
--- /dev/null
+++ b/db/fixtures/development/36_achievements.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+class Gitlab::Seeder::Achievements
+ attr_reader :group, :user_ids, :maintainer_ids
+
+ def initialize(group, user_ids)
+ @group = group
+ @maintainer_ids = group.members.maintainers.pluck(:user_id)
+ @user_ids = user_ids
+ end
+
+ def seed!
+ achievement_ids = []
+
+ ['revoked', 'first mr', 'hero', 'legend'].each do |achievement_name|
+ achievement_ids << ::Achievements::Achievement.create!(
+ namespace_id: group.id,
+ name: achievement_name
+ ).id
+
+ print '.'
+ end
+
+ user_ids.each_with_index do |user_id, user_index|
+ (user_index + 1).times do |achievement_index|
+ ::Achievements::UserAchievement.create!(
+ user_id: user_id,
+ achievement_id: achievement_ids[achievement_index],
+ awarded_by_user_id: maintainer_ids.sample,
+ revoked_by_user_id: achievement_index == 0 ? maintainer_ids.sample : nil,
+ revoked_at: achievement_index == 0 ? DateTime.current : nil
+ )
+
+ print '.'
+ end
+ end
+ end
+end
+
+Gitlab::Seeder.quiet do
+ puts "\nGenerating ahievements"
+
+ group = Group.first
+ users = User.last(4).pluck(:id)
+ Gitlab::Seeder::Achievements.new(group, users).seed!
+rescue => e
+ warn "\nError seeding achievements: #{e}"
+end
diff --git a/db/migrate/20230217065736_add_projects_api_rate_limit_unauthenticated_to_application_settings.rb b/db/migrate/20230217065736_add_projects_api_rate_limit_unauthenticated_to_application_settings.rb
new file mode 100644
index 00000000000..f11560c33e9
--- /dev/null
+++ b/db/migrate/20230217065736_add_projects_api_rate_limit_unauthenticated_to_application_settings.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddProjectsApiRateLimitUnauthenticatedToApplicationSettings < Gitlab::Database::Migration[2.1]
+ def change
+ add_column :application_settings, :projects_api_rate_limit_unauthenticated, :integer, default: 400, null: false
+ end
+end
diff --git a/db/schema_migrations/20230217065736 b/db/schema_migrations/20230217065736
new file mode 100644
index 00000000000..a355b107c40
--- /dev/null
+++ b/db/schema_migrations/20230217065736
@@ -0,0 +1 @@
+c772f3d2b46d48bfae68f2b420d38851ecea3105029e5154a58bed29359393f2 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index f593891995a..4f8e8f04f6e 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -11734,6 +11734,7 @@ CREATE TABLE application_settings (
git_rate_limit_users_alertlist integer[] DEFAULT '{}'::integer[] NOT NULL,
allow_deploy_tokens_and_keys_with_external_authn boolean DEFAULT false NOT NULL,
security_policy_global_group_approvers_enabled boolean DEFAULT true NOT NULL,
+ projects_api_rate_limit_unauthenticated integer DEFAULT 400 NOT NULL,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_container_registry_pre_import_tags_rate_positive CHECK ((container_registry_pre_import_tags_rate >= (0)::numeric)),
CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)),
diff --git a/doc/administration/geo/replication/configuration.md b/doc/administration/geo/replication/configuration.md
index 912de360e43..ec17b434a84 100644
--- a/doc/administration/geo/replication/configuration.md
+++ b/doc/administration/geo/replication/configuration.md
@@ -205,15 +205,16 @@ keys must be manually replicated to the **secondary** site.
1. On the top bar, select **Main menu > Admin**.
1. On the left sidebar, select **Geo > Sites**.
1. Select **Add site**.
- ![Add secondary site](img/adding_a_secondary_v13_3.png)
- 1. Fill in **Name** with the `gitlab_rails['geo_node_name']` in
- `/etc/gitlab/gitlab.rb`. These values must always match *exactly*, character
+ ![Add secondary site](img/adding_a_secondary_v15_8.png)
+ 1. In **Name**, enter the value for `gitlab_rails['geo_node_name']` in
+ `/etc/gitlab/gitlab.rb`. These values must always match **exactly**, character
for character.
- 1. Fill in **URL** with the `external_url` in `/etc/gitlab/gitlab.rb`. These
+ 1. In **External URL**, enter the value for `external_url` in `/etc/gitlab/gitlab.rb`. These
values must always match, but it doesn't matter if one ends with a `/` and
the other doesn't.
- 1. (Optional) Choose which groups or storage shards should be replicated by the
- **secondary** site. Leave blank to replicate all. Read more in
+ 1. Optional. In **Internal URL (optional)**, enter an internal URL for the primary site.
+ 1. Optional. Select which groups or storage shards should be replicated by the
+ **secondary** site. Leave blank to replicate all. For more information, see
[selective synchronization](#selective-synchronization).
1. Select **Save changes** to add the **secondary** site.
1. SSH into **each Rails, and Sidekiq node on your secondary** site and restart the services:
diff --git a/doc/administration/geo/replication/img/adding_a_secondary_v15_8.png b/doc/administration/geo/replication/img/adding_a_secondary_v15_8.png
new file mode 100644
index 00000000000..9aad5e889dd
--- /dev/null
+++ b/doc/administration/geo/replication/img/adding_a_secondary_v15_8.png
Binary files differ
diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md
index 4f2eb20877e..5b0a9c318a1 100644
--- a/doc/administration/job_artifacts.md
+++ b/doc/administration/job_artifacts.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Insights
+group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 3205fe8f7b4..c593e16056a 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -745,6 +745,30 @@ mutation($id: NoteableID!, $body: String!) {
}
```
+### `Mutation.achievementsAward`
+
+WARNING:
+**Introduced** in 15.10.
+This feature is in Alpha. It can be changed or removed at any time.
+
+Input type: `AchievementsAwardInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationachievementsawardachievementid"></a>`achievementId` | [`AchievementsAchievementID!`](#achievementsachievementid) | Global ID of the achievement being awarded. |
+| <a id="mutationachievementsawardclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationachievementsawarduserid"></a>`userId` | [`UserID!`](#userid) | Global ID of the user being awarded the achievement. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationachievementsawardclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationachievementsawarderrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationachievementsawarduserachievement"></a>`userAchievement` | [`UserAchievement`](#userachievement) | Achievement award. |
+
### `Mutation.achievementsCreate`
Input type: `AchievementsCreateInput`
@@ -10353,6 +10377,29 @@ The edge type for [`UsageTrendsMeasurement`](#usagetrendsmeasurement).
| <a id="usagetrendsmeasurementedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="usagetrendsmeasurementedgenode"></a>`node` | [`UsageTrendsMeasurement`](#usagetrendsmeasurement) | The item at the end of the edge. |
+#### `UserAchievementConnection`
+
+The connection type for [`UserAchievement`](#userachievement).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="userachievementconnectionedges"></a>`edges` | [`[UserAchievementEdge]`](#userachievementedge) | A list of edges. |
+| <a id="userachievementconnectionnodes"></a>`nodes` | [`[UserAchievement]`](#userachievement) | A list of nodes. |
+| <a id="userachievementconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `UserAchievementEdge`
+
+The edge type for [`UserAchievement`](#userachievement).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="userachievementedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="userachievementedgenode"></a>`node` | [`UserAchievement`](#userachievement) | The item at the end of the edge. |
+
#### `UserCalloutConnection`
The connection type for [`UserCallout`](#usercallout).
@@ -10650,6 +10697,7 @@ Representation of a GitLab user.
| <a id="achievementname"></a>`name` | [`String!`](#string) | Name of the achievement. |
| <a id="achievementnamespace"></a>`namespace` | [`Namespace!`](#namespace) | Namespace of the achievement. |
| <a id="achievementupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp the achievement was last updated. |
+| <a id="achievementuserachievements"></a>`userAchievements` **{warning-solid}** | [`UserAchievementConnection`](#userachievementconnection) | **Introduced** in 15.10. This feature is in Alpha. It can be changed or removed at any time. Recipients for the achievement. |
### `AgentConfiguration`
@@ -15819,6 +15867,7 @@ A user assigned to a merge request.
| <a id="mergerequestassigneesavedreplies"></a>`savedReplies` | [`SavedReplyConnection`](#savedreplyconnection) | Saved replies authored by the user. Will not return saved replies if `saved_replies` feature flag is disabled. (see [Connections](#connections)) |
| <a id="mergerequestassigneestate"></a>`state` | [`UserState!`](#userstate) | State of the user. |
| <a id="mergerequestassigneestatus"></a>`status` | [`UserStatus`](#userstatus) | User status. |
+| <a id="mergerequestassigneeuserachievements"></a>`userAchievements` **{warning-solid}** | [`UserAchievementConnection`](#userachievementconnection) | **Introduced** in 15.10. This feature is in Alpha. It can be changed or removed at any time. Achievements for the user. Only returns for namespaces where the `achievements` feature flag is enabled. |
| <a id="mergerequestassigneeuserpermissions"></a>`userPermissions` | [`UserPermissions!`](#userpermissions) | Permissions for the current user on the resource. |
| <a id="mergerequestassigneeusername"></a>`username` | [`String!`](#string) | Username of the user. Unique within this instance of GitLab. |
| <a id="mergerequestassigneewebpath"></a>`webPath` | [`String!`](#string) | Web path of the user. |
@@ -16065,6 +16114,7 @@ The author of the merge request.
| <a id="mergerequestauthorsavedreplies"></a>`savedReplies` | [`SavedReplyConnection`](#savedreplyconnection) | Saved replies authored by the user. Will not return saved replies if `saved_replies` feature flag is disabled. (see [Connections](#connections)) |
| <a id="mergerequestauthorstate"></a>`state` | [`UserState!`](#userstate) | State of the user. |
| <a id="mergerequestauthorstatus"></a>`status` | [`UserStatus`](#userstatus) | User status. |
+| <a id="mergerequestauthoruserachievements"></a>`userAchievements` **{warning-solid}** | [`UserAchievementConnection`](#userachievementconnection) | **Introduced** in 15.10. This feature is in Alpha. It can be changed or removed at any time. Achievements for the user. Only returns for namespaces where the `achievements` feature flag is enabled. |
| <a id="mergerequestauthoruserpermissions"></a>`userPermissions` | [`UserPermissions!`](#userpermissions) | Permissions for the current user on the resource. |
| <a id="mergerequestauthorusername"></a>`username` | [`String!`](#string) | Username of the user. Unique within this instance of GitLab. |
| <a id="mergerequestauthorwebpath"></a>`webPath` | [`String!`](#string) | Web path of the user. |
@@ -16330,6 +16380,7 @@ A user participating in a merge request.
| <a id="mergerequestparticipantsavedreplies"></a>`savedReplies` | [`SavedReplyConnection`](#savedreplyconnection) | Saved replies authored by the user. Will not return saved replies if `saved_replies` feature flag is disabled. (see [Connections](#connections)) |
| <a id="mergerequestparticipantstate"></a>`state` | [`UserState!`](#userstate) | State of the user. |
| <a id="mergerequestparticipantstatus"></a>`status` | [`UserStatus`](#userstatus) | User status. |
+| <a id="mergerequestparticipantuserachievements"></a>`userAchievements` **{warning-solid}** | [`UserAchievementConnection`](#userachievementconnection) | **Introduced** in 15.10. This feature is in Alpha. It can be changed or removed at any time. Achievements for the user. Only returns for namespaces where the `achievements` feature flag is enabled. |
| <a id="mergerequestparticipantuserpermissions"></a>`userPermissions` | [`UserPermissions!`](#userpermissions) | Permissions for the current user on the resource. |
| <a id="mergerequestparticipantusername"></a>`username` | [`String!`](#string) | Username of the user. Unique within this instance of GitLab. |
| <a id="mergerequestparticipantwebpath"></a>`webPath` | [`String!`](#string) | Web path of the user. |
@@ -16595,6 +16646,7 @@ A user assigned to a merge request as a reviewer.
| <a id="mergerequestreviewersavedreplies"></a>`savedReplies` | [`SavedReplyConnection`](#savedreplyconnection) | Saved replies authored by the user. Will not return saved replies if `saved_replies` feature flag is disabled. (see [Connections](#connections)) |
| <a id="mergerequestreviewerstate"></a>`state` | [`UserState!`](#userstate) | State of the user. |
| <a id="mergerequestreviewerstatus"></a>`status` | [`UserStatus`](#userstatus) | User status. |
+| <a id="mergerequestrevieweruserachievements"></a>`userAchievements` **{warning-solid}** | [`UserAchievementConnection`](#userachievementconnection) | **Introduced** in 15.10. This feature is in Alpha. It can be changed or removed at any time. Achievements for the user. Only returns for namespaces where the `achievements` feature flag is enabled. |
| <a id="mergerequestrevieweruserpermissions"></a>`userPermissions` | [`UserPermissions!`](#userpermissions) | Permissions for the current user on the resource. |
| <a id="mergerequestreviewerusername"></a>`username` | [`String!`](#string) | Username of the user. Unique within this instance of GitLab. |
| <a id="mergerequestreviewerwebpath"></a>`webPath` | [`String!`](#string) | Web path of the user. |
@@ -20803,6 +20855,21 @@ Represents a recorded measurement (object count) for the Admins.
| <a id="usagetrendsmeasurementidentifier"></a>`identifier` | [`MeasurementIdentifier!`](#measurementidentifier) | Type of objects being measured. |
| <a id="usagetrendsmeasurementrecordedat"></a>`recordedAt` | [`Time`](#time) | Time the measurement was recorded. |
+### `UserAchievement`
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="userachievementachievement"></a>`achievement` | [`Achievement!`](#achievement) | Achievement awarded. |
+| <a id="userachievementawardedbyuser"></a>`awardedByUser` | [`UserCore!`](#usercore) | Awarded by. |
+| <a id="userachievementcreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp the achievement was created. |
+| <a id="userachievementid"></a>`id` | [`AchievementsUserAchievementID!`](#achievementsuserachievementid) | ID of the user achievement. |
+| <a id="userachievementrevokedat"></a>`revokedAt` | [`Time`](#time) | Timestamp the achievement was revoked. |
+| <a id="userachievementrevokedbyuser"></a>`revokedByUser` | [`UserCore`](#usercore) | Revoked by. |
+| <a id="userachievementupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp the achievement was last updated. |
+| <a id="userachievementuser"></a>`user` | [`UserCore!`](#usercore) | Achievement recipient. |
+
### `UserCallout`
#### Fields
@@ -20841,6 +20908,7 @@ Core represention of a GitLab user.
| <a id="usercoresavedreplies"></a>`savedReplies` | [`SavedReplyConnection`](#savedreplyconnection) | Saved replies authored by the user. Will not return saved replies if `saved_replies` feature flag is disabled. (see [Connections](#connections)) |
| <a id="usercorestate"></a>`state` | [`UserState!`](#userstate) | State of the user. |
| <a id="usercorestatus"></a>`status` | [`UserStatus`](#userstatus) | User status. |
+| <a id="usercoreuserachievements"></a>`userAchievements` **{warning-solid}** | [`UserAchievementConnection`](#userachievementconnection) | **Introduced** in 15.10. This feature is in Alpha. It can be changed or removed at any time. Achievements for the user. Only returns for namespaces where the `achievements` feature flag is enabled. |
| <a id="usercoreuserpermissions"></a>`userPermissions` | [`UserPermissions!`](#userpermissions) | Permissions for the current user on the resource. |
| <a id="usercoreusername"></a>`username` | [`String!`](#string) | Username of the user. Unique within this instance of GitLab. |
| <a id="usercorewebpath"></a>`webPath` | [`String!`](#string) | Web path of the user. |
@@ -24187,6 +24255,12 @@ A `AchievementsAchievementID` is a global ID. It is encoded as a string.
An example `AchievementsAchievementID` is: `"gid://gitlab/Achievements::Achievement/1"`.
+### `AchievementsUserAchievementID`
+
+A `AchievementsUserAchievementID` is a global ID. It is encoded as a string.
+
+An example `AchievementsUserAchievementID` is: `"gid://gitlab/Achievements::UserAchievement/1"`.
+
### `AlertManagementAlertID`
A `AlertManagementAlertID` is a global ID. It is encoded as a string.
@@ -25289,6 +25363,7 @@ Implementations:
| <a id="usersavedreplies"></a>`savedReplies` | [`SavedReplyConnection`](#savedreplyconnection) | Saved replies authored by the user. Will not return saved replies if `saved_replies` feature flag is disabled. (see [Connections](#connections)) |
| <a id="userstate"></a>`state` | [`UserState!`](#userstate) | State of the user. |
| <a id="userstatus"></a>`status` | [`UserStatus`](#userstatus) | User status. |
+| <a id="useruserachievements"></a>`userAchievements` **{warning-solid}** | [`UserAchievementConnection`](#userachievementconnection) | **Introduced** in 15.10. This feature is in Alpha. It can be changed or removed at any time. Achievements for the user. Only returns for namespaces where the `achievements` feature flag is enabled. |
| <a id="useruserpermissions"></a>`userPermissions` | [`UserPermissions!`](#userpermissions) | Permissions for the current user on the resource. |
| <a id="userusername"></a>`username` | [`String!`](#string) | Username of the user. Unique within this instance of GitLab. |
| <a id="userwebpath"></a>`webPath` | [`String!`](#string) | Web path of the user. |
diff --git a/doc/api/group_level_variables.md b/doc/api/group_level_variables.md
index 4037a778d7f..921a9922c9b 100644
--- a/doc/api/group_level_variables.md
+++ b/doc/api/group_level_variables.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Authoring
+group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/api/instance_level_ci_variables.md b/doc/api/instance_level_ci_variables.md
index e71cf777b2c..840744bcae1 100644
--- a/doc/api/instance_level_ci_variables.md
+++ b/doc/api/instance_level_ci_variables.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Authoring
+group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/api/job_artifacts.md b/doc/api/job_artifacts.md
index b73eafea3c4..715b0614cc4 100644
--- a/doc/api/job_artifacts.md
+++ b/doc/api/job_artifacts.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Insights
+group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/api/project_level_variables.md b/doc/api/project_level_variables.md
index 92ca2849e8e..fa699c34a8a 100644
--- a/doc/api/project_level_variables.md
+++ b/doc/api/project_level_variables.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Authoring
+group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
type: reference, api
---
diff --git a/doc/api/secure_files.md b/doc/api/secure_files.md
index 11c718dde01..0fb12cf3ad2 100644
--- a/doc/api/secure_files.md
+++ b/doc/api/secure_files.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Authoring
+group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
type: reference, api
---
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 5c7890fef23..2b7bb3ab626 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -227,7 +227,8 @@ Example response:
"can_create_group": false,
"jira_connect_application_key": "123",
"jira_connect_proxy_url": "http://gitlab.example.com",
- "user_defaults_to_private_profile": true
+ "user_defaults_to_private_profile": true,
+ "projects_api_rate_limit_unauthenticated": 400
}
```
@@ -435,6 +436,7 @@ listed in the descriptions of the relevant settings.
| `plantuml_url` | string | required by: `plantuml_enabled` | The PlantUML instance URL for integration. |
| `polling_interval_multiplier` | decimal | no | Interval multiplier used by endpoints that perform polling. Set to `0` to disable polling. |
| `project_export_enabled` | boolean | no | Enable project export. |
+| `projects_api_rate_limit_unauthenticated` | integer | no | [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112283) in GitLab 15.10. Max number of requests per 10 minutes per IP address for unauthenticated requests to the [list all projects API](projects.md#list-all-projects). Default: 400. To disable throttling set to 0.|
| `prometheus_metrics_enabled` | boolean | no | Enable Prometheus metrics. |
| `protected_ci_variables` | boolean | no | CI/CD variables are protected by default. |
| `push_event_activities_limit` | integer | no | Number of changes (branches or tags) in a single push to determine whether individual push events or bulk push events are created. [Bulk push events are created](../user/admin_area/settings/push_event_activities_limit.md) if it surpasses that value. |
diff --git a/doc/api/visual_review_discussions.md b/doc/api/visual_review_discussions.md
index f966af82b10..561dc1a26a5 100644
--- a/doc/api/visual_review_discussions.md
+++ b/doc/api/visual_review_discussions.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Insights
+group: Pipeline Execution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/architecture/blueprints/ci_data_decay/pipeline_partitioning.md b/doc/architecture/blueprints/ci_data_decay/pipeline_partitioning.md
index ebe3c72adfc..61396108d41 100644
--- a/doc/architecture/blueprints/ci_data_decay/pipeline_partitioning.md
+++ b/doc/architecture/blueprints/ci_data_decay/pipeline_partitioning.md
@@ -1,7 +1,11 @@
---
-stage: none
-group: unassigned
comments: false
+status: ongoing
+creation-date: "2022-05-31"
+authors: [ "@grzesiek" ]
+coach: [ "@ayufan", "@grzesiek" ]
+approvers: [ "@jreporter", "@cheryl.li" ]
+owning-stage: "~devops::verify"
description: 'Pipeline data partitioning design'
---
@@ -803,9 +807,9 @@ DRIs:
| Role | Who |
|---------------------|------------------------------------------------|
| Author | Grzegorz Bizon, Principal Engineer |
-| Recommender | Kamil Trzciński, Senior Distingiushed Engineer |
-| Product Manager | James Heimbuck, Senior Product Manager |
-| Engineering Manager | Scott Hampton, Engineering Manager |
+| Recommender | Kamil Trzciński, Senior Distinguished Engineer |
+| Product Leadership | Jackie Porter, Director of Product Management |
+| Engineering Leadership | Caroline Simpson, Engineering Manager / Cheryl Li, Senior Engineering Manager |
| Lead Engineer | Marius Bobin, Senior Backend Engineer |
<!-- vale gitlab.Spelling = YES -->
diff --git a/doc/ci/cloud_services/aws/index.md b/doc/ci/cloud_services/aws/index.md
index 484a159cd2b..733105e9ddf 100644
--- a/doc/ci/cloud_services/aws/index.md
+++ b/doc/ci/cloud_services/aws/index.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Authoring
+group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/ci/cloud_services/azure/index.md b/doc/ci/cloud_services/azure/index.md
index 321f9849632..c99e30e8bac 100644
--- a/doc/ci/cloud_services/azure/index.md
+++ b/doc/ci/cloud_services/azure/index.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Authoring
+group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/ci/cloud_services/google_cloud/index.md b/doc/ci/cloud_services/google_cloud/index.md
index 516a2d05cd1..bcb2681403a 100644
--- a/doc/ci/cloud_services/google_cloud/index.md
+++ b/doc/ci/cloud_services/google_cloud/index.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Authoring
+group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/ci/cloud_services/index.md b/doc/ci/cloud_services/index.md
index 9304e3562d9..88e7d88e22a 100644
--- a/doc/ci/cloud_services/index.md
+++ b/doc/ci/cloud_services/index.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Authoring
+group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/ci/examples/authenticating-with-hashicorp-vault/index.md b/doc/ci/examples/authenticating-with-hashicorp-vault/index.md
index 7bb14c4c900..6295c131fb9 100644
--- a/doc/ci/examples/authenticating-with-hashicorp-vault/index.md
+++ b/doc/ci/examples/authenticating-with-hashicorp-vault/index.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Authoring
+group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
type: tutorial
---
diff --git a/doc/ci/examples/end_to_end_testing_webdriverio/index.md b/doc/ci/examples/end_to_end_testing_webdriverio/index.md
index 7b5690e2d3d..3385aca1ef2 100644
--- a/doc/ci/examples/end_to_end_testing_webdriverio/index.md
+++ b/doc/ci/examples/end_to_end_testing_webdriverio/index.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Insights
+group: Pipeline Execution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
author: Vincent Tunru
author_gitlab: Vinnl
diff --git a/doc/ci/introduction/index.md b/doc/ci/introduction/index.md
index e62c38fc1ec..bf07af5e761 100644
--- a/doc/ci/introduction/index.md
+++ b/doc/ci/introduction/index.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Execution
+group: Pipeline Authoring
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
description: "An overview of Continuous Integration, Continuous Delivery, and Continuous Deployment, as well as an introduction to GitLab CI/CD."
type: concepts
diff --git a/doc/ci/jobs/ci_job_token.md b/doc/ci/jobs/ci_job_token.md
index 95b8a49f408..772c71657cc 100644
--- a/doc/ci/jobs/ci_job_token.md
+++ b/doc/ci/jobs/ci_job_token.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Execution
+group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/ci/jobs/index.md b/doc/ci/jobs/index.md
index 4aa12d12bdb..b9c2ee409b8 100644
--- a/doc/ci/jobs/index.md
+++ b/doc/ci/jobs/index.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Execution
+group: Pipeline Authoring
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/ci/pipelines/job_artifacts.md b/doc/ci/pipelines/job_artifacts.md
index 32b9923e811..7db4596bb1d 100644
--- a/doc/ci/pipelines/job_artifacts.md
+++ b/doc/ci/pipelines/job_artifacts.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Insights
+group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
disqus_identifier: 'https://docs.gitlab.com/ee/user/project/pipelines/job_artifacts.html'
---
diff --git a/doc/ci/pipelines/pipeline_architectures.md b/doc/ci/pipelines/pipeline_architectures.md
index e4a3416f60b..dadf631e2bb 100644
--- a/doc/ci/pipelines/pipeline_architectures.md
+++ b/doc/ci/pipelines/pipeline_architectures.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Execution
+group: Pipeline Authoring
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
type: reference
---
diff --git a/doc/ci/pipelines/pipeline_artifacts.md b/doc/ci/pipelines/pipeline_artifacts.md
index aacaa110041..31c848d5274 100644
--- a/doc/ci/pipelines/pipeline_artifacts.md
+++ b/doc/ci/pipelines/pipeline_artifacts.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Insights
+group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md
index 7a0f43f9ec5..9e69ab1ccb8 100644
--- a/doc/ci/review_apps/index.md
+++ b/doc/ci/review_apps/index.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Insights
+group: Pipeline Execution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/ci/secrets/id_token_authentication.md b/doc/ci/secrets/id_token_authentication.md
index f622bc2a7b1..0eb85a94b58 100644
--- a/doc/ci/secrets/id_token_authentication.md
+++ b/doc/ci/secrets/id_token_authentication.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Authoring
+group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
type: tutorial
---
diff --git a/doc/ci/secrets/index.md b/doc/ci/secrets/index.md
index 14f771431e5..fa8fe2a36a8 100644
--- a/doc/ci/secrets/index.md
+++ b/doc/ci/secrets/index.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Authoring
+group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
type: concepts, howto
---
diff --git a/doc/ci/secure_files/index.md b/doc/ci/secure_files/index.md
index 10baa57cabb..d432db351bb 100644
--- a/doc/ci/secure_files/index.md
+++ b/doc/ci/secure_files/index.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Authoring
+group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
type: reference
---
diff --git a/doc/ci/ssh_keys/index.md b/doc/ci/ssh_keys/index.md
index c1154485018..ab767697cbc 100644
--- a/doc/ci/ssh_keys/index.md
+++ b/doc/ci/ssh_keys/index.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Execution
+group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
type: tutorial
---
diff --git a/doc/ci/testing/accessibility_testing.md b/doc/ci/testing/accessibility_testing.md
index fa57371a7d5..58bb4308095 100644
--- a/doc/ci/testing/accessibility_testing.md
+++ b/doc/ci/testing/accessibility_testing.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Insights
+group: Pipeline Execution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/ci/testing/browser_performance_testing.md b/doc/ci/testing/browser_performance_testing.md
index ff013f0037e..175ecef7f09 100644
--- a/doc/ci/testing/browser_performance_testing.md
+++ b/doc/ci/testing/browser_performance_testing.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Insights
+group: Pipeline Execution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/ci/testing/fail_fast_testing.md b/doc/ci/testing/fail_fast_testing.md
index 4ad1656c8d6..57e21beef06 100644
--- a/doc/ci/testing/fail_fast_testing.md
+++ b/doc/ci/testing/fail_fast_testing.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Insights
+group: Pipeline Execution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/ci/testing/index.md b/doc/ci/testing/index.md
index 41d474f0e60..5fd86561b9e 100644
--- a/doc/ci/testing/index.md
+++ b/doc/ci/testing/index.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Insights
+group: Pipeline Execution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/ci/testing/load_performance_testing.md b/doc/ci/testing/load_performance_testing.md
index 7e527fae562..cf870bcec55 100644
--- a/doc/ci/testing/load_performance_testing.md
+++ b/doc/ci/testing/load_performance_testing.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Insights
+group: Pipeline Execution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/ci/testing/metrics_reports.md b/doc/ci/testing/metrics_reports.md
index e084e4d3bc7..9257ba8990e 100644
--- a/doc/ci/testing/metrics_reports.md
+++ b/doc/ci/testing/metrics_reports.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Insights
+group: Pipeline Execution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/ci/testing/test_coverage_visualization.md b/doc/ci/testing/test_coverage_visualization.md
index e2a1a4a2c5e..a54b1f2a610 100644
--- a/doc/ci/testing/test_coverage_visualization.md
+++ b/doc/ci/testing/test_coverage_visualization.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Insights
+group: Pipeline Execution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/ci/testing/unit_test_report_examples.md b/doc/ci/testing/unit_test_report_examples.md
index 87426fc8496..41dfc8fc60b 100644
--- a/doc/ci/testing/unit_test_report_examples.md
+++ b/doc/ci/testing/unit_test_report_examples.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Insights
+group: Pipeline Execution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/ci/testing/unit_test_reports.md b/doc/ci/testing/unit_test_reports.md
index 0fe9b2b6d64..88a5d90d30e 100644
--- a/doc/ci/testing/unit_test_reports.md
+++ b/doc/ci/testing/unit_test_reports.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Insights
+group: Pipeline Execution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/ci/triggers/index.md b/doc/ci/triggers/index.md
index f1f9f3e2485..393b128c658 100644
--- a/doc/ci/triggers/index.md
+++ b/doc/ci/triggers/index.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Execution
+group: Pipeline Authoring
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
type: tutorial
---
diff --git a/doc/ci/troubleshooting.md b/doc/ci/troubleshooting.md
index 17ce184ee28..cd0e6bd2ee5 100644
--- a/doc/ci/troubleshooting.md
+++ b/doc/ci/troubleshooting.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Execution
+group: Pipeline Authoring
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
type: reference
---
diff --git a/doc/ci/variables/index.md b/doc/ci/variables/index.md
index 9d134f0f2d4..f573b921207 100644
--- a/doc/ci/variables/index.md
+++ b/doc/ci/variables/index.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Authoring
+group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
type: reference
---
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index b9557b98066..45dc1a7fe37 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Authoring
+group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
type: reference
---
diff --git a/doc/ci/variables/where_variables_can_be_used.md b/doc/ci/variables/where_variables_can_be_used.md
index 6ea1f454467..c20d9be51e7 100644
--- a/doc/ci/variables/where_variables_can_be_used.md
+++ b/doc/ci/variables/where_variables_can_be_used.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Authoring
+group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
type: reference
---
diff --git a/doc/ci/yaml/artifacts_reports.md b/doc/ci/yaml/artifacts_reports.md
index 151a043da5e..8740cf100ac 100644
--- a/doc/ci/yaml/artifacts_reports.md
+++ b/doc/ci/yaml/artifacts_reports.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Insights
+group: Pipeline Execution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
@@ -22,7 +22,7 @@ date for the artifacts.
Some `artifacts:reports` types can be generated by multiple jobs in the same pipeline, and used by merge request or
pipeline features from each job.
-To be able to browse the report output files, include the [`artifacts:paths`](index.md#artifactspaths) keyword.
+To browse the report output files, ensure you include the [`artifacts:paths`](index.md#artifactspaths) keyword in your job definition.
NOTE:
Combined reports in parent pipelines using [artifacts from child pipelines](index.md#needspipelinejob) is
diff --git a/doc/user/admin_area/appearance.md b/doc/user/admin_area/appearance.md
index a1fae7e8712..b9850048e7d 100644
--- a/doc/user/admin_area/appearance.md
+++ b/doc/user/admin_area/appearance.md
@@ -71,6 +71,27 @@ to review the saved appearance settings:
NOTE:
You can add also add a [customized help message](settings/help_page.md) below the sign in message or add [a Sign in text message](settings/sign_in_restrictions.md#sign-in-information).
+## Progressive Web App
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/375708) in GitLab 15.9.
+
+GitLab can be installed as a [Progressive Web App](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps) (PWA).
+Use the Progressive Web App settings to customize its appearance, including its name,
+description, and icon.
+
+### Configure the PWA settings
+
+To configure the PWA settings:
+
+1. On the top bar, select **Main menu > Admin**.
+1. On the left sidebar, select **Settings > Appearance**.
+1. Scroll to the **Progressive Web App (PWA)** section.
+1. Complete the fields.
+ - **Icon**: If you use the standard GitLab icon, it is available in sizes 192x192 pixels,
+ 512x512 pixels, also as a maskable icon. If you use a custom icon, it must be in either size
+ 192x192 pixels, or 512x512 pixels.
+1. Select **Update appearance settings**.
+
## New project pages
You can add a new project guidelines message to the **New project page** in GitLab.
diff --git a/doc/user/admin_area/settings/rate_limit_on_projects_api.md b/doc/user/admin_area/settings/rate_limit_on_projects_api.md
new file mode 100644
index 00000000000..eb4066fed00
--- /dev/null
+++ b/doc/user/admin_area/settings/rate_limit_on_projects_api.md
@@ -0,0 +1,33 @@
+---
+type: reference
+stage: Manage
+group: Organization
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
+---
+
+# Rate limit on Projects API **(FREE SELF)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112283) in GitLab 15.10 behind a feature flag, disabled by default.
+
+You can configure the rate limit per IP address for unauthenticated requests to the [list all projects API](../../../api/projects.md#list-all-projects).
+
+To change the rate limit:
+
+1. On the top bar, select **Main menu > Admin**.
+1. On the left sidebar, select **Settings > Network**.
+1. Expand **Projects API rate limit**.
+1. In the **Maximum requests per 10 minutes per IP address** text box, enter the new value.
+1. Select **Save changes**.
+
+The rate limit:
+
+- Applies per IP address.
+- Doesn't apply to authenticated requests.
+- Can be set to 0 to disable rate limiting.
+
+The default value of the rate limit is `400`.
+
+Requests over the rate limit are logged into the `auth.log` file.
+
+For example, if you set a limit of 400, unauthenticated requests to the `GET /projects` API endpoint that
+exceed a rate of 400 within 10 minutes are blocked. Access to the endpoint is restored after ten minutes have elapsed.
diff --git a/doc/user/application_security/vulnerabilities/index.md b/doc/user/application_security/vulnerabilities/index.md
index 67a1257799b..c13f8ccc18e 100644
--- a/doc/user/application_security/vulnerabilities/index.md
+++ b/doc/user/application_security/vulnerabilities/index.md
@@ -195,6 +195,10 @@ To manually apply the patch that GitLab generated for a vulnerability:
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/6176) in GitLab 14.9.
+NOTE:
+Security training is not available in an offline environment because it uses content from
+third-party vendors.
+
Security training helps your developers learn how to fix vulnerabilities. Developers can view security training from selected educational providers, relevant to the detected vulnerability.
To enable security training for vulnerabilities in your project:
@@ -204,8 +208,6 @@ To enable security training for vulnerabilities in your project:
1. On the tab bar, select **Vulnerability Management**.
1. To enable a security training provider, turn on the toggle.
-Security training uses content from third-party vendors. You must have an internet connection to use this feature.
-
## View security training for a vulnerability
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/6176) in GitLab 14.9.
diff --git a/doc/user/group/repositories_analytics/index.md b/doc/user/group/repositories_analytics/index.md
index 9971457f2ac..edb3e1b5079 100644
--- a/doc/user/group/repositories_analytics/index.md
+++ b/doc/user/group/repositories_analytics/index.md
@@ -1,6 +1,6 @@
---
stage: Verify
-group: Pipeline Insights
+group: Pipeline Execution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 6eea56ea117..fbe30aad120 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -94,6 +94,12 @@ module API
Gitlab::AppLogger.info({ message: "File exceeds maximum size", file_bytes: file.size, project_id: user_project.id, project_path: user_project.full_path, upload_allowed: allowed })
end
end
+
+ def validate_projects_api_rate_limit_for_unauthenticated_users!
+ return unless Feature.enabled?(:rate_limit_for_unauthenticated_projects_api_access)
+
+ check_rate_limit!(:projects_api_rate_limit_unauthenticated, scope: [ip_address]) if current_user.blank?
+ end
end
helpers do
@@ -266,6 +272,8 @@ module API
end
# TODO: Set higher urgency https://gitlab.com/gitlab-org/gitlab/-/issues/211495
get feature_category: :projects, urgency: :low do
+ validate_projects_api_rate_limit_for_unauthenticated_users!
+
present_projects load_projects
end
diff --git a/lib/gitlab/application_rate_limiter.rb b/lib/gitlab/application_rate_limiter.rb
index 466538df56e..8c1cd7c21c2 100644
--- a/lib/gitlab/application_rate_limiter.rb
+++ b/lib/gitlab/application_rate_limiter.rb
@@ -56,7 +56,10 @@ module Gitlab
namespace_exists: { threshold: 20, interval: 1.minute },
fetch_google_ip_list: { threshold: 10, interval: 1.minute },
jobs_index: { threshold: 600, interval: 1.minute },
- bulk_import: { threshold: 6, interval: 1.minute }
+ bulk_import: { threshold: 6, interval: 1.minute },
+ projects_api_rate_limit_unauthenticated: {
+ threshold: -> { application_settings.projects_api_rate_limit_unauthenticated }, interval: 10.minutes
+ }
}.freeze
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 47ccbb6e0f2..8eabffd78b7 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -26304,6 +26304,9 @@ msgstr ""
msgid "Maximum push size (MB)"
msgstr ""
+msgid "Maximum requests per 10 minutes per IP address"
+msgstr ""
+
msgid "Maximum requests per 10 minutes per user"
msgstr ""
@@ -34267,6 +34270,9 @@ msgstr ""
msgid "Projects API"
msgstr ""
+msgid "Projects API rate limit"
+msgstr ""
+
msgid "Projects Successfully Retrieved"
msgstr ""
@@ -35467,6 +35473,9 @@ msgstr ""
msgid "Refresh the page and try again."
msgstr ""
+msgid "Refresh the page to view sync status"
+msgstr ""
+
msgid "Refreshing in a second to show the updated status..."
msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
@@ -39745,6 +39754,9 @@ msgstr ""
msgid "Set the milestone to %{milestone_reference}."
msgstr ""
+msgid "Set the per-IP address rate limit applicable to unauthenticated requests for getting a list of projects via the API."
+msgstr ""
+
msgid "Set the per-user rate limit for getting a user by ID via the API."
msgstr ""
diff --git a/spec/factories/achievements/user_achievements.rb b/spec/factories/achievements/user_achievements.rb
new file mode 100644
index 00000000000..a5fd1df38dd
--- /dev/null
+++ b/spec/factories/achievements/user_achievements.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :user_achievement, class: 'Achievements::UserAchievement' do
+ user
+ achievement
+ awarded_by_user factory: :user
+
+ trait :revoked do
+ revoked_by_user factory: :user
+ revoked_at { Time.now }
+ end
+ end
+end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 26efed85513..740ac202011 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -762,6 +762,18 @@ RSpec.describe 'Admin updates settings', feature_category: :not_owned do
expect(current_settings.users_get_by_id_limit_allowlist).to eq(%w[someone someone_else])
end
+ it 'changes Projects API rate limits settings' do
+ visit network_admin_application_settings_path
+
+ page.within('.as-projects-api-limits') do
+ fill_in 'Maximum requests per 10 minutes per IP address', with: 100
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+ expect(current_settings.projects_api_rate_limit_unauthenticated).to eq(100)
+ end
+
shared_examples 'regular throttle rate limit settings' do
it 'changes rate limit settings' do
visit network_admin_application_settings_path
diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js
index ee43c5619a4..49b8ff2a0d4 100644
--- a/spec/frontend/issues/show/components/description_spec.js
+++ b/spec/frontend/issues/show/components/description_spec.js
@@ -1,25 +1,19 @@
import $ from 'jquery';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { GlModal } from '@gitlab/ui';
import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import setWindowLocation from 'helpers/set_window_location_helper';
-import { stubComponent } from 'helpers/stub_component';
import { TEST_HOST } from 'helpers/test_constants';
-import { mockTracking } from 'helpers/tracking_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createAlert } from '~/flash';
import Description from '~/issues/show/components/description.vue';
import eventHub from '~/issues/show/event_hub';
-import { updateHistory } from '~/lib/utils/url_utility';
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';
-import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
-import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
import {
createWorkItemMutationErrorResponse,
@@ -31,7 +25,6 @@ import {
descriptionProps as initialProps,
descriptionHtmlWithList,
descriptionHtmlWithCheckboxes,
- descriptionHtmlWithTask,
} from '../mock_data/mock_data';
jest.mock('~/flash');
@@ -43,9 +36,6 @@ jest.mock('~/task_list');
jest.mock('~/behaviors/markdown/render_gfm');
const mockSpriteIcons = '/icons.svg';
-const showModal = jest.fn();
-const hideModal = jest.fn();
-const showDetailsModal = jest.fn();
const $toast = {
show: jest.fn(),
};
@@ -70,16 +60,12 @@ describe('Description component', () => {
const findTextarea = () => wrapper.find('[data-testid="textarea"]');
const findListItems = () => findGfmContent().findAll('ul > li');
const findTaskActionButtons = () => wrapper.findAll('.task-list-item-actions');
- const findTaskLink = () => wrapper.find('a.gfm-issue');
- const findModal = () => wrapper.findComponent(GlModal);
- const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
function createComponent({
props = {},
provide,
issueDetailsQueryHandler = jest.fn().mockResolvedValue(issueDetailsResponse),
createWorkItemMutationHandler,
- ...options
} = {}) {
wrapper = shallowMountExtended(Description, {
propsData: {
@@ -101,20 +87,6 @@ describe('Description component', () => {
mocks: {
$toast,
},
- stubs: {
- GlModal: stubComponent(GlModal, {
- methods: {
- show: showModal,
- hide: hideModal,
- },
- }),
- WorkItemDetailModal: stubComponent(WorkItemDetailModal, {
- methods: {
- show: showDetailsModal,
- },
- }),
- },
- ...options,
});
}
@@ -284,7 +256,6 @@ describe('Description component', () => {
props: {
descriptionHtml: descriptionHtmlWithList,
},
- attachTo: document.body,
});
await nextTick();
});
@@ -337,18 +308,6 @@ describe('Description component', () => {
it('renders a list of hidden buttons corresponding to checkboxes in description HTML', () => {
expect(findTaskActionButtons()).toHaveLength(3);
});
-
- it('does not show a modal by default', () => {
- expect(findModal().exists()).toBe(false);
- });
-
- it('shows toast after delete success', async () => {
- const newDesc = 'description';
- findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc);
-
- expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]);
- expect($toast.show).toHaveBeenCalledWith('Task deleted');
- });
});
describe('task list item actions', () => {
@@ -463,109 +422,4 @@ describe('Description component', () => {
});
});
});
-
- describe('work items detail', () => {
- describe('when opening and closing', () => {
- beforeEach(() => {
- createComponent({
- props: {
- descriptionHtml: descriptionHtmlWithTask,
- },
- });
- return nextTick();
- });
-
- it('opens when task button is clicked', async () => {
- await findTaskLink().trigger('click');
-
- expect(showDetailsModal).toHaveBeenCalled();
- expect(updateHistory).toHaveBeenCalledWith({
- url: `${TEST_HOST}/?work_item_id=2`,
- replace: true,
- });
- });
-
- it('closes from an open state', async () => {
- await findTaskLink().trigger('click');
-
- findWorkItemDetailModal().vm.$emit('close');
- await nextTick();
-
- expect(updateHistory).toHaveBeenLastCalledWith({
- url: `${TEST_HOST}/`,
- replace: true,
- });
- });
-
- it('tracks when opened', async () => {
- const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
-
- await findTaskLink().trigger('click');
-
- expect(trackingSpy).toHaveBeenCalledWith(
- TRACKING_CATEGORY_SHOW,
- 'viewed_work_item_from_modal',
- {
- category: TRACKING_CATEGORY_SHOW,
- label: 'work_item_view',
- property: 'type_task',
- },
- );
- });
- });
-
- describe('when url query `work_item_id` exists', () => {
- it.each`
- behavior | workItemId | modalOpened
- ${'opens'} | ${'2'} | ${1}
- ${'does not open'} | ${'123'} | ${0}
- ${'does not open'} | ${'123e'} | ${0}
- ${'does not open'} | ${'12e3'} | ${0}
- ${'does not open'} | ${'1e23'} | ${0}
- ${'does not open'} | ${'x'} | ${0}
- ${'does not open'} | ${'undefined'} | ${0}
- `(
- '$behavior when url contains `work_item_id=$workItemId`',
- async ({ workItemId, modalOpened }) => {
- setWindowLocation(`?work_item_id=${workItemId}`);
-
- createComponent({
- props: { descriptionHtml: descriptionHtmlWithTask },
- });
-
- expect(showDetailsModal).toHaveBeenCalledTimes(modalOpened);
- },
- );
- });
- });
-
- describe('when hovering task links', () => {
- beforeEach(() => {
- createComponent({
- props: {
- descriptionHtml: descriptionHtmlWithTask,
- },
- });
- return nextTick();
- });
-
- it('prefetches work item detail after work item link is hovered for 150ms', async () => {
- await findTaskLink().trigger('mouseover');
- jest.advanceTimersByTime(150);
- await waitForPromises();
-
- expect(queryHandler).toHaveBeenCalledWith({
- id: 'gid://gitlab/WorkItem/2',
- });
- });
-
- it('does not work item detail after work item link is hovered for less than 150ms', async () => {
- await findTaskLink().trigger('mouseover');
- await findTaskLink().trigger('mouseout');
- jest.advanceTimersByTime(150);
- await waitForPromises();
-
- expect(queryHandler).not.toHaveBeenCalled();
- });
- });
});
diff --git a/spec/frontend/issues/show/components/task_list_item_actions_spec.js b/spec/frontend/issues/show/components/task_list_item_actions_spec.js
index d52f9d57453..8caa5236796 100644
--- a/spec/frontend/issues/show/components/task_list_item_actions_spec.js
+++ b/spec/frontend/issues/show/components/task_list_item_actions_spec.js
@@ -17,7 +17,7 @@ describe('TaskListItemActions component', () => {
document.body.appendChild(li);
wrapper = shallowMount(TaskListItemActions, {
- provide: { canUpdate: true, toggleClass: 'task-list-item-actions' },
+ provide: { canUpdate: true },
attachTo: document.querySelector('div'),
});
};
diff --git a/spec/frontend/issues/show/mock_data/mock_data.js b/spec/frontend/issues/show/mock_data/mock_data.js
index 9f0b6fb1148..441fa4f922c 100644
--- a/spec/frontend/issues/show/mock_data/mock_data.js
+++ b/spec/frontend/issues/show/mock_data/mock_data.js
@@ -80,33 +80,3 @@ export const descriptionHtmlWithCheckboxes = `
</li>
</ul>
`;
-
-export const descriptionHtmlWithTask = `
- <ul data-sourcepos="1:1-3:7" class="task-list" dir="auto">
- <li data-sourcepos="1:1-1:10" class="task-list-item">
- <input type="checkbox" class="task-list-item-checkbox" disabled>
- <a href="/gitlab-org/gitlab-test/-/issues/48" data-original="#48+" data-link="false" data-link-reference="false" data-project="1" data-issue="2" data-reference-format="+" data-reference-type="task" data-container="body" data-placement="top" title="1" class="gfm gfm-issue has-tooltip" data-issue-type="task">1 (#48)</a>
- </li>
- <li data-sourcepos="2:1-2:7" class="task-list-item">
- <input type="checkbox" class="task-list-item-checkbox" disabled> 2
- </li>
- <li data-sourcepos="3:1-3:7" class="task-list-item">
- <input type="checkbox" class="task-list-item-checkbox" disabled> 3
- </li>
- </ul>
-`;
-
-export const descriptionHtmlWithIssue = `
- <ul data-sourcepos="1:1-3:7" class="task-list" dir="auto">
- <li data-sourcepos="1:1-1:10" class="task-list-item">
- <input type="checkbox" class="task-list-item-checkbox" disabled>
- <a href="/gitlab-org/gitlab-test/-/issues/48" data-original="#48+" data-link="false" data-link-reference="false" data-project="1" data-issue="2" data-reference-format="+" data-reference-type="task" data-container="body" data-placement="top" title="1" class="gfm gfm-issue has-tooltip" data-issue-type="issue">1 (#48)</a>
- </li>
- <li data-sourcepos="2:1-2:7" class="task-list-item">
- <input type="checkbox" class="task-list-item-checkbox" disabled> 2
- </li>
- <li data-sourcepos="3:1-3:7" class="task-list-item">
- <input type="checkbox" class="task-list-item-checkbox" disabled> 3
- </li>
- </ul>
-`;
diff --git a/spec/graphql/mutations/achievements/award_spec.rb b/spec/graphql/mutations/achievements/award_spec.rb
new file mode 100644
index 00000000000..1bfad46a616
--- /dev/null
+++ b/spec/graphql/mutations/achievements/award_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Achievements::Award, feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:recipient) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+
+ describe '#resolve' do
+ subject(:resolve_mutation) do
+ described_class.new(object: nil, context: { current_user: current_user }, field: nil).resolve(
+ achievement_id: achievement&.to_global_id, user_id: recipient&.to_global_id
+ )
+ end
+
+ before_all do
+ group.add_developer(developer)
+ group.add_maintainer(maintainer)
+ end
+
+ context 'when the user does not have permission' do
+ let(:current_user) { developer }
+
+ it 'raises an error' do
+ expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ .with_message(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
+ end
+ end
+
+ context 'when the user has permission' do
+ let(:current_user) { maintainer }
+
+ context 'when the params are invalid' do
+ let(:achievement) { nil }
+
+ it 'returns the validation error' do
+ expect { resolve_mutation }.to raise_error { Gitlab::Graphql::Errors::ArgumentError }
+ end
+ end
+
+ it 'creates user_achievement with correct values' do
+ expect(resolve_mutation[:user_achievement]).to have_attributes({ achievement: achievement, user: recipient })
+ end
+ end
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:award_achievement) }
+end
diff --git a/spec/graphql/resolvers/achievements/achievements_resolver_spec.rb b/spec/graphql/resolvers/achievements/achievements_resolver_spec.rb
new file mode 100644
index 00000000000..666610dca33
--- /dev/null
+++ b/spec/graphql/resolvers/achievements/achievements_resolver_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Achievements::AchievementsResolver, feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+
+ specify do
+ expect(described_class).to have_nullable_graphql_type(Types::Achievements::AchievementType.connection_type)
+ end
+
+ describe '#resolve' do
+ it 'is not empty' do
+ expect(resolve_achievements).not_to be_empty
+ end
+
+ context 'when `achievements` feature flag is diabled' do
+ before do
+ stub_feature_flags(achievements: false)
+ end
+
+ it 'is empty' do
+ expect(resolve_achievements).to be_empty
+ end
+ end
+ end
+
+ def resolve_achievements
+ resolve(described_class, obj: group)
+ end
+end
diff --git a/spec/graphql/types/achievements/achievement_type_spec.rb b/spec/graphql/types/achievements/achievement_type_spec.rb
index f967dc8e25e..08fadcdff22 100644
--- a/spec/graphql/types/achievements/achievement_type_spec.rb
+++ b/spec/graphql/types/achievements/achievement_type_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe GitlabSchema.types['Achievement'], feature_category: :user_profil
description
created_at
updated_at
+ user_achievements
]
end
diff --git a/spec/graphql/types/achievements/user_achievement_type_spec.rb b/spec/graphql/types/achievements/user_achievement_type_spec.rb
new file mode 100644
index 00000000000..6b1512ff841
--- /dev/null
+++ b/spec/graphql/types/achievements/user_achievement_type_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['UserAchievement'], feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let(:fields) do
+ %w[
+ id
+ achievement
+ user
+ awarded_by_user
+ revoked_by_user
+ created_at
+ updated_at
+ revoked_at
+ ]
+ end
+
+ it { expect(described_class.graphql_name).to eq('UserAchievement') }
+ it { expect(described_class).to have_graphql_fields(fields) }
+ it { expect(described_class).to require_graphql_authorizations(:read_achievement) }
+end
diff --git a/spec/graphql/types/user_type_spec.rb b/spec/graphql/types/user_type_spec.rb
index a6b5d454b60..7a0b6c90ace 100644
--- a/spec/graphql/types/user_type_spec.rb
+++ b/spec/graphql/types/user_type_spec.rb
@@ -47,6 +47,7 @@ RSpec.describe GitlabSchema.types['User'], feature_category: :user_profile do
profileEnableGitpodPath
savedReplies
savedReply
+ user_achievements
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/helpers/plan_limits_helper_spec.rb b/spec/helpers/plan_limits_helper_spec.rb
new file mode 100644
index 00000000000..121338c9cf2
--- /dev/null
+++ b/spec/helpers/plan_limits_helper_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe PlanLimitsHelper, feature_category: :continuous_integration do
+ describe '#plan_limit_setting_description' do
+ it 'describes known limits', :aggregate_failures do
+ [
+ :ci_pipeline_size,
+ :ci_active_jobs,
+ :ci_active_pipelines,
+ :ci_project_subscriptions,
+ :ci_pipeline_schedules,
+ :ci_needs_size_limit,
+ :ci_registered_group_runners,
+ :ci_registered_project_runners,
+ :pipeline_hierarchy_size
+ ].each do |limit_name|
+ expect(helper.plan_limit_setting_description(limit_name)).to be_present
+ end
+ end
+
+ it 'raises an ArgumentError on invalid arguments' do
+ expect { helper.plan_limit_setting_description(:some_invalid_limit) }.to(
+ raise_error(ArgumentError, /No description/)
+ )
+ end
+ end
+end
diff --git a/spec/models/achievements/user_achievement_spec.rb b/spec/models/achievements/user_achievement_spec.rb
index 9d88bfdd477..291f843b824 100644
--- a/spec/models/achievements/user_achievement_spec.rb
+++ b/spec/models/achievements/user_achievement_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Achievements::UserAchievement, type: :model, feature_category: :u
it { is_expected.to belong_to(:achievement).inverse_of(:user_achievements).required }
it { is_expected.to belong_to(:user).inverse_of(:user_achievements).required }
- it { is_expected.to belong_to(:awarded_by_user).class_name('User').inverse_of(:awarded_user_achievements).optional }
+ it { is_expected.to belong_to(:awarded_by_user).class_name('User').inverse_of(:awarded_user_achievements).required }
it { is_expected.to belong_to(:revoked_by_user).class_name('User').inverse_of(:revoked_user_achievements).optional }
end
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 762b613a585..8de9932a519 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -182,7 +182,8 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
it { is_expected.not_to allow_value('default' => 101).for(:repository_storages_weighted).with_message("value for 'default' must be between 0 and 100") }
it { is_expected.not_to allow_value('default' => 100, shouldntexist: 50).for(:repository_storages_weighted).with_message("can't include: shouldntexist") }
- %i[notes_create_limit search_rate_limit search_rate_limit_unauthenticated users_get_by_id_limit].each do |setting|
+ %i[notes_create_limit search_rate_limit search_rate_limit_unauthenticated users_get_by_id_limit
+ projects_api_rate_limit_unauthenticated].each do |setting|
it { is_expected.to allow_value(400).for(setting) }
it { is_expected.not_to allow_value('two').for(setting) }
it { is_expected.not_to allow_value(nil).for(setting) }
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index e9980657634..61b7beb7e54 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -1569,4 +1569,22 @@ RSpec.describe GroupPolicy, feature_category: :system_access do
end
end
end
+
+ describe 'achievements' do
+ let(:current_user) { owner }
+
+ specify { is_expected.to be_allowed(:read_achievement) }
+ specify { is_expected.to be_allowed(:admin_achievement) }
+ specify { is_expected.to be_allowed(:award_achievement) }
+
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(achievements: false)
+ end
+
+ specify { is_expected.to be_disallowed(:read_achievement) }
+ specify { is_expected.to be_disallowed(:admin_achievement) }
+ specify { is_expected.to be_disallowed(:award_achievement) }
+ end
+ end
end
diff --git a/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb b/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb
new file mode 100644
index 00000000000..dc19f3bdc91
--- /dev/null
+++ b/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'UserAchievements', feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+ let_it_be(:user_achievement1) { create(:user_achievement, achievement: achievement, user: user) }
+ let_it_be(:user_achievement2) { create(:user_achievement, :revoked, achievement: achievement, user: user) }
+ let_it_be(:fields) do
+ <<~HEREDOC
+ id
+ achievements {
+ nodes {
+ userAchievements {
+ nodes {
+ id
+ achievement {
+ id
+ }
+ user {
+ id
+ }
+ awardedByUser {
+ id
+ }
+ revokedByUser {
+ id
+ }
+ }
+ }
+ }
+ }
+ HEREDOC
+ end
+
+ let_it_be(:query) do
+ graphql_query_for('namespace', { full_path: group.full_path }, fields)
+ end
+
+ before_all do
+ group.add_guest(user)
+ end
+
+ before do
+ post_graphql(query, current_user: user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns all user_achievements' do
+ expect(graphql_data_at(:namespace, :achievements, :nodes, :userAchievements, :nodes))
+ .to contain_exactly(
+ a_graphql_entity_for(user_achievement1),
+ a_graphql_entity_for(user_achievement2)
+ )
+ end
+
+ it 'can lookahead to eliminate N+1 queries', :use_clean_rails_memory_store_caching do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, current_user: user)
+ end.count
+
+ user2 = create(:user)
+ create_list(:achievement, 3, namespace: group) do |a|
+ create(:user_achievement, achievement: a, user: user2)
+ end
+
+ expect { post_graphql(query, current_user: user) }.not_to exceed_all_query_limit(control_count)
+ end
+
+ context 'when the achievements feature flag is disabled' do
+ before do
+ stub_feature_flags(achievements: false)
+ post_graphql(query, current_user: user)
+ end
+
+ specify { expect(graphql_data_at(:namespace, :achievements, :nodes, :userAchievements, :nodes)).to be_empty }
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/achievements/award_spec.rb b/spec/requests/api/graphql/mutations/achievements/award_spec.rb
new file mode 100644
index 00000000000..9bc0751e924
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/achievements/award_spec.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Achievements::Award, feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+ let_it_be(:recipient) { create(:user) }
+
+ let(:mutation) { graphql_mutation(:achievements_award, params) }
+ let(:achievement_id) { achievement&.to_global_id }
+ let(:recipient_id) { recipient&.to_global_id }
+ let(:params) do
+ {
+ achievement_id: achievement_id,
+ user_id: recipient_id
+ }
+ end
+
+ subject { post_graphql_mutation(mutation, current_user: current_user) }
+
+ def mutation_response
+ graphql_mutation_response(:achievements_create)
+ end
+
+ before_all do
+ group.add_developer(developer)
+ group.add_maintainer(maintainer)
+ end
+
+ context 'when the user does not have permission' do
+ let(:current_user) { developer }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not create an achievement' do
+ expect { subject }.not_to change { Achievements::UserAchievement.count }
+ end
+ end
+
+ context 'when the user has permission' do
+ let(:current_user) { maintainer }
+
+ context 'when the params are invalid' do
+ let(:achievement) { nil }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s).to include('invalid value for achievementId (Expected value to not be null)')
+ end
+ end
+
+ context 'when the recipient_id is invalid' do
+ let(:recipient_id) { "gid://gitlab/User/#{non_existing_record_id}" }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_data_at(:achievements_award,
+ :errors)).to include("Couldn't find User with 'id'=#{non_existing_record_id}")
+ end
+ end
+
+ context 'when the achievement_id is invalid' do
+ let(:achievement_id) { "gid://gitlab/Achievements::Achievement/#{non_existing_record_id}" }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(achievements: false)
+ end
+
+ it 'returns the relevant error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ it 'creates an achievement' do
+ expect { subject }.to change { Achievements::UserAchievement.count }.by(1)
+ end
+
+ it 'returns the new achievement' do
+ subject
+
+ expect(graphql_data_at(:achievements_award, :user_achievement, :achievement, :id))
+ .to eq(achievement.to_global_id.to_s)
+ expect(graphql_data_at(:achievements_award, :user_achievement, :user, :id))
+ .to eq(recipient.to_global_id.to_s)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/user/user_achievements_query_spec.rb b/spec/requests/api/graphql/user/user_achievements_query_spec.rb
new file mode 100644
index 00000000000..bf9b2b429cc
--- /dev/null
+++ b/spec/requests/api/graphql/user/user_achievements_query_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'UserAchievements', feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+ let_it_be(:user_achievements) { create_list(:user_achievement, 2, achievement: achievement, user: user) }
+ let_it_be(:fields) do
+ <<~HEREDOC
+ userAchievements {
+ nodes {
+ id
+ achievement {
+ id
+ }
+ user {
+ id
+ }
+ awardedByUser {
+ id
+ }
+ revokedByUser {
+ id
+ }
+ }
+ }
+ HEREDOC
+ end
+
+ let_it_be(:query) do
+ graphql_query_for('user', { id: user.to_global_id.to_s }, fields)
+ end
+
+ let(:current_user) { user }
+
+ before_all do
+ group.add_guest(user)
+ end
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns all user_achievements' do
+ expect(graphql_data_at(:user, :userAchievements, :nodes)).to contain_exactly(
+ a_graphql_entity_for(user_achievements[0]),
+ a_graphql_entity_for(user_achievements[1])
+ )
+ end
+
+ it 'can lookahead to eliminate N+1 queries', :use_clean_rails_memory_store_caching do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, current_user: user)
+ end.count
+
+ achievement2 = create(:achievement, namespace: group)
+ create_list(:user_achievement, 2, achievement: achievement2, user: user)
+
+ expect { post_graphql(query, current_user: user) }.not_to exceed_all_query_limit(control_count)
+ end
+
+ context 'when the achievements feature flag is disabled for a namespace' do
+ let_it_be(:group2) { create(:group) }
+ let_it_be(:achievement2) { create(:achievement, namespace: group2) }
+ let_it_be(:user_achievement2) { create(:user_achievement, achievement: achievement2, user: user) }
+
+ before do
+ stub_feature_flags(achievements: false)
+ stub_feature_flags(achievements: group2)
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'does not return user_achievements for that namespace' do
+ expect(graphql_data_at(:user, :userAchievements, :nodes)).to contain_exactly(
+ a_graphql_entity_for(user_achievement2)
+ )
+ end
+ end
+
+ context 'when current user is not a member of the private group' do
+ let(:current_user) { create(:user) }
+
+ specify { expect(graphql_data_at(:user, :userAchievements, :nodes)).to be_empty }
+ end
+end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index e78ef2f7630..de30c6ff420 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1109,6 +1109,66 @@ RSpec.describe API::Projects, feature_category: :projects do
end.not_to exceed_query_limit(control)
end
end
+
+ context 'rate limiting' do
+ let_it_be(:current_user) { create(:user) }
+
+ shared_examples_for 'does not log request and does not block the request' do
+ specify do
+ request
+ request
+
+ expect(response).not_to have_gitlab_http_status(:too_many_requests)
+ expect(Gitlab::AuthLogger).not_to receive(:error)
+ end
+ end
+
+ before do
+ stub_application_setting(projects_api_rate_limit_unauthenticated: 1)
+ end
+
+ context 'when the user is signed in' do
+ it_behaves_like 'does not log request and does not block the request' do
+ def request
+ get api('/projects', current_user)
+ end
+ end
+ end
+
+ context 'when the user is not signed in' do
+ let_it_be(:current_user) { nil }
+
+ it_behaves_like 'rate limited endpoint', rate_limit_key: :projects_api_rate_limit_unauthenticated do
+ def request
+ get api('/projects', current_user)
+ end
+ end
+ end
+
+ context 'when the feature flag `rate_limit_for_unauthenticated_projects_api_access` is disabled' do
+ before do
+ stub_feature_flags(rate_limit_for_unauthenticated_projects_api_access: false)
+ end
+
+ context 'when the user is not signed in' do
+ let_it_be(:current_user) { nil }
+
+ it_behaves_like 'does not log request and does not block the request' do
+ def request
+ get api('/projects', current_user)
+ end
+ end
+ end
+
+ context 'when the user is signed in' do
+ it_behaves_like 'does not log request and does not block the request' do
+ def request
+ get api('/projects', current_user)
+ end
+ end
+ end
+ end
+ end
end
describe 'POST /projects' do
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index cc377e94038..49403cf4142 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -67,6 +67,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
expect(json_response['jira_connect_proxy_url']).to eq(nil)
expect(json_response['user_defaults_to_private_profile']).to eq(false)
expect(json_response['default_syntax_highlighting_theme']).to eq(1)
+ expect(json_response['projects_api_rate_limit_unauthenticated']).to eq(400)
end
end
@@ -171,7 +172,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
bulk_import_enabled: false,
allow_runner_registration_token: true,
user_defaults_to_private_profile: true,
- default_syntax_highlighting_theme: 2
+ default_syntax_highlighting_theme: 2,
+ projects_api_rate_limit_unauthenticated: 100
}
expect(response).to have_gitlab_http_status(:ok)
@@ -240,6 +242,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
expect(json_response['allow_runner_registration_token']).to be(true)
expect(json_response['user_defaults_to_private_profile']).to be(true)
expect(json_response['default_syntax_highlighting_theme']).to eq(2)
+ expect(json_response['projects_api_rate_limit_unauthenticated']).to be(100)
end
end
diff --git a/spec/services/achievements/award_service_spec.rb b/spec/services/achievements/award_service_spec.rb
new file mode 100644
index 00000000000..fb45a634ddd
--- /dev/null
+++ b/spec/services/achievements/award_service_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Achievements::AwardService, feature_category: :user_profile do
+ describe '#execute' do
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+ let_it_be(:recipient) { create(:user) }
+
+ let(:achievement_id) { achievement.id }
+ let(:recipient_id) { recipient.id }
+
+ subject(:response) { described_class.new(current_user, achievement_id, recipient_id).execute }
+
+ before_all do
+ group.add_developer(developer)
+ group.add_maintainer(maintainer)
+ end
+
+ context 'when user does not have permission' do
+ let(:current_user) { developer }
+
+ it 'returns an error' do
+ expect(response).to be_error
+ expect(response.message).to match_array(
+ ['You have insufficient permissions to award this achievement'])
+ end
+ end
+
+ context 'when user has permission' do
+ let(:current_user) { maintainer }
+
+ it 'creates an achievement' do
+ expect(response).to be_success
+ end
+
+ context 'when the achievement is not persisted' do
+ let(:user_achievement) { instance_double('Achievements::UserAchievement') }
+
+ it 'returns the correct error' do
+ allow(user_achievement).to receive(:persisted?).and_return(false)
+ allow(user_achievement).to receive(:errors).and_return(nil)
+ allow(Achievements::UserAchievement).to receive(:create).and_return(user_achievement)
+
+ expect(response).to be_error
+ expect(response.message).to match_array(["Failed to award achievement"])
+ end
+ end
+
+ context 'when the achievement does not exist' do
+ let(:achievement_id) { non_existing_record_id }
+
+ it 'returns the correct error' do
+ expect(response).to be_error
+ expect(response.message)
+ .to contain_exactly("Couldn't find Achievements::Achievement with 'id'=#{non_existing_record_id}")
+ end
+ end
+
+ context 'when the recipient does not exist' do
+ let(:recipient_id) { non_existing_record_id }
+
+ it 'returns the correct error' do
+ expect(response).to be_error
+ expect(response.message).to contain_exactly("Couldn't find User with 'id'=#{non_existing_record_id}")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
index 4c081c8464e..ccfd403bebd 100644
--- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -59,6 +59,7 @@ RSpec.shared_context 'GroupPolicy context' do
create_cluster update_cluster admin_cluster add_cluster
destroy_upload
admin_achievement
+ award_achievement
]
end
diff --git a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
index bb33a7559dc..ed193d06161 100644
--- a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
@@ -42,6 +42,7 @@ RSpec.shared_examples "a user type with merge request interaction type" do
profileEnableGitpodPath
savedReplies
savedReply
+ user_achievements
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/views/admin/application_settings/network.html.haml_spec.rb b/spec/views/admin/application_settings/network.html.haml_spec.rb
new file mode 100644
index 00000000000..3df55be92d5
--- /dev/null
+++ b/spec/views/admin/application_settings/network.html.haml_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'admin/application_settings/network.html.haml', feature_category: :projects do
+ let_it_be(:admin) { build_stubbed(:admin) }
+ let_it_be(:application_setting) { build(:application_setting) }
+
+ before do
+ assign(:application_setting, application_setting)
+ allow(view).to receive(:current_user) { admin }
+ end
+
+ context 'for Projects API rate limit' do
+ it 'renders the `projects_api_rate_limit_unauthenticated` field' do
+ render
+
+ expect(rendered).to have_field('application_setting_projects_api_rate_limit_unauthenticated')
+ end
+
+ context 'when the feature flag `rate_limit_for_unauthenticated_projects_api_access` is turned off' do
+ before do
+ stub_feature_flags(rate_limit_for_unauthenticated_projects_api_access: false)
+ end
+
+ it 'does not render the `projects_api_rate_limit_unauthenticated` field' do
+ render
+
+ expect(rendered).not_to have_field('application_setting_projects_api_rate_limit_unauthenticated')
+ end
+ end
+ end
+end