diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index 8587312bb0893267278dcac50142e71e30ccaa36..3199739a33fadee1672392dc9d295671b087dca9 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -1043,7 +1043,6 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab /app/assets/javascripts/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql @gitlab-org/manage/authentication-and-authorization/approvers /app/assets/javascripts/authentication/ @gitlab-org/manage/authentication-and-authorization/approvers /app/assets/javascripts/ide/components/shared/tokened_input.vue @gitlab-org/manage/authentication-and-authorization/approvers -/app/assets/javascripts/invite_members/components/members_token_select.vue @gitlab-org/manage/authentication-and-authorization/approvers /app/assets/javascripts/packages_and_registries/package_registry/components/list/tokens/ @gitlab-org/manage/authentication-and-authorization/approvers /app/assets/javascripts/pages/admin/impersonation_tokens/ @gitlab-org/manage/authentication-and-authorization/approvers /app/assets/javascripts/pages/groups/settings/access_tokens/ @gitlab-org/manage/authentication-and-authorization/approvers diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index 0c178f349a53494778278b4580059100610ec94f..203f4826ae4f33fcf244648599a64276a2a62827 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -365,3 +365,8 @@ docker run --rm --privileged ${QEMU_IMAGE} --install all; fi - docker buildx create --use # creates and set's to active buildkit builder + +.use-kube-context: + before_script: + - export KUBE_CONTEXT="gitlab-org/gitlab:review-apps" + - kubectl config use-context ${KUBE_CONTEXT} diff --git a/.gitlab/ci/review-apps/main.gitlab-ci.yml b/.gitlab/ci/review-apps/main.gitlab-ci.yml index 27e2014baf1250e994417005ecdeec3bd7036f9a..cda13dd4be9d9ba5cadb5bcfc2de3317ab946e07 100644 --- a/.gitlab/ci/review-apps/main.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/main.gitlab-ci.yml @@ -123,6 +123,7 @@ review-deploy: - echo "${CI_ENVIRONMENT_URL}" > environment_url.txt - echo "QA_GITLAB_URL=${CI_ENVIRONMENT_URL}" > environment.env - *base-before_script + - !reference [".use-kube-context", before_script] script: - run_timed_command "check_kube_domain" - run_timed_command "download_chart" @@ -156,6 +157,7 @@ review-deploy-sample-projects: - export GITLAB_WORKHORSE_VERSION=$(<GITLAB_WORKHORSE_VERSION) - echo "${CI_ENVIRONMENT_URL}" > environment_url.txt - *base-before_script + - !reference [".use-kube-context", before_script] script: - date - create_sample_projects @@ -173,6 +175,7 @@ review-deploy-sample-projects: before_script: - source ./scripts/utils.sh - source ./scripts/review_apps/review-apps.sh + - !reference [".use-kube-context", before_script] review-delete-deployment: extends: @@ -186,7 +189,7 @@ review-stop: extends: - .review-stop-base - .review:rules:review-stop - resource_group: review/${CI_COMMIT_REF_SLUG}${SCHEDULE_TYPE} # CI_ENVIRONMENT_SLUG is not available here and we want this to be the same as the environment + resource_group: review/${CI_COMMIT_REF_SLUG}${SCHEDULE_TYPE} # CI_ENVIRONMENT_SLUG is not available here and we want this to be the same as the environment stage: deploy needs: [] script: diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index 43a3a41819add03d400631432f3cfec46d4bba5a..1fb8985aba168a39e47545bd446ef80a63021679 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -7,9 +7,12 @@ review-cleanup: environment: name: review/regular-cleanup action: access + variables: + KUBE_NAMESPACE: "review-apps" # gcp_cleanup.sh requires passing KUBE_NAMESPACE variable before_script: - source scripts/utils.sh - source scripts/review_apps/gcp_cleanup.sh + - !reference [".use-kube-context", before_script] - install_gitlab_gem - setup_gcp_dependencies script: @@ -38,6 +41,8 @@ review-k8s-resources-count-checks: environment: name: review/k8s-resources-count-checks action: verify + before_script: + - !reference [".use-kube-context", before_script] script: - scripts/review_apps/k8s-resources-count-checks.sh || (scripts/slack review-apps-monitoring "â˜ ï¸ \`${CI_JOB_NAME}\` failed! â˜ ï¸ See ${CI_JOB_URL}" warning "GitLab Bot" && exit 1); @@ -49,6 +54,8 @@ review-gcp-quotas-checks: environment: name: review/gcp-quotas-checks action: verify + before_script: + - !reference [".use-kube-context", before_script] script: - ruby scripts/review_apps/gcp-quotas-checks.rb || (scripts/slack review-apps-monitoring "â˜ ï¸ \`${CI_JOB_NAME}\` failed! â˜ ï¸ See ${CI_JOB_URL}" warning "GitLab Bot" && exit 1); diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 830fbf64d38a7c460b2ea6dca57d4a8f241483be..c40f574468688307a8b129ece7d2d236537afaba 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -1903,14 +1903,17 @@ - <<: *if-dot-com-gitlab-org-merge-request changes: *controllers-patterns variables: *review-change-pattern + when: manual allow_failure: true - <<: *if-dot-com-gitlab-org-merge-request changes: *models-patterns variables: *review-change-pattern + when: manual allow_failure: true - <<: *if-dot-com-gitlab-org-merge-request changes: *lib-gitlab-patterns variables: *review-change-pattern + when: manual allow_failure: true - <<: *if-dot-com-gitlab-org-merge-request changes: *qa-patterns diff --git a/app/assets/javascripts/issues/list/components/issue_card_statistics.vue b/app/assets/javascripts/issues/list/components/issue_card_statistics.vue new file mode 100644 index 0000000000000000000000000000000000000000..2d00c3e549da99c0c362dc87ff21d93ef482d1db --- /dev/null +++ b/app/assets/javascripts/issues/list/components/issue_card_statistics.vue @@ -0,0 +1,56 @@ +<script> +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; +import { i18n } from '../constants'; + +export default { + i18n, + components: { + GlIcon, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + issue: { + type: Object, + required: true, + }, + }, +}; +</script> + +<template> + <ul class="gl-display-contents"> + <li + v-if="issue.mergeRequestsCount" + v-gl-tooltip + class="gl-display-none gl-sm-display-block gl-mr-3" + :title="$options.i18n.relatedMergeRequests" + data-testid="merge-requests" + > + <gl-icon name="merge-request" /> + {{ issue.mergeRequestsCount }} + </li> + <li + v-if="issue.upvotes" + v-gl-tooltip + class="gl-display-none gl-sm-display-block gl-mr-3" + :title="$options.i18n.upvotes" + data-testid="issuable-upvotes" + > + <gl-icon name="thumb-up" /> + {{ issue.upvotes }} + </li> + <li + v-if="issue.downvotes" + v-gl-tooltip + class="gl-display-none gl-sm-display-block gl-mr-3" + :title="$options.i18n.downvotes" + data-testid="issuable-downvotes" + > + <gl-icon name="thumb-down" /> + {{ issue.downvotes }} + </li> + <slot></slot> + </ul> +</template> diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue index 021e3c867b001631f9b3578dde37fed26d4ad188..29faf6ed16cf2c151cb2089caab9d585ef196333 100644 --- a/app/assets/javascripts/issues/list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue @@ -1,13 +1,8 @@ <script> -import { - GlButton, - GlEmptyState, - GlFilteredSearchToken, - GlIcon, - GlTooltipDirective, -} from '@gitlab/ui'; +import { GlButton, GlEmptyState, GlFilteredSearchToken, GlTooltipDirective } from '@gitlab/ui'; import * as Sentry from '@sentry/browser'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; +import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue'; import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue'; import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql'; import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql'; @@ -116,9 +111,9 @@ export default { EmptyStateSignedOut, GlButton, GlEmptyState, - GlIcon, IssuableByEmail, IssuableList, + IssueCardStatistics, IssueCardTimeInfo, NewIssueDropdown, }, @@ -854,37 +849,7 @@ export default { </template> <template #statistics="{ issuable = {} }"> - <li - v-if="issuable.mergeRequestsCount" - v-gl-tooltip - class="gl-display-none gl-sm-display-block" - :title="$options.i18n.relatedMergeRequests" - data-testid="merge-requests" - > - <gl-icon name="merge-request" /> - {{ issuable.mergeRequestsCount }} - </li> - <li - v-if="issuable.upvotes" - v-gl-tooltip - class="gl-display-none gl-sm-display-block" - :title="$options.i18n.upvotes" - data-testid="issuable-upvotes" - > - <gl-icon name="thumb-up" /> - {{ issuable.upvotes }} - </li> - <li - v-if="issuable.downvotes" - v-gl-tooltip - class="gl-display-none gl-sm-display-block" - :title="$options.i18n.downvotes" - data-testid="issuable-downvotes" - > - <gl-icon name="thumb-down" /> - {{ issuable.downvotes }} - </li> - <slot name="blocking-count" :issuable="issuable"></slot> + <issue-card-statistics :issue="issuable" /> </template> <template #empty-state> diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js index fd7d53e7a369e77f5aef6edd4df984fb2d0f9aa1..16d28e8404cf67ef20b9b916b7c39fec999f4527 100644 --- a/app/assets/javascripts/issues/list/constants.js +++ b/app/assets/javascripts/issues/list/constants.js @@ -147,7 +147,7 @@ export const specialFilterValues = [ export const TYPE_TOKEN_TASK_OPTION = { icon: 'issue-type-task', title: 'task', value: 'task' }; export const TYPE_TOKEN_OBJECTIVE_OPTION = { - icon: 'issue-type-issue', + icon: 'issue-type-objective', title: 'objective', value: 'objective', }; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue index f71b1fbc539847a7eeca81081f68fd3d3119f7c0..79ea2624ec510923d5351075e3bbe80a5aa685d7 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue @@ -1,8 +1,11 @@ <script> -import { GlTooltipDirective } from '@gitlab/ui'; +import { GlTooltipDirective, GlLink } from '@gitlab/ui'; export default { name: 'MrWidgetAuthor', + components: { + GlLink, + }, directives: { GlTooltip: GlTooltipDirective, }, @@ -28,13 +31,16 @@ export default { }; </script> <template> - <a + <gl-link v-gl-tooltip :href="authorUrl" :title="showAuthorName ? null : author.name" - class="author-link inline" + class="mr-widget-author" > - <img :src="avatarUrl" class="avatar avatar-inline s16" /> - <span v-if="showAuthorName" class="author">{{ author.name }}</span> - </a> + <img :src="avatarUrl" :alt="author.name" class="avatar avatar-inline s16" /><span + v-if="showAuthorName" + class="author" + >{{ author.name }}</span + > + </gl-link> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index a15bf6fadd89bac284020ab433500d682debdda2..6b9a8f59aa89a0881917e882a2e6983ef9a866d3 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -467,8 +467,9 @@ export default { <template> <div + :class="{ 'gl-bg-gray-10': mr.state !== 'closed' && mr.state !== 'merged' }" data-testid="ready_to_merge_state" - class="gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-pl-7" + class="gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-pl-7" > <div v-if="loading" class="mr-widget-body"> <div class="gl-w-full mr-ready-to-merge-loader"> diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index cdb69586969a7308e42444122f011aca304edbc0..aef488739eec5031580b856352da88c1e25875ce 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -112,7 +112,7 @@ export const WORK_ITEMS_TYPE_MAP = { name: s__('WorkItem|Requirements'), }, [WORK_ITEM_TYPE_ENUM_OBJECTIVE]: { - icon: `issue-type-issue`, + icon: `issue-type-objective`, name: s__('WorkItem|Objective'), }, [WORK_ITEM_TYPE_ENUM_KEY_RESULT]: { diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss index b4ede515a7c02f9f013bbe0d71696f864a32a67b..f36f6d652ba427f3e31801a0983658d7da36fd05 100644 --- a/app/assets/stylesheets/page_bundles/merge_requests.scss +++ b/app/assets/stylesheets/page_bundles/merge_requests.scss @@ -1053,15 +1053,14 @@ $tabs-holder-z-index: 250; } } -.mr-ready-merge-related-links, -.mr-widget-merge-details { - a { - @include gl-text-decoration-underline; +.mr-ready-merge-related-links a, +.mr-widget-merge-details a, +.mr-widget-author { + @include gl-text-decoration-underline; - &:hover, - &:focus { - @include gl-text-decoration-none; - } + &:hover, + &:focus { + @include gl-text-decoration-none; } } diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb index 8acbba0621bd12edfcc419c3f093189dcf748119..2ebc1d67ec767cacd0f2ee98784d589eb543a4d9 100644 --- a/app/controllers/concerns/lfs_request.rb +++ b/app/controllers/concerns/lfs_request.rb @@ -94,10 +94,15 @@ def lfs_upload_access? next false unless has_authentication_ability?(:push_code) next false if limit_exceeded? - lfs_deploy_token? || can?(user, :push_code, project) || can?(deploy_token, :push_code, project) + lfs_deploy_token? || can?(user, :push_code, +project) || can?(deploy_token, :push_code, project) || any_branch_allows_collaboration? end end + def any_branch_allows_collaboration? + project.merge_requests_allowing_push_to_user(user).any? + end + def lfs_deploy_token? authentication_result.lfs_deploy_token?(project) end diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index 90d5f945d782feb1f85ea46a8ae86287051edab9..39e8f6c500d5bb370fc591663e40c1073bf75279 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -37,6 +37,6 @@ def destroy private def key_params - params.require(:key).permit(:title, :key, :expires_at) + params.require(:key).permit(:title, :key, :usage_type, :expires_at) end end diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb index 9c2462b42a6dfc96a1c3e633f6f44154f1831cd5..11e3c341c1fff78a803f7a78f1d3b4489f81e73c 100644 --- a/app/finders/users_finder.rb +++ b/app/finders/users_finder.rb @@ -55,7 +55,7 @@ def execute private def base_scope - scope = current_user&.admin? ? User.all : User.without_forbidden_states + scope = current_user&.can_admin_all_resources? ? User.all : User.without_forbidden_states scope.order_id_desc end @@ -80,7 +80,7 @@ def by_admins(users) def by_search(users) return users unless params[:search].present? - users.search(params[:search], with_private_emails: current_user&.admin?) + users.search(params[:search], with_private_emails: current_user&.can_admin_all_resources?) end def by_blocked(users) @@ -97,7 +97,7 @@ def by_active(users) # rubocop: disable CodeReuse/ActiveRecord def by_external_identity(users) - return users unless current_user&.admin? && params[:extern_uid] && params[:provider] + return users unless current_user&.can_admin_all_resources? && params[:extern_uid] && params[:provider] users.joins(:identities).merge(Identity.with_extern_uid(params[:provider], params[:extern_uid])) end diff --git a/app/graphql/types/subscription_type.rb b/app/graphql/types/subscription_type.rb index 9d5edec82b2da119cb8ca9776925482ba0ba9eaa..f7f26ba4c5aa8e652a70f771bd58a51bedf98afa 100644 --- a/app/graphql/types/subscription_type.rb +++ b/app/graphql/types/subscription_type.rb @@ -34,6 +34,11 @@ class SubscriptionType < ::Types::BaseObject subscription: Subscriptions::IssuableUpdated, null: true, description: 'Triggered when the merge status of a merge request is updated.' + + field :merge_request_approval_state_updated, + subscription: Subscriptions::IssuableUpdated, + null: true, + description: 'Triggered when approval state of a merge request is updated.' end end diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb index bfe39bbc2114330ae4f4f44b0f89088c352f3fa7..979b979fba713a692873c33e7367d87030041a86 100644 --- a/app/helpers/profiles_helper.rb +++ b/app/helpers/profiles_helper.rb @@ -46,6 +46,14 @@ def middle_dot_divider_classes(stacking, breakpoint) end end + def ssh_key_usage_types + { + s_('SSHKey|Authentication & Signing') => 'auth_and_signing', + s_('SSHKey|Authentication') => 'auth', + s_('SSHKey|Signing') => 'signing' + } + end + # Overridden in EE::ProfilesHelper#ssh_key_expiration_tooltip def ssh_key_expiration_tooltip(key) return key.errors.full_messages.join(', ') if key.errors.full_messages.any? diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index b8ac2afa7d67cace64a4e455e28424480a7cc6de..54700e634b66169e97377f35b3398beff86274d9 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -40,6 +40,7 @@ def resource_results(term) [ groups_autocomplete(term), projects_autocomplete(term), + users_autocomplete(term), issue_autocomplete(term) ].flatten end @@ -351,6 +352,25 @@ def projects_autocomplete(term, limit = 5) end end + def users_autocomplete(term, limit = 5) + return [] unless current_user && Ability.allowed?(current_user, :read_users_list) + + SearchService + .new(current_user, { scope: 'users', search: term }) + .search_objects + .limit(limit) + .map do |user| + { + category: "Users", + id: user.id, + value: "#{search_result_sanitize(user.name)}", + label: "#{search_result_sanitize(user.username)}", + url: user_path(user), + avatar_url: user.avatar_url || '' + } + end + end + def recent_merge_requests_autocomplete(term) return [] unless current_user diff --git a/app/models/group_deploy_key.rb b/app/models/group_deploy_key.rb index c65b00a6de05f28ca49157d43bab71ed7d190770..9495df7ab6dfc295888d989864aa30325b325410 100644 --- a/app/models/group_deploy_key.rb +++ b/app/models/group_deploy_key.rb @@ -12,6 +12,11 @@ class GroupDeployKey < Key joins(:group_deploy_keys_groups).where(group_deploy_keys_groups: { group_id: group_ids }).uniq end + # Remove usage_type because it defined in Key class but doesn't have a column in group_deploy_keys table + def self.defined_enums + super.without('usage_type') + end + def type 'DeployKey' end diff --git a/app/models/key.rb b/app/models/key.rb index 78b0a38bcaae68ca1bd5fc967c9901c854f3d9ab..35fc42a935fa079e5838409678bab8df491f96b1 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -32,6 +32,12 @@ class Key < ApplicationRecord delegate :name, :email, to: :user, prefix: true + enum usage_type: { + auth_and_signing: 0, + auth: 1, + signing: 2 + } + after_commit :add_to_authorized_keys, on: :create after_create :post_create_hook after_create :refresh_user_cache @@ -45,6 +51,8 @@ class Key < ApplicationRecord scope :preload_users, -> { preload(:user) } scope :for_user, -> (user) { where(user: user) } scope :order_last_used_at_desc, -> { reorder(arel_table[:last_used_at].desc.nulls_last) } + scope :auth, -> { where(usage_type: [:auth, :auth_and_signing]) } + scope :signing, -> { where(usage_type: [:signing, :auth_and_signing]) } # Date is set specifically in this scope to improve query time. scope :expired_today_and_not_notified, -> { where(["date(expires_at AT TIME ZONE 'UTC') = CURRENT_DATE AND expiry_notification_delivered_at IS NULL"]) } diff --git a/app/models/user.rb b/app/models/user.rb index b2235bff456e443d412a084cb94a0683098475ad..dae7ea75583381fc41c5949b9aff54c684bfbf95 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -811,7 +811,7 @@ def find_by_username!(username) # Returns a user for the given SSH key. def find_by_ssh_key_id(key_id) - find_by('EXISTS (?)', Key.select(1).where('keys.user_id = users.id').where(id: key_id)) + find_by('EXISTS (?)', Key.select(1).where('keys.user_id = users.id').auth.where(id: key_id)) end def find_by_full_path(path, follow_redirects: false) diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb index dc30899d24fb6cc6e603ac568fe2d24b2e527862..51ec5676ae6dd34252efb5fc81740659bd1b44b3 100644 --- a/app/models/work_items/type.rb +++ b/app/models/work_items/type.rb @@ -10,17 +10,29 @@ class Type < ApplicationRecord include CacheMarkdownField + # type name is used in restrictions DB seeder to assure restrictions for + # default types are pre-filled + TYPE_NAMES = { + issue: 'Issue', + incident: 'Incident', + test_case: 'Test Case', + requirement: 'Requirement', + task: 'Task', + objective: 'Objective', + key_result: 'Key Result' + }.freeze + # Base types need to exist on the DB on app startup # This constant is used by the DB seeder # TODO - where to add new icon names created? BASE_TYPES = { - issue: { name: 'Issue', icon_name: 'issue-type-issue', enum_value: 0 }, - incident: { name: 'Incident', icon_name: 'issue-type-incident', enum_value: 1 }, - test_case: { name: 'Test Case', icon_name: 'issue-type-test-case', enum_value: 2 }, ## EE-only - requirement: { name: 'Requirement', icon_name: 'issue-type-requirements', enum_value: 3 }, ## EE-only - task: { name: 'Task', icon_name: 'issue-type-task', enum_value: 4 }, - objective: { name: 'Objective', icon_name: 'issue-type-objective', enum_value: 5 }, ## EE-only - key_result: { name: 'Key Result', icon_name: 'issue-type-keyresult', enum_value: 6 } ## EE-only + issue: { name: TYPE_NAMES[:issue], icon_name: 'issue-type-issue', enum_value: 0 }, + incident: { name: TYPE_NAMES[:incident], icon_name: 'issue-type-incident', enum_value: 1 }, + test_case: { name: TYPE_NAMES[:test_case], icon_name: 'issue-type-test-case', enum_value: 2 }, ## EE-only + requirement: { name: TYPE_NAMES[:requirement], icon_name: 'issue-type-requirements', enum_value: 3 }, ## EE-only + task: { name: TYPE_NAMES[:task], icon_name: 'issue-type-task', enum_value: 4 }, + objective: { name: TYPE_NAMES[:objective], icon_name: 'issue-type-objective', enum_value: 5 }, ## EE-only + key_result: { name: TYPE_NAMES[:key_result], icon_name: 'issue-type-keyresult', enum_value: 6 } ## EE-only }.freeze WIDGETS_FOR_TYPE = { @@ -66,6 +78,7 @@ def self.default_by_type(type) return found_type if found_type Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.upsert_types + Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter.upsert_restrictions find_by(namespace_id: nil, base_type: type) end diff --git a/app/services/users/keys_count_service.rb b/app/services/users/keys_count_service.rb index f82d27eded9c6b49d51d6ff9c3f78274ee363dde..378093f2e1bf73e539c30c1f6b9aca84192ed051 100644 --- a/app/services/users/keys_count_service.rb +++ b/app/services/users/keys_count_service.rb @@ -11,7 +11,7 @@ def initialize(user) end def relation_for_count - user.keys + user.keys.auth end def raw? diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml index e6d91543585be0a203590a4a2ea8222ffd37c1f8..cf51d120edfc5012207bc58256dd1f95f59edfd5 100644 --- a/app/views/profiles/keys/_form.html.haml +++ b/app/views/profiles/keys/_form.html.haml @@ -12,7 +12,12 @@ = f.label :title, s_('Profiles|Title'), class: 'label-bold' = f.text_field :title, class: "form-control gl-form-input input-lg", required: true, placeholder: s_('Profiles|Example: MacBook key'), data: { qa_selector: 'key_title_field' } %p.form-text.text-muted= s_('Profiles|Key titles are publicly visible.') - + - if Feature.enabled?(:ssh_key_usage_types, current_user) + .form-row + .col.form-group + = f.label :usage_type, s_('Profiles|Usage type') + .gl-md-form-input-lg + = f.select :usage_type, options_for_select(ssh_key_usage_types, :auth_and_signing), {}, { class: 'gl-form-select custom-select' } .form-row .col.form-group .js-access-tokens-expires-at{ data: {min_date: Date.tomorrow, max_date: max_date, default_date_offset: 365, description: ssh_key_expires_field_description } } diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml index de4a19bdad74ee39ca52b70a8d48bd2219e5f344..f0e4b143d0d41c84264e7a06c808384c066e41a1 100644 --- a/app/views/profiles/keys/_key.html.haml +++ b/app/views/profiles/keys/_key.html.haml @@ -25,6 +25,10 @@ %span.expires.gl-mr-3 = key.expired? ? s_('Profiles|Expired:') : s_('Profiles|Expires:') = key.expires_at ? key.expires_at.to_date : _('Never') + - if Feature.enabled?(:ssh_key_usage_types, current_user) + %span.last-used-at.gl-mr-3 + = s_('Profiles|Usage type:') + = ssh_key_usage_types.invert[key.usage_type] %span.key-created-at.gl-display-flex.gl-align-items-center - if key.can_delete? .gl-ml-3 diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml index 04fa1d9620440d3b7efbb74b1e57f1fa1fd23272..1a8de16471f1db03ef3869020745fee86279651d 100644 --- a/app/views/profiles/keys/_key_details.html.haml +++ b/app/views/profiles/keys/_key_details.html.haml @@ -9,6 +9,10 @@ %li %span.light= _('Title:') %strong= @key.title + - if Feature.enabled?(:ssh_key_usage_types, current_user) + %li + %span.light= s_('Profiles|Usage type:') + %strong= ssh_key_usage_types.invert[@key.usage_type] %li %span.light= _('Created on:') %strong= @key.created_at.to_s(:medium) diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml index 73ace033dc6a49cd59fa4893b66939cab2bc97fd..a749d1037a1685b9eb0d07bc6507b80d1693d45c 100644 --- a/app/views/shared/_file_highlight.html.haml +++ b/app/views/shared/_file_highlight.html.haml @@ -1,16 +1,28 @@ +-# We're not using `link_to` in the line loop because it is too slow once we get to thousands of lines. + +- offset = defined?(first_line_number) ? first_line_number : 1 +- highlight = defined?(highlight_line) && highlight_line ? highlight_line - offset : nil +- file_line_blame = Feature.enabled?(:file_line_blame) + +- if file_line_blame + - line_class = "js-line-links" + - blame_path = project_blame_path(@project, tree_join(@ref, blob.path)) +- else + - line_class = nil + - blame_path = nil + +- highlighted_blob = blob.present.highlight + #blob-content.file-content.code.js-syntax-highlight - - offset = defined?(first_line_number) ? first_line_number : 1 - - if Feature.enabled?(:file_line_blame) - - blame_path = project_blame_path(@project, tree_join(@ref, blob.path)) .line-numbers{ class: "gl-px-0!", data: { blame_path: blame_path } } - if blob.data.present? - - blob.data.each_line.each_with_index do |_, index| + - highlighted_blob.lines.count.times do |index| - i = index + offset - -# We're not using `link_to` because it is too slow once we get to thousands of lines. - %a.file-line-num.diff-line-num{ class: ("js-line-links" if Feature.enabled?(:file_line_blame)), href: "#L#{i}", id: "L#{i}", 'data-line-number' => i } + + %a.file-line-num.diff-line-num{ class: line_class, href: "#L#{i}", id: "L#{i}", 'data-line-number' => i } = i - - highlight = defined?(highlight_line) && highlight_line ? highlight_line - offset : nil + .blob-content{ data: { blob_id: blob.id, path: blob.path, highlight_line: highlight, qa_selector: 'file_content' } } %pre.code.highlight %code - = blob.present.highlight + = highlighted_blob diff --git a/config/feature_flags/development/ssh_key_usage_types.yml b/config/feature_flags/development/ssh_key_usage_types.yml new file mode 100644 index 0000000000000000000000000000000000000000..fc7c719c13a894cc934ee5ae71ce362cfea433d4 --- /dev/null +++ b/config/feature_flags/development/ssh_key_usage_types.yml @@ -0,0 +1,8 @@ +--- +name: ssh_key_usage_types +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104283 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/383046 +milestone: '15.7' +type: development +group: group::source code +default_enabled: false diff --git a/config/open_api.yml b/config/open_api.yml index e01b526552582e56bd39afdf2600d0714c0b6bb5..100bf4df67e1790b389f60e89f3c163e027c832e 100644 --- a/config/open_api.yml +++ b/config/open_api.yml @@ -29,6 +29,8 @@ metadata: description: Operations related to clusters - name: container_registry description: Operations related to container registry + - name: container_registry_event + description: Operations related to container registry events - name: dashboard_annotations description: Operations related to dashboard annotations - name: dependency_proxy @@ -99,6 +101,8 @@ metadata: description: Operations related to project packages - name: protected environments description: Operations related to protected environments + - name: pypi_packages + description: Operations related to PyPI packages - name: release_links description: Operations related to release assets (links) - name: releases diff --git a/db/fixtures/development/50_create_work_item_hierarchy_restrictions.rb b/db/fixtures/development/50_create_work_item_hierarchy_restrictions.rb new file mode 100644 index 0000000000000000000000000000000000000000..b5c5d0cacdd9e03ef6692787eda414769e785d3c --- /dev/null +++ b/db/fixtures/development/50_create_work_item_hierarchy_restrictions.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Gitlab::Seeder.quiet do + Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter.upsert_restrictions +end diff --git a/db/fixtures/production/020_create_work_item_hierarchy_restrictions.rb b/db/fixtures/production/020_create_work_item_hierarchy_restrictions.rb new file mode 100644 index 0000000000000000000000000000000000000000..b5c5d0cacdd9e03ef6692787eda414769e785d3c --- /dev/null +++ b/db/fixtures/production/020_create_work_item_hierarchy_restrictions.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Gitlab::Seeder.quiet do + Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter.upsert_restrictions +end diff --git a/db/migrate/20221116161126_add_auth_signing_type_to_keys.rb b/db/migrate/20221116161126_add_auth_signing_type_to_keys.rb new file mode 100644 index 0000000000000000000000000000000000000000..795074fa0ca610fb2ab6c234cd23574d36e2c3e9 --- /dev/null +++ b/db/migrate/20221116161126_add_auth_signing_type_to_keys.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddAuthSigningTypeToKeys < Gitlab::Database::Migration[2.0] + def change + add_column :keys, :usage_type, :integer, limit: 2, null: false, default: 0 + end +end diff --git a/db/post_migrate/20221116143854_add_okr_hierarchy_restrictions.rb b/db/post_migrate/20221116143854_add_okr_hierarchy_restrictions.rb new file mode 100644 index 0000000000000000000000000000000000000000..658ce0287f840b5381bda5d0dc03fc4ecb5d263f --- /dev/null +++ b/db/post_migrate/20221116143854_add_okr_hierarchy_restrictions.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +class AddOkrHierarchyRestrictions < Gitlab::Database::Migration[2.0] + class WorkItemType < MigrationRecord + self.table_name = 'work_item_types' + end + + class HierarchyRestriction < MigrationRecord + self.table_name = 'work_item_hierarchy_restrictions' + end + + restrict_gitlab_migration gitlab_schema: :gitlab_main + disable_ddl_transaction! + + def up + objective = WorkItemType.find_by_name_and_namespace_id('Objective', nil) + key_result = WorkItemType.find_by_name_and_namespace_id('Key Result', nil) + issue = WorkItemType.find_by_name_and_namespace_id('Issue', nil) + task = WorkItemType.find_by_name_and_namespace_id('Task', nil) + incident = WorkItemType.find_by_name_and_namespace_id('Incident', nil) + + # work item default types should be filled, if this is not the case + # then restrictions will be created together with work item types + unless objective && key_result && issue && task && incident + Gitlab::AppLogger.warn('default types are missing, not adding restrictions') + + return + end + + restrictions = [ + { parent_type_id: objective.id, child_type_id: objective.id, maximum_depth: 9 }, + { parent_type_id: objective.id, child_type_id: key_result.id, maximum_depth: 1 }, + { parent_type_id: issue.id, child_type_id: task.id, maximum_depth: 1 }, + { parent_type_id: incident.id, child_type_id: task.id, maximum_depth: 1 } + ] + + HierarchyRestriction.upsert_all( + restrictions, + unique_by: :index_work_item_hierarchy_restrictions_on_parent_and_child + ) + end + + def down + # so far restrictions table was empty so we can delete all records when + # migrating down + HierarchyRestriction.delete_all + end +end diff --git a/db/schema_migrations/20221116143854 b/db/schema_migrations/20221116143854 new file mode 100644 index 0000000000000000000000000000000000000000..9f0b0815c7974fee7152f973bf84379fd228cc24 --- /dev/null +++ b/db/schema_migrations/20221116143854 @@ -0,0 +1 @@ +a6caf06dd18f096219d5ce0752c956ef099a92df71899c1b9164d3a16f6ef0ba \ No newline at end of file diff --git a/db/schema_migrations/20221116161126 b/db/schema_migrations/20221116161126 new file mode 100644 index 0000000000000000000000000000000000000000..5d65ed55915ad44596b899549880ad0794040ac1 --- /dev/null +++ b/db/schema_migrations/20221116161126 @@ -0,0 +1 @@ +93286f75aec167041985c2cde8ef1fc32447eae4f520c87131b89c28c402675c \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 69b25972812eeae7b487d0630fc6bc49e56a09ef..ab7b632b04a400fcadc1e16ba3360da45ed198c4 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -17126,7 +17126,8 @@ CREATE TABLE keys ( fingerprint_sha256 bytea, expires_at timestamp with time zone, expiry_notification_delivered_at timestamp with time zone, - before_expiry_notification_delivered_at timestamp with time zone + before_expiry_notification_delivered_at timestamp with time zone, + usage_type smallint DEFAULT 0 NOT NULL ); CREATE SEQUENCE keys_id_seq diff --git a/doc/administration/feature_flags.md b/doc/administration/feature_flags.md index f2a40b605368a5f5297a799dd917745512b5ef3e..f7237b167e5552a7a11b7d853fb9ae6bc71c152f 100644 --- a/doc/administration/feature_flags.md +++ b/doc/administration/feature_flags.md @@ -45,8 +45,7 @@ Features that are disabled by default may change or be removed without notice in Data corruption, stability degradation, performance degradation, or security issues might occur if you enable a feature that's disabled by default. Problems caused by using a default -disabled feature aren't covered by GitLab support, unless you were directed by GitLab -to enable the feature. +disabled feature aren't covered by GitLab Support. Security issues found in features that are disabled by default are patched in regular releases and do not follow our regular [maintenance policy](../policy/maintenance.md#security-releases) diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index e1fe6a0c1bfb04b7c908224ac839d0d68814c45c..1b606cc50bfe08780979221e9efac84461829492 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -5795,7 +5795,7 @@ Input type: `VulnerabilityDismissInput` | Name | Type | Description | | ---- | ---- | ----------- | | <a id="mutationvulnerabilitydismissclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | -| <a id="mutationvulnerabilitydismisscomment"></a>`comment` | [`String`](#string) | Comment why vulnerability should be dismissed. | +| <a id="mutationvulnerabilitydismisscomment"></a>`comment` | [`String`](#string) | Comment why vulnerability should be dismissed (max. 50 000 characters). | | <a id="mutationvulnerabilitydismissdismissalreason"></a>`dismissalReason` | [`VulnerabilityDismissalReason`](#vulnerabilitydismissalreason) | Reason why vulnerability should be dismissed. | | <a id="mutationvulnerabilitydismissid"></a>`id` | [`VulnerabilityID!`](#vulnerabilityid) | ID of the vulnerability to be dismissed. | diff --git a/doc/api/merge_request_approvals.md b/doc/api/merge_request_approvals.md index 0476035784a47a9be84fb1cadb1106aa23d64f81..4d52bf36f20d5ae6a3ef0665ccb9484a23504413 100644 --- a/doc/api/merge_request_approvals.md +++ b/doc/api/merge_request_approvals.md @@ -433,6 +433,7 @@ Supported attributes: | `applies_to_all_protected_branches` | boolean | **{dotted-circle}** No | Whether the rule is applied to all protected branches. If set to `true`, the value of `protected_branch_ids` is ignored. Default is `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/335316) in GitLab 15.3. | | `group_ids` | Array | **{dotted-circle}** No | The IDs of groups as approvers. | | `protected_branch_ids` | Array | **{dotted-circle}** No | The IDs of protected branches to scope the rule by. To identify the ID, [use the API](protected_branches.md#list-protected-branches). | +| `remove_hidden_groups` | boolean | **{dotted-circle}** No | Whether hidden groups should be removed. | | `user_ids` | Array | **{dotted-circle}** No | The IDs of users as approvers. | ```json @@ -964,6 +965,7 @@ Supported attributes: | `merge_request_iid` | integer | **{check-circle}** Yes | The IID of a merge request. | | `name` | string | **{check-circle}** Yes | The name of the approval rule. | | `group_ids` | Array | **{dotted-circle}** No | The IDs of groups as approvers. | +| `remove_hidden_groups` | boolean | **{dotted-circle}** No | Whether hidden groups should be removed. | | `user_ids` | Array | **{dotted-circle}** No | The IDs of users as approvers. | ```json diff --git a/doc/api/packages/pypi.md b/doc/api/packages/pypi.md index 4e4c060d99bc78636a07fe6fc7fe375458e76cea..e9546f50e36ce4df3bb458673a9d0de583e6394b 100644 --- a/doc/api/packages/pypi.md +++ b/doc/api/packages/pypi.md @@ -32,7 +32,7 @@ Download a PyPI package file. The [simple API](#group-level-simple-api-entry-poi normally supplies this URL. ```plaintext -GET groups/:id/packages/pypi/files/:sha256/:file_identifier +GET groups/:id/-/packages/pypi/files/:sha256/:file_identifier ``` | Attribute | Type | Required | Description | @@ -42,13 +42,13 @@ GET groups/:id/packages/pypi/files/:sha256/:file_identifier | `file_identifier` | string | yes | The PyPI package file's name. | ```shell -curl --user <username>:<personal_access_token> "https://gitlab.example.com/api/v4/groups/1/packages/pypi/files/5y57017232013c8ac80647f4ca153k3726f6cba62d055cd747844ed95b3c65ff/my.pypi.package-0.0.1.tar.gz" +curl --user <username>:<personal_access_token> "https://gitlab.example.com/api/v4/groups/1/-/packages/pypi/files/5y57017232013c8ac80647f4ca153k3726f6cba62d055cd747844ed95b3c65ff/my.pypi.package-0.0.1.tar.gz" ``` To write the output to a file: ```shell -curl --user <username>:<personal_access_token> "https://gitlab.example.com/api/v4/groups/1/packages/pypi/files/5y57017232013c8ac80647f4ca153k3726f6cba62d055cd747844ed95b3c65ff/my.pypi.package-0.0.1.tar.gz" >> my.pypi.package-0.0.1.tar.gz +curl --user <username>:<personal_access_token> "https://gitlab.example.com/api/v4/groups/1/-/packages/pypi/files/5y57017232013c8ac80647f4ca153k3726f6cba62d055cd747844ed95b3c65ff/my.pypi.package-0.0.1.tar.gz" >> my.pypi.package-0.0.1.tar.gz ``` This writes the downloaded file to `my.pypi.package-0.0.1.tar.gz` in the current diff --git a/doc/integration/mattermost/index.md b/doc/integration/mattermost/index.md index 04b0157b7376afc888a622e4ef4fc5f14115d976..df6130a754066fcad780ed929a9402ff67471aca 100644 --- a/doc/integration/mattermost/index.md +++ b/doc/integration/mattermost/index.md @@ -123,7 +123,7 @@ http://mattermost.example.com/signup/gitlab/complete http://mattermost.example.com/login/gitlab/complete ``` -Note that you do not need to select any options under **Scopes**. Choose **Save application**. +Make sure to select the **Trusted** and **Confidential** settings. Under **Scopes**, select `read_user`. Then, choose **Save application**. Once the application is created you are provided with an `Application ID` and `Secret`. One other piece of information needed is the URL of GitLab instance. Return to the server running GitLab Mattermost and edit the `/etc/gitlab/gitlab.rb` configuration file as follows using the values you received above: @@ -132,7 +132,7 @@ Return to the server running GitLab Mattermost and edit the `/etc/gitlab/gitlab. mattermost['gitlab_enable'] = true mattermost['gitlab_id'] = "12345656" mattermost['gitlab_secret'] = "123456789" -mattermost['gitlab_scope'] = "" +mattermost['gitlab_scope'] = "read_user" mattermost['gitlab_auth_endpoint'] = "http://gitlab.example.com/oauth/authorize" mattermost['gitlab_token_endpoint'] = "http://gitlab.example.com/oauth/token" mattermost['gitlab_user_api_endpoint'] = "http://gitlab.example.com/api/v4/user" diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md index 296bb9ece396d37bd42a1383b29d22b7f14245e1..f71a9030630478f9bf30b65c2dc062bbe9e024f2 100644 --- a/doc/user/packages/container_registry/index.md +++ b/doc/user/packages/container_registry/index.md @@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w > Searching by image repository name was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31322) in GitLab 13.0. NOTE: -If you pull container images from Docker Hub, you can use the [GitLab Dependency Proxy](../dependency_proxy/index.md#use-the-dependency-proxy-for-docker-images) +If you pull container images from Docker Hub, you can use the [GitLab Dependency Proxy](../dependency_proxy/index.md#use-the-dependency-proxy-for-docker-images) to avoid rate limits and speed up your pipelines. With the Docker Container Registry integrated into GitLab, every GitLab project can @@ -32,6 +32,7 @@ You can search, sort, filter, and [delete](#delete-images-using-the-gitlab-ui) containers on this page. You can share a filtered view by copying the URL from your browser. Only members of the project or group can access a private project's Container Registry. +Images downloaded from a private registry may be [available to other users in a shared runner](https://docs.gitlab.com/runner/security/index.html#usage-of-private-docker-images-with-if-not-present-pull-policy). If a project is public, so is the Container Registry. diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 3c9f154415846f84c6e1d29f921ed0721ff6978b..d70a1a4ba1db19cf60e0123d24bc53a4e3698fce 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -342,7 +342,7 @@ This table shows granted privileges for jobs triggered by specific types of user | Push source and LFS | | | | | 1. Only if the triggering user is not an external one. -1. Only if the triggering user is a member of the project. +1. Only if the triggering user is a member of the project. See also [Usage of private Docker images with `if-not-present` pull policy](http://docs.gitlabl.com/runner/security/index.html#usage-of-private-docker-images-with-if-not-present-pull-policy). ### Wiki and issues diff --git a/doc/user/search/index.md b/doc/user/search/index.md index 9bff2a91ec82819ed38bb4f417c1ae0584714d6b..e27ce075e0a779bcf7a8e9026f82409c4bfb5f5e 100644 --- a/doc/user/search/index.md +++ b/doc/user/search/index.md @@ -150,6 +150,7 @@ To delete filter tokens one at a time, the <kbd>⌥</kbd> (Mac) / <kbd>Control</ In the search bar, you can view autocomplete suggestions for: - Projects and groups +- Users - Various help pages (try and type **API help**) - Project feature pages (try and type **milestones**) - Various settings pages (try and type **user settings**) diff --git a/ee/app/assets/images/illustrations/epics/list.svg b/ee/app/assets/images/illustrations/epics/list.svg deleted file mode 100644 index f4d5295b67383ca5a05eca4e6ccfae57c94bf546..0000000000000000000000000000000000000000 --- a/ee/app/assets/images/illustrations/epics/list.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="312" height="240" viewBox="0 0 312 240"><defs><rect id="a" width="280" height="180" x="77" y="60" rx="10"/><rect id="c" width="100" height="48" rx="10"/><filter id="b" width="105%" height="120.8%" x="-2.5%" y="-5.2%" filterUnits="objectBoundingBox"><feOffset dy="5" in="SourceAlpha" result="shadowOffsetOuter1"/><feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/><feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 1 0"/></filter><rect id="e" width="84" height="76" rx="10"/><filter id="d" width="106%" height="113.2%" x="-3%" y="-3.3%" filterUnits="objectBoundingBox"><feOffset dy="5" in="SourceAlpha" result="shadowOffsetOuter1"/><feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/><feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 1 0"/></filter><rect id="g" width="52" height="76" rx="10"/><filter id="f" width="109.6%" height="113.2%" x="-4.8%" y="-3.3%" filterUnits="objectBoundingBox"><feOffset dy="5" in="SourceAlpha" result="shadowOffsetOuter1"/><feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/><feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 1 0"/></filter><rect id="i" width="36" height="132" rx="10"/><filter id="h" width="113.9%" height="107.6%" x="-6.9%" y="-1.9%" filterUnits="objectBoundingBox"><feOffset dy="5" in="SourceAlpha" result="shadowOffsetOuter1"/><feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/><feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 1 0"/></filter><rect id="k" width="52" height="48" rx="10"/><filter id="j" width="109.6%" height="120.8%" x="-4.8%" y="-5.2%" filterUnits="objectBoundingBox"><feOffset dy="5" in="SourceAlpha" result="shadowOffsetOuter1"/><feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/><feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 0 0.976470588 0 0 0 1 0"/></filter></defs><g fill="none" fill-rule="evenodd" transform="translate(-57 -30)"><path d="M0 0h430v300H0z"/><use fill="#FFF" xlink:href="#a"/><rect width="276" height="176" x="79" y="62" stroke="#EEE" stroke-width="4" rx="10"/><rect width="8" height="20" x="244" y="141" fill="#EEE" rx="4"/><rect width="8" height="20" x="311" y="103" fill="#EEE" rx="4"/><rect width="8" height="20" x="272" y="166" fill="#FEE1D3" rx="4"/><rect width="8" height="20" x="257" y="203" fill="#EEE" rx="4"/><rect width="8" height="20" x="169" y="77" fill="#EEE" rx="4"/><rect width="8" height="20" x="191" y="104" fill="#FFF" rx="4"/><rect width="8" height="20" x="140" y="157" fill="#E1DBF1" rx="4"/><rect width="8" height="20" x="154" y="119" fill="#FEE1D3" rx="4"/><rect width="8" height="20" x="163" y="205" fill="#FEE1D3" rx="4"/><rect width="8" height="20" x="294" y="186" fill="#E1DBF1" rx="4"/><rect width="8" height="20" x="327" y="73" fill="#FEE1D3" rx="4"/><g transform="translate(197 79)"><use fill="#000" filter="url(#b)" xlink:href="#c"/><use fill="#FFF" xlink:href="#c"/><rect width="96" height="44" x="2" y="2" stroke="#FEE1D3" stroke-width="4" rx="10"/><rect width="8" height="20" x="62" y="14" fill="#FEE1D3" rx="4"/><rect width="8" height="20" x="78" y="14" fill="#FDC4A8" rx="4"/><rect width="8" height="20" x="46" y="14" fill="#FC6D26" rx="4"/><rect width="8" height="20" x="30" y="14" fill="#FEE1D3" rx="4"/><rect width="8" height="20" x="14" y="14" fill="#EEE" rx="4"/></g><g transform="translate(57 30)"><use fill="#000" filter="url(#d)" xlink:href="#e"/><use fill="#FFF" xlink:href="#e"/><rect width="80" height="72" x="2" y="2" stroke="#E1DBF1" stroke-width="4" rx="10"/><rect width="8" height="20" x="62" y="42" fill="#6B4FBB" rx="4"/><rect width="8" height="20" x="46" y="42" fill="#E1DBF1" rx="4"/><rect width="8" height="20" x="30" y="42" fill="#C3B8E3" rx="4"/><rect width="8" height="20" x="14" y="42" fill="#E1DBF1" rx="4"/><rect width="8" height="20" x="62" y="14" fill="#E1DBF1" rx="4"/><rect width="8" height="20" x="46" y="14" fill="#C3B8E3" rx="4"/><rect width="8" height="20" x="30" y="14" fill="#EEE" rx="4"/><rect width="8" height="20" x="14" y="14" fill="#C3B8E3" rx="4"/></g><g transform="translate(317 133)"><use fill="#000" filter="url(#f)" xlink:href="#g"/><use fill="#FFF" xlink:href="#g"/><rect width="48" height="72" x="2" y="2" stroke="#FEE1D3" stroke-width="4" rx="10"/><rect width="8" height="20" x="14" y="14" fill="#FEE1D3" rx="4"/><rect width="8" height="20" x="14" y="42" fill="#FC6D26" rx="4"/><rect width="8" height="20" x="30" y="14" fill="#EEE" rx="4"/><rect width="8" height="20" x="30" y="42" fill="#FDC4A8" rx="4"/></g><g transform="translate(89 133)"><use fill="#000" filter="url(#h)" xlink:href="#i"/><use fill="#FFF" xlink:href="#i"/><rect width="32" height="128" x="2" y="2" stroke="#FEE1D3" stroke-width="4" rx="10"/><rect width="8" height="20" x="14" y="14" fill="#FDC4A8" rx="4"/><rect width="8" height="20" x="14" y="42" fill="#EEE" rx="4"/><rect width="8" height="20" x="14" y="70" fill="#FEE1D3" rx="4"/><rect width="8" height="20" x="14" y="98" fill="#FC6D26" rx="4"/></g><g transform="translate(181 161)"><use fill="#000" filter="url(#j)" xlink:href="#k"/><use fill="#FFF" xlink:href="#k"/><rect width="48" height="44" x="2" y="2" stroke="#E1DBF1" stroke-width="4" rx="10"/><rect width="8" height="20" x="30" y="14" fill="#6B4FBB" rx="4"/><rect width="8" height="20" x="14" y="14" fill="#C3B8E3" rx="4"/></g></g></svg> \ No newline at end of file diff --git a/ee/app/assets/images/illustrations/epics/roadmap.svg b/ee/app/assets/images/illustrations/epics/roadmap.svg deleted file mode 100644 index a14f14b91c90e4c9a60bbc2a07bb57fe3bb3cee7..0000000000000000000000000000000000000000 --- a/ee/app/assets/images/illustrations/epics/roadmap.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="415" height="289" viewBox="0 0 415 289"><g fill="none" fill-rule="evenodd"><path d="M-9-5h430v300H-9z"/><g transform="translate(68 47)"><rect width="284" height="208" y="5" fill="#F9F9F9" fill-rule="nonzero" rx="10"/><rect width="284" height="208" fill="#FFF" fill-rule="nonzero" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M10 4a6 6 0 0 0-6 6v188a6 6 0 0 0 6 6h264a6 6 0 0 0 6-6V10a6 6 0 0 0-6-6H10zm0-4h264c5.523 0 10 4.477 10 10v188c0 5.523-4.477 10-10 10H10c-5.523 0-10-4.477-10-10V10C0 4.477 4.477 0 10 0z"/><path fill="#E1DBF1" d="M25.168 153.995c3.837-.215 7.173.028 10.119.691a3 3 0 1 0 1.318-5.853c-3.509-.79-7.4-1.074-11.773-.828a3 3 0 1 0 .336 5.99z"/><path fill="#C3B8E3" d="M44.211 158.655c2.401 1.704 4.388 3.61 7.569 7.083a3 3 0 1 0 4.424-4.054c-3.448-3.763-5.686-5.911-8.522-7.923a3 3 0 1 0-3.471 4.894z"/><path fill="#E1DBF1" d="M59.786 173.828c3.181 2.675 6.52 4.665 10.397 6.039a3 3 0 0 0 2.004-5.655c-3.162-1.121-5.884-2.743-8.54-4.976a3 3 0 1 0-3.861 4.592zm22.133 8.148c1.02.037 2.067.045 3.143.023a72.664 72.664 0 0 0 8.346-.638 3 3 0 0 0-.812-5.945c-2.442.334-4.996.53-7.658.585a48.55 48.55 0 0 1-2.796-.021 3 3 0 1 0-.223 5.996z"/><path fill="#C3B8E3" d="M104.697 178.69c3.9-1.37 7.427-3.15 10.54-5.305a3 3 0 0 0-3.415-4.933c-2.665 1.845-5.712 3.382-9.114 4.578a3 3 0 0 0 1.989 5.66z"/><path fill="#E1DBF1" d="M123.853 165.07a33.752 33.752 0 0 0 5.276-10.817 3 3 0 1 0-5.773-1.633 27.753 27.753 0 0 1-4.341 8.9 3 3 0 0 0 4.838 3.55zm6.577-22.657c-.187-3.817-.926-7.71-2.204-11.596a3 3 0 1 0-5.7 1.874c1.113 3.384 1.75 6.745 1.91 10.016a3 3 0 1 0 5.994-.294z"/><path fill="#C3B8E3" d="M123.333 120.153c-1.897-3.2-4.152-6.325-6.748-9.344a3 3 0 0 0-4.55 3.913c2.372 2.756 4.421 5.597 6.136 8.49a3 3 0 1 0 5.162-3.06v.001z"/><path fill="#E1DBF1" d="M111.787 102.36c-.938-3.025-1.402-6.42-1.365-9.976a3 3 0 1 0-6-.063c-.043 4.163.506 8.177 1.634 11.816a3 3 0 1 0 5.731-1.777z"/><path fill="#C3B8E3" d="M111.84 82.253c.905-3.341 2.22-6.538 3.904-9.448a3 3 0 0 0-5.194-3.004c-1.948 3.368-3.463 7.048-4.501 10.884a3 3 0 1 0 5.791 1.568z"/><path fill="#E1DBF1" d="M121.974 64.948c2.475-2.28 5.265-4.09 8.335-5.374a3 3 0 1 0-2.314-5.536c-3.725 1.558-7.105 3.75-10.086 6.497a3 3 0 1 0 4.065 4.413zm18.177-7.586c3.202-.18 6.599.092 10.18.843a3 3 0 0 0 1.23-5.872c-4.086-.857-8.009-1.172-11.747-.962a3 3 0 1 0 .337 5.99v.001z"/><path fill="#C3B8E3" d="M160.198 61.312c3.068 1.268 6.232 2.842 9.487 4.728a3 3 0 1 0 3.009-5.191c-3.48-2.017-6.883-3.71-10.204-5.083a3 3 0 0 0-2.292 5.545v.001z"/><path fill="#E1DBF1" d="M179.776 71.267c3.711 1.586 7.376 2.77 10.997 3.565a3 3 0 0 0 1.286-5.86c-3.248-.713-6.555-1.782-9.925-3.222a3 3 0 1 0-2.358 5.517zm22.591 4.789c3.94-.04 7.808-.553 11.61-1.513a3 3 0 1 0-1.468-5.817 43.358 43.358 0 0 1-10.203 1.33 3 3 0 0 0 .061 6z"/><path fill="#C3B8E3" d="M224.887 70.498c3.335-1.637 6.607-3.613 9.845-5.916a3 3 0 0 0-3.477-4.89c-2.984 2.122-5.98 3.931-9.011 5.42a3 3 0 1 0 2.643 5.386z"/><path fill="#E1DBF1" d="M243.565 57.444a3 3 0 0 1-4.02-4.454 130.547 130.547 0 0 0 5.31-5.088 3 3 0 1 1 4.265 4.22 136.506 136.506 0 0 1-5.555 5.322z"/><path fill="#FDC4A8" d="M194.843 83.085a3 3 0 1 1 4.314-4.17c3.056 3.16 5.075 6.744 6.172 10.754a3 3 0 0 1-5.787 1.584c-.834-3.047-2.35-5.739-4.699-8.168z"/><path fill="#FEE1D3" d="M200.19 101.134a3 3 0 1 1 5.978.52c-.282 3.232-.805 6.273-1.832 11.206a3 3 0 0 1-5.874-1.222c.981-4.717 1.473-7.572 1.728-10.504z"/><path fill="#FDC4A8" d="M196.413 122.689a3 3 0 0 1 5.953.747c-.5 3.988-.397 7.09.399 9.67a3 3 0 1 1-5.733 1.769c-1.087-3.52-1.217-7.426-.62-12.186h.001z"/><path fill="#FEE1D3" d="M203.806 145.133a3 3 0 0 1 4.461-4.013c2.703 3.005 5.224 5.296 7.594 6.947a3 3 0 0 1-3.429 4.924c-2.775-1.932-5.632-4.53-8.626-7.858zm20.352 12.28a3 3 0 1 1 .334-5.99c2.77.154 5.453-.554 9.224-2.254a3 3 0 0 1 2.466 5.47c-4.57 2.06-8.103 2.993-12.024 2.775v-.001z"/><path fill="#FDC4A8" d="M245.942 150.355a3 3 0 0 1-1.815-5.719c4.227-1.342 8.24-1.61 12.496-.572a3 3 0 0 1-1.421 5.83c-3.116-.76-6.025-.566-9.26.46v.001zM106.53 56.038a3 3 0 0 1-3.45 4.909c-1.074-.755-6.723-6.044-8.083-7.204l-.332-.281a3 3 0 1 1 3.865-4.59l.362.306c1.643 1.402 6.971 6.391 7.638 6.86z"/><path fill="#FEE1D3" d="M88.536 42.422a3 3 0 0 1-2.285 5.548c-3.14-1.293-5.78-1.34-8.105-.05a3 3 0 0 1-2.91-5.247c4.087-2.266 8.597-2.187 13.3-.25v-.001z"/><path fill="#FDC4A8" d="M66.698 48.73a3 3 0 0 1 2.029 5.647c-4.432 1.592-8.786.835-13.166-1.88a3 3 0 1 1 3.16-5.1c2.93 1.816 5.425 2.25 7.977 1.333z"/><path fill="#FEE1D3" d="M51.062 40.692a3 3 0 0 1-4.352 4.13c-.911-.96-1.85-1.98-3.061-3.32-.295-.325-2.437-2.703-3.07-3.4-.47-.518-.9-.988-1.313-1.436a3 3 0 0 1 4.41-4.068c.425.46.866.942 1.346 1.47.642.709 2.79 3.092 3.076 3.41a180.865 180.865 0 0 0 2.964 3.214z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M254.66 72.196l2-3.464a2 2 0 1 0-3.464-2l-2 3.464-3.464-2a2 2 0 0 0-2 3.464l3.464 2-2 3.464a2 2 0 0 0 3.464 2l2-3.464 3.464 2a2 2 0 1 0 2-3.464l-3.464-2zm-151.904 78.732l2.829-2.828a2 2 0 1 0-2.829-2.829l-2.828 2.829-2.828-2.829a2 2 0 1 0-2.829 2.829l2.829 2.828-2.829 2.829a2 2 0 0 0 2.829 2.828l2.828-2.828 2.828 2.828a2 2 0 1 0 2.829-2.828l-2.829-2.829z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M210.66 173.66l3.464-2a2 2 0 1 0-2-3.464l-3.464 2-2-3.464a2 2 0 0 0-3.464 2l2 3.464-3.464 2a2 2 0 1 0 2 3.464l3.464-2 2 3.464a2 2 0 1 0 3.464-2l-2-3.464z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M27 181a8 8 0 1 1 0-16 8 8 0 0 1 0 16zm0-4a4 4 0 1 0 0-8 4 4 0 0 0 0 8z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M138 85a7 7 0 1 1 0-14 7 7 0 0 1 0 14zm0-4a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M200 57a7 7 0 1 1 0-14 7 7 0 0 1 0 14zm0-4a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/><path fill="#FC6D26" fill-rule="nonzero" d="M222.647 121.647v5h5v-5h-5zm0-4h5a4 4 0 0 1 4 4v5a4 4 0 0 1-4 4h-5a4 4 0 0 1-4-4v-5a4 4 0 0 1 4-4z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M103.647 28.647v5h5v-5h-5zm0-4h5a4 4 0 0 1 4 4v5a4 4 0 0 1-4 4h-5a4 4 0 0 1-4-4v-5a4 4 0 0 1 4-4z"/><path fill="#FC6D26" fill-rule="nonzero" d="M85 103.488L81.841 108h6.318L85 103.488zm6.436 2.218A4 4 0 0 1 88.159 112H81.84a4 4 0 0 1-3.277-6.294l3.16-4.512a4 4 0 0 1 6.553 0l3.159 4.512h.001z"/></g><path fill="#F9F9F9" fill-rule="nonzero" d="M327.376 93.43A48.805 48.805 0 0 0 359 105c27.062 0 49-21.938 49-49S386.062 7 359 7s-49 21.938-49 49c0 9.454 2.677 18.283 7.315 25.77l-3.05 11.306a2.5 2.5 0 0 0 3.064 3.065l10.047-2.71v-.001z"/><path fill="#FFF" fill-rule="nonzero" d="M332.376 88.43A48.805 48.805 0 0 0 364 100c27.062 0 49-21.938 49-49S391.062 2 364 2s-49 21.938-49 49c0 9.454 2.677 18.283 7.315 25.77l-3.05 11.306a2.5 2.5 0 0 0 3.064 3.065l10.047-2.71v-.001z"/><path fill="#EEE" fill-rule="nonzero" d="M322.85 93.072a4.5 4.5 0 0 1-5.516-5.517l2.827-10.48C315.501 69.258 313 60.31 313 51c0-28.167 22.833-51 51-51s51 22.833 51 51-22.833 51-51 51c-11.859 0-23.096-4.064-32.102-11.37l-9.048 2.442zm10.817-6.169C342.091 94.027 352.737 98 364 98c25.957 0 47-21.043 47-47S389.957 4 364 4s-47 21.043-47 47c0 8.859 2.453 17.351 7.016 24.716l.456.737-3.277 12.144c.072.527.347.685.613.613l11.059-2.984.8.677z"/><g fill-rule="nonzero" transform="translate(347 28)"><path fill="#E1DBF1" d="M13 4a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-8zm0-4h8a5 5 0 0 1 5 5v1a5 5 0 0 1-5 5h-8a5 5 0 0 1-5-5V5a5 5 0 0 1 5-5z"/><path fill="#6B4FBB" d="M5 11a1 1 0 0 0 0 2h24a1 1 0 0 0 0-2H5zm0-4h24a5 5 0 0 1 0 10H5A5 5 0 0 1 5 7z"/><rect width="12" height="4" x="11" y="31" fill="#C3B8E3" rx="2"/><rect width="12" height="4" x="11" y="19" fill="#C3B8E3" rx="2"/><rect width="12" height="4" x="11" y="37" fill="#E1DBF1" rx="2"/><rect width="12" height="4" x="11" y="43" fill="#C3B8E3" rx="2"/><rect width="12" height="4" x="11" y="25" fill="#E1DBF1" rx="2"/></g><g fill-rule="nonzero"><path fill="#F9F9F9" d="M337.238 219.072A38.83 38.83 0 0 1 361 211c21.54 0 39 17.46 39 39s-17.46 39-39 39-39-17.46-39-39a38.84 38.84 0 0 1 4.001-17.227l-3.737-13.85a2.5 2.5 0 0 1 3.065-3.064l11.91 3.213h-.001z"/><path fill="#FFF" d="M341.238 215.072A38.83 38.83 0 0 1 365 207c21.54 0 39 17.46 39 39s-17.46 39-39 39-39-17.46-39-39a38.84 38.84 0 0 1 4.001-17.227l-3.737-13.85a2.5 2.5 0 0 1 3.065-3.064l11.91 3.213h-.001z"/><path fill="#EEE" d="M329.85 209.928a4.5 4.5 0 0 0-5.516 5.517l3.543 13.13A40.848 40.848 0 0 0 324 246c0 22.644 18.356 41 41 41s41-18.356 41-41-18.356-41-41-41a40.82 40.82 0 0 0-24.182 7.887l-10.968-2.96v.001zm12.608 6.73A36.824 36.824 0 0 1 365 209c20.435 0 37 16.565 37 37s-16.565 37-37 37-37-16.565-37-37c0-5.747 1.31-11.304 3.795-16.343l.334-.677-3.934-14.577a.5.5 0 0 1 .613-.613l12.865 3.471.785-.604v.001z"/><path fill="#FEE1D3" d="M348.097 250.962a7 7 0 0 0 8.81 10.88l1.093-.885v1.454a7 7 0 0 0 14 0v-1.454l1.092.885a7 7 0 1 0 8.81-10.88l-1.185-.96 1.455-.337a7 7 0 1 0-3.15-13.64l-1.4.323.623-1.278a7 7 0 0 0-12.583-6.137l-.662 1.356-.662-1.356a7 7 0 0 0-12.583 6.137l.623 1.278-1.4-.324a7 7 0 1 0-3.15 13.641l1.455.336-1.186.96v.001zm5.464-.913a11.914 11.914 0 0 1-.444-1.95l-.19-1.362-4.2-.97a3 3 0 0 1 1.35-5.845l4.178.964.768-1.145c.373-.557.793-1.082 1.254-1.57l.95-1.006-1.877-3.849a3 3 0 0 1 5.393-2.63l1.892 3.879 1.363-.113a12.188 12.188 0 0 1 2.004 0l1.363.113 1.892-3.879a3 3 0 0 1 5.393 2.63l-1.877 3.849.95 1.006c.461.488.88 1.013 1.254 1.57l.768 1.145 4.178-.964a3 3 0 1 1 1.35 5.846l-4.2.97-.19 1.36a11.914 11.914 0 0 1-.444 1.95l-.413 1.302 3.36 2.72a3 3 0 1 1-3.776 4.663l-3.32-2.688-1.196.706a11.94 11.94 0 0 1-1.808.873l-1.286.492v4.295a3 3 0 0 1-6 0v-4.295l-1.286-.492a11.94 11.94 0 0 1-1.808-.873l-1.196-.706-3.32 2.688a3 3 0 0 1-3.776-4.663l3.36-2.72-.413-1.301z"/><path fill="#FC6D26" d="M365 240.411a6 6 0 1 0 0 12 6 6 0 0 0 0-12zm0 4a2 2 0 1 1 0 4 2 2 0 0 1 0-4z"/></g><g fill-rule="nonzero"><path fill="#F9F9F9" d="M87.624 156.43A48.805 48.805 0 0 1 56 168c-27.062 0-49-21.938-49-49s21.938-49 49-49 49 21.938 49 49c0 9.454-2.677 18.283-7.315 25.77l3.05 11.306a2.5 2.5 0 0 1-3.064 3.065l-10.047-2.71v-.001z"/><path fill="#FFF" stroke="#EEE" stroke-width="4" d="M82.624 151.43A48.805 48.805 0 0 1 51 163c-27.062 0-49-21.938-49-49s21.938-49 49-49 49 21.938 49 49c0 9.454-2.677 18.283-7.315 25.77l3.05 11.306a2.5 2.5 0 0 1-3.064 3.065l-10.047-2.71v-.001z"/><path fill="#EEE" d="M92.15 156.072a4.5 4.5 0 0 0 5.516-5.517l-2.827-10.48C99.499 132.258 102 123.31 102 114c0-28.167-22.833-51-51-51S0 85.833 0 114s22.833 51 51 51c11.859 0 23.096-4.064 32.102-11.37l9.048 2.442zm-10.817-6.169C72.909 157.027 62.263 161 51 161c-25.957 0-47-21.043-47-47s21.043-47 47-47 47 21.043 47 47c0 8.859-2.453 17.351-7.016 24.716l-.456.737 3.277 12.144c-.072.527-.347.685-.613.613l-11.059-2.984-.8.677z"/><path fill="#FEE1D3" d="M48.47 88.47l-16.148 6.688a4 4 0 0 0-2.164 2.164l-6.689 16.147a4 4 0 0 0 0 3.062l6.689 16.147a4 4 0 0 0 2.164 2.164l16.147 6.689a4 4 0 0 0 3.062 0l16.147-6.689a4 4 0 0 0 2.164-2.164l6.689-16.147a4 4 0 0 0 0-3.062l-6.689-16.147a4 4 0 0 0-2.164-2.164L51.53 88.469a4 4 0 0 0-3.062 0l.002.001zM50 92.164l16.147 6.688L72.835 115l-6.688 16.147L50 137.835l-16.147-6.688L27.165 115l6.688-16.147L50 92.165v-.001zM50 101a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm12 4a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm4 12a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-4 11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-12 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-12-6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-4-11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm4-11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm12 20c6.075 0 11-4.925 11-11s-4.925-11-11-11-11 4.925-11 11 4.925 11 11 11zm0-4a7 7 0 1 1 0-14 7 7 0 0 1 0 14z"/><path fill="#FC6D26" d="M50 120.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0-3a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5z"/></g></g></svg> \ No newline at end of file diff --git a/ee/app/assets/images/illustrations/shield-check.svg b/ee/app/assets/images/illustrations/shield-check.svg deleted file mode 100644 index cfd8271dcb27d4ce292fe084a19979c29fb7ea78..0000000000000000000000000000000000000000 --- a/ee/app/assets/images/illustrations/shield-check.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="72" height="72" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M61 42V11c-10 0-18-2-25-9-7 7-15 9-25 9v31c0 21 25 28 25 28s25-7 25-28Z" stroke="#6E49CB" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><circle cx="36" cy="36" r="12" fill="#4BEACC" stroke="#6E49CB" stroke-width="4" stroke-linecap="round"/><path d="M32.042 35.65a1 1 0 0 0-1.417 1.412l1.417-1.411Zm2.036 3.461-.708.706a1 1 0 0 0 1.417 0l-.709-.706Zm7.297-5.905a1 1 0 1 0-1.417-1.412l1.417 1.412Zm-10.75 3.856 2.745 2.755 1.417-1.412-2.745-2.754-1.417 1.411Zm4.162 2.755 6.588-6.611-1.417-1.412-6.588 6.611 1.417 1.412Z" fill="#6E49CB"/></svg> \ No newline at end of file diff --git a/ee/app/assets/javascripts/issues/components/blocking_issues_count.vue b/ee/app/assets/javascripts/issues/components/blocking_issues_count.vue index a74dac9f120ac35ba7bb24a39c6e3af55a23bc48..473304c26e9062c9efb1139d8cbdf32f9751f8ce 100644 --- a/ee/app/assets/javascripts/issues/components/blocking_issues_count.vue +++ b/ee/app/assets/javascripts/issues/components/blocking_issues_count.vue @@ -8,11 +8,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, - inject: { - hasBlockedIssuesFeature: { - default: false, - }, - }, + inject: ['hasBlockedIssuesFeature'], props: { blockingIssuesCount: { type: Number, diff --git a/ee/app/assets/javascripts/issues/components/weight_count.vue b/ee/app/assets/javascripts/issues/components/weight_count.vue index ba26a36874936b66fa6a9b442bb75ac1cf539dfc..9e9a759e765b7347b5fda00776815713eb6d6207 100644 --- a/ee/app/assets/javascripts/issues/components/weight_count.vue +++ b/ee/app/assets/javascripts/issues/components/weight_count.vue @@ -8,11 +8,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, - inject: { - hasIssueWeightsFeature: { - default: false, - }, - }, + inject: ['hasIssueWeightsFeature'], props: { weight: { type: Number, diff --git a/ee/app/assets/javascripts/issues/list/components/issue_card_statistics.vue b/ee/app/assets/javascripts/issues/list/components/issue_card_statistics.vue new file mode 100644 index 0000000000000000000000000000000000000000..ce1f3f6506f69a554423288fcd4f771137cf7e99 --- /dev/null +++ b/ee/app/assets/javascripts/issues/list/components/issue_card_statistics.vue @@ -0,0 +1,28 @@ +<script> +import IssueCardStatistics from '~/issues/list/components/issue_card_statistics.vue'; +import BlockingIssuesCount from '../../components/blocking_issues_count.vue'; + +export default { + components: { + BlockingIssuesCount, + IssueCardStatistics, + }, + props: { + issue: { + type: Object, + required: true, + }, + }, +}; +</script> + +<template> + <issue-card-statistics :issue="issue"> + <blocking-issues-count + class="gl-display-none gl-sm-display-block gl-mr-3" + :blocking-issues-count="issue.blockingCount" + is-list-item + data-testid="blocking-issues" + /> + </issue-card-statistics> +</template> diff --git a/ee/app/assets/javascripts/issues/list/components/issues_list_app.vue b/ee/app/assets/javascripts/issues/list/components/issues_list_app.vue index 9cb7c6d67bea52735464d6d4fc0dd071a389c661..06f87fa78ea92f02c960206d679bbf2d2c77ce8f 100644 --- a/ee/app/assets/javascripts/issues/list/components/issues_list_app.vue +++ b/ee/app/assets/javascripts/issues/list/components/issues_list_app.vue @@ -17,7 +17,6 @@ import { } from 'ee/vue_shared/components/filtered_search_bar/constants'; import { TYPE_TOKEN_OBJECTIVE_OPTION } from '~/issues/list/constants'; import { WORK_ITEM_TYPE_ENUM_OBJECTIVE } from '~/work_items/constants'; -import BlockingIssuesCount from 'ee/issues/components/blocking_issues_count.vue'; import CreateWorkItemObjective from 'ee/work_items/components/create_work_item_objective.vue'; import searchIterationsQuery from '../queries/search_iterations.query.graphql'; @@ -35,7 +34,6 @@ const HealthToken = () => export default { name: 'IssuesListAppEE', components: { - BlockingIssuesCount, IssuesListApp, CreateWorkItemObjective, NewIssueDropdown, @@ -171,14 +169,6 @@ export default { :ee-type-token-options="typeTokenOptions" :ee-search-tokens="searchTokens" > - <template #blocking-count="{ issuable }"> - <blocking-issues-count - class="gl-display-none gl-sm-display-block" - :blocking-issues-count="issuable.blockingCount" - is-list-item - data-testid="blocking-issues" - /> - </template> <template v-if="isOkrsEnabled" #new-objective-button> <new-issue-dropdown @new-objective-clicked="handleNewObjectiveButtonClick()" /> </template> diff --git a/ee/app/assets/javascripts/product_analytics/dashboards/components/visualizations/line_chart.vue b/ee/app/assets/javascripts/product_analytics/dashboards/components/visualizations/line_chart.vue index 9f22e26a33c5ca078336083bdb66f7d97a3958fd..7948014519820896ebeb24f552caa4fa54130b65 100644 --- a/ee/app/assets/javascripts/product_analytics/dashboards/components/visualizations/line_chart.vue +++ b/ee/app/assets/javascripts/product_analytics/dashboards/components/visualizations/line_chart.vue @@ -1,13 +1,16 @@ <script> -import { s__ } from '~/locale'; +import { GlLineChart } from '@gitlab/ui/dist/charts'; export default { name: 'LineChart', + components: { + GlLineChart, + }, props: { data: { - type: Object, + type: Array, required: false, - default: () => ({}), + default: () => [], }, options: { type: Object, @@ -15,12 +18,9 @@ export default { default: () => ({}), }, }, - i18n: { - content: s__('ProductAnalytics|Widgets content'), - }, }; </script> <template> - <p>{{ $options.i18n.content }}</p> + <gl-line-chart :data="data" :option="options" responsive /> </template> diff --git a/ee/app/assets/javascripts/product_analytics/dashboards/data_sources/cube_analytics.js b/ee/app/assets/javascripts/product_analytics/dashboards/data_sources/cube_analytics.js index fdd7b8606a67f2dc38e69251b25bb7a3ec0bd63f..1bfcc32667f22dee5730a0444def680052f2c99c 100644 --- a/ee/app/assets/javascripts/product_analytics/dashboards/data_sources/cube_analytics.js +++ b/ee/app/assets/javascripts/product_analytics/dashboards/data_sources/cube_analytics.js @@ -1,5 +1,35 @@ -export const fetch = async (query) => { - // TODO: Call the cube library to fetch data from the product analytics proxy. - // Will be added with https://gitlab.com/gitlab-org/gitlab/-/issues/377708 - return query; +import { CubejsApi, HttpTransport } from '@cubejs-client/core'; +import csrf from '~/lib/utils/csrf'; + +// This can be any value because the cube proxy adds the real API token. +const CUBE_API_TOKEN = '1'; + +const PRODUCT_ANALYTICS_CUBE_PROXY = '/api/v4/projects/:id/product_analytics/request'; + +const convertToEChartFormat = (resultSet) => { + const seriesNames = resultSet.seriesNames(); + const pivot = resultSet.chartPivot(); + + return seriesNames.map((series) => ({ + name: series.title, + data: pivot.map((p) => [p.x, p[series.key]]), + })); +}; + +export const fetch = async (projectId, query, queryOverrides = {}) => { + const cubejsApi = new CubejsApi(CUBE_API_TOKEN, { + transport: new HttpTransport({ + apiUrl: PRODUCT_ANALYTICS_CUBE_PROXY.replace(':id', projectId), + method: 'POST', + headers: { + [csrf.headerKey]: csrf.token, + 'X-Requested-With': 'XMLHttpRequest', + }, + credentials: 'same-origin', + }), + }); + + const resultSet = await cubejsApi.load({ ...query, ...queryOverrides }); + + return convertToEChartFormat(resultSet); }; diff --git a/ee/app/assets/javascripts/product_analytics/dashboards/gl_dashboards/dashboard_overview.json b/ee/app/assets/javascripts/product_analytics/dashboards/gl_dashboards/dashboard_overview.json index c95ccf8276828c7cb6aa2a4bf622c6391551550b..13b17cc7572326bb97bb5549429a40b163766b0e 100644 --- a/ee/app/assets/javascripts/product_analytics/dashboards/gl_dashboards/dashboard_overview.json +++ b/ee/app/assets/javascripts/product_analytics/dashboards/gl_dashboards/dashboard_overview.json @@ -6,8 +6,8 @@ "title": "Audience", "gridAttributes": { "size": { - "width": 12, - "height": 2 + "width": 6, + "height": 5 } } }, @@ -17,8 +17,8 @@ "gridAttributes": { "size": { "yPos": 4, - "width": 12, - "height": 2 + "width": 6, + "height": 5 } } } diff --git a/ee/app/assets/javascripts/product_analytics/dashboards/gl_dashboards/visualizations/cube_analytics_line_chart.json b/ee/app/assets/javascripts/product_analytics/dashboards/gl_dashboards/visualizations/cube_analytics_line_chart.json index edc6fef96af2b800bbd48aa7ed63308d0af09e20..ee6af1fd1ed0d654345968315eeb5e29543b8460 100644 --- a/ee/app/assets/javascripts/product_analytics/dashboards/gl_dashboards/visualizations/cube_analytics_line_chart.json +++ b/ee/app/assets/javascripts/product_analytics/dashboards/gl_dashboards/visualizations/cube_analytics_line_chart.json @@ -4,14 +4,19 @@ "data": { "type": "cube_analytics", "query": { - "users": { - "measures": [ - "Jitsu.count" - ], - "dimensions": [ - "Jitsu.eventType" - ] - } + "measures": [ + "Jitsu.count" + ], + "dimensions": [ + "Jitsu.eventType" + ], + "timeDimensions": [ + { + "granularity": "day", + "dimension": "Jitsu.utcTime" + } + ], + "limit": 5000 } }, "options": { diff --git a/ee/app/assets/javascripts/product_analytics/dashboards/index.js b/ee/app/assets/javascripts/product_analytics/dashboards/index.js index a6806150c9cc6678d8f90f322930500d7b573efe..9f0f2e0bdb74d8c60edf7d0071d70bc289bccef7 100644 --- a/ee/app/assets/javascripts/product_analytics/dashboards/index.js +++ b/ee/app/assets/javascripts/product_analytics/dashboards/index.js @@ -5,6 +5,8 @@ import createRouter from './router'; export default () => { const el = document.getElementById('js-analytics-dashboard'); + const { projectId } = el.dataset; + if (!el) { return false; } @@ -12,6 +14,9 @@ export default () => { return new Vue({ el, router: createRouter(), + provide: { + projectId, + }, render(h) { return h(AnalyticsApp); }, diff --git a/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/customizable_dashboard.vue b/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/customizable_dashboard.vue index e42b7d2edc2b9932604ebff0921f1b7a745f1c6a..06d0c9f2e9ea854a993f8d5b7dd40c720b1b93a5 100644 --- a/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/customizable_dashboard.vue +++ b/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/customizable_dashboard.vue @@ -2,6 +2,8 @@ import { GridStack } from 'gridstack'; import * as Sentry from '@sentry/browser'; import { loadCSSFile } from '~/lib/utils/css_utils'; +import { createAlert } from '~/flash'; +import { s__, sprintf } from '~/locale'; import WidgetsBase from './widgets_base.vue'; import { GRIDSTACK_MARGIN, GRIDSTACK_CSS_HANDLE } from './constants'; @@ -78,6 +80,16 @@ export default { return undefined; }, + handleWidgetError(widgetTitle, error) { + createAlert({ + message: sprintf( + s__('ProductAnalytics|An error occured while loading the %{widgetTitle} widget.'), + { widgetTitle }, + ), + error, + captureError: true, + }); + }, }, }; </script> @@ -104,6 +116,7 @@ export default { :title="widget.title" :visualization="widget.visualization" :query-overrides="widget.queryOverrides" + @error="handleWidgetError(widget.title, $event)" /> </div> </div> diff --git a/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/widgets_base.vue b/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/widgets_base.vue index 99129aacf4e219d6a5d135a1963b40f1e3bd99f4..38c4974b3309eff0fc69d3f84ec137967ac7f387 100644 --- a/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/widgets_base.vue +++ b/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/widgets_base.vue @@ -9,6 +9,7 @@ export default { LineChart: () => import('ee/product_analytics/dashboards/components/visualizations/line_chart.vue'), }, + inject: ['projectId'], props: { visualization: { type: Object, @@ -35,12 +36,12 @@ export default { async created() { const { type, query } = this.visualization.data; this.loading = true; + this.error = null; try { const { fetch } = await dataSources[type](); - this.data = await fetch(query, this.queryOverrides); + this.data = await fetch(this.projectId, query, this.queryOverrides); } catch (error) { - // TODO: add the ability to display multiple errors on the dashboard page in https://gitlab.com/gitlab-org/gitlab/-/issues/377708 this.error = error; this.$emit('error', error); } finally { diff --git a/ee/app/controllers/projects/product_analytics_controller.rb b/ee/app/controllers/projects/product_analytics_controller.rb index bc192b2012b183f1fc17069deea929e998776266..40a47349a4c2e39e8f88e321415aa8fc173442ea 100644 --- a/ee/app/controllers/projects/product_analytics_controller.rb +++ b/ee/app/controllers/projects/product_analytics_controller.rb @@ -12,10 +12,22 @@ def dashboards; end private def dashboards_enabled! - unless ::Feature.enabled?(:product_analytics_internal_preview, project) && - project.licensed_feature_available?(:product_analytics) - render_404 - end + render_404 unless all_application_settings_defined? && + ::Feature.enabled?(:product_analytics_internal_preview, project) && + project.licensed_feature_available?(:product_analytics) + end + + def all_application_settings_defined? + return false unless ::Gitlab::CurrentSettings.product_analytics_enabled? + return false unless ::Gitlab::CurrentSettings.jitsu_host.present? + return false unless ::Gitlab::CurrentSettings.jitsu_project_xid.present? + return false unless ::Gitlab::CurrentSettings.jitsu_administrator_email.present? + return false unless ::Gitlab::CurrentSettings.jitsu_administrator_password.present? + return false unless ::Gitlab::CurrentSettings.clickhouse_connection_string.present? + return false unless ::Gitlab::CurrentSettings.cube_api_base_url.present? + return false unless ::Gitlab::CurrentSettings.cube_api_key.present? + + true end end end diff --git a/ee/app/graphql/mutations/vulnerabilities/dismiss.rb b/ee/app/graphql/mutations/vulnerabilities/dismiss.rb index f773b57b65155756ded03e067e56e5ca1b9f1594..e716e1e90b7a3ab2f7be8b47d92a4199ed3d09c5 100644 --- a/ee/app/graphql/mutations/vulnerabilities/dismiss.rb +++ b/ee/app/graphql/mutations/vulnerabilities/dismiss.rb @@ -19,7 +19,7 @@ class Dismiss < BaseMutation argument :comment, GraphQL::Types::String, required: false, - description: 'Comment why vulnerability should be dismissed.' + description: 'Comment why vulnerability should be dismissed (max. 50 000 characters).' argument :dismissal_reason, Types::Vulnerabilities::DismissalReasonEnum, diff --git a/ee/app/models/vulnerabilities/feedback.rb b/ee/app/models/vulnerabilities/feedback.rb index 32e237de9ea484468368e5d4c07146b741128019..6abaf01c6d927427eb1687e9debd8bacfd6b0037 100644 --- a/ee/app/models/vulnerabilities/feedback.rb +++ b/ee/app/models/vulnerabilities/feedback.rb @@ -32,6 +32,7 @@ class Feedback < ApplicationRecord validates :project, presence: true validates :author, presence: true + validates :comment, length: { maximum: 50_000 } validates :comment_timestamp, :comment_author, presence: true, if: :comment? validates :issue, presence: true, if: :for_issue? validates :merge_request, presence: true, if: :for_merge_request? diff --git a/ee/app/services/slash_commands/global_slack_handler.rb b/ee/app/services/slash_commands/global_slack_handler.rb index 8d56d21752326d686aad0283b58eb307ac7ad1a8..f0a0b7e414c01404fd7da29dd0c10485e7ec55e1 100644 --- a/ee/app/services/slash_commands/global_slack_handler.rb +++ b/ee/app/services/slash_commands/global_slack_handler.rb @@ -12,6 +12,10 @@ def initialize(params) def trigger return false unless valid_token? + if help_command? + return Gitlab::SlashCommands::ApplicationHelp.new(nil, params).execute + end + unless slack_integration = find_slack_integration error_message = 'GitLab error: project or alias not found' return Gitlab::SlashCommands::Presenters::Error.new(error_message).message @@ -22,10 +26,6 @@ def trigger chat_user = ChatNames::FindUserService.new(integration, params).execute - if help_command? - return Gitlab::SlashCommands::ApplicationHelp.new(nil, chat_user, params).execute - end - if chat_user&.user Gitlab::SlashCommands::Command.new(project, chat_user, params).execute else diff --git a/ee/app/views/projects/product_analytics/dashboards.haml b/ee/app/views/projects/product_analytics/dashboards.haml index c0d381c290ee5fe743fd73dfc4f2b428d5fe93d8..2d5f2ae0108c5201cdb535070e249d5c943137c6 100644 --- a/ee/app/views/projects/product_analytics/dashboards.haml +++ b/ee/app/views/projects/product_analytics/dashboards.haml @@ -1,4 +1,4 @@ - page_title _('Dashboards') -#js-analytics-dashboard +#js-analytics-dashboard{ data: { project_id: @project.id } } = gl_loading_icon(size: 'lg', css_class: 'gl-my-7') diff --git a/ee/spec/finders/users_finder_spec.rb b/ee/spec/finders/users_finder_spec.rb index 1359d9746cfd1895dbe59a87a2b81cc96c5bd059..439af95454e9e5950288fdb6bff4ada04dab7106 100644 --- a/ee/spec/finders/users_finder_spec.rb +++ b/ee/spec/finders/users_finder_spec.rb @@ -6,20 +6,23 @@ describe '#execute' do include_context 'UsersFinder#execute filter by project context' - context 'with a normal user' do + shared_examples 'executes users finder' do |as_admin: false| + let_it_be(:normal_users) { [normal_user, unconfirmed_user, omniauth_user, external_user, internal_user, admin_user] } + let_it_be(:users_visible_to_admin) { as_admin ? [blocked_user, banned_user] : [] } + context 'with LDAP users' do let_it_be(:ldap_user) { create(:omniauth_user, provider: 'ldap') } it 'returns ldap users by default' do - users = described_class.new(normal_user).execute + users = described_class.new(user).execute - expect(users).to contain_exactly(normal_user, unconfirmed_user, omniauth_user, external_user, ldap_user, internal_user, admin_user) + expect(users).to contain_exactly(ldap_user, *normal_users, *users_visible_to_admin) end it 'returns only non-ldap users with skip_ldap: true' do - users = described_class.new(normal_user, skip_ldap: true).execute + users = described_class.new(user, skip_ldap: true).execute - expect(users).to contain_exactly(normal_user, unconfirmed_user, omniauth_user, external_user, internal_user, admin_user) + expect(users).to contain_exactly(*normal_users, *users_visible_to_admin) end end @@ -34,56 +37,39 @@ end it 'returns all users by default' do - users = described_class.new(normal_user).execute + users = described_class.new(user).execute - expect(users).to contain_exactly(normal_user, unconfirmed_user, omniauth_user, external_user, internal_user, admin_user, saml_user, non_saml_user) + expect(users).to contain_exactly(saml_user, non_saml_user, *normal_users, *users_visible_to_admin) end it 'returns only saml users from the provided saml_provider_id' do - users = described_class.new(normal_user, saml_provider_id: saml_provider.id).execute + users = described_class.new(user, saml_provider_id: saml_provider.id).execute expect(users).to contain_exactly(saml_user) end end end - context 'with an admin user' do - context 'with LDAP users' do - let_it_be(:ldap_user) { create(:omniauth_user, provider: 'ldap') } - - it 'returns ldap users by default' do - users = described_class.new(admin_user).execute + context 'with a normal user' do + let_it_be(:user) { normal_user } - expect(users).to contain_exactly(normal_user, blocked_user, unconfirmed_user, banned_user, omniauth_user, external_user, ldap_user, internal_user, admin_user) - end + it_behaves_like 'executes users finder' + end - it 'returns only non-ldap users with skip_ldap: true' do - users = described_class.new(admin_user, skip_ldap: true).execute + context 'with an admin user' do + let_it_be(:user) { admin_user } - expect(users).to contain_exactly(normal_user, blocked_user, unconfirmed_user, banned_user, omniauth_user, external_user, internal_user, admin_user) - end + context 'when admin mode setting is disabled', :do_not_mock_admin_mode_setting do + it_behaves_like 'executes users finder', as_admin: true end - context 'with SAML users' do - let_it_be(:group) { create(:group) } - let_it_be(:saml_provider) { create(:saml_provider, group: group, enabled: true, enforced_sso: true) } - let_it_be(:saml_user) { create(:user) } - let_it_be(:non_saml_user) { create(:user) } - - before do - create(:identity, provider: 'group_saml1', saml_provider_id: saml_provider.id, user: saml_user) - end - - it 'returns all users by default' do - users = described_class.new(admin_user).execute - - expect(users).to contain_exactly(normal_user, blocked_user, unconfirmed_user, banned_user, omniauth_user, external_user, internal_user, admin_user, saml_user, non_saml_user) + context 'when admin mode setting is enabled' do + context 'when in admin mode', :enable_admin_mode do + it_behaves_like 'executes users finder', as_admin: true end - it 'returns only saml users from the provided saml_provider_id' do - users = described_class.new(admin_user, saml_provider_id: saml_provider.id).execute - - expect(users).to contain_exactly(saml_user) + context 'when not in admin mode' do + it_behaves_like 'executes users finder' end end end diff --git a/ee/spec/frontend/issues/list/components/issue_card_statistics_spec.js b/ee/spec/frontend/issues/list/components/issue_card_statistics_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..07bf75690976f3e2ab9d4dcebba5841d3c70181e --- /dev/null +++ b/ee/spec/frontend/issues/list/components/issue_card_statistics_spec.js @@ -0,0 +1,28 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import IssueCardStatistics from 'ee/issues/list/components/issue_card_statistics.vue'; +import BlockingIssuesCount from 'ee/issues/components/blocking_issues_count.vue'; + +describe('IssueCardStatistics EE component', () => { + let wrapper; + + const findBlockingIssuesCount = () => wrapper.findComponent(BlockingIssuesCount); + + const mountComponent = ({ blockingCount = 1 } = {}) => { + wrapper = shallowMountExtended(IssueCardStatistics, { + propsData: { + issue: { + blockingCount, + }, + }, + }); + }; + + it('renders blocking issues count', () => { + mountComponent(); + + expect(findBlockingIssuesCount().props()).toEqual({ + blockingIssuesCount: 1, + isListItem: true, + }); + }); +}); diff --git a/ee/spec/frontend/product_analytics/dashboards/components/data_sources/cube_analytics_spec.js b/ee/spec/frontend/product_analytics/dashboards/components/data_sources/cube_analytics_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..11b2f9002498a5294db330586924a556526aff7b --- /dev/null +++ b/ee/spec/frontend/product_analytics/dashboards/components/data_sources/cube_analytics_spec.js @@ -0,0 +1,60 @@ +import { CubejsApi, HttpTransport } from '@cubejs-client/core'; +import { fetch } from 'ee/product_analytics/dashboards/data_sources/cube_analytics'; +import { mockResultSet } from '../mock_data'; + +const mockLoad = jest.fn().mockImplementation(() => mockResultSet); + +jest.mock('@cubejs-client/core', () => ({ + CubejsApi: jest.fn().mockImplementation(() => ({ + load: mockLoad, + })), + HttpTransport: jest.fn(), +})); + +jest.mock('~/lib/utils/csrf', () => ({ + headerKey: 'mock-csrf-header', + token: 'mock-csrf-token', +})); + +describe('Cube Analytics Data Source', () => { + describe('fetch', () => { + let result; + + beforeEach(async () => { + result = await fetch('TEST_ID', { alpha: 'one' }, { alpha: 'two' }); + }); + + afterEach(() => { + result = null; + }); + + it('creates a new CubejsApi connection', () => { + expect(CubejsApi).toHaveBeenCalledWith('1', { transport: {} }); + }); + + it('creates a new HttpTransport with the proxy URL and csrf headers', () => { + expect(HttpTransport).toHaveBeenCalledWith( + expect.objectContaining({ + apiUrl: '/api/v4/projects/TEST_ID/product_analytics/request', + headers: expect.objectContaining({ + 'mock-csrf-header': 'mock-csrf-token', + }), + }), + ); + }); + + it('loads the query with the query override', () => { + expect(mockLoad).toHaveBeenCalledWith({ alpha: 'two' }); + }); + + it('returns the data in the expected charts format', () => { + expect(result[0]).toMatchObject({ + data: [ + ['2022-11-09T00:00:00.000', 55], + ['2022-11-10T00:00:00.000', 14], + ], + name: 'pageview, Jitsu Count', + }); + }); + }); +}); diff --git a/ee/spec/frontend/product_analytics/dashboards/components/mock_data.js b/ee/spec/frontend/product_analytics/dashboards/components/mock_data.js new file mode 100644 index 0000000000000000000000000000000000000000..696ba6cc6aa8fe3ebd6d95eecad23802ee8f9f89 --- /dev/null +++ b/ee/spec/frontend/product_analytics/dashboards/components/mock_data.js @@ -0,0 +1,21 @@ +export const mockResultSet = { + seriesNames: () => [ + { + title: 'pageview, Jitsu Count', + key: 'pageview,Jitsu.count', + yValues: ['pageview', 'Jitsu.count'], + }, + ], + chartPivot: () => [ + { + x: '2022-11-09T00:00:00.000', + xValues: ['2022-11-09T00:00:00.000'], + 'pageview,Jitsu.count': 55, + }, + { + x: '2022-11-10T00:00:00.000', + xValues: ['2022-11-10T00:00:00.000'], + 'pageview,Jitsu.count': 14, + }, + ], +}; diff --git a/ee/spec/frontend/product_analytics/dashboards/components/visualizations/line_chart_spec.js b/ee/spec/frontend/product_analytics/dashboards/components/visualizations/line_chart_spec.js index a3234d05148561946414bcb046f9b65fc429b819..61334084be9e54cedefa110e1742694e0674e6f4 100644 --- a/ee/spec/frontend/product_analytics/dashboards/components/visualizations/line_chart_spec.js +++ b/ee/spec/frontend/product_analytics/dashboards/components/visualizations/line_chart_spec.js @@ -1,13 +1,16 @@ +import { GlLineChart } from '@gitlab/ui/dist/charts'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import LineChart from 'ee/product_analytics/dashboards/components/visualizations/line_chart.vue'; describe('LineChart Visualization', () => { let wrapper; + const findLineChart = () => wrapper.findComponent(GlLineChart); + const createWrapper = (props = {}) => { wrapper = shallowMountExtended(LineChart, { propsData: { - data: {}, + data: [], options: {}, ...props, }, @@ -16,11 +19,18 @@ describe('LineChart Visualization', () => { describe('when mounted', () => { beforeEach(() => { - createWrapper(); + createWrapper({ + data: [{ name: 'foo' }], + options: { yAxis: {}, xAxis: {} }, + }); }); - it('should render', () => { - expect(wrapper.exists()).toBe(true); + it('should render the line chart with the provided data and option', () => { + expect(findLineChart().props()).toMatchObject({ + data: [{ name: 'foo' }], + option: { yAxis: {}, xAxis: {} }, + }); + expect(findLineChart().attributes('responsive')).toBe(''); }); }); }); diff --git a/ee/spec/frontend/vue_shared/components/customizable_dashboard/customizable_dashboard_spec.js b/ee/spec/frontend/vue_shared/components/customizable_dashboard/customizable_dashboard_spec.js index 68ff517ea8697a84fae209427403c5e498577609..3ca96c9192a85ee953af01aca121a0fe63689b36 100644 --- a/ee/spec/frontend/vue_shared/components/customizable_dashboard/customizable_dashboard_spec.js +++ b/ee/spec/frontend/vue_shared/components/customizable_dashboard/customizable_dashboard_spec.js @@ -8,9 +8,11 @@ import { GRIDSTACK_CSS_HANDLE, } from 'ee/vue_shared/components/customizable_dashboard/constants'; import { loadCSSFile } from '~/lib/utils/css_utils'; +import { createAlert } from '~/flash'; import waitForPromises from 'helpers/wait_for_promises'; import { dashboard } from './mock_data'; +jest.mock('~/flash'); jest.mock('gridstack', () => ({ GridStack: { init: jest.fn(), @@ -38,6 +40,7 @@ describe('CustomizableDashboard', () => { const findGridStackWidgets = () => wrapper.findAllByTestId('grid-stack-widget'); const findWidgets = () => wrapper.findAllComponents(WidgetsBase); + describe('when being created an error occurs while loading the CSS', () => { beforeEach(() => { jest.spyOn(Sentry, 'captureException'); @@ -95,5 +98,17 @@ describe('CustomizableDashboard', () => { }); }, ); + + it('calls createAlert when a widget emits an error', () => { + const error = new Error('foo'); + + findWidgets().at(0).vm.$emit('error', error); + + expect(createAlert).toHaveBeenCalledWith({ + message: `An error occured while loading the ${dashboard.widgets[0].title} widget.`, + captureError: true, + error, + }); + }); }); }); diff --git a/ee/spec/frontend/vue_shared/components/customizable_dashboard/widgets_base_spec.js b/ee/spec/frontend/vue_shared/components/customizable_dashboard/widgets_base_spec.js index dccd74a416261d7d92abadc1b86b7c3ecceb9bfb..f10be4931ac1e0d476f3eb20b2f7aa350d8b78d5 100644 --- a/ee/spec/frontend/vue_shared/components/customizable_dashboard/widgets_base_spec.js +++ b/ee/spec/frontend/vue_shared/components/customizable_dashboard/widgets_base_spec.js @@ -8,7 +8,7 @@ import { dashboard } from './mock_data'; jest.mock('ee/product_analytics/dashboards/data_sources', () => ({ cube_analytics: jest.fn().mockReturnValue({ - fetch: jest.fn(), + fetch: jest.fn().mockReturnValue([]), }), })); @@ -19,6 +19,7 @@ describe('WidgetsBase', () => { const createWrapper = (props = {}) => { wrapper = shallowMountExtended(WidgetsBase, { + provide: { projectId: '1' }, propsData: { title: widgetConfig.title, visualization: widgetConfig.visualization, @@ -67,7 +68,7 @@ describe('WidgetsBase', () => { }); describe('when the data has been fetched', () => { - const mockData = { hello: 'world' }; + const mockData = [{ name: 'foo' }]; beforeEach(() => { jest.spyOn(dataSources.cube_analytics(), 'fetch').mockReturnValue(mockData); diff --git a/ee/spec/models/integrations/gitlab_slack_application_spec.rb b/ee/spec/models/integrations/gitlab_slack_application_spec.rb index 2969b3584c3f9fd9a8b8df153514cea3e3e408d7..75d4729d9609032fbcecca99a545a3af7fc8f9c3 100644 --- a/ee/spec/models/integrations/gitlab_slack_application_spec.rb +++ b/ee/spec/models/integrations/gitlab_slack_application_spec.rb @@ -91,12 +91,14 @@ def stub_slack_request(channel: '#push_channel', success: true) stub_slack_request(success: false) expect(Gitlab::IntegrationsLogger).to receive(:error).with( - integration_class: described_class.name, - integration_id: integration.id, - project_id: integration.project_id, - project_path: kind_of(String), - message: 'Slack API error when notifying', - api_response: { 'ok' => false } + { + integration_class: described_class.name, + integration_id: integration.id, + project_id: integration.project_id, + project_path: kind_of(String), + message: 'Slack API error when notifying', + api_response: { 'ok' => false } + } ) expect(integration.execute(data)).to be false end diff --git a/ee/spec/models/vulnerabilities/feedback_spec.rb b/ee/spec/models/vulnerabilities/feedback_spec.rb index 5951da3cc2fc4661f3fd9f0164d0bc2680892cf9..e0eb757f8ba94acc79a4b870666b7be9f49dae5d 100644 --- a/ee/spec/models/vulnerabilities/feedback_spec.rb +++ b/ee/spec/models/vulnerabilities/feedback_spec.rb @@ -31,6 +31,7 @@ it { is_expected.to validate_presence_of(:feedback_type) } it { is_expected.to validate_presence_of(:category) } it { is_expected.to validate_presence_of(:project_fingerprint) } + it { is_expected.to validate_length_of(:comment).is_at_most(50_000) } let_it_be(:project) { create(:project) } diff --git a/ee/spec/requests/projects/product_analytics_controller_spec.rb b/ee/spec/requests/projects/product_analytics_controller_spec.rb index cb5db96710882a82ec97d1c0667a5b0d02840032..8402e6cb42f61a43678add50fe6ba0d4f25b645c 100644 --- a/ee/spec/requests/projects/product_analytics_controller_spec.rb +++ b/ee/spec/requests/projects/product_analytics_controller_spec.rb @@ -10,7 +10,14 @@ before do stub_feature_flags(product_analytics_internal_preview: true) stub_licensed_features(product_analytics: true) - + stub_application_setting(jitsu_host: 'https://jitsu.example.com') + stub_application_setting(jitsu_project_xid: '123') + stub_application_setting(jitsu_administrator_email: 'test@example.com') + stub_application_setting(jitsu_administrator_password: 'password') + stub_application_setting(clickhouse_connection_string: 'clickhouse://localhost:9000') + stub_application_setting(cube_api_base_url: 'https://cube.example.com') + stub_application_setting(cube_api_key: '123') + stub_application_setting(product_analytics_enabled: true) login_as(user) end @@ -46,6 +53,79 @@ it_behaves_like 'returns not found' end + context 'without multiple settings' do + before do + stub_application_setting(jitsu_host: nil) + stub_licensed_features(product_analytics: false) + end + + it_behaves_like 'returns not found' + end + + context 'without jitsu_host application setting' do + before do + stub_application_setting(jitsu_host: nil) + end + + it_behaves_like 'returns not found' + end + + context 'when product_analytics_enabled application setting is false' do + before do + stub_application_setting(product_analytics_enabled: false) + end + + it_behaves_like 'returns not found' + end + + context 'without jitsu_project_xid application setting' do + before do + stub_application_setting(jitsu_project_xid: nil) + end + + it_behaves_like 'returns not found' + end + + context 'without jitsu_administrator_email application setting' do + before do + stub_application_setting(jitsu_administrator_email: nil) + end + + it_behaves_like 'returns not found' + end + + context 'without jitsu_administrator_password application setting' do + before do + stub_application_setting(jitsu_administrator_password: nil) + end + + it_behaves_like 'returns not found' + end + + context 'without clickhouse_connection_string application setting' do + before do + stub_application_setting(clickhouse_connection_string: nil) + end + + it_behaves_like 'returns not found' + end + + context 'without cube_api_base_url application setting' do + before do + stub_application_setting(cube_api_base_url: nil) + end + + it_behaves_like 'returns not found' + end + + context 'without cube_api_key application setting' do + before do + stub_application_setting(cube_api_key: nil) + end + + it_behaves_like 'returns not found' + end + def send_dashboards_request get project_product_analytics_dashboards_path(project) end diff --git a/ee/spec/services/slash_commands/global_slack_handler_spec.rb b/ee/spec/services/slash_commands/global_slack_handler_spec.rb index 33d5fded755fcafde6f63410c6ce1016c321bde0..c1aa508450a8a3ee0c015d1e7a59a5ef1e46776f 100644 --- a/ee/spec/services/slash_commands/global_slack_handler_spec.rb +++ b/ee/spec/services/slash_commands/global_slack_handler_spec.rb @@ -95,15 +95,9 @@ def handler_with_valid_token(params) it 'calls help presenter' do expect_any_instance_of(Gitlab::SlashCommands::ApplicationHelp).to receive(:execute) - expect_any_instance_of(ChatNames::FindUserService).to receive(:execute).and_return(chat_name) - - enable_slack_application(project) - - slack_integration = create(:slack_integration, integration: project.gitlab_slack_application_integration) handler_with_valid_token( - text: "help", - team_id: slack_integration.team_id + text: "help" ).trigger end end diff --git a/lib/api/api.rb b/lib/api/api.rb index 10fdb8d7682a1b02cabdea99c076fe6c166c9487..46d0aa3d19d9cae228596439a03e235f1e398ca7 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -196,6 +196,7 @@ class API < ::API::Base mount ::API::Clusters::Agents mount ::API::Commits mount ::API::CommitStatuses + mount ::API::ContainerRegistryEvent mount ::API::DependencyProxy mount ::API::DeployKeys mount ::API::DeployTokens @@ -252,6 +253,7 @@ class API < ::API::Base mount ::API::ProjectTemplates mount ::API::ProtectedBranches mount ::API::ProtectedTags + mount ::API::PypiPackages mount ::API::Releases mount ::API::Release::Links mount ::API::RemoteMirrors @@ -289,7 +291,6 @@ class API < ::API::Base mount ::API::ComposerPackages mount ::API::ConanInstancePackages mount ::API::ConanProjectPackages - mount ::API::ContainerRegistryEvent mount ::API::ContainerRepositories mount ::API::DebianGroupPackages mount ::API::DebianProjectPackages @@ -318,7 +319,6 @@ class API < ::API::Base mount ::API::ProjectMilestones mount ::API::Projects mount ::API::ProtectedTags - mount ::API::PypiPackages mount ::API::ResourceLabelEvents mount ::API::ResourceStateEvents mount ::API::RpmProjectPackages diff --git a/lib/api/container_registry_event.rb b/lib/api/container_registry_event.rb index 9acf2fca1b3496d153515a0b80bdb6b70134b6e3..9e59401ddf61e0e67161537c04601514584fc745 100644 --- a/lib/api/container_registry_event.rb +++ b/lib/api/container_registry_event.rb @@ -26,15 +26,21 @@ def authenticate_registry_notification! desc 'Receives notifications from the container registry when an operation occurs' do detail 'This feature was introduced in GitLab 12.10' consumes [:json, DOCKER_DISTRIBUTION_EVENTS_V1_JSON] + success code: 200, message: 'Success' + failure [ + { code: 401, message: 'Invalid Token' } + ] + tags %w[container_registry_event] end params do requires :events, type: Array, desc: 'Event notifications' do requires :action, type: String, desc: 'The action to perform, `push`, `delete`', values: %w[push delete].freeze optional :target, type: Hash, desc: 'The target of the action' do - optional :tag, type: String, desc: 'The target tag' - optional :repository, type: String, desc: 'The target repository' - optional :digest, type: String, desc: 'Unique identifier for target image manifest' + optional :tag, type: String, desc: 'The target tag', documentation: { example: 'latest' } + optional :repository, type: String, desc: 'The target repository', documentation: { example: 'group/p1' } + optional :digest, type: String, desc: 'Unique identifier for target image manifest', + documentation: { example: 'imagedigest' } end end end diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb index c4464666020153384ff6206b939c69a5714c2334..dbd5c5f9db1842c5ca56377e5faf4fd0097d88d4 100644 --- a/lib/api/internal/base.rb +++ b/lib/api/internal/base.rb @@ -191,7 +191,7 @@ def with_admin_mode_bypass!(actor_id) get '/authorized_keys', feature_category: :source_code_management, urgency: :high do fingerprint = Gitlab::InsecureKeyFingerprint.new(params.fetch(:key)).fingerprint_sha256 - key = Key.find_by_fingerprint_sha256(fingerprint) + key = Key.auth.find_by_fingerprint_sha256(fingerprint) not_found!('Key') if key.nil? present key, with: Entities::SSHKey end diff --git a/lib/api/pypi_packages.rb b/lib/api/pypi_packages.rb index 0707a0b0ec47222b475411e3fd3376c4581342cb..f9470ce1cb641ab2a17b81833edff1a0f4851eb8 100644 --- a/lib/api/pypi_packages.rb +++ b/lib/api/pypi_packages.rb @@ -32,12 +32,12 @@ class PypiPackages < ::API::Base helpers do params :package_download do - requires :file_identifier, type: String, desc: 'The PyPi package file identifier', file_path: true - requires :sha256, type: String, desc: 'The PyPi package sha256 check sum' + requires :file_identifier, type: String, desc: 'The PyPi package file identifier', file_path: true, documentation: { example: 'my.pypi.package-0.0.1.tar.gz' } + requires :sha256, type: String, desc: 'The PyPi package sha256 check sum', documentation: { example: '5y57017232013c8ac80647f4ca153k3726f6cba62d055cd747844ed95b3c65ff' } end params :package_name do - requires :package_name, type: String, file_path: true, desc: 'The PyPi package name' + requires :package_name, type: String, file_path: true, desc: 'The PyPi package name', documentation: { example: 'my.pypi.package' } end def present_simple_index(group_or_project) @@ -102,7 +102,7 @@ def project!(action: :read_package) end params do - requires :id, type: String, desc: 'The ID of a group' + requires :id, types: [Integer, String], desc: 'The ID or full path of the group.' end resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do after_validation do @@ -110,6 +110,16 @@ def project!(action: :read_package) end namespace ':id/-/packages/pypi' do + desc 'Download a package file from a group' do + detail 'This feature was introduced in GitLab 13.12' + success code: 200 + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not Found' } + ] + tags %w[pypi_packages] + end params do use :package_download end @@ -130,6 +140,13 @@ def project!(action: :read_package) desc 'The PyPi Simple Group Index Endpoint' do detail 'This feature was introduced in GitLab 15.1' + success code: 200 + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not Found' } + ] + tags %w[pypi_packages] end # An API entry point but returns an HTML file instead of JSON. @@ -141,6 +158,13 @@ def project!(action: :read_package) desc 'The PyPi Simple Group Package Endpoint' do detail 'This feature was introduced in GitLab 12.10' + success code: 200 + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not Found' } + ] + tags %w[pypi_packages] end params do @@ -164,6 +188,13 @@ def project!(action: :read_package) namespace ':id/packages/pypi' do desc 'The PyPi package download endpoint' do detail 'This feature was introduced in GitLab 12.10' + success code: 200 + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not Found' } + ] + tags %w[pypi_packages] end params do @@ -185,6 +216,13 @@ def project!(action: :read_package) desc 'The PyPi Simple Project Index Endpoint' do detail 'This feature was introduced in GitLab 15.1' + success code: 200 + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not Found' } + ] + tags %w[pypi_packages] end # An API entry point but returns an HTML file instead of JSON. @@ -196,6 +234,13 @@ def project!(action: :read_package) desc 'The PyPi Simple Project Package Endpoint' do detail 'This feature was introduced in GitLab 12.10' + success code: 200 + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not Found' } + ] + tags %w[pypi_packages] end params do @@ -211,15 +256,24 @@ def project!(action: :read_package) desc 'The PyPi Package upload endpoint' do detail 'This feature was introduced in GitLab 12.10' + success code: 201 + failure [ + { code: 400, message: 'Bad Request' }, + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not Found' }, + { code: 422, message: 'Unprocessable Entity' } + ] + tags %w[pypi_packages] end params do requires :content, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)', documentation: { type: 'file' } - requires :name, type: String - requires :version, type: String - optional :requires_python, type: String - optional :md5_digest, type: String - optional :sha256_digest, type: String, regexp: Gitlab::Regex.sha256_regex + requires :name, type: String, documentation: { example: 'my.pypi.package' } + requires :version, type: String, documentation: { example: '1.3.7' } + optional :requires_python, type: String, documentation: { example: '>=3.7' } + optional :md5_digest, type: String, documentation: { example: '900150983cd24fb0d6963f7d28e17f72' } + optional :sha256_digest, type: String, regexp: Gitlab::Regex.sha256_regex, documentation: { example: 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' } end route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth @@ -243,6 +297,17 @@ def project!(action: :read_package) forbidden! end + desc 'Authorize the PyPi package upload from workhorse' do + detail 'This feature was introduced in GitLab 12.10' + success code: 200 + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not Found' } + ] + tags %w[pypi_packages] + end + route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth post 'authorize' do project = project!(action: :read_project) diff --git a/lib/api/support/git_access_actor.rb b/lib/api/support/git_access_actor.rb index 16861a146aed1e26f85c84562ec67e334bc9f449..7a4e6f3e14cce7e042a468870ac77f6e7f9cc897 100644 --- a/lib/api/support/git_access_actor.rb +++ b/lib/api/support/git_access_actor.rb @@ -16,7 +16,7 @@ def initialize(user: nil, key: nil) def self.from_params(params) if params[:key_id] - new(key: Key.find_by_id(params[:key_id])) + new(key: Key.auth.find_by_id(params[:key_id])) elsif params[:user_id] new(user: UserFinder.new(params[:user_id]).find_by_id) elsif params[:username] diff --git a/lib/gitlab/ci/config/entry/default.rb b/lib/gitlab/ci/config/entry/default.rb index 12d68b755b3fdf62fd6f707e751dac66c54faba2..e996b6b1312d73995a6d89334c614031b9714e11 100644 --- a/lib/gitlab/ci/config/entry/default.rb +++ b/lib/gitlab/ci/config/entry/default.rb @@ -13,9 +13,8 @@ class Default < ::Gitlab::Config::Entry::Node include ::Gitlab::Config::Entry::Configurable include ::Gitlab::Config::Entry::Inheritable - ALLOWED_KEYS = %i[before_script image services - after_script cache interruptible - timeout retry tags artifacts].freeze + ALLOWED_KEYS = %i[before_script after_script hooks cache image services + interruptible timeout retry tags artifacts].freeze validations do validates :config, allowed_keys: ALLOWED_KEYS @@ -25,22 +24,27 @@ class Default < ::Gitlab::Config::Entry::Node description: 'Script that will be executed before each job.', inherit: true - entry :image, Entry::Image, - description: 'Docker image that will be used to execute jobs.', - inherit: true - - entry :services, Entry::Services, - description: 'Docker images that will be linked to the container.', - inherit: true - entry :after_script, Entry::Commands, description: 'Script that will be executed after each job.', inherit: true + entry :hooks, Entry::Hooks, + description: 'Commands that will be executed on Runner before/after some events ' \ + 'such as `clone` and `build-script`.', + inherit: false + entry :cache, Entry::Caches, description: 'Configure caching between build jobs.', inherit: true + entry :image, Entry::Image, + description: 'Docker image that will be used to execute jobs.', + inherit: true + + entry :services, Entry::Services, + description: 'Docker images that will be linked to the container.', + inherit: true + entry :interruptible, ::Gitlab::Config::Entry::Boolean, description: 'Set jobs interruptible default value.', inherit: false diff --git a/lib/gitlab/ci/config/entry/hooks.rb b/lib/gitlab/ci/config/entry/hooks.rb index d979dd497b262a2387464203033d890ce713cfac..28bc2e4e7ce8a4705f67a8eb4d245fc0fc3c8ecc 100644 --- a/lib/gitlab/ci/config/entry/hooks.rb +++ b/lib/gitlab/ci/config/entry/hooks.rb @@ -8,6 +8,8 @@ class Hooks < ::Gitlab::Config::Entry::Node # `Configurable` alreadys adds `Validatable` include ::Gitlab::Config::Entry::Configurable + # NOTE: If a new hook is added, inheriting should be changed because a `job:hooks` overrides all + # `default:hooks` now. We should implement merging; each hook must be overridden individually. ALLOWED_HOOKS = %i[pre_get_sources_script].freeze validations do diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index 29335c4679c30b84e75a20c4c4c398a92b8c992c..7c49b59a7f07ec5974ce687e323164174660caa2 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -61,7 +61,7 @@ class Job < ::Gitlab::Config::Entry::Node entry :hooks, Entry::Hooks, description: 'Commands that will be executed on Runner before/after some events; clone, build-script.', - inherit: false # This will be true in next iterations + inherit: true entry :cache, Entry::Caches, description: 'Cache definition for this job.', diff --git a/lib/gitlab/ci/pipeline/logger.rb b/lib/gitlab/ci/pipeline/logger.rb index 9659cec488964979937d8720310ff910fadfa458..08e731148c6983b450dae4fd7e92c3d8d6f7d710 100644 --- a/lib/gitlab/ci/pipeline/logger.rb +++ b/lib/gitlab/ci/pipeline/logger.rb @@ -53,6 +53,7 @@ def observe(operation, value, once: false) if once observations[operation.to_s] = value else + observations[operation.to_s] ||= [] observations[operation.to_s].push(value) end end @@ -116,13 +117,12 @@ def log? end def enabled? - strong_memoize(:enabled) do - ::Feature.enabled?(:ci_pipeline_creation_logger, project, type: :ops) - end + ::Feature.enabled?(:ci_pipeline_creation_logger, project, type: :ops) end + strong_memoize_attr :enabled?, :enabled def observations - @observations ||= Hash.new { |hash, key| hash[key] = [] } + @observations ||= {} end def observe_sql_counters(operation, start_db_counters, end_db_counters, once: false) diff --git a/lib/gitlab/database_importers/work_items/hierarchy_restrictions_importer.rb b/lib/gitlab/database_importers/work_items/hierarchy_restrictions_importer.rb new file mode 100644 index 0000000000000000000000000000000000000000..1181c259a5cac85fa7c5948fcc5f18f9c4373030 --- /dev/null +++ b/lib/gitlab/database_importers/work_items/hierarchy_restrictions_importer.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Gitlab + module DatabaseImporters + module WorkItems + module HierarchyRestrictionsImporter + def self.upsert_restrictions + objective = find_or_create_type(::WorkItems::Type::TYPE_NAMES[:objective]) + key_result = find_or_create_type(::WorkItems::Type::TYPE_NAMES[:key_result]) + issue = find_or_create_type(::WorkItems::Type::TYPE_NAMES[:issue]) + task = find_or_create_type(::WorkItems::Type::TYPE_NAMES[:task]) + incident = find_or_create_type(::WorkItems::Type::TYPE_NAMES[:incident]) + + restrictions = [ + { parent_type_id: objective.id, child_type_id: objective.id, maximum_depth: 9 }, + { parent_type_id: objective.id, child_type_id: key_result.id, maximum_depth: 1 }, + { parent_type_id: issue.id, child_type_id: task.id, maximum_depth: 1 }, + { parent_type_id: incident.id, child_type_id: task.id, maximum_depth: 1 } + ] + + ::WorkItems::HierarchyRestriction.upsert_all( + restrictions, + unique_by: :index_work_item_hierarchy_restrictions_on_parent_and_child + ) + end + + def self.find_or_create_type(name) + type = ::WorkItems::Type.find_by_name_and_namespace_id(name, nil) + return type if type + + Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.upsert_types + ::WorkItems::Type.find_by_name_and_namespace_id(name, nil) + end + end + end + end +end diff --git a/lib/gitlab/slash_commands/application_help.rb b/lib/gitlab/slash_commands/application_help.rb index bfdb65a816d601ce7c1b2ed90e135b14f1937f4a..94abc8b450886751e297a39ba871b6cb690d16dd 100644 --- a/lib/gitlab/slash_commands/application_help.rb +++ b/lib/gitlab/slash_commands/application_help.rb @@ -3,6 +3,11 @@ module Gitlab module SlashCommands class ApplicationHelp < BaseCommand + def initialize(project, params) + @project = project + @params = params + end + def execute Gitlab::SlashCommands::Presenters::Help .new(project, commands, params) @@ -16,11 +21,7 @@ def trigger end def commands - Gitlab::SlashCommands::Command.new( - project, - chat_name, - params - ).commands + Gitlab::SlashCommands::Command.commands end end end diff --git a/lib/gitlab/slash_commands/command.rb b/lib/gitlab/slash_commands/command.rb index 265eda46489ad997d9a66ea163c0161c8fd3bff5..f8b55f1a91d837c61d99a034a3ba828956ee62a7 100644 --- a/lib/gitlab/slash_commands/command.rb +++ b/lib/gitlab/slash_commands/command.rb @@ -3,7 +3,7 @@ module Gitlab module SlashCommands class Command < BaseCommand - def commands + def self.commands commands = [ Gitlab::SlashCommands::IssueShow, Gitlab::SlashCommands::IssueNew, @@ -15,7 +15,7 @@ def commands Gitlab::SlashCommands::Run ] - if Feature.enabled?(:incident_declare_slash_command, current_user) + if Feature.enabled?(:incident_declare_slash_command) commands << Gitlab::SlashCommands::IncidentManagement::IncidentNew end @@ -50,7 +50,7 @@ def match_command private def available_commands - commands.keep_if do |klass| + self.class.commands.keep_if do |klass| klass.available?(project) end end diff --git a/lib/gitlab/ssh/signature.rb b/lib/gitlab/ssh/signature.rb index 3b4df9a8d0c11e34532f61343ba79a2d3f02d269..b1cad8d76c9db0b8f6914d74d404f8c6ff0e5da1 100644 --- a/lib/gitlab/ssh/signature.rb +++ b/lib/gitlab/ssh/signature.rb @@ -30,7 +30,7 @@ def signed_by_key strong_memoize(:signed_by_key) do next unless key_fingerprint - Key.find_by_fingerprint_sha256(key_fingerprint) + Key.signing.find_by_fingerprint_sha256(key_fingerprint) end end diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index cf9876366aa6bc42a8de6d17442e43c9a3fee6dc..59c87c2b01ba7f28d814645c787df5ad0c26586a 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -81,7 +81,7 @@ namespace :gitlab do authorized_keys.clear - Key.find_in_batches(batch_size: 1000) do |keys| + Key.auth.find_in_batches(batch_size: 1000) do |keys| unless authorized_keys.batch_add_keys(keys) puts "Failed to add keys...".color(:red) exit 1 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f23218e2f9e924ac34b537d5fbf6696fa989eff4..af6b8faf5ff7c3fe46cd485a864f2a2effd51495 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -31225,6 +31225,9 @@ msgstr "" msgid "ProductAnalytics|Add to Dashboard" msgstr "" +msgid "ProductAnalytics|An error occured while loading the %{widgetTitle} widget." +msgstr "" + msgid "ProductAnalytics|Audience" msgstr "" @@ -31234,9 +31237,6 @@ msgstr "" msgid "ProductAnalytics|There is no data for this type of chart currently. Please see the Setup tab if you have not configured the product analytics tool already." msgstr "" -msgid "ProductAnalytics|Widgets content" -msgstr "" - msgid "Productivity" msgstr "" @@ -31597,6 +31597,12 @@ msgstr "" msgid "Profiles|Upload new avatar" msgstr "" +msgid "Profiles|Usage type" +msgstr "" + +msgid "Profiles|Usage type:" +msgstr "" + msgid "Profiles|Use a private email - %{email}" msgstr "" @@ -35818,6 +35824,15 @@ msgstr "" msgid "SSH public key" msgstr "" +msgid "SSHKey|Authentication" +msgstr "" + +msgid "SSHKey|Authentication & Signing" +msgstr "" + +msgid "SSHKey|Signing" +msgstr "" + msgid "SSL Verification:" msgstr "" diff --git a/package.json b/package.json index 37b400455c95c9511b107cbc4d83797863f3cf11..a85dd77a0ada881dddecdc4e10ddb0192ae00cf3 100644 --- a/package.json +++ b/package.json @@ -51,10 +51,11 @@ "@babel/core": "^7.18.5", "@babel/preset-env": "^7.18.2", "@codesandbox/sandpack-client": "^1.2.2", + "@cubejs-client/core": "^0.31.0", "@gitlab/at.js": "1.5.7", "@gitlab/favicon-overlay": "2.0.0", - "@gitlab/svgs": "3.8.0", - "@gitlab/ui": "49.11.1", + "@gitlab/svgs": "3.11.0", + "@gitlab/ui": "49.11.2", "@gitlab/visual-review-tools": "1.7.3", "@gitlab/web-ide": "0.0.1-dev-20221114183058", "@rails/actioncable": "6.1.4-7", diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb index a7fe9c7630e68ac92603cd946cff6695fb915eb6..82df0b23312fb26029724b182744a018fb257324 100644 --- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb +++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb @@ -225,8 +225,8 @@ def verify_mrs_or_issues(type) comment_diff = verify_comments(type, actual, expected) { - "missing_#{type}s": (expected.keys - actual.keys).map { |it| actual[it]&.slice(:title, :url) }.compact, - "extra_#{type}s": (actual.keys - expected.keys).map { |it| expected[it]&.slice(:title, :url) }.compact, + "missing_#{type}s": (expected.keys - actual.keys).map { |it| expected[it]&.slice(:title, :url) }.compact, + "extra_#{type}s": (actual.keys - expected.keys).map { |it| actual[it]&.slice(:title, :url) }.compact, "#{type}_comments": comment_diff } end diff --git a/qa/qa/tools/reliable_report.rb b/qa/qa/tools/reliable_report.rb index b3df6de3d5492547c934af72ba596696134d6ec1..08e87d994f8ee1105b4be6a00e3450094d9d880a 100644 --- a/qa/qa/tools/reliable_report.rb +++ b/qa/qa/tools/reliable_report.rb @@ -341,7 +341,7 @@ def test_runs(reliable:) runs = records.count failed = records.count { |r| r.values["status"] == "failed" } - failure_rate = (failed.to_f / runs.to_f) * 100 + failure_rate = (failed.to_f / runs) * 100 result[stage][name] = { file: file, @@ -358,7 +358,7 @@ def test_runs(reliable:) # @return [String] def query(reliable) <<~QUERY - from(bucket: "#{Support::InfluxdbTools::INFLUX_TEST_METRICS_BUCKET}") + from(bucket: "#{Support::InfluxdbTools::INFLUX_MAIN_TEST_METRICS_BUCKET}") |> range(start: -#{range}d) |> filter(fn: (r) => r._measurement == "test-stats") |> filter(fn: (r) => r.run_type == "staging-full" or diff --git a/qa/spec/tools/reliable_report_spec.rb b/qa/spec/tools/reliable_report_spec.rb index f08af8a717ac0e92b570ba513605ef2df0f3f7d3..c1ac5899279491fdb3ac2b5885a896cbac08ccee 100644 --- a/qa/spec/tools/reliable_report_spec.rb +++ b/qa/spec/tools/reliable_report_spec.rb @@ -56,7 +56,7 @@ def flux_query(reliable:) <<~QUERY - from(bucket: "e2e-test-stats") + from(bucket: "e2e-test-stats-main") |> range(start: -#{range}d) |> filter(fn: (r) => r._measurement == "test-stats") |> filter(fn: (r) => r.run_type == "staging-full" or diff --git a/scripts/api/create_issue_discussion.rb b/scripts/api/create_issue_discussion.rb new file mode 100644 index 0000000000000000000000000000000000000000..74a9f3ae378b3c96b777b3ee751a4082381ee4c5 --- /dev/null +++ b/scripts/api/create_issue_discussion.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'gitlab' +require_relative 'default_options' + +class CreateIssueDiscussion + def initialize(options) + @project = options.fetch(:project) + + # Force the token to be a string so that if api_token is nil, it's set to '', + # allowing unauthenticated requests (for forks). + api_token = options.delete(:api_token).to_s + + warn "No API token given." if api_token.empty? + + @client = Gitlab.client( + endpoint: options.delete(:endpoint) || API::DEFAULT_OPTIONS[:endpoint], + private_token: api_token + ) + end + + def execute(discussion_data) + client.post( + "/projects/#{client.url_encode project}/issues/#{discussion_data.delete(:issue_iid)}/discussions", + body: discussion_data + ) + end + + private + + attr_reader :project, :client +end diff --git a/scripts/create-pipeline-failure-incident.rb b/scripts/create-pipeline-failure-incident.rb index c38f80699e65b38421633e447991e9cffa1a4c1c..bbabfb7dfce2b1662444947e268eb0d6ed8b40f0 100755 --- a/scripts/create-pipeline-failure-incident.rb +++ b/scripts/create-pipeline-failure-incident.rb @@ -7,6 +7,7 @@ require_relative 'api/pipeline_failed_jobs' require_relative 'api/create_issue' +require_relative 'api/create_issue_discussion' class CreatePipelineFailureIncident DEFAULT_OPTIONS = { @@ -28,7 +29,12 @@ def execute labels: incident_labels } - CreateIssue.new(project: project, api_token: api_token).execute(payload) + CreateIssue.new(project: project, api_token: api_token).execute(payload).tap do |incident| + CreateIssueDiscussion.new(project: project, api_token: api_token) + .execute(issue_iid: incident.iid, body: "## Root Cause Analysis") + CreateIssueDiscussion.new(project: project, api_token: api_token) + .execute(issue_iid: incident.iid, body: "## Investigation Steps") + end end private @@ -44,8 +50,16 @@ def now end def title - "#{now.strftime('%A %F %R UTC')} - `#{ENV['CI_PROJECT_PATH']}` broken `#{ENV['CI_COMMIT_REF_NAME']}` " \ - "with #{failed_jobs.size} failed jobs" + @title ||= begin + full_title = "#{now.strftime('%A %F %R UTC')} - `#{ENV['CI_PROJECT_PATH']}` " \ + "broken `#{ENV['CI_COMMIT_REF_NAME']}` with #{failed_jobs.map(&:name).join(', ')}" + + if full_title.size >= 255 + "#{full_title[...252]}..." # max title length is 255, and we add an elipsis + else + full_title + end + end end def description diff --git a/scripts/review_apps/k8s-resources-count-checks.sh b/scripts/review_apps/k8s-resources-count-checks.sh index ae4c8e163e5b5f7b825ab57cc7e08cb55b42af9f..b63fa043065c7dd5886c94330eb81bfad4da2591 100755 --- a/scripts/review_apps/k8s-resources-count-checks.sh +++ b/scripts/review_apps/k8s-resources-count-checks.sh @@ -56,8 +56,6 @@ cat > k8s-resources-count.out <<COMMANDS $(k8s_resource_count daemonsets.apps) daemonsets.apps $(k8s_resource_count deployments.apps) deployments.apps $(k8s_resource_count endpoints) endpoints - $(k8s_resource_count endpointslices.discovery.k8s.io) endpointslices.discovery.k8s.io - $(k8s_resource_count events) events $(k8s_resource_count frontendconfigs.networking.gke.io) frontendconfigs.networking.gke.io $(k8s_resource_count horizontalpodautoscalers.autoscaling) horizontalpodautoscalers.autoscaling $(k8s_resource_count ingressclasses) ingressclasses @@ -71,7 +69,6 @@ cat > k8s-resources-count.out <<COMMANDS $(k8s_resource_count orders.acme.cert-manager.io) orders.acme.cert-manager.io $(k8s_resource_count persistentvolumeclaims) persistentvolumeclaims $(k8s_resource_count poddisruptionbudgets.policy) poddisruptionbudgets.policy - $(k8s_resource_count pods.metrics.k8s.io) pods.metrics.k8s.io $(k8s_resource_count pods) pods $(k8s_resource_count podtemplates) podtemplates $(k8s_resource_count replicasets.apps) replicasets.apps diff --git a/scripts/used-feature-flags b/scripts/used-feature-flags index eb7e85be2299ee07fd9d6f2cbbd59620e83db1ea..74180d02a9181f7176d1141bcdf45e9750224e89 100755 --- a/scripts/used-feature-flags +++ b/scripts/used-feature-flags @@ -114,6 +114,9 @@ if unused_flags.count > 0 puts puts "If they are really no longer needed REMOVE their .yml definition".red puts "If they are needed you need to ENSURE that their usage is covered with specs to continue.".red + puts "Feature flag usage is detected via Rubocop, which is unable to resolve dynamic feature flag usage,".red.bold + puts "interpolated strings however are optimistically matched. For more details consult test suite:".red + puts "https://gitlab.com/gitlab-org/gitlab/-/blob/69cb5d36db95881b495966c95655672cfb816f62/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb".red puts unused_flags.keys.sort.each do |name| puts "- #{name}".yellow diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb index 638183377221258ea63d424c2cacbbe7ee56d357..ed9022faf1b1202130b2af6d4f97ca5f21156558 100644 --- a/spec/controllers/profiles/keys_controller_spec.rb +++ b/spec/controllers/profiles/keys_controller_spec.rb @@ -14,13 +14,14 @@ expires_at = 3.days.from_now expect do - post :create, params: { key: build(:key, expires_at: expires_at).attributes } + post :create, params: { key: build(:key, usage_type: :signing, expires_at: expires_at).attributes } end.to change { Key.count }.by(1) key = Key.last expect(key.expires_at).to be_like_time(expires_at) expect(key.fingerprint_md5).to be_present expect(key.fingerprint_sha256).to be_present + expect(key.usage_type).to eq('signing') end context 'with FIPS mode', :fips_mode do diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index 21df53fb074ab6131786ea1e3c9ba19990393c46..37fc5a033ba4b6d0a82e27dca38f8ffe920ee5da 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -421,6 +421,12 @@ def request expect(json_response.count).to eq(1) expect(json_response.first['label']).to match(/User settings/) end + + it 'makes a call to search_autocomplete_opts' do + expect(controller).to receive(:search_autocomplete_opts).once + + get :autocomplete, params: { term: 'setting', filter: 'generic' } + end end describe '#append_info_to_payload' do diff --git a/spec/db/development/create_work_item_hierarchy_restrictions_spec.rb b/spec/db/development/create_work_item_hierarchy_restrictions_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..0e60ecd08c0655536d72d8da44d3bb34c522af5e --- /dev/null +++ b/spec/db/development/create_work_item_hierarchy_restrictions_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Create work item hierarchy restrictions in development', feature_category: :portfolio_management do + subject { load Rails.root.join('db/fixtures/development/50_create_work_item_hierarchy_restrictions.rb') } + + it_behaves_like 'work item hierarchy restrictions importer' +end diff --git a/spec/db/production/create_work_item_hierarchy_restrictions_spec.rb b/spec/db/production/create_work_item_hierarchy_restrictions_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5b47d88d71a0a559ace1a2efd67dd2cd144711bc --- /dev/null +++ b/spec/db/production/create_work_item_hierarchy_restrictions_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Create work item hierarchy restrictions in production', feature_category: :portfolio_management do + subject { load Rails.root.join('db/fixtures/production/020_create_work_item_hierarchy_restrictions.rb') } + + it_behaves_like 'work item hierarchy restrictions importer' +end diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb index d73ff0284cde24eef5bf22687f2b34465ca186d7..cb11c6fdbb4f38408aa39f979623f996dd905866 100644 --- a/spec/features/projects/settings/repository_settings_spec.rb +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -273,7 +273,12 @@ def select_direction(direction = 'push') end end - expect(page).to have_content('Repository cleanup has started') + # TODO: The following line is skipped because a toast with + # "An error occurred while loading branch rules. Please try again." + # shows up right after which hides the below message. It is causing flakiness. + # https://gitlab.com/gitlab-org/gitlab/-/issues/383717#note_1185091998 + + # expect(page).to have_content('Repository cleanup has started') expect(RepositoryCleanupWorker.jobs.count).to eq(1) end end diff --git a/spec/finders/users_finder_spec.rb b/spec/finders/users_finder_spec.rb index 271dce44db7563c3eb127fa6c327ccc95d4c935a..5cf845a87b24e59d937c2f5097e3211478b47bc1 100644 --- a/spec/finders/users_finder_spec.rb +++ b/spec/finders/users_finder_spec.rb @@ -8,9 +8,7 @@ let_it_be(:project_bot) { create(:user, :project_bot) } - context 'with a normal user' do - let_it_be(:user) { create(:user) } - + shared_examples 'executes users finder as normal user' do it 'returns searchable users' do users = described_class.new(user).execute @@ -97,37 +95,35 @@ end end - context 'with an admin user', :enable_admin_mode do - let_it_be(:admin) { create(:admin) } - + shared_examples 'executes users finder as admin' do it 'filters by external users' do - users = described_class.new(admin, external: true).execute + users = described_class.new(user, external: true).execute expect(users).to contain_exactly(external_user) end it 'returns all users' do - users = described_class.new(admin).execute + users = described_class.new(user).execute - expect(users).to contain_exactly(admin, normal_user, blocked_user, unconfirmed_user, banned_user, external_user, omniauth_user, internal_user, admin_user, project_bot) + expect(users).to contain_exactly(user, normal_user, blocked_user, unconfirmed_user, banned_user, external_user, omniauth_user, internal_user, admin_user, project_bot) end it 'filters by blocked users' do - users = described_class.new(admin, blocked: true).execute + users = described_class.new(user, blocked: true).execute expect(users).to contain_exactly(blocked_user) end it 'filters by active users' do - users = described_class.new(admin, active: true).execute + users = described_class.new(user, active: true).execute - expect(users).to contain_exactly(admin, normal_user, unconfirmed_user, external_user, omniauth_user, admin_user, project_bot) + expect(users).to contain_exactly(user, normal_user, unconfirmed_user, external_user, omniauth_user, admin_user, project_bot) end it 'returns only admins' do - users = described_class.new(admin, admins: true).execute + users = described_class.new(user, admins: true).execute - expect(users).to contain_exactly(admin, admin_user) + expect(users).to contain_exactly(user, admin_user) end it 'filters by custom attributes' do @@ -137,7 +133,7 @@ create :user_custom_attribute, user: internal_user, key: 'foo', value: 'foo' users = described_class.new( - admin, + user, custom_attributes: { foo: 'foo', bar: 'bar' } ).execute @@ -145,10 +141,34 @@ end it 'filters by private emails search' do - users = described_class.new(admin, search: normal_user.email).execute + users = described_class.new(user, search: normal_user.email).execute expect(users).to contain_exactly(normal_user) end end + + context 'with a normal user' do + let_it_be(:user) { create(:user) } + + it_behaves_like 'executes users finder as normal user' + end + + context 'with an admin user' do + let_it_be(:user) { create(:admin) } + + context 'when admin mode setting is disabled', :do_not_mock_admin_mode_setting do + it_behaves_like 'executes users finder as admin' + end + + context 'when admin mode setting is enabled' do + context 'when in admin mode', :enable_admin_mode do + it_behaves_like 'executes users finder as admin' + end + + context 'when not in admin mode' do + it_behaves_like 'executes users finder as normal user' + end + end + end end end diff --git a/spec/frontend/issues/list/components/issue_card_statistics_spec.js b/spec/frontend/issues/list/components/issue_card_statistics_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..180d4ab7eb670af135d7c1e7f6fbe6fb5cca0ec9 --- /dev/null +++ b/spec/frontend/issues/list/components/issue_card_statistics_spec.js @@ -0,0 +1,64 @@ +import { GlIcon } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import IssueCardStatistics from '~/issues/list/components/issue_card_statistics.vue'; +import { i18n } from '~/issues/list/constants'; + +describe('IssueCardStatistics CE component', () => { + let wrapper; + + const findMergeRequests = () => wrapper.findByTestId('merge-requests'); + const findUpvotes = () => wrapper.findByTestId('issuable-upvotes'); + const findDownvotes = () => wrapper.findByTestId('issuable-downvotes'); + + const mountComponent = ({ mergeRequestsCount, upvotes, downvotes } = {}) => { + wrapper = shallowMountExtended(IssueCardStatistics, { + propsData: { + issue: { + mergeRequestsCount, + upvotes, + downvotes, + }, + }, + }); + }; + + describe('when issue attributes are undefined', () => { + it('does not render the attributes', () => { + mountComponent(); + + expect(findMergeRequests().exists()).toBe(false); + expect(findUpvotes().exists()).toBe(false); + expect(findDownvotes().exists()).toBe(false); + }); + }); + + describe('when issue attributes are defined', () => { + beforeEach(() => { + mountComponent({ mergeRequestsCount: 1, upvotes: 5, downvotes: 9 }); + }); + + it('renders merge requests', () => { + const mergeRequests = findMergeRequests(); + + expect(mergeRequests.text()).toBe('1'); + expect(mergeRequests.attributes('title')).toBe(i18n.relatedMergeRequests); + expect(mergeRequests.findComponent(GlIcon).props('name')).toBe('merge-request'); + }); + + it('renders upvotes', () => { + const upvotes = findUpvotes(); + + expect(upvotes.text()).toBe('5'); + expect(upvotes.attributes('title')).toBe(i18n.upvotes); + expect(upvotes.findComponent(GlIcon).props('name')).toBe('thumb-up'); + }); + + it('renders downvotes', () => { + const downvotes = findDownvotes(); + + expect(downvotes.text()).toBe('9'); + expect(downvotes.attributes('title')).toBe(i18n.downvotes); + expect(downvotes.findComponent(GlIcon).props('name')).toBe('thumb-down'); + }); + }); +}); diff --git a/spec/graphql/types/subscription_type_spec.rb b/spec/graphql/types/subscription_type_spec.rb index 04f0c72b06f3aca9f2c4c1a5604883252828baf7..a57a8e751ac0439cbcb2d095851e8bf4f995dfab 100644 --- a/spec/graphql/types/subscription_type_spec.rb +++ b/spec/graphql/types/subscription_type_spec.rb @@ -14,6 +14,7 @@ issuable_milestone_updated merge_request_reviewers_updated merge_request_merge_status_updated + merge_request_approval_state_updated ] expect(described_class).to include_graphql_fields(*expected_fields) diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index 192dfaa9caf939d9b50f3417343e4861912758d8..74a59aa37ce234336ced06d944f39a8728c76fa3 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -60,6 +60,34 @@ def simple_sanitize(str) expect(search_autocomplete_opts(project.name).size).to eq(1) end + context 'for users' do + let_it_be(:another_user) { create(:user, name: 'Jane Doe') } + let(:term) { 'jane' } + + it 'makes a call to SearchService' do + expect(SearchService).to receive(:new).with(current_user, { search: term, scope: 'users' }).and_call_original + + search_autocomplete_opts(term) + end + + it 'returns users matching the term' do + result = search_autocomplete_opts(term) + expect(result.size).to eq(1) + expect(result.first[:id]).to eq(another_user.id) + end + + context 'when current_user cannot read_users_list' do + before do + allow(Ability).to receive(:allowed?).and_return(true) + allow(Ability).to receive(:allowed?).with(current_user, :read_users_list).and_return(false) + end + + it 'returns an empty array' do + expect(search_autocomplete_opts(term)).to eq([]) + end + end + end + it "includes the required project attrs" do project = create(:project, namespace: create(:namespace, owner: user)) result = search_autocomplete_opts(project.name).first diff --git a/spec/lib/api/support/git_access_actor_spec.rb b/spec/lib/api/support/git_access_actor_spec.rb index e1c800d25a7c07aef629e9fe0c7e3560894ca37e..b3e8787583c5e73e637cfcaf8af75062c852359f 100644 --- a/spec/lib/api/support/git_access_actor_spec.rb +++ b/spec/lib/api/support/git_access_actor_spec.rb @@ -9,7 +9,8 @@ subject { described_class.new(user: user, key: key) } describe '.from_params' do - let(:key) { create(:key) } + let_it_be(:user) { create(:user) } + let_it_be(:key) { create(:key, user: user) } context 'with params that are valid' do it 'returns an instance of API::Support::GitAccessActor' do @@ -31,6 +32,42 @@ expect(described_class.from_params(identifier: "key-#{key.id}").user).to eq(key.user) end end + + context 'when passing a signing key' do + let_it_be(:key) { create(:key, usage_type: :signing, user: user) } + + it 'does not identify the user' do + actor = described_class.from_params({ identifier: "key-#{key.id}" }) + + expect(actor).to be_instance_of(described_class) + expect(actor.user).to be_nil + end + + it 'does not identify the key' do + actor = described_class.from_params({ key_id: key.id }) + + expect(actor).to be_instance_of(described_class) + expect(actor.key).to be_nil + end + end + + context 'when passing an auth-only key' do + let_it_be(:key) { create(:key, usage_type: :auth, user: user) } + + it 'identifies the user' do + actor = described_class.from_params({ identifier: "key-#{key.id}" }) + + expect(actor).to be_instance_of(described_class) + expect(actor.user).to eq(key.user) + end + + it 'identifies the key' do + actor = described_class.from_params({ key_id: key.id }) + + expect(actor).to be_instance_of(described_class) + expect(actor.key).to eq(key) + end + end end describe 'attributes' do diff --git a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb index 8da46561b738d9c560343b8df84fac888a260768..736c184a289ebdbaa0b4d0fe11e2552cdaaa4d88 100644 --- a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb @@ -13,7 +13,7 @@ # that we know that we don't want to inherit # as they do not have sense in context of Bridge let(:ignored_inheritable_columns) do - %i[before_script after_script image services cache interruptible timeout + %i[before_script after_script hooks image services cache interruptible timeout retry tags artifacts] end end diff --git a/spec/lib/gitlab/ci/config/entry/default_spec.rb b/spec/lib/gitlab/ci/config/entry/default_spec.rb index 5613b0f09d118939e5ced39158014376defaf72f..46e96843ee38e7b2515907851fb267de3eb8cb52 100644 --- a/spec/lib/gitlab/ci/config/entry/default_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/default_spec.rb @@ -26,9 +26,8 @@ context 'when filtering all the entry/node names' do it 'contains the expected node names' do expect(described_class.nodes.keys) - .to match_array(%i[before_script image services - after_script cache interruptible - timeout retry tags artifacts]) + .to match_array(%i[before_script after_script hooks cache image services + interruptible timeout retry tags artifacts]) end end end diff --git a/spec/lib/gitlab/ci/pipeline/logger_spec.rb b/spec/lib/gitlab/ci/pipeline/logger_spec.rb index 4492ca716ae1aad080ce8a39ab2ab729a923981c..60cc34864147c87268a41f43d43f38fc19837bf4 100644 --- a/spec/lib/gitlab/ci/pipeline/logger_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/logger_spec.rb @@ -213,6 +213,21 @@ def expected_data(count:, db_count: nil) expect(commit).to be_truthy end + + context 'with unexistent observations in condition' do + it 'does not commit the log' do + logger.log_when do |observations| + value = observations['non_existent_value'] + next false unless value + + value > 0 + end + + expect(Gitlab::AppJsonLogger).not_to receive(:info) + + expect(commit).to be_falsey + end + end end context 'when project is not passed and pipeline is not persisted' do diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index fee3731c662f8bd10dacd936b5d416ee759e4c7f..0d1deb863b1e2e268176fd7997bc988e945a954d 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -901,6 +901,37 @@ module Ci ) end end + + context 'when receiving from the default' do + let(:config) do + { + default: { hooks: { pre_get_sources_script: ["echo 1", "echo 2", "pwd"] } }, + test: { script: ["script"] } + } + end + + it "inherits hooks" do + expect(subject[:options][:hooks]).to eq( + { pre_get_sources_script: ["echo 1", "echo 2", "pwd"] } + ) + end + end + + context 'when overriding the default' do + let(:config) do + { + default: { hooks: { pre_get_sources_script: ["echo 1", "echo 2", "pwd"] } }, + test: { script: ["script"], + hooks: { pre_get_sources_script: ["echo 3", "echo 4", "pwd"] } } + } + end + + it "overrides hooks" do + expect(subject[:options][:hooks]).to eq( + { pre_get_sources_script: ["echo 3", "echo 4", "pwd"] } + ) + end + end end end diff --git a/spec/lib/gitlab/database_importers/work_items/hierarchy_restrictions_importer_spec.rb b/spec/lib/gitlab/database_importers/work_items/hierarchy_restrictions_importer_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..d8173794b3f5130b9786e2ee258478a7730759b3 --- /dev/null +++ b/spec/lib/gitlab/database_importers/work_items/hierarchy_restrictions_importer_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter, + feature_category: :portfolio_management do + subject { described_class.upsert_restrictions } + + it_behaves_like 'work item hierarchy restrictions importer' +end diff --git a/spec/lib/gitlab/slash_commands/application_help_spec.rb b/spec/lib/gitlab/slash_commands/application_help_spec.rb index b182c0e5cc6c7aa89fa33172dbec8ffeb143fd07..d0cefdf489546d84d99ea57ea5f305ce81efc6f0 100644 --- a/spec/lib/gitlab/slash_commands/application_help_spec.rb +++ b/spec/lib/gitlab/slash_commands/application_help_spec.rb @@ -4,13 +4,11 @@ RSpec.describe Gitlab::SlashCommands::ApplicationHelp do let(:params) { { command: '/gitlab', text: 'help' } } - let_it_be(:user) { create(:user) } - let_it_be(:chat_user) { create(:chat_name, user: user) } let(:project) { build(:project) } describe '#execute' do subject do - described_class.new(project, chat_user, params).execute + described_class.new(project, params).execute end it 'displays the help section' do diff --git a/spec/lib/gitlab/ssh/signature_spec.rb b/spec/lib/gitlab/ssh/signature_spec.rb index e8d366f076230d28412cee8061468be28cf767f3..f3f1ba84f9e236568cd1fdf000586030b1b5ab98 100644 --- a/spec/lib/gitlab/ssh/signature_spec.rb +++ b/spec/lib/gitlab/ssh/signature_spec.rb @@ -7,7 +7,7 @@ let_it_be(:committer_email) { 'ssh-commit-test@example.com' } let_it_be(:public_key_text) { 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJKOfqOH0fDde+Ua/1SObkXB1CEDF5M6UfARMpW3F87u' } let_it_be_with_reload(:user) { create(:user, email: committer_email) } - let_it_be_with_reload(:key) { create(:key, key: public_key_text, user: user) } + let_it_be_with_reload(:key) { create(:key, usage_type: :signing, key: public_key_text, user: user) } let(:signed_text) { 'This message was signed by an ssh key' } @@ -204,13 +204,25 @@ it_behaves_like 'unverified signature' end - context 'when key does not exist in GitLab' do - before do - key.delete + context 'when the signing key does not exist in GitLab' do + context 'when the key is not a signing one' do + before do + key.auth! + end + + it 'reports unknown_key status' do + expect(signature.verification_status).to eq(:unknown_key) + end end - it 'reports unknown_key status' do - expect(signature.verification_status).to eq(:unknown_key) + context 'when the key is removed' do + before do + key.delete + end + + it 'reports unknown_key status' do + expect(signature.verification_status).to eq(:unknown_key) + end end end diff --git a/spec/migrations/add_okr_hierarchy_restrictions_spec.rb b/spec/migrations/add_okr_hierarchy_restrictions_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..9923795925dabe1b67b613f053464aec680b2e65 --- /dev/null +++ b/spec/migrations/add_okr_hierarchy_restrictions_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe AddOkrHierarchyRestrictions, :migration, feature_category: :portfolio_management do + include MigrationHelpers::WorkItemTypesHelper + + let_it_be(:restrictions) { table(:work_item_hierarchy_restrictions) } + let_it_be(:work_item_types) { table(:work_item_types) } + + it 'creates default restrictions' do + restrictions.delete_all + + reversible_migration do |migration| + migration.before -> { + expect(restrictions.count).to eq(0) + } + + migration.after -> { + expect(restrictions.count).to eq(4) + } + end + end + + context 'when work items are missing' do + before do + work_item_types.delete_all + end + + it 'does nothing' do + expect { migrate! }.not_to change { restrictions.count } + end + end +end diff --git a/spec/models/group_deploy_key_spec.rb b/spec/models/group_deploy_key_spec.rb index dfb4fee593f9ea273c3bd2d0289c4934736eb5f8..c1fd88ad748e81de9b3e5334bd18fa6ecf820a97 100644 --- a/spec/models/group_deploy_key_spec.rb +++ b/spec/models/group_deploy_key_spec.rb @@ -30,6 +30,12 @@ end end + describe '.defined_enums' do + it 'excludes the inherited enum' do + expect(described_class.defined_enums).to eq({}) + end + end + describe '#can_be_edited_for' do let_it_be(:user) { create(:user) } diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index b98c0e8eae02309a851f4d3e03ba1963320d9f3c..92f4d6d85316bc899540e74ee8307fa19c6979fd 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Key, :mailer do + it_behaves_like 'having unique enum values' + describe "Associations" do it { is_expected.to belong_to(:user) } end @@ -216,6 +218,20 @@ end end end + + context 'usage type scopes' do + let_it_be(:auth_key) { create(:key, usage_type: :auth) } + let_it_be(:auth_and_signing_key) { create(:key, usage_type: :auth_and_signing) } + let_it_be(:signing_key) { create(:key, usage_type: :signing) } + + it 'auth scope returns auth and auth_and_signing keys' do + expect(described_class.auth).to match_array([auth_key, auth_and_signing_key]) + end + + it 'signing scope returns signing and auth_and_signing keys' do + expect(described_class.signing).to match_array([signing_key, auth_and_signing_key]) + end + end end context 'validation of uniqueness (based on fingerprint uniqueness)' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index fdeb98f52aea9f29d319d358a0bfb3d3594d9524..43541250904abe744d3a3e20e49f824350049a2f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -3090,6 +3090,14 @@ expect(described_class.find_by_ssh_key_id(-1)).to be_nil end end + + it 'does not return a signing-only key', :aggregate_failures do + signing_key = create(:key, usage_type: :signing, user: user) + auth_and_signing_key = create(:key, usage_type: :auth_and_signing, user: user) + + expect(described_class.find_by_ssh_key_id(signing_key.id)).to be_nil + expect(described_class.find_by_ssh_key_id(auth_and_signing_key.id)).to eq(user) + end end shared_examples "find user by login" do diff --git a/spec/models/work_items/type_spec.rb b/spec/models/work_items/type_spec.rb index 6685720778abcc90f8774e24aa0aef18e36ab3f4..1d8c5e79bf2a5902943fdf7761b1afd6fa947b76 100644 --- a/spec/models/work_items/type_spec.rb +++ b/spec/models/work_items/type_spec.rb @@ -75,6 +75,32 @@ end end + describe '.default_by_type' do + let(:default_issue_type) { described_class.find_by(namespace_id: nil, base_type: :issue) } + + subject { described_class.default_by_type(:issue) } + + it 'returns default work item type by base type without calling importer' do + expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).not_to receive(:upsert_types) + expect(Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter).not_to receive(:upsert_restrictions) + + expect(subject).to eq(default_issue_type) + end + + context 'when default types are missing' do + before do + described_class.delete_all + end + + it 'creates types and restrictions and returns default work item type by base type' do + expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).to receive(:upsert_types) + expect(Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter).to receive(:upsert_restrictions) + + expect(subject).to eq(default_issue_type) + end + end + end + describe '#default?' do subject { build(:work_item_type, namespace: namespace).default? } diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index 32cacfc713c77acdeb73e9f6549900529dfba261..5ad56d43f884f03cb356f9474f38601046d08a70 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -325,6 +325,28 @@ def perform_request(headers: gitlab_shell_internal_api_request_header) expect(json_response['name']).to eq(user.name) end + context 'when signing key is passed' do + it 'does not authenticate user' do + key.signing! + + get(api("/internal/discover"), params: { key_id: key.id }, headers: gitlab_shell_internal_api_request_header) + + expect(json_response).to be_nil + end + end + + context 'when auth-only key is passed' do + it 'authenticates user' do + key.auth! + + get(api("/internal/discover"), params: { key_id: key.id }, headers: gitlab_shell_internal_api_request_header) + + expect(response).to have_gitlab_http_status(:ok) + + expect(json_response['name']).to eq(user.name) + end + end + it "finds a user by username" do get(api("/internal/discover"), params: { username: user.username }, headers: gitlab_shell_internal_api_request_header) @@ -360,6 +382,30 @@ def perform_request(headers: gitlab_shell_internal_api_request_header) expect(json_response['key'].split[1]).to eq(key.key.split[1]) end + context 'when signing key is passed' do + it 'does not return the key' do + key.signing! + + get(api('/internal/authorized_keys'), params: { key: key.key.split[1] }, headers: gitlab_shell_internal_api_request_header) + + expect(response).to have_gitlab_http_status(:not_found) + + expect(json_response['id']).to be_nil + end + end + + context 'when auth-only key is passed' do + it 'authenticates user' do + key.auth! + + get(api('/internal/authorized_keys'), params: { key: key.key.split[1] }, headers: gitlab_shell_internal_api_request_header) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['id']).to eq(key.id) + expect(json_response['key'].split[1]).to eq(key.key.split[1]) + end + end + it 'exposes the comment of the key as a simple identifier of username + hostname' do get(api('/internal/authorized_keys'), params: { key: key.key.split[1] }, headers: gitlab_shell_internal_api_request_header) diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index c072e6d48db1b58d389cdae3c012debf6193de05..a3fa85dee73a4ab371e3dff26df76406feef6428 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -1031,7 +1031,7 @@ def authorization_in_action(action) end describe 'to a forked project' do - let_it_be(:upstream_project) { create(:project, :public) } + let_it_be_with_reload(:upstream_project) { create(:project, :public) } let_it_be(:project_owner) { create(:user) } let(:project) { fork_project(upstream_project, project_owner) } @@ -1069,6 +1069,56 @@ def authorization_in_action(action) end end + describe 'when user has push access to upstream project' do + before do + upstream_project.add_maintainer(user) + end + + context 'an MR exists on target forked project' do + let(:allow_collaboration) { true } + let(:merge_request) do + create(:merge_request, + target_project: upstream_project, + source_project: project, + allow_collaboration: allow_collaboration) + end + + before do + merge_request + end + + context 'with allow_collaboration option set to true' do + context 'and request is sent by gitlab-workhorse to authorize the request' do + before do + put_authorize + end + + it_behaves_like 'LFS http 200 workhorse response' + end + + context 'and request is sent by gitlab-workhorse to finalize the upload' do + before do + put_finalize + end + + it_behaves_like 'LFS http 200 response' + end + end + + context 'with allow_collaboration option set to false' do + context 'request is sent by gitlab-workhorse to authorize the request' do + let(:allow_collaboration) { false } + + before do + put_authorize + end + + it_behaves_like 'forbidden' + end + end + end + end + describe 'and user does not have push access' do it_behaves_like 'forbidden' end diff --git a/spec/services/ci/create_pipeline_service/scripts_spec.rb b/spec/services/ci/create_pipeline_service/scripts_spec.rb index 493e341395b28afc6cb6deacae92bb0b18fc589b..50b558e505a5638b99e9a9dd08afe8e1cf539cf1 100644 --- a/spec/services/ci/create_pipeline_service/scripts_spec.rb +++ b/spec/services/ci/create_pipeline_service/scripts_spec.rb @@ -39,24 +39,74 @@ end end - context 'when job has hooks' do + context 'when job has hooks and default hooks' do let(:config) do <<-CI_CONFIG - job: + default: hooks: - pre_get_sources_script: echo 'hello job pre_get_sources_script' - script: echo 'hello job script' + pre_get_sources_script: + - echo 'hello default pre_get_sources_script' + + job1: + hooks: + pre_get_sources_script: + - echo 'hello job1 pre_get_sources_script' + script: echo 'hello job1 script' + + job2: + script: echo 'hello job2 script' + + job3: + inherit: + default: false + script: echo 'hello job3 script' CI_CONFIG end - it 'creates a job with script data' do + it 'creates jobs with hook data' do expect(pipeline).to be_created_successfully - expect(pipeline.builds.first).to have_attributes( - name: 'job', + expect(pipeline.builds.find_by(name: 'job1')).to have_attributes( + name: 'job1', stage: 'test', - options: { script: ["echo 'hello job script'"], - hooks: { pre_get_sources_script: ["echo 'hello job pre_get_sources_script'"] } } + options: { script: ["echo 'hello job1 script'"], + hooks: { pre_get_sources_script: ["echo 'hello job1 pre_get_sources_script'"] } } ) + expect(pipeline.builds.find_by(name: 'job2')).to have_attributes( + name: 'job2', + stage: 'test', + options: { script: ["echo 'hello job2 script'"], + hooks: { pre_get_sources_script: ["echo 'hello default pre_get_sources_script'"] } } + ) + expect(pipeline.builds.find_by(name: 'job3')).to have_attributes( + name: 'job3', + stage: 'test', + options: { script: ["echo 'hello job3 script'"] } + ) + end + + context 'when the FF ci_hooks_pre_get_sources_script is disabled' do + before do + stub_feature_flags(ci_hooks_pre_get_sources_script: false) + end + + it 'creates jobs without hook data' do + expect(pipeline).to be_created_successfully + expect(pipeline.builds.find_by(name: 'job1')).to have_attributes( + name: 'job1', + stage: 'test', + options: { script: ["echo 'hello job1 script'"] } + ) + expect(pipeline.builds.find_by(name: 'job2')).to have_attributes( + name: 'job2', + stage: 'test', + options: { script: ["echo 'hello job2 script'"] } + ) + expect(pipeline.builds.find_by(name: 'job3')).to have_attributes( + name: 'job3', + stage: 'test', + options: { script: ["echo 'hello job3 script'"] } + ) + end end end end diff --git a/spec/services/users/keys_count_service_spec.rb b/spec/services/users/keys_count_service_spec.rb index aff267cce5e0b99f33dbcc10df0d631c15db2d71..607d2946b2cc69803642faa067a77073adeba7e4 100644 --- a/spec/services/users/keys_count_service_spec.rb +++ b/spec/services/users/keys_count_service_spec.rb @@ -17,6 +17,12 @@ it 'returns the number of SSH keys as an Integer' do expect(subject.count).to eq(1) end + + it 'does not count signing keys' do + create(:key, usage_type: :signing, user: user) + + expect(subject.count).to eq(1) + end end describe '#uncached_count' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e083bb062415129f154ac8a4d50f1b273e4d7f34..943105a57c44f18f2886c9a0decd697d3f77d8a3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -457,6 +457,13 @@ config.before(:each, :js) do allow_any_instance_of(VersionCheck).to receive(:response).and_return({ "severity" => "success" }) end + + # Add warning for example missing feature_category + config.before do |example| + if example.metadata[:feature_category].blank? && !ENV['CI'] + warn "Missing metadata feature_category: #{example.location} See https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#feature-category-metadata" + end + end end ActiveRecord::Migration.maintain_test_schema! diff --git a/spec/support/shared_examples/work_item_hierarchy_restrictions_importer.rb b/spec/support/shared_examples/work_item_hierarchy_restrictions_importer.rb new file mode 100644 index 0000000000000000000000000000000000000000..a1bccb7b7a39e20b01c19fc2ab132cb44860637c --- /dev/null +++ b/spec/support/shared_examples/work_item_hierarchy_restrictions_importer.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'work item hierarchy restrictions importer' do + shared_examples_for 'adds restrictions' do + it "adds all restrictions if they don't exist" do + expect { subject }.to change { WorkItems::HierarchyRestriction.count }.from(0).to(4) + end + end + + it_behaves_like 'adds restrictions' + + context 'when base types are missing' do + before do + WorkItems::Type.delete_all + end + + it_behaves_like 'adds restrictions' + end + + context 'when restrictions already exist' do + before do + Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter.upsert_restrictions + end + + it 'upserts restrictions' do + restriction = WorkItems::HierarchyRestriction.first + depth = restriction.maximum_depth + + restriction.update!(maximum_depth: depth + 1) + + expect do + subject + restriction.reload + end.to not_change { WorkItems::HierarchyRestriction.count }.and( + change { restriction.maximum_depth }.from(depth + 1).to(depth) + ) + end + end + + context 'when some restrictions are missing' do + before do + Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter.upsert_restrictions + WorkItems::HierarchyRestriction.limit(1).delete_all + end + + it 'inserts missing restrictions and does nothing if some already existed' do + expect { subject }.to make_queries_matching(/INSERT/, 1).and( + change { WorkItems::HierarchyRestriction.count }.by(1) + ) + expect(WorkItems::HierarchyRestriction.count).to eq(4) + end + end +end diff --git a/spec/tasks/gitlab/shell_rake_spec.rb b/spec/tasks/gitlab/shell_rake_spec.rb index 52a9738fb51bf11d0f8dbcf267c909c618af1ec8..195859eac7064da71d2ad56e9506ba192b0ed53b 100644 --- a/spec/tasks/gitlab/shell_rake_spec.rb +++ b/spec/tasks/gitlab/shell_rake_spec.rb @@ -22,4 +22,23 @@ run_rake_task('gitlab:shell:install') end end + + describe 'setup task' do + it 'writes authorized keys into the file' do + allow(Gitlab::CurrentSettings).to receive(:authorized_keys_enabled?).and_return(true) + stub_env('force', 'yes') + + auth_key = create(:key) + auth_and_signing_key = create(:key, usage_type: :auth_and_signing) + create(:key, usage_type: :signing) + + expect_next_instance_of(Gitlab::AuthorizedKeys) do |instance| + expect(instance).to receive(:batch_add_keys).once do |keys| + expect(keys).to match_array([auth_key, auth_and_signing_key]) + end + end + + run_rake_task('gitlab:shell:setup') + end + end end diff --git a/spec/views/profiles/keys/_form.html.haml_spec.rb b/spec/views/profiles/keys/_form.html.haml_spec.rb index 3c61afb21c5c879a894c52c8ad2157e6068648c2..2a1bb5334b622553a2ac4faa5b045c79b39ca07c 100644 --- a/spec/views/profiles/keys/_form.html.haml_spec.rb +++ b/spec/views/profiles/keys/_form.html.haml_spec.rb @@ -32,6 +32,11 @@ expect(rendered).to have_text('Key titles are publicly visible.') end + it 'has the usage type field', :aggregate_failures do + expect(page).to have_select _('Usage type'), + selected: 'Authentication & Signing', options: ['Authentication & Signing', 'Authentication', 'Signing'] + end + it 'has the expires at field', :aggregate_failures do expect(rendered).to have_field('Expiration date', type: 'text') expect(page.find_field('Expiration date')['min']).to eq(l(1.day.from_now, format: "%Y-%m-%d")) @@ -47,4 +52,17 @@ expect(rendered).to have_button('Add key') end end + + context 'when ssh_key_usage_types is disabled' do + before do + stub_feature_flags(ssh_key_usage_types: false) + end + + it 'has the usage type field', :aggregate_failures do + render + + expect(rendered).not_to have_field('Usage type', type: 'text') + expect(rendered).not_to have_text('Authentication & Signing') + end + end end diff --git a/spec/views/profiles/keys/_key.html.haml_spec.rb b/spec/views/profiles/keys/_key.html.haml_spec.rb index 1040541332df55aa0592958a6370c566f1e368c1..821e7ea794d67a2193268ff191b86814802023e6 100644 --- a/spec/views/profiles/keys/_key.html.haml_spec.rb +++ b/spec/views/profiles/keys/_key.html.haml_spec.rb @@ -30,6 +30,38 @@ expect(response).to render_template(partial: 'shared/ssh_keys/_key_delete') end + context 'displays the usage type' do + where(:usage_type, :usage_type_text) do + [ + [:auth, 'Authentication'], + [:auth_and_signing, 'Authentication & Signing'], + [:signing, 'Signing'] + ] + end + + with_them do + let(:key) { create(:key, user: user, usage_type: usage_type) } + + it 'renders usage type text' do + render + + expect(rendered).to have_text(usage_type_text) + end + + context 'when ssh_key_usage_types is disabled' do + before do + stub_feature_flags(ssh_key_usage_types: false) + end + + it 'does not render usage type text' do + render + + expect(rendered).not_to have_text(usage_type_text) + end + end + end + end + context 'when the key has not been used' do let_it_be(:key) do create(:personal_key, diff --git a/spec/views/profiles/keys/_key_details.html.haml_spec.rb b/spec/views/profiles/keys/_key_details.html.haml_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..acb22b5657ed81da782ddc863d218fc6417a68b9 --- /dev/null +++ b/spec/views/profiles/keys/_key_details.html.haml_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'profiles/keys/_key_details.html.haml' do + let_it_be(:user) { create(:user) } + + before do + assign(:key, key) + allow(view).to receive(:is_admin).and_return(false) + end + + describe 'displays the usage type' do + where(:usage_type, :usage_type_text) do + [ + [:auth, 'Authentication'], + [:auth_and_signing, 'Authentication & Signing'], + [:signing, 'Signing'] + ] + end + + with_them do + let(:key) { create(:key, user: user, usage_type: usage_type) } + + it 'renders usage type text' do + render + + expect(rendered).to have_text(usage_type_text) + end + + context 'when ssh_key_usage_types is disabled' do + before do + stub_feature_flags(ssh_key_usage_types: false) + end + + it 'does not render usage type text' do + render + + expect(rendered).not_to have_text(usage_type_text) + end + end + end + end +end diff --git a/tooling/config/CODEOWNERS.yml b/tooling/config/CODEOWNERS.yml index d729ae5d5321fe93a458e89d81a14478decc9836..07fddde056c2584a862d7c6d5fe115885d26006b 100644 --- a/tooling/config/CODEOWNERS.yml +++ b/tooling/config/CODEOWNERS.yml @@ -60,6 +60,7 @@ - 'config/audit_events/' - 'runner_token_expiration/' - '*metadata_id_tokens*' + - '/app/assets/javascripts/invite_members/' patterns: - '%{keyword}' diff --git a/yarn.lock b/yarn.lock index e6e7d5cb9cae966afddd252eab795379af5a2668..82f0d01c3990ffc3fdf376d3d27ef87002ac1d9b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1041,6 +1041,18 @@ resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.1.tgz#b6b8d81780b9a9f6459f4bfe9226ac6aefaefe87" integrity sha512-aG20vknL4/YjQF9BSV7ts4EWm/yrjagAN7OWBNmlbEOUiu0llj4OGrFoOKK3g2vey4/p2omKCoHrWtPxSwV3HA== +"@cubejs-client/core@^0.31.0": + version "0.31.15" + resolved "https://registry.yarnpkg.com/@cubejs-client/core/-/core-0.31.15.tgz#db0ee90f5ba7f33a3fae6c81e5e13ab1cf2cd71b" + integrity sha512-VQqvvJn++nqO8aOr/dFtyUURNFYAlP3XlDiupiGLXmSsuUn0BuozJQAmJ5XxPPhvz5k9qBko7KkZuC6ikZTdcA== + dependencies: + core-js "^3.6.5" + cross-fetch "^3.0.2" + dayjs "^1.10.4" + ramda "^0.27.2" + url-search-params-polyfill "^7.0.0" + uuid "^8.3.2" + "@discoveryjs/json-ext@^0.5.0": version "0.5.6" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz#d5e0706cf8c6acd8c6032f8d54070af261bbbb2f" @@ -1108,15 +1120,15 @@ stylelint-declaration-strict-value "1.8.0" stylelint-scss "4.2.0" -"@gitlab/svgs@3.8.0": - version "3.8.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.8.0.tgz#bc7fa51e345e26cff56fdff629ea439adfa1e0cb" - integrity sha512-DUWeG2Vx+1ntZ/1GT6S36ZOtXvM5Wm02MtDRrQS4GuOX4rkTeG9aoutSJuwQ2h9BNtxl0U/jkf5GVBxacj18XA== +"@gitlab/svgs@3.11.0": + version "3.11.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.11.0.tgz#91e8e25583cddef48c0c79175203e5b0a4eaa519" + integrity sha512-1cJu1WXPoOHfGgv5fT3nmA9cgAQ3U1Fm/oMSVYUgBxU35R0I8W704GMLsIZwBuQ/S/Ow7WLwIkoOhLb/spNKPg== -"@gitlab/ui@49.11.1": - version "49.11.1" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-49.11.1.tgz#ce18f23ac4f48159e8f57f8dedef2f05890a97f2" - integrity sha512-iFhhi03Vrz+wxxUzwVmaaP1s0qeJtACCEpj7xESDVbevjDEqf1muMz/PTH10NslrVbf1VchqNwSC+Ww6C7yAKQ== +"@gitlab/ui@49.11.2": + version "49.11.2" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-49.11.2.tgz#290bba7a3d4682365ad81747cf54a2f9927526c1" + integrity sha512-qu5qcl+4niYBCPIZS9ZU0i1h/IGL4ZOp4hDsEAIUFGJg9Sp0TBmwdjwKJQbvnexDS3xs1eSBzi+kQ57H+c9wQQ== dependencies: "@popperjs/core" "^2.11.2" bootstrap-vue "2.20.1" @@ -3906,7 +3918,7 @@ core-js-pure@^3.0.0: resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813" integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA== -core-js@^3.26.1: +core-js@^3.26.1, core-js@^3.6.5: version "3.26.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.26.1.tgz#7a9816dabd9ee846c1c0fe0e8fcad68f3709134e" integrity sha512-21491RRQVzUn0GGM9Z1Jrpr6PNPxPi+Za8OM9q4tksTSnlbXXGKK1nXNg/QvwFYettXvSX6zWKCtHHfjN4puyA== @@ -3991,6 +4003,13 @@ cropper@^2.3.0: dependencies: jquery ">= 1.9.1" +cross-fetch@^3.0.2: + version "3.1.5" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" + integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== + dependencies: + node-fetch "2.6.7" + cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -4689,6 +4708,11 @@ dateformat@^5.0.1: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-5.0.1.tgz#60a27a2deb339f888ba4532f533e25ac73ca3d19" integrity sha512-DrcKxOW2am3mtqoJwBTK3OlWcF0QSk1p8diEWwpu3Mf//VdURD7XVaeOV738JvcaBiFfm9o2fisoMhiJH0aYxg== +dayjs@^1.10.4: + version "1.11.6" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.6.tgz#2e79a226314ec3ec904e3ee1dd5a4f5e5b1c7afb" + integrity sha512-zZbY5giJAinCG+7AGaw0wIhNZ6J8AhWuSXKvuc1KAyMiRsvGQWqh4L+MomvhdAYjN+lqvVCMq1I41e3YHvXkyQ== + de-indent@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" @@ -9257,7 +9281,7 @@ node-domexception@1.0.0: resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== -node-fetch@^2.6.1, node-fetch@^2.6.7: +node-fetch@2.6.7, node-fetch@^2.6.1, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== @@ -10290,6 +10314,11 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +ramda@^0.27.2: + version "0.27.2" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.2.tgz#84463226f7f36dc33592f6f4ed6374c48306c3f1" + integrity sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA== + randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -12117,6 +12146,11 @@ url-loader@^4.1.1: mime-types "^2.1.27" schema-utils "^3.0.0" +url-search-params-polyfill@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/url-search-params-polyfill/-/url-search-params-polyfill-7.0.1.tgz#b900cd9a0d9d2ff757d500135256f2344879cbff" + integrity sha512-bAw7L2E+jn9XHG5P9zrPnHdO0yJub4U+yXJOdpcpkr7OBd9T8oll4lUos0iSGRcDvfZoLUKfx9a6aNmIhJ4+mQ== + url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"