diff options
57 files changed, 798 insertions, 152 deletions
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index 5010d60f374..bd06c064ab7 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -11,7 +11,6 @@ import { GlIntersectionObserver, } from '@gitlab/ui'; import { escapeRegExp } from 'lodash'; -import filesQuery from 'shared_queries/repository/files.query.graphql'; import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql'; import { escapeFileUrl } from '~/lib/utils/url_utility'; import { TREE_PAGE_SIZE } from '~/repository/constants'; @@ -178,8 +177,7 @@ export default { return this.isFolder ? this.loadFolder() : this.loadBlob(); }, loadFolder() { - const query = this.glFeatures.paginatedTreeGraphqlQuery ? paginatedTreeQuery : filesQuery; - this.apolloQuery(query, { + this.apolloQuery(paginatedTreeQuery, { projectPath: this.projectPath, ref: this.ref, path: this.path, diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue index 16dfe3cfb14..ffe8d5531f8 100644 --- a/app/assets/javascripts/repository/components/tree_content.vue +++ b/app/assets/javascripts/repository/components/tree_content.vue @@ -1,5 +1,4 @@ <script> -import filesQuery from 'shared_queries/repository/files.query.graphql'; import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql'; import createFlash from '~/flash'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -72,9 +71,6 @@ export default { hasShowMore() { return !this.clickedShowMore && this.pageLimitReached; }, - paginatedTreeEnabled() { - return this.glFeatures.paginatedTreeGraphqlQuery; - }, }, watch: { @@ -101,7 +97,7 @@ export default { return this.$apollo .query({ - query: this.paginatedTreeEnabled ? paginatedTreeQuery : filesQuery, + query: paginatedTreeQuery, variables: { projectPath: this.projectPath, ref: this.ref, @@ -114,20 +110,19 @@ export default { if (data.errors) throw data.errors; if (!data?.project?.repository || originalPath !== (this.path || '/')) return; - const pageInfo = this.paginatedTreeEnabled - ? data.project.repository.paginatedTree.pageInfo - : this.hasNextPage(data.project.repository.tree); + const { + project: { + repository: { + paginatedTree: { pageInfo }, + }, + }, + } = data; this.isLoadingFiles = false; this.entries = Object.keys(this.entries).reduce( (acc, key) => ({ ...acc, - [key]: this.normalizeData( - key, - this.paginatedTreeEnabled - ? data.project.repository.paginatedTree.nodes[0][key] - : data.project.repository.tree[key].edges, - ), + [key]: this.normalizeData(key, data.project.repository.paginatedTree.nodes[0][key]), }), {}, ); @@ -149,9 +144,7 @@ export default { }); }, normalizeData(key, data) { - return this.entries[key].concat( - this.paginatedTreeEnabled ? data.nodes : data.map(({ node }) => node), - ); + return this.entries[key].concat(data.nodes); }, hasNextPage(data) { return [] diff --git a/app/assets/javascripts/repository/mixins/preload.js b/app/assets/javascripts/repository/mixins/preload.js index a2ddcbf0e4c..30c36dee48f 100644 --- a/app/assets/javascripts/repository/mixins/preload.js +++ b/app/assets/javascripts/repository/mixins/preload.js @@ -1,4 +1,3 @@ -import filesQuery from 'shared_queries/repository/files.query.graphql'; import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql'; import projectPathQuery from '../queries/project_path.query.graphql'; import getRefMixin from './get_ref'; @@ -22,7 +21,7 @@ export default { return this.$apollo .query({ - query: gon.features.paginatedTreeGraphqlQuery ? paginatedTreeQuery : filesQuery, + query: paginatedTreeQuery, variables: { projectPath: this.projectPath, ref: this.ref, diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue index 40044e518c3..2c74d56f617 100644 --- a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue @@ -1,5 +1,5 @@ <script> -import { GlIcon } from '@gitlab/ui'; +import { GlIcon, GlSafeHtmlDirective } from '@gitlab/ui'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { HIGHLIGHT_CLASS_NAME } from './constants'; import ViewerMixin from './mixins'; @@ -9,6 +9,9 @@ export default { components: { GlIcon, }, + directives: { + SafeHtml: GlSafeHtmlDirective, + }, mixins: [ViewerMixin, glFeatureFlagsMixin()], inject: ['blobHash'], data() { @@ -65,7 +68,7 @@ export default { <div class="blob-content"> <pre class="code highlight" - ><code :data-blob-hash="blobHash" v-html="content /* eslint-disable-line vue/no-v-html */"></code></pre> + ><code v-safe-html="content" :data-blob-hash="blobHash"></code></pre> </div> </div> </div> diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb index be2abc5cddf..7fba6cc5bf4 100644 --- a/app/controllers/projects/releases_controller.rb +++ b/app/controllers/projects/releases_controller.rb @@ -5,9 +5,6 @@ class Projects::ReleasesController < Projects::ApplicationController before_action :require_non_empty_project, except: [:index] before_action :release, only: %i[edit show update downloads] before_action :authorize_read_release! - # We have to check `download_code` permission because detail URL path - # contains git-tag name. - before_action :authorize_download_code!, except: [:index] before_action :authorize_update_release!, only: %i[edit update] before_action :authorize_create_release!, only: :new before_action only: :index do diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index c36b30e198b..f8f2c1f0836 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -17,7 +17,6 @@ class Projects::TreeController < Projects::ApplicationController before_action do push_frontend_feature_flag(:lazy_load_commits, @project, default_enabled: :yaml) - push_frontend_feature_flag(:paginated_tree_graphql_query, @project, default_enabled: :yaml) push_frontend_feature_flag(:new_dir_modal, @project, default_enabled: :yaml) push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml) end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index a5fa1807cc9..5b17b75a963 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -37,7 +37,6 @@ class ProjectsController < Projects::ApplicationController push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml) push_frontend_feature_flag(:refactor_text_viewer, @project, default_enabled: :yaml) push_frontend_feature_flag(:increase_page_size_exponentially, @project, default_enabled: :yaml) - push_frontend_feature_flag(:paginated_tree_graphql_query, @project, default_enabled: :yaml) push_frontend_feature_flag(:new_dir_modal, @project, default_enabled: :yaml) end diff --git a/app/finders/issuables/label_filter.rb b/app/finders/issuables/label_filter.rb index 98e98c0d44e..9a6ca107b19 100644 --- a/app/finders/issuables/label_filter.rb +++ b/app/finders/issuables/label_filter.rb @@ -135,11 +135,7 @@ module Issuables # rubocop: disable CodeReuse/ActiveRecord def label_link_query(target_model, label_ids: nil, label_names: nil) - relation = LabelLink - .where(target_type: target_model.name) - .where(LabelLink.arel_table['target_id'].eq(target_model.arel_table['id'])) - - relation = relation.where(label_id: label_ids) if label_ids + relation = LabelLink.by_target_for_exists_query(target_model.name, target_model.arel_table['id'], label_ids) relation = relation.joins(:label).where(labels: { name: label_names }) if label_names relation diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index dd73fb8dceb..b6cb9cd3302 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -306,7 +306,7 @@ module Types null: true, description: 'A single release of the project.', resolver: Resolvers::ReleasesResolver.single, - authorize: :download_code + authorize: :read_release field :container_expiration_policy, Types::ContainerExpirationPolicyType, diff --git a/app/graphql/types/release_links_type.rb b/app/graphql/types/release_links_type.rb index 7830e29f3cd..37ad52ce6d0 100644 --- a/app/graphql/types/release_links_type.rb +++ b/app/graphql/types/release_links_type.rb @@ -4,7 +4,7 @@ module Types class ReleaseLinksType < BaseObject graphql_name 'ReleaseLinks' - authorize :download_code + authorize :read_release alias_method :release, :object @@ -16,14 +16,19 @@ module Types description: "HTTP URL of the release's edit page.", authorize: :update_release field :opened_merge_requests_url, GraphQL::Types::String, null: true, - description: 'HTTP URL of the merge request page, filtered by this release and `state=open`.' + description: 'HTTP URL of the merge request page, filtered by this release and `state=open`.', + authorize: :download_code field :merged_merge_requests_url, GraphQL::Types::String, null: true, - description: 'HTTP URL of the merge request page , filtered by this release and `state=merged`.' + description: 'HTTP URL of the merge request page , filtered by this release and `state=merged`.', + authorize: :download_code field :closed_merge_requests_url, GraphQL::Types::String, null: true, - description: 'HTTP URL of the merge request page , filtered by this release and `state=closed`.' + description: 'HTTP URL of the merge request page , filtered by this release and `state=closed`.', + authorize: :download_code field :opened_issues_url, GraphQL::Types::String, null: true, - description: 'HTTP URL of the issues page, filtered by this release and `state=open`.' + description: 'HTTP URL of the issues page, filtered by this release and `state=open`.', + authorize: :download_code field :closed_issues_url, GraphQL::Types::String, null: true, - description: 'HTTP URL of the issues page, filtered by this release and `state=closed`.' + description: 'HTTP URL of the issues page, filtered by this release and `state=closed`.', + authorize: :download_code end end diff --git a/app/graphql/types/release_type.rb b/app/graphql/types/release_type.rb index 6dda93c7329..fcc9ec49252 100644 --- a/app/graphql/types/release_type.rb +++ b/app/graphql/types/release_type.rb @@ -14,8 +14,7 @@ module Types present_using ReleasePresenter field :tag_name, GraphQL::Types::String, null: true, method: :tag, - description: 'Name of the tag associated with the release.', - authorize: :download_code + description: 'Name of the tag associated with the release.' field :tag_path, GraphQL::Types::String, null: true, description: 'Relative web path to the tag associated with the release.', authorize: :download_code diff --git a/app/graphql/types/repository_type.rb b/app/graphql/types/repository_type.rb index 63d1eef5b59..fc9860900c9 100644 --- a/app/graphql/types/repository_type.rb +++ b/app/graphql/types/repository_type.rb @@ -16,8 +16,7 @@ module Types description: 'Tree of the repository.' field :paginated_tree, Types::Tree::TreeType.connection_type, null: true, resolver: Resolvers::PaginatedTreeResolver, calls_gitaly: true, max_page_size: 100, - description: 'Paginated tree of the repository.', - feature_flag: :paginated_tree_graphql_query + description: 'Paginated tree of the repository.' field :blobs, Types::Repository::BlobType.connection_type, null: true, resolver: Resolvers::BlobsResolver, calls_gitaly: true, description: 'Blobs contained within the repository' field :branch_names, [GraphQL::Types::String], null: true, calls_gitaly: true, diff --git a/app/models/analytics/cycle_analytics/issue_stage_event.rb b/app/models/analytics/cycle_analytics/issue_stage_event.rb index 099bb671f10..837eb35c839 100644 --- a/app/models/analytics/cycle_analytics/issue_stage_event.rb +++ b/app/models/analytics/cycle_analytics/issue_stage_event.rb @@ -20,6 +20,10 @@ module Analytics def self.issuable_id_column :issue_id end + + def self.issuable_model + ::Issue + end end end end diff --git a/app/models/analytics/cycle_analytics/merge_request_stage_event.rb b/app/models/analytics/cycle_analytics/merge_request_stage_event.rb index 632b7079b2e..0dfa322b2c3 100644 --- a/app/models/analytics/cycle_analytics/merge_request_stage_event.rb +++ b/app/models/analytics/cycle_analytics/merge_request_stage_event.rb @@ -20,6 +20,10 @@ module Analytics def self.issuable_id_column :merge_request_id end + + def self.issuable_model + ::MergeRequest + end end end end diff --git a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb index cca1a0b41cc..324e0fb57cb 100644 --- a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb +++ b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb @@ -18,6 +18,10 @@ module Analytics scope :end_event_is_not_happened_yet, -> { where(end_event_timestamp: nil) } end + def issuable_id + attributes[self.class.issuable_id_column.to_s] + end + class_methods do def upsert_data(data) upsert_values = data.map do |row| @@ -26,8 +30,8 @@ module Analytics :issuable_id, :group_id, :project_id, - :author_id, :milestone_id, + :author_id, :state_id, :start_event_timestamp, :end_event_timestamp diff --git a/app/models/label_link.rb b/app/models/label_link.rb index 4fb5fd8c58a..d326b07ad31 100644 --- a/app/models/label_link.rb +++ b/app/models/label_link.rb @@ -11,4 +11,16 @@ class LabelLink < ApplicationRecord validates :label, presence: true, unless: :importing? scope :for_target, -> (target_id, target_type) { where(target_id: target_id, target_type: target_type) } + + # Example: Issues has at least one label within a project + # > Issue.where(project_id: 100) # or any scope on issues + # > .where(LabelLink.by_target_for_exists_query('Issue', Issue.arel_table[:id]).arel.exists) + scope :by_target_for_exists_query, -> (target_type, arel_join_column, label_ids = nil) do + relation = LabelLink + .where(target_type: target_type) + .where(arel_table['target_id'].eq(arel_join_column)) + + relation = relation.where(label_id: label_ids) if label_ids + relation + end end diff --git a/app/models/webauthn_registration.rb b/app/models/webauthn_registration.rb index 76f8faa11c7..71b50192e29 100644 --- a/app/models/webauthn_registration.rb +++ b/app/models/webauthn_registration.rb @@ -5,7 +5,8 @@ class WebauthnRegistration < ApplicationRecord belongs_to :user - validates :credential_xid, :public_key, :name, :counter, presence: true + validates :credential_xid, :public_key, :counter, presence: true + validates :name, length: { minimum: 0, allow_nil: false } validates :counter, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**32 - 1 } end diff --git a/app/presenters/release_presenter.rb b/app/presenters/release_presenter.rb index c919c7f4c60..dac42af38bf 100644 --- a/app/presenters/release_presenter.rb +++ b/app/presenters/release_presenter.rb @@ -22,8 +22,6 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated end def self_url - return unless can_download_code? - project_release_url(project, release) end @@ -64,7 +62,7 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated delegator_override :name def name - can_download_code? ? release.name : "Release-#{release.id}" + release.name end def download_url(filepath) diff --git a/app/services/packages/npm/create_package_service.rb b/app/services/packages/npm/create_package_service.rb index 6945c94e0bb..ae9c92a3d3a 100644 --- a/app/services/packages/npm/create_package_service.rb +++ b/app/services/packages/npm/create_package_service.rb @@ -4,6 +4,8 @@ module Packages class CreatePackageService < ::Packages::CreatePackageService include Gitlab::Utils::StrongMemoize + PACKAGE_JSON_NOT_ALLOWED_FIELDS = %w[readme readmeFilename].freeze + def execute return error('Version is empty.', 400) if version.blank? return error('Package already exists.', 403) if current_package_exists? @@ -22,7 +24,7 @@ module Packages ::Packages::Npm::CreateTagService.new(package, dist_tag).execute if Feature.enabled?(:packages_npm_abbreviated_metadata, project, default_enabled: :yaml) - package.create_npm_metadatum!(package_json: version_data) + package.create_npm_metadatum!(package_json: package_json) end package @@ -50,6 +52,10 @@ module Packages params[:versions][version] end + def package_json + version_data.except(*PACKAGE_JSON_NOT_ALLOWED_FIELDS) + end + def dist_tag params['dist-tags'].each_key.first end diff --git a/config/application.rb b/config/application.rb index 1fc963a1c1e..a2dce593677 100644 --- a/config/application.rb +++ b/config/application.rb @@ -224,7 +224,7 @@ module Gitlab config.assets.precompile << "page_bundles/build.css" config.assets.precompile << "page_bundles/ci_status.css" config.assets.precompile << "page_bundles/cycle_analytics.css" - config.assets.precompile << "page_bundles/dev_ops_report.css" + config.assets.precompile << "page_bundles/dev_ops_reports.css" config.assets.precompile << "page_bundles/environments.css" config.assets.precompile << "page_bundles/epics.css" config.assets.precompile << "page_bundles/error_tracking_details.css" diff --git a/config/feature_flags/development/paginated_tree_graphql_query.yml b/config/feature_flags/development/paginated_tree_graphql_query.yml deleted file mode 100644 index d56d8fc336c..00000000000 --- a/config/feature_flags/development/paginated_tree_graphql_query.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: paginated_tree_graphql_query -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66751 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/337214 -milestone: '14.2' -type: development -group: group::source code -default_enabled: true diff --git a/db/post_migrate/20211112113300_remove_ci_pipeline_chat_data_fk_on_chat_names.rb b/db/post_migrate/20211112113300_remove_ci_pipeline_chat_data_fk_on_chat_names.rb new file mode 100644 index 00000000000..19adc6c7084 --- /dev/null +++ b/db/post_migrate/20211112113300_remove_ci_pipeline_chat_data_fk_on_chat_names.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class RemoveCiPipelineChatDataFkOnChatNames < Gitlab::Database::Migration[1.0] + disable_ddl_transaction! + + def up + with_lock_retries do + remove_foreign_key_if_exists(:ci_pipeline_chat_data, :chat_names, name: "fk_rails_f300456b63") + end + end + + def down + # Remove orphaned rows + execute <<~SQL + DELETE FROM ci_pipeline_chat_data + WHERE + NOT EXISTS (SELECT 1 FROM chat_names WHERE chat_names.id=ci_pipeline_chat_data.chat_name_id) + SQL + + add_concurrent_foreign_key(:ci_pipeline_chat_data, :chat_names, name: "fk_rails_f300456b63", column: :chat_name_id, target_column: :id, on_delete: "cascade") + end +end diff --git a/db/schema_migrations/20211112113300 b/db/schema_migrations/20211112113300 new file mode 100644 index 00000000000..8074c437751 --- /dev/null +++ b/db/schema_migrations/20211112113300 @@ -0,0 +1 @@ +be11c0b1c7b9c99c28d44c164742815da57bfc4a32afd54df9135e3ce6edeff9
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 7e366cd67d3..5b6614be454 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -31078,9 +31078,6 @@ ALTER TABLE ONLY snippet_repositories ALTER TABLE ONLY elastic_reindexing_subtasks ADD CONSTRAINT fk_rails_f2cc190164 FOREIGN KEY (elastic_reindexing_task_id) REFERENCES elastic_reindexing_tasks(id) ON DELETE CASCADE; -ALTER TABLE ONLY ci_pipeline_chat_data - ADD CONSTRAINT fk_rails_f300456b63 FOREIGN KEY (chat_name_id) REFERENCES chat_names(id) ON DELETE CASCADE; - ALTER TABLE ONLY approval_project_rules_users ADD CONSTRAINT fk_rails_f365da8250 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index ccbdce386dc..ec09d581483 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -14030,7 +14030,7 @@ Returns [`[String!]`](#string). ##### `Repository.paginatedTree` -Paginated tree of the repository. Available only when feature flag `paginated_tree_graphql_query` is enabled. This flag is enabled by default. +Paginated tree of the repository. Returns [`TreeConnection`](#treeconnection). diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md index 8f633ad3561..c67282643a4 100644 --- a/doc/ci/review_apps/index.md +++ b/doc/ci/review_apps/index.md @@ -98,7 +98,7 @@ The following are example projects that demonstrate Review App configuration: - [NGINX](https://gitlab.com/gitlab-examples/review-apps-nginx). - [OpenShift](https://gitlab.com/gitlab-examples/review-apps-openshift). -- [HashiCorp Nomad](https://gitlab.com/gitlab-examples/review-apps-nomad) +- [HashiCorp Nomad](https://gitlab.com/gitlab-examples/review-apps-nomad). Other examples of Review Apps: diff --git a/doc/ci/test_cases/index.md b/doc/ci/test_cases/index.md index 4bd38f4043a..384bfc10779 100644 --- a/doc/ci/test_cases/index.md +++ b/doc/ci/test_cases/index.md @@ -18,7 +18,9 @@ use external test planning tools, which require additional overhead, context swi ## Create a test case -Users with Reporter or higher [permissions](../../user/permissions.md) can create test cases. +Prerequisite: + +- You must have at least the Reporter [role](../../user/permissions.md). To create a test case in a GitLab project: @@ -32,7 +34,9 @@ To create a test case in a GitLab project: You can view all test cases in the project in the Test Cases list. Filter the issue list with a search query, including labels or the test case's title. -Users with Guest or higher [permissions](../../user/permissions.md) can view test cases. +Prerequisite: + +- You must have at least the Guest [role](../../user/permissions.md). To view a test case: @@ -45,8 +49,10 @@ To view a test case: You can edit a test case's title and description. -Users with Reporter or higher [permissions](../../user/permissions.md) can edit test cases. -Users demoted to the Guest role can continue to edit the test cases they created +Prerequisite: + +- You must have at least the Reporter [role](../../user/permissions.md). +- Users demoted to the Guest role can continue to edit the test cases they created when they were in the higher role. To edit a test case: @@ -60,7 +66,9 @@ To edit a test case: When you want to stop using a test case, you can archive it. You can [reopen an archived test case](#reopen-an-archived-test-case) later. -Users with Reporter or higher [permissions](../../user/permissions.md) can archive test cases. +Prerequisite: + +- You must have at least the Reporter [role](../../user/permissions.md). To archive a test case, on the test case's page, select the **Archive test case** button. @@ -73,6 +81,6 @@ To view archived test cases: If you decide to start using an archived test case again, you can reopen it. -Users with Reporter or higher [permissions](../../user/permissions.md) can reopen test cases. +You must have at least the Reporter [role](../../user/permissions.md). To reopen an archived test case, on the test case's page, select **Reopen test case**. diff --git a/doc/development/documentation/feature_flags.md b/doc/development/documentation/feature_flags.md index f234ec6f2e2..8fc6f2e2641 100644 --- a/doc/development/documentation/feature_flags.md +++ b/doc/development/documentation/feature_flags.md @@ -90,7 +90,7 @@ The following examples show the progression of a feature flag. FLAG: On self-managed GitLab, by default this feature is not available. To make it available, -ask an administrator to [enable the featured flag](../administration/feature_flags.md) named `forti_token_cloud`. +ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `forti_token_cloud`. The feature is not ready for production use. ``` diff --git a/doc/development/fe_guide/accessibility.md b/doc/development/fe_guide/accessibility.md index c4ebef4c289..e71e414002a 100644 --- a/doc/development/fe_guide/accessibility.md +++ b/doc/development/fe_guide/accessibility.md @@ -23,6 +23,17 @@ they found that "ARIA correlated to higher detectable errors". It is likely that *misuse* of ARIA is a big cause of increased errors, so when in doubt don't use `aria-*`, `role`, and `tabindex` and stick with semantic HTML. +## Enable keyboard navigation on macOS + +By default, macOS limits the <kbd>tab</kbd> key to **Text boxes and lists only**. To enable full keyboard navigation: + +1. Open **System Preferences**. +1. Select **Keyboard**. +1. Open the **Shortcuts** tab. +1. Enable the setting **Use keyboard navigation to move focus between controls**. + +You can read more about enabling browser-specific keyboard navigation on [a11yproject](https://www.a11yproject.com/posts/2017-12-29-macos-browser-keyboard-navigation/). + ## Quick checklist - [Text](#text-inputs-with-accessible-names), diff --git a/doc/update/index.md b/doc/update/index.md index 216ead2128f..bb7fdaa93c0 100644 --- a/doc/update/index.md +++ b/doc/update/index.md @@ -338,6 +338,23 @@ or [init scripts](upgrading_from_source.md#configure-sysv-init-script) by [follo Workhorse can no longer connect. As a workaround, [disable the temporary `workhorse_use_sidechannel`](../administration/feature_flags.md#enable-or-disable-the-feature) feature flag. If you need a proxy between Workhorse and Gitaly, use a TCP proxy. If you have feedback about this change, please go to [this issue](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1301). +- In 14.1 we introduced a background migration that changes how we store merge request diff commits + in order to significantly reduce the amount of storage needed. + In 14.5 we introduce a set of migrations that wrap up this process by making sure + that all remaining jobs over the `merge_request_diff_commits` table are completed. + These jobs will have already been processed in most cases so that no extra time is necessary during an upgrade to 14.5. + But if there are remaining jobs, the deployment may take a few extra minutes to complete. + + All merge request diff commits will automatically incorporate these changes, and there are no + additional requirements to perform the upgrade. + Existing data in the `merge_request_diff_commits` table remains unpacked until you run `VACUUM FULL merge_request_diff_commits`. + But note that the `VACUUM FULL` operation locks and rewrites the entire `merge_request_diff_commits` table, + so the operation takes some time to complete and it blocks access to this table until the end of the process. + We advise you to only run this command while GitLab is not actively used or it is taken offline for the duration of the process. + The time it takes to complete depends on the size of the table, which can be obtained by using `select pg_size_pretty(pg_total_relation_size('merge_request_diff_commits'));`. + + For more information, refer to [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/331823). + ### 14.4.0 Git 2.33.x and later is required. We recommend you use the diff --git a/doc/user/clusters/agent/install/index.md b/doc/user/clusters/agent/install/index.md index 68ff1b8579b..fad9d4f08f1 100644 --- a/doc/user/clusters/agent/install/index.md +++ b/doc/user/clusters/agent/install/index.md @@ -70,7 +70,8 @@ synchronizations is: ```yaml gitops: manifest_projects: - - id: "path-to/your-manifest-project-1" + # The `id` is the path to the Git repository holding your manifest files + - id: "path/to/your-manifest-project-1" paths: - glob: '/**/*.{yaml,yml,json}' ``` diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md index 813e358e72b..a3aecff6f73 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -298,6 +298,7 @@ after the limits change in January, 2021: | **All** traffic (from a given **IP address**) | **600** requests per minute | **2,000** requests per minute | **2,000** requests per minute | | **Issue creation** | | **300** requests per minute | **300** requests per minute | | **Note creation** (on issues and merge requests) | | **300** requests per minute | **60** requests per minute | +| **Advanced, project, and group search** API (for a given **IP address**) | | | **10** requests per minute | More details are available on the rate limits for [protected paths](#protected-paths-throttle) and [raw diff --git a/doc/user/permissions.md b/doc/user/permissions.md index f87603895ec..47163a824c7 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -216,7 +216,7 @@ The following table lists project permissions available for each role: 1. If **Public pipelines** is enabled in **Project Settings > CI/CD**. 1. Not allowed for Guest, Reporter, Developer, Maintainer, or Owner. See [protected branches](project/protected_branches.md). 1. If the [branch is protected](project/protected_branches.md), this depends on the access Developers and Maintainers are given. -1. Guest users can access GitLab [**Releases**](project/releases/index.md) for downloading assets but are not allowed to download the source code nor see repository information like tags and commits. +1. Guest users can access GitLab [**Releases**](project/releases/index.md) for downloading assets but are not allowed to download the source code nor see [repository information like commits and release evidence](project/releases/index.md#view-a-release-and-download-assets). 1. Actions are limited only to records owned (referenced) by user. 1. When [Share Group Lock](group/index.md#prevent-a-project-from-being-shared-with-groups) is enabled the project can't be shared with other groups. It does not affect group with group sharing. 1. For information on eligible approvers for merge requests, see diff --git a/doc/user/project/releases/index.md b/doc/user/project/releases/index.md index d16c6cb24f1..9177e63369d 100644 --- a/doc/user/project/releases/index.md +++ b/doc/user/project/releases/index.md @@ -393,14 +393,6 @@ The release title can be customized using the **Release title** field when creating or editing a release. If no title is provided, the release's tag name is used instead. -Guest users of private projects are allowed to view the **Releases** page -but are _not_ allowed to view details about the Git repository (in particular, -tag names). Because of this, release titles are replaced with a generic -title like "Release-1234" for Guest users to avoid leaking tag name information. - -See the [Release permissions](#release-permissions) section for -more information about permissions. - ### Tag name The release tag name should include the release version. GitLab uses [Semantic Versioning](https://semver.org/) @@ -724,11 +716,14 @@ In the API: ### View a release and download assets +> [The Guest permission for read action was adjusted](https://gitlab.com/gitlab-org/gitlab/-/issues/335209) in GitLab 14.5. + - Users with [Reporter role or above](../../../user/permissions.md#project-members-permissions) have read and download access to the project releases. - Users with [Guest role](../../../user/permissions.md#project-members-permissions) - have read and download access to the project releases, however, - repository-related information are redacted (for example the Git tag name). + have read and download access to the project releases. + This includes associated Git-tag-names, release description, author information of the releases. + However, other repository-related information, such as [source code](#source-code), [release evidence](#release-evidence) are redacted. ### Create, update, and delete a release and its assets diff --git a/doc/user/tasks.md b/doc/user/tasks.md index a266b386618..e5e5510407e 100644 --- a/doc/user/tasks.md +++ b/doc/user/tasks.md @@ -14,7 +14,7 @@ For the latest updates, check the [Tasks Roadmap](https://gitlab.com/groups/gitl FLAG: On self-managed GitLab, by default this feature is not available. To make it available, -ask an administrator to [enable the featured flag](../administration/feature_flags.md) named `work_items`. +ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `work_items`. The feature is not ready for production use. Use tasks to track steps needed for the [issue](project/issues/index.md) to be closed. diff --git a/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder.rb b/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder.rb index ee459f2790a..1e50c980a3a 100644 --- a/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder.rb +++ b/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder.rb @@ -63,23 +63,12 @@ module Gitlab def filter_label_names(query) return query if params[:label_name].blank? - all_label_ids = Issuables::LabelFilter - .new(group: root_ancestor, project: nil, params: { label_name: params[:label_name] }) - .find_label_ids(params[:label_name]) - - return query.none if params[:label_name].size != all_label_ids.size - - all_label_ids.each do |label_ids| - relation = LabelLink - .where(target_type: stage.subject_class.name) - .where(LabelLink.arel_table['target_id'].eq(query.model.arel_table[query.model.issuable_id_column])) - - relation = relation.where(label_id: label_ids) - - query = query.where(relation.arel.exists) - end - - query + LabelFilter.new( + stage: stage, + params: params, + project: nil, + group: root_ancestor + ).filter(query) end def filter_assignees(query) diff --git a/lib/gitlab/analytics/cycle_analytics/aggregated/data_collector.rb b/lib/gitlab/analytics/cycle_analytics/aggregated/data_collector.rb index ab3ae93f5ff..c8b11ecb4a8 100644 --- a/lib/gitlab/analytics/cycle_analytics/aggregated/data_collector.rb +++ b/lib/gitlab/analytics/cycle_analytics/aggregated/data_collector.rb @@ -30,6 +30,12 @@ module Gitlab strong_memoize(:count) { limit_count } end + def records_fetcher + strong_memoize(:records_fetcher) do + RecordsFetcher.new(stage: stage, query: query, params: params) + end + end + private attr_reader :stage, :params diff --git a/lib/gitlab/analytics/cycle_analytics/aggregated/label_filter.rb b/lib/gitlab/analytics/cycle_analytics/aggregated/label_filter.rb new file mode 100644 index 00000000000..6d87ae91a9c --- /dev/null +++ b/lib/gitlab/analytics/cycle_analytics/aggregated/label_filter.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Gitlab + module Analytics + module CycleAnalytics + module Aggregated + # This class makes it possible to add label filters to stage event tables + class LabelFilter < Issuables::LabelFilter + extend ::Gitlab::Utils::Override + + def initialize(stage:, project:, group:, **kwargs) + @stage = stage + + super(project: project, group: group, **kwargs) + end + + private + + attr_reader :stage + + override :label_link_query + def label_link_query(target_model, label_ids: nil) + join_column = target_model.arel_table[target_model.issuable_id_column] + + LabelLink.by_target_for_exists_query(stage.subject_class.name, join_column, label_ids) + end + end + end + end + end +end diff --git a/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb b/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb new file mode 100644 index 00000000000..7dce757cdc8 --- /dev/null +++ b/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +module Gitlab + module Analytics + module CycleAnalytics + module Aggregated + class RecordsFetcher + include Gitlab::Utils::StrongMemoize + include StageQueryHelpers + + MAX_RECORDS = 20 + + MAPPINGS = { + Issue => { + serializer_class: AnalyticsIssueSerializer, + includes_for_query: { project: { namespace: [:route] }, author: [] }, + columns_for_select: %I[title iid id created_at author_id project_id] + }, + MergeRequest => { + serializer_class: AnalyticsMergeRequestSerializer, + includes_for_query: { target_project: [:namespace], author: [] }, + columns_for_select: %I[title iid id created_at author_id state_id target_project_id] + } + }.freeze + + def initialize(stage:, query:, params: {}) + @stage = stage + @query = query + @params = params + @sort = params[:sort] || :end_event + @direction = params[:direction] || :desc + @page = params[:page] || 1 + @per_page = MAX_RECORDS + @stage_event_model = query.model + end + + def serialized_records + strong_memoize(:serialized_records) do + records = ordered_and_limited_query.select(stage_event_model.arel_table[Arel.star], duration.as('total_time')) + + yield records if block_given? + issuables_and_records = load_issuables(records) + + preload_associations(issuables_and_records.map(&:first)) + + issuables_and_records.map do |issuable, record| + project = issuable.project + attributes = issuable.attributes.merge({ + project_path: project.path, + namespace_path: project.namespace.route.path, + author: issuable.author, + total_time: record.total_time + }) + serializer.represent(attributes) + end + end + end + + # rubocop: disable CodeReuse/ActiveRecord + def ordered_and_limited_query + sorting_options = { + end_event: { + asc: -> { query.order(end_event_timestamp: :asc) }, + desc: -> { query.order(end_event_timestamp: :desc) } + }, + duration: { + asc: -> { query.order(duration.asc) }, + desc: -> { query.order(duration.desc) } + } + } + + sort_lambda = sorting_options.dig(sort, direction) || sorting_options.dig(:end_event, :desc) + + sort_lambda.call + .page(page) + .per(per_page) + .without_count + end + # rubocop: enable CodeReuse/ActiveRecord + + private + + attr_reader :stage, :query, :sort, :direction, :params, :page, :per_page, :stage_event_model + + delegate :subject_class, to: :stage + + def load_issuables(stage_event_records) + stage_event_records_by_issuable_id = stage_event_records.index_by(&:issuable_id) + + issuable_model = stage_event_model.issuable_model + issuables_by_id = issuable_model.id_in(stage_event_records_by_issuable_id.keys).index_by(&:id) + + stage_event_records_by_issuable_id.map do |issuable_id, record| + [issuables_by_id[issuable_id], record] if issuables_by_id[issuable_id] + end.compact + end + + def serializer + MAPPINGS.fetch(subject_class).fetch(:serializer_class).new + end + + # rubocop: disable CodeReuse/ActiveRecord + def preload_associations(records) + ActiveRecord::Associations::Preloader.new.preload( + records, + MAPPINGS.fetch(subject_class).fetch(:includes_for_query) + ) + + records + end + # rubocop: enable CodeReuse/ActiveRecord + end + end + end + end +end diff --git a/lib/gitlab/analytics/cycle_analytics/data_collector.rb b/lib/gitlab/analytics/cycle_analytics/data_collector.rb index a26df55bd0a..a20481dd39e 100644 --- a/lib/gitlab/analytics/cycle_analytics/data_collector.rb +++ b/lib/gitlab/analytics/cycle_analytics/data_collector.rb @@ -23,7 +23,11 @@ module Gitlab def records_fetcher strong_memoize(:records_fetcher) do - RecordsFetcher.new(stage: stage, query: query, params: params) + if use_aggregated_data_collector? + aggregated_data_collector.records_fetcher + else + RecordsFetcher.new(stage: stage, query: query, params: params) + end end end diff --git a/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb b/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb index f94696e3186..140c4a300ca 100644 --- a/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb +++ b/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb @@ -8,23 +8,11 @@ module Gitlab include StageQueryHelpers include Gitlab::CycleAnalytics::MetricsTables - MAX_RECORDS = 20 - - MAPPINGS = { - Issue => { - serializer_class: AnalyticsIssueSerializer, - includes_for_query: { project: { namespace: [:route] }, author: [] }, - columns_for_select: %I[title iid id created_at author_id project_id] - }, - MergeRequest => { - serializer_class: AnalyticsMergeRequestSerializer, - includes_for_query: { target_project: [:namespace], author: [] }, - columns_for_select: %I[title iid id created_at author_id state_id target_project_id] - } - }.freeze - delegate :subject_class, to: :stage + MAX_RECORDS = Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher::MAX_RECORDS + MAPPINGS = Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher::MAPPINGS + def initialize(stage:, query:, params: {}) @stage = stage @query = query diff --git a/lib/gitlab/ci/parsers/security/common.rb b/lib/gitlab/ci/parsers/security/common.rb index 2086ac5668c..0c969daf7fd 100644 --- a/lib/gitlab/ci/parsers/security/common.rb +++ b/lib/gitlab/ci/parsers/security/common.rb @@ -114,7 +114,7 @@ module Gitlab flags: flags, links: links, remediations: remediations, - raw_metadata: data.to_json, + original_data: data, metadata_version: report_version, details: data['details'] || {}, signatures: signatures, diff --git a/lib/gitlab/ci/reports/security/finding.rb b/lib/gitlab/ci/reports/security/finding.rb index 7a81c471267..47ec82ac86c 100644 --- a/lib/gitlab/ci/reports/security/finding.rb +++ b/lib/gitlab/ci/reports/security/finding.rb @@ -17,7 +17,6 @@ module Gitlab attr_reader :name attr_reader :old_location attr_reader :project_fingerprint - attr_reader :raw_metadata attr_reader :report_type attr_reader :scanner attr_reader :scan @@ -28,10 +27,13 @@ module Gitlab attr_reader :details attr_reader :signatures attr_reader :project_id + attr_reader :original_data delegate :file_path, :start_line, :end_line, to: :location - def initialize(compare_key:, identifiers:, flags: [], links: [], remediations: [], location:, metadata_version:, name:, raw_metadata:, report_type:, scanner:, scan:, uuid:, confidence: nil, severity: nil, details: {}, signatures: [], project_id: nil, vulnerability_finding_signatures_enabled: false) # rubocop:disable Metrics/ParameterLists + alias_method :cve, :compare_key + + def initialize(compare_key:, identifiers:, flags: [], links: [], remediations: [], location:, metadata_version:, name:, original_data:, report_type:, scanner:, scan:, uuid:, confidence: nil, severity: nil, details: {}, signatures: [], project_id: nil, vulnerability_finding_signatures_enabled: false) # rubocop:disable Metrics/ParameterLists @compare_key = compare_key @confidence = confidence @identifiers = identifiers @@ -40,7 +42,7 @@ module Gitlab @location = location @metadata_version = metadata_version @name = name - @raw_metadata = raw_metadata + @original_data = original_data @report_type = report_type @scanner = scanner @scan = scan @@ -74,6 +76,10 @@ module Gitlab uuid details signatures + description + message + cve + solution ].each_with_object({}) do |key, hash| hash[key] = public_send(key) # rubocop:disable GitlabSecurity/PublicSend end @@ -145,6 +151,26 @@ module Gitlab signatures.present? end + def raw_metadata + @raw_metadata ||= original_data.to_json + end + + def description + original_data['description'] + end + + def message + original_data['message'] + end + + def solution + original_data['solution'] + end + + def location_data + original_data['location'] + end + private def generate_project_fingerprint diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb index a1e36ec5c4c..120020273f9 100644 --- a/spec/controllers/projects/releases_controller_spec.rb +++ b/spec/controllers/projects/releases_controller_spec.rb @@ -207,7 +207,18 @@ RSpec.describe Projects::ReleasesController do let(:project) { private_project } let(:user) { guest } - it_behaves_like 'not found' + it_behaves_like 'successful request' + end + + context 'when user is an external user for the project' do + let(:project) { private_project } + let(:user) { create(:user) } + + it 'behaves like not found' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end end end diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index 7785233875c..aebe13c39fe 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -29,6 +29,7 @@ RSpec.describe 'Database schema' do ci_builds: %w[erased_by_id runner_id trigger_request_id user_id], ci_namespace_monthly_usages: %w[namespace_id], ci_pipelines: %w[user_id], + ci_pipeline_chat_data: %w[chat_name_id], # it uses the loose foreign key featue ci_runner_projects: %w[runner_id], ci_trigger_requests: %w[commit_id], cluster_providers_aws: %w[security_group_id vpc_id access_key_id], diff --git a/spec/factories/ci/reports/security/findings.rb b/spec/factories/ci/reports/security/findings.rb index e3971bc48f3..8a39fce971f 100644 --- a/spec/factories/ci/reports/security/findings.rb +++ b/spec/factories/ci/reports/security/findings.rb @@ -9,7 +9,7 @@ FactoryBot.define do metadata_version { 'sast:1.0' } name { 'Cipher with no integrity' } report_type { :sast } - raw_metadata do + original_data do { description: "The cipher does not provide data integrity update 1", solution: "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.", @@ -26,7 +26,7 @@ FactoryBot.define do url: "https://crypto.stackexchange.com/questions/31428/pbewithmd5anddes-cipher-does-not-check-for-integrity-first" } ] - }.to_json + }.deep_stringify_keys end scanner factory: :ci_reports_security_scanner severity { :high } diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb index 0580e28512d..69361f66a71 100644 --- a/spec/features/cycle_analytics_spec.rb +++ b/spec/features/cycle_analytics_spec.rb @@ -34,6 +34,10 @@ RSpec.describe 'Value Stream Analytics', :js do wait_for_all_requests end + before do + stub_feature_flags(use_vsa_aggregated_tables: false) + end + context 'as an allowed user' do context 'when project is new' do before do diff --git a/spec/features/projects/releases/user_views_releases_spec.rb b/spec/features/projects/releases/user_views_releases_spec.rb index 6bc4c66b8ca..98935fdf872 100644 --- a/spec/features/projects/releases/user_views_releases_spec.rb +++ b/spec/features/projects/releases/user_views_releases_spec.rb @@ -123,11 +123,11 @@ RSpec.describe 'User views releases', :js do within('.release-block', match: :first) do expect(page).to have_content(release_v3.description) + expect(page).to have_content(release_v3.tag) + expect(page).to have_content(release_v3.name) # The following properties (sometimes) include Git info, # so they are not rendered for Guest users - expect(page).not_to have_content(release_v3.name) - expect(page).not_to have_content(release_v3.tag) expect(page).not_to have_content(release_v3.commit.short_id) end end diff --git a/spec/graphql/types/release_links_type_spec.rb b/spec/graphql/types/release_links_type_spec.rb index 38c38d58baa..e77c4e3ddd1 100644 --- a/spec/graphql/types/release_links_type_spec.rb +++ b/spec/graphql/types/release_links_type_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe GitlabSchema.types['ReleaseLinks'] do - it { expect(described_class).to require_graphql_authorizations(:download_code) } + it { expect(described_class).to require_graphql_authorizations(:read_release) } it 'has the expected fields' do expected_fields = %w[ @@ -18,4 +18,46 @@ RSpec.describe GitlabSchema.types['ReleaseLinks'] do expect(described_class).to include_graphql_fields(*expected_fields) end + + context 'individual field authorization' do + def fetch_authorizations(field_name) + described_class.fields.dig(field_name).instance_variable_get(:@authorize) + end + + describe 'openedMergeRequestsUrl' do + it 'has valid authorization' do + expect(fetch_authorizations('openedMergeRequestsUrl')).to include(:download_code) + end + end + + describe 'mergedMergeRequestsUrl' do + it 'has valid authorization' do + expect(fetch_authorizations('mergedMergeRequestsUrl')).to include(:download_code) + end + end + + describe 'closedMergeRequestsUrl' do + it 'has valid authorization' do + expect(fetch_authorizations('closedMergeRequestsUrl')).to include(:download_code) + end + end + + describe 'openedIssuesUrl' do + it 'has valid authorization' do + expect(fetch_authorizations('openedIssuesUrl')).to include(:download_code) + end + end + + describe 'closedIssuesUrl' do + it 'has valid authorization' do + expect(fetch_authorizations('closedIssuesUrl')).to include(:download_code) + end + end + + describe 'editUrl' do + it 'has valid authorization' do + expect(fetch_authorizations('editUrl')).to include(:update_release) + end + end + end end diff --git a/spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb new file mode 100644 index 00000000000..045cdb129cb --- /dev/null +++ b/spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher do + let_it_be(:project) { create(:project) } + let_it_be(:issue_1) { create(:issue, project: project) } + let_it_be(:issue_2) { create(:issue, project: project) } + let_it_be(:issue_3) { create(:issue, project: project) } + + let_it_be(:stage_event_1) { create(:cycle_analytics_issue_stage_event, issue_id: issue_1.id, start_event_timestamp: 2.years.ago, end_event_timestamp: 1.year.ago) } # duration: 1 year + let_it_be(:stage_event_2) { create(:cycle_analytics_issue_stage_event, issue_id: issue_2.id, start_event_timestamp: 5.years.ago, end_event_timestamp: 2.years.ago) } # duration: 3 years + let_it_be(:stage_event_3) { create(:cycle_analytics_issue_stage_event, issue_id: issue_3.id, start_event_timestamp: 6.years.ago, end_event_timestamp: 3.months.ago) } # duration: 5+ years + + let_it_be(:stage) { create(:cycle_analytics_project_stage, start_event_identifier: :issue_created, end_event_identifier: :issue_deployed_to_production, project: project) } + + let(:params) { {} } + + subject(:records_fetcher) do + described_class.new(stage: stage, query: Analytics::CycleAnalytics::IssueStageEvent.all, params: params) + end + + shared_examples 'match returned records' do + it 'returns issues in the correct order' do + returned_iids = records_fetcher.serialized_records.pluck(:iid).map(&:to_i) + + expect(returned_iids).to eq(expected_issue_ids) + end + end + + describe '#serialized_records' do + describe 'sorting' do + context 'when sorting by end event DESC' do + let(:expected_issue_ids) { [issue_3.iid, issue_1.iid, issue_2.iid] } + + before do + params[:sort] = :end_event + params[:direction] = :desc + end + + it_behaves_like 'match returned records' + end + + context 'when sorting by end event ASC' do + let(:expected_issue_ids) { [issue_2.iid, issue_1.iid, issue_3.iid] } + + before do + params[:sort] = :end_event + params[:direction] = :asc + end + + it_behaves_like 'match returned records' + end + + context 'when sorting by duration DESC' do + let(:expected_issue_ids) { [issue_3.iid, issue_2.iid, issue_1.iid] } + + before do + params[:sort] = :duration + params[:direction] = :desc + end + + it_behaves_like 'match returned records' + end + + context 'when sorting by duration ASC' do + let(:expected_issue_ids) { [issue_1.iid, issue_2.iid, issue_3.iid] } + + before do + params[:sort] = :duration + params[:direction] = :asc + end + + it_behaves_like 'match returned records' + end + end + + describe 'pagination' do + let(:expected_issue_ids) { [issue_3.iid] } + + before do + params[:sort] = :duration + params[:direction] = :asc + params[:page] = 2 + + stub_const('Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher::MAX_RECORDS', 2) + end + + it_behaves_like 'match returned records' + end + + context 'when passing a block to serialized_records method' do + before do + params[:sort] = :duration + params[:direction] = :asc + end + + it 'yields the underlying stage event scope' do + stage_event_records = [] + + records_fetcher.serialized_records do |scope| + stage_event_records.concat(scope.to_a) + end + + expect(stage_event_records.map(&:issue_id)).to eq([issue_1.id, issue_2.id, issue_3.id]) + end + end + + context 'when the issue record no longer exists' do + it 'skips non-existing issue records' do + create(:cycle_analytics_issue_stage_event, { + issue_id: 0, # non-existing id + start_event_timestamp: 5.months.ago, + end_event_timestamp: 3.months.ago + }) + + stage_event_count = nil + + records_fetcher.serialized_records do |scope| + stage_event_count = scope.to_a.size + end + + issue_count = records_fetcher.serialized_records.to_a.size + + expect(stage_event_count).to eq(4) + expect(issue_count).to eq(3) + end + end + end +end diff --git a/spec/models/u2f_registration_spec.rb b/spec/models/u2f_registration_spec.rb index aba2f27d104..7a70cf69566 100644 --- a/spec/models/u2f_registration_spec.rb +++ b/spec/models/u2f_registration_spec.rb @@ -5,9 +5,11 @@ require 'spec_helper' RSpec.describe U2fRegistration do let_it_be(:user) { create(:user) } + let(:u2f_registration_name) { 'u2f_device' } + let(:u2f_registration) do device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5)) - create(:u2f_registration, name: 'u2f_device', + create(:u2f_registration, name: u2f_registration_name, user: user, certificate: Base64.strict_encode64(device.cert_raw), key_handle: U2F.urlsafe_encode64(device.key_handle_raw), @@ -16,11 +18,27 @@ RSpec.describe U2fRegistration do describe 'callbacks' do describe '#create_webauthn_registration' do - it 'creates webauthn registration' do - u2f_registration.save! + shared_examples_for 'creates webauthn registration' do + it 'creates webauthn registration' do + u2f_registration.save! + + webauthn_registration = WebauthnRegistration.where(u2f_registration_id: u2f_registration.id) + expect(webauthn_registration).to exist + end + end + + it_behaves_like 'creates webauthn registration' + + context 'when the u2f_registration has a blank name' do + let(:u2f_registration_name) { '' } + + it_behaves_like 'creates webauthn registration' + end + + context 'when the u2f_registration has the name as `nil`' do + let(:u2f_registration_name) { nil } - webauthn_registration = WebauthnRegistration.where(u2f_registration_id: u2f_registration.id) - expect(webauthn_registration).to exist + it_behaves_like 'creates webauthn registration' end it 'logs error' do diff --git a/spec/models/webauthn_registration_spec.rb b/spec/models/webauthn_registration_spec.rb new file mode 100644 index 00000000000..6813854bf6c --- /dev/null +++ b/spec/models/webauthn_registration_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WebauthnRegistration do + describe 'relations' do + it { is_expected.to belong_to(:user) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:credential_xid) } + it { is_expected.to validate_presence_of(:public_key) } + it { is_expected.to validate_presence_of(:counter) } + it { is_expected.to validate_length_of(:name).is_at_least(0) } + it { is_expected.not_to allow_value(nil).for(:name) } + it do + is_expected.to validate_numericality_of(:counter) + .only_integer + .is_greater_than_or_equal_to(0) + .is_less_than_or_equal_to(4294967295) + end + end +end diff --git a/spec/presenters/release_presenter_spec.rb b/spec/presenters/release_presenter_spec.rb index b2e7b684644..925a69ca92d 100644 --- a/spec/presenters/release_presenter_spec.rb +++ b/spec/presenters/release_presenter_spec.rb @@ -63,12 +63,6 @@ RSpec.describe ReleasePresenter do it 'returns its own url' do is_expected.to eq(project_release_url(project, release)) end - - context 'when user is guest' do - let(:user) { guest } - - it { is_expected.to be_nil } - end end describe '#opened_merge_requests_url' do @@ -147,13 +141,5 @@ RSpec.describe ReleasePresenter do it 'returns the release name' do is_expected.to eq release.name end - - context "when a user is not allowed to access any repository information" do - let(:presenter) { described_class.new(release, current_user: guest) } - - it 'returns a replacement name to avoid potentially leaking tag information' do - is_expected.to eq "Release-#{release.id}" - end - end end end diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb index 7f24d051457..77abac4ef04 100644 --- a/spec/requests/api/graphql/project/release_spec.rb +++ b/spec/requests/api/graphql/project/release_spec.rb @@ -228,6 +228,189 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do end end + shared_examples 'restricted access to release fields' do + describe 'scalar fields' do + let(:path) { path_prefix } + + let(:release_fields) do + %{ + tagName + tagPath + description + descriptionHtml + name + createdAt + releasedAt + upcomingRelease + } + end + + before do + post_query + end + + it 'finds all release data' do + expect(data).to eq({ + 'tagName' => release.tag, + 'tagPath' => nil, + 'description' => release.description, + 'descriptionHtml' => release.description_html, + 'name' => release.name, + 'createdAt' => release.created_at.iso8601, + 'releasedAt' => release.released_at.iso8601, + 'upcomingRelease' => false + }) + end + end + + describe 'milestones' do + let(:path) { path_prefix + %w[milestones nodes] } + + let(:release_fields) do + query_graphql_field(:milestones, nil, 'nodes { id title }') + end + + it 'finds milestones associated to a release' do + post_query + + expected = release.milestones.order_by_dates_and_title.map do |milestone| + { 'id' => global_id_of(milestone), 'title' => milestone.title } + end + + expect(data).to eq(expected) + end + end + + describe 'author' do + let(:path) { path_prefix + %w[author] } + + let(:release_fields) do + query_graphql_field(:author, nil, 'id username') + end + + it 'finds the author of the release' do + post_query + + expect(data).to eq( + 'id' => global_id_of(release.author), + 'username' => release.author.username + ) + end + end + + describe 'commit' do + let(:path) { path_prefix + %w[commit] } + + let(:release_fields) do + query_graphql_field(:commit, nil, 'sha') + end + + it 'restricts commit associated with the release' do + post_query + + expect(data).to eq(nil) + end + end + + describe 'assets' do + describe 'count' do + let(:path) { path_prefix + %w[assets] } + + let(:release_fields) do + query_graphql_field(:assets, nil, 'count') + end + + it 'returns non source release links count' do + post_query + + expect(data).to eq('count' => release.assets_count(except: [:sources])) + end + end + + describe 'links' do + let(:path) { path_prefix + %w[assets links nodes] } + + let(:release_fields) do + query_graphql_field(:assets, nil, + query_graphql_field(:links, nil, 'nodes { id name url external, directAssetUrl }')) + end + + it 'finds all non source external release links' do + post_query + + expected = release.links.map do |link| + { + 'id' => global_id_of(link), + 'name' => link.name, + 'url' => link.url, + 'external' => true, + 'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << "/downloads#{link.filepath}" : link.url + } + end + + expect(data).to match_array(expected) + end + end + + describe 'sources' do + let(:path) { path_prefix + %w[assets sources nodes] } + + let(:release_fields) do + query_graphql_field(:assets, nil, + query_graphql_field(:sources, nil, 'nodes { format url }')) + end + + it 'restricts release sources' do + post_query + + expect(data).to match_array([]) + end + end + end + + describe 'links' do + let(:path) { path_prefix + %w[links] } + + let(:release_fields) do + query_graphql_field(:links, nil, %{ + selfUrl + openedMergeRequestsUrl + mergedMergeRequestsUrl + closedMergeRequestsUrl + openedIssuesUrl + closedIssuesUrl + }) + end + + it 'finds only selfUrl' do + post_query + + expect(data).to eq( + 'selfUrl' => project_release_url(project, release), + 'openedMergeRequestsUrl' => nil, + 'mergedMergeRequestsUrl' => nil, + 'closedMergeRequestsUrl' => nil, + 'openedIssuesUrl' => nil, + 'closedIssuesUrl' => nil + ) + end + end + + describe 'evidences' do + let(:path) { path_prefix + %w[evidences] } + + let(:release_fields) do + query_graphql_field(:evidences, nil, 'nodes { id sha filepath collectedAt }') + end + + it 'restricts all evidence fields' do + post_query + + expect(data).to eq('nodes' => []) + end + end + end + shared_examples 'no access to the release field' do describe 'repository-related fields' do let(:path) { path_prefix } @@ -302,7 +485,8 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do context 'when the user has Guest permissions' do let(:current_user) { guest } - it_behaves_like 'no access to the release field' + it_behaves_like 'restricted access to release fields' + it_behaves_like 'no access to editUrl' end context 'when the user has Reporter permissions' do diff --git a/spec/requests/api/graphql/project/releases_spec.rb b/spec/requests/api/graphql/project/releases_spec.rb index 2816ce90a6b..c28a6fa7666 100644 --- a/spec/requests/api/graphql/project/releases_spec.rb +++ b/spec/requests/api/graphql/project/releases_spec.rb @@ -129,10 +129,12 @@ RSpec.describe 'Query.project(fullPath).releases()' do end it 'does not return data for fields that expose repository information' do + tag_name = release.tag + release_name = release.name expect(data).to eq( - 'tagName' => nil, + 'tagName' => tag_name, 'tagPath' => nil, - 'name' => "Release-#{release.id}", + 'name' => release_name, 'commit' => nil, 'assets' => { 'count' => release.assets_count(except: [:sources]), @@ -143,7 +145,14 @@ RSpec.describe 'Query.project(fullPath).releases()' do 'evidences' => { 'nodes' => [] }, - 'links' => nil + 'links' => { + 'closedIssuesUrl' => nil, + 'closedMergeRequestsUrl' => nil, + 'mergedMergeRequestsUrl' => nil, + 'openedIssuesUrl' => nil, + 'openedMergeRequestsUrl' => nil, + 'selfUrl' => project_release_url(project, release) + } ) end end diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb index 9598355a640..b1beb2adb3b 100644 --- a/spec/services/packages/npm/create_package_service_spec.rb +++ b/spec/services/packages/npm/create_package_service_spec.rb @@ -73,6 +73,23 @@ RSpec.describe Packages::Npm::CreatePackageService do end end + described_class::PACKAGE_JSON_NOT_ALLOWED_FIELDS.each do |field| + context "with not allowed #{field} field" do + before do + params[:versions][version][field] = 'test' + end + + it 'is persisted without the field' do + expect { subject } + .to change { Packages::Package.count }.by(1) + .and change { Packages::Package.npm.count }.by(1) + .and change { Packages::Tag.count }.by(1) + .and change { Packages::Npm::Metadatum.count }.by(1) + expect(subject.npm_metadatum.package_json[field]).to be_blank + end + end + end + context 'with packages_npm_abbreviated_metadata disabled' do before do stub_feature_flags(packages_npm_abbreviated_metadata: false) diff --git a/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb b/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb index b853d517d83..d823e7ac221 100644 --- a/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb +++ b/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb @@ -36,8 +36,8 @@ RSpec.shared_examples 'StageEventModel' do described_class.issuable_id_column, :group_id, :project_id, - :milestone_id, :author_id, + :milestone_id, :state_id, :start_event_timestamp, :end_event_timestamp |