diff options
103 files changed, 2207 insertions, 565 deletions
diff --git a/.rubocop_todo/rspec/verified_doubles.yml b/.rubocop_todo/rspec/verified_doubles.yml index 0fa36d1941c..a225dbf21ae 100644 --- a/.rubocop_todo/rspec/verified_doubles.yml +++ b/.rubocop_todo/rspec/verified_doubles.yml @@ -448,7 +448,6 @@ RSpec/VerifiedDoubles: - spec/lib/gitlab/ci/config/external/file/project_spec.rb - spec/lib/gitlab/ci/config/external/rules_spec.rb - spec/lib/gitlab/ci/parsers/test/junit_spec.rb - - spec/lib/gitlab/ci/pipeline/chain/command_spec.rb - spec/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules_spec.rb - spec/lib/gitlab/ci/pipeline/chain/helpers_spec.rb - spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb diff --git a/app/assets/javascripts/jira_connect/subscriptions/api.js b/app/assets/javascripts/jira_connect/subscriptions/api.js index 8b32a67c23b..c79d7002111 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/api.js +++ b/app/assets/javascripts/jira_connect/subscriptions/api.js @@ -92,6 +92,6 @@ export const fetchOAuthApplicationId = () => { return axiosInstance.get(JIRA_CONNECT_OAUTH_APPLICATION_ID_PATH); }; -export const fetchOAuthToken = (oauthTokenURL, data = {}) => { - return axiosInstance.post(oauthTokenURL, data); +export const fetchOAuthToken = (oauthTokenPath, data = {}) => { + return axiosInstance.post(oauthTokenPath, data); }; diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue index 8df906dd292..4cf3a1a0279 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue @@ -159,9 +159,9 @@ export default { async getOAuthToken(code) { const { oauth_token_payload: oauthTokenPayload, - oauth_token_url: oauthTokenURL, + oauth_token_path: oauthTokenPath, } = this.oauthMetadata; - const { data } = await fetchOAuthToken(oauthTokenURL, { + const { data } = await fetchOAuthToken(oauthTokenPath, { ...oauthTokenPayload, code, code_verifier: this.codeVerifier, diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index e5b6c9811b5..bdb8f758137 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -285,19 +285,6 @@ $gl-line-height-42: px-to-rem(42px); padding-right: $gl-spacing-scale-10; } -// TODO: will be moved to @gitlab/ui as part of https://gitlab.com/gitlab-org/gitlab/-/issues/349008 -.gl-sm-mt-6 { - @include media-breakpoint-up(sm) { - margin-top: $gl-spacing-scale-6; - } -} - -.gl-sm-mt-6\! { - @include media-breakpoint-up(sm) { - margin-top: $gl-spacing-scale-6 !important; - } -} - /* All of the following (up until the "End gitlab-ui#1709" comment) will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709 diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb index b0d7c8cb8f2..d66b3cb4366 100644 --- a/app/controllers/admin/applications_controller.rb +++ b/app/controllers/admin/applications_controller.rb @@ -14,7 +14,7 @@ class Admin::ApplicationsController < Admin::ApplicationController end def show - @created = get_created_session + @created = get_created_session if Feature.disabled?('hash_oauth_secrets') end def new @@ -30,9 +30,14 @@ class Admin::ApplicationsController < Admin::ApplicationController if @application.persisted? flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) - set_created_session + if Feature.enabled?('hash_oauth_secrets') + @created = true + render :show + else + set_created_session - redirect_to admin_application_url(@application) + redirect_to admin_application_url(@application) + end else render :new end diff --git a/app/controllers/concerns/harbor/access.rb b/app/controllers/concerns/harbor/access.rb index 70de72f15fc..211566aeda7 100644 --- a/app/controllers/concerns/harbor/access.rb +++ b/app/controllers/concerns/harbor/access.rb @@ -17,7 +17,7 @@ module Harbor private def harbor_registry_enabled! - render_404 unless Feature.enabled?(:harbor_registry_integration) + render_404 unless Feature.enabled?(:harbor_registry_integration, defined?(group) ? group : project) end def authorize_read_harbor_registry! diff --git a/app/controllers/groups/settings/applications_controller.rb b/app/controllers/groups/settings/applications_controller.rb index bfe61696e0f..3557d485422 100644 --- a/app/controllers/groups/settings/applications_controller.rb +++ b/app/controllers/groups/settings/applications_controller.rb @@ -16,7 +16,7 @@ module Groups end def show - @created = get_created_session + @created = get_created_session if Feature.disabled?('hash_oauth_secrets') end def edit @@ -28,9 +28,15 @@ module Groups if @application.persisted? flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) - set_created_session + if Feature.enabled?('hash_oauth_secrets') - redirect_to group_settings_application_url(@group, @application) + @created = true + render :show + else + set_created_session + + redirect_to group_settings_application_url(@group, @application) + end else set_index_vars render :index diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index a996bad3fac..ff466fd5fbb 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -25,7 +25,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController end def show - @created = get_created_session + @created = get_created_session if Feature.disabled?('hash_oauth_secrets') end def create @@ -34,9 +34,14 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController if @application.persisted? flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) - set_created_session + if Feature.enabled?('hash_oauth_secrets') + @created = true + render :show + else + set_created_session - redirect_to oauth_application_url(@application) + redirect_to oauth_application_url(@application) + end else set_index_vars render :index diff --git a/app/helpers/jira_connect_helper.rb b/app/helpers/jira_connect_helper.rb index 4ddfb0224d1..0971fdae8dd 100644 --- a/app/helpers/jira_connect_helper.rb +++ b/app/helpers/jira_connect_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module JiraConnectHelper - def jira_connect_app_data(subscriptions) + def jira_connect_app_data(subscriptions, installation) skip_groups = subscriptions.map(&:namespace_id) { @@ -11,14 +11,16 @@ module JiraConnectHelper subscriptions_path: jira_connect_subscriptions_path(format: :json), users_path: current_user ? nil : jira_connect_users_path, # users_path is used to determine if user is signed in gitlab_user_path: current_user ? user_path(current_user) : nil, - oauth_metadata: Feature.enabled?(:jira_connect_oauth, current_user) ? jira_connect_oauth_data.to_json : nil + oauth_metadata: Feature.enabled?(:jira_connect_oauth, current_user) ? jira_connect_oauth_data(installation).to_json : nil } end private - def jira_connect_oauth_data - oauth_authorize_url = oauth_authorization_url( + def jira_connect_oauth_data(installation) + oauth_instance_url = installation.oauth_authorization_url + + oauth_authorize_path = oauth_authorization_path( client_id: Gitlab::CurrentSettings.jira_connect_application_key, response_type: 'code', scope: 'api', @@ -27,8 +29,8 @@ module JiraConnectHelper ) { - oauth_authorize_url: oauth_authorize_url, - oauth_token_url: oauth_token_url, + oauth_authorize_url: Gitlab::Utils.append_path(oauth_instance_url, oauth_authorize_path), + oauth_token_path: oauth_token_path, state: oauth_state, oauth_token_payload: { grant_type: :authorization_code, diff --git a/app/models/jira_connect_installation.rb b/app/models/jira_connect_installation.rb index 8befe9a9230..0a2d3ba0749 100644 --- a/app/models/jira_connect_installation.rb +++ b/app/models/jira_connect_installation.rb @@ -24,4 +24,10 @@ class JiraConnectInstallation < ApplicationRecord def client Atlassian::JiraConnect::Client.new(base_url, shared_secret) end + + def oauth_authorization_url + return Gitlab.config.gitlab.url if instance_url.blank? || Feature.disabled?(:jira_connect_oauth_self_managed) + + instance_url + end end diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 61a95e49228..d75e74f3b19 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -28,9 +28,6 @@ module Issues return if issue.relative_position.nil? return if NO_REBALANCING_NEEDED.cover?(issue.relative_position) - gates = [issue.project, issue.project.group].compact - return unless gates.any? { |gate| Feature.enabled?(:rebalance_issues, gate) } - Issues::RebalancingWorker.perform_async(nil, *issue.project.self_or_root_group_ids) end diff --git a/app/services/issues/relative_position_rebalancing_service.rb b/app/services/issues/relative_position_rebalancing_service.rb index 23bb409f3cd..b5c10430e83 100644 --- a/app/services/issues/relative_position_rebalancing_service.rb +++ b/app/services/issues/relative_position_rebalancing_service.rb @@ -16,8 +16,6 @@ module Issues end def execute - return unless Feature.enabled?(:rebalance_issues, root_namespace) - # Given can_start_rebalance? and track_new_running_rebalance are not atomic # it can happen that we end up with more than Rebalancing::State::MAX_NUMBER_OF_CONCURRENT_REBALANCES running. # Considering the number of allowed Rebalancing::State::MAX_NUMBER_OF_CONCURRENT_REBALANCES is small we should be ok, diff --git a/app/views/jira_connect/subscriptions/index.html.haml b/app/views/jira_connect/subscriptions/index.html.haml index d4ced15b869..f66aa0840aa 100644 --- a/app/views/jira_connect/subscriptions/index.html.haml +++ b/app/views/jira_connect/subscriptions/index.html.haml @@ -1,4 +1,4 @@ -.js-jira-connect-app{ data: jira_connect_app_data(@subscriptions) } +.js-jira-connect-app{ data: jira_connect_app_data(@subscriptions, @current_jira_installation) } = webpack_bundle_tag 'performance_bar' if performance_bar_enabled? = webpack_bundle_tag 'jira_connect_app' diff --git a/app/views/shared/doorkeeper/applications/_show.html.haml b/app/views/shared/doorkeeper/applications/_show.html.haml index f533b5b5a4d..562b1aee4ca 100644 --- a/app/views/shared/doorkeeper/applications/_show.html.haml +++ b/app/views/shared/doorkeeper/applications/_show.html.haml @@ -15,7 +15,14 @@ %td = _('Secret') %td - = clipboard_button(clipboard_text: @application.secret, button_text: _('Copy'), title: _("Copy secret"), class: "btn btn-default btn-md gl-button") + - if Feature.enabled?('hash_oauth_secrets') + - if @application.plaintext_secret + = clipboard_button(clipboard_text: @application.plaintext_secret, button_text: _('Copy'), title: _("Copy secret"), class: "btn btn-default btn-md gl-button") + %span= _('This is the only time the secret is accessible. Copy the secret and store it securely.') + - else + = _('The secret is only available when you first create the application.') + - else + = clipboard_button(clipboard_text: @application.secret, button_text: _('Copy'), title: _("Copy secret"), class: "btn btn-default btn-md gl-button") %tr %td = _('Callback URL') diff --git a/config/feature_flags/development/hash_oauth_secrets.yml b/config/feature_flags/development/hash_oauth_secrets.yml new file mode 100644 index 00000000000..7730d319bab --- /dev/null +++ b/config/feature_flags/development/hash_oauth_secrets.yml @@ -0,0 +1,8 @@ +--- +name: hash_oauth_secrets +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96252 +rollout_issue_url: +milestone: '15.4' +type: development +group: group::authentication and authorization +default_enabled: false diff --git a/config/feature_flags/development/rebalance_issues.yml b/config/feature_flags/development/rebalance_issues.yml deleted file mode 100644 index 5651b02b073..00000000000 --- a/config/feature_flags/development/rebalance_issues.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: rebalance_issues -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40124 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/239344 -milestone: '13.4' -type: development -group: group::project management -default_enabled: true diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 867f3fd47cc..918b2767c4d 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -90,7 +90,9 @@ Doorkeeper.configure do # Check out the wiki for more information on customization access_token_methods :from_access_token_param, :from_bearer_authorization, :from_bearer_param - hash_token_secrets using: '::Gitlab::DoorkeeperSecretStoring::Pbkdf2Sha512', fallback: :plain + hash_token_secrets using: '::Gitlab::DoorkeeperSecretStoring::Token::Pbkdf2Sha512', fallback: :plain + + hash_application_secrets using: '::Gitlab::DoorkeeperSecretStoring::Secret::Pbkdf2Sha512', fallback: :plain # Specify what grant flows are enabled in array of Strings. The valid # strings and the flows they enable are: diff --git a/config/initializers/zz_metrics.rb b/config/initializers/zz_metrics.rb index 3a729e34135..940d8eed61f 100644 --- a/config/initializers/zz_metrics.rb +++ b/config/initializers/zz_metrics.rb @@ -40,6 +40,8 @@ if Gitlab::Metrics.enabled? && !Rails.env.test? && !(Rails.env.development? && d if Gitlab::Runtime.puma? Gitlab::Metrics::RequestsRackMiddleware.initialize_metrics Gitlab::Metrics::GlobalSearchSlis.initialize_slis! + elsif Gitlab.ee? && Gitlab::Runtime.sidekiq? + Gitlab::Metrics::GlobalSearchIndexingSlis.initialize_slis! end GC::Profiler.enable diff --git a/config/metrics/counts_all/20210216180242_web_ide_commits.yml b/config/metrics/counts_all/20210216180242_web_ide_commits.yml index 3d1d416a7c2..f86b5bd5f84 100644 --- a/config/metrics/counts_all/20210216180242_web_ide_commits.yml +++ b/config/metrics/counts_all/20210216180242_web_ide_commits.yml @@ -10,6 +10,11 @@ value_type: number status: active time_frame: all data_source: redis +instrumentation_class: RedisMetric +options: + prefix: web_ide + event: commits_count + include_usage_prefix: false distribution: - ce - ee diff --git a/config/metrics/counts_all/20210216180244_web_ide_views.yml b/config/metrics/counts_all/20210216180244_web_ide_views.yml index 7bc32b3dbc9..63149b86e0f 100644 --- a/config/metrics/counts_all/20210216180244_web_ide_views.yml +++ b/config/metrics/counts_all/20210216180244_web_ide_views.yml @@ -10,6 +10,11 @@ value_type: number status: active time_frame: all data_source: redis +instrumentation_class: RedisMetric +options: + prefix: web_ide + event: views_count + include_usage_prefix: false distribution: - ce - ee diff --git a/config/metrics/counts_all/20210216180246_web_ide_merge_requests.yml b/config/metrics/counts_all/20210216180246_web_ide_merge_requests.yml index eb02d98dc85..f620447e615 100644 --- a/config/metrics/counts_all/20210216180246_web_ide_merge_requests.yml +++ b/config/metrics/counts_all/20210216180246_web_ide_merge_requests.yml @@ -10,6 +10,11 @@ value_type: number status: active time_frame: all data_source: redis +instrumentation_class: RedisMetric +options: + prefix: web_ide + event: merge_requests_count + include_usage_prefix: false distribution: - ce - ee diff --git a/config/metrics/counts_all/20210216180248_web_ide_previews.yml b/config/metrics/counts_all/20210216180248_web_ide_previews.yml index 4d581fc7f7e..c785e95e105 100644 --- a/config/metrics/counts_all/20210216180248_web_ide_previews.yml +++ b/config/metrics/counts_all/20210216180248_web_ide_previews.yml @@ -10,6 +10,11 @@ value_type: number status: active time_frame: all data_source: redis +instrumentation_class: RedisMetric +options: + prefix: web_ide + event: previews_count + include_usage_prefix: false distribution: - ce - ee diff --git a/config/metrics/counts_all/20210216180250_web_ide_terminals.yml b/config/metrics/counts_all/20210216180250_web_ide_terminals.yml index e8c1f425639..cd64a877341 100644 --- a/config/metrics/counts_all/20210216180250_web_ide_terminals.yml +++ b/config/metrics/counts_all/20210216180250_web_ide_terminals.yml @@ -10,6 +10,11 @@ value_type: number status: active time_frame: all data_source: redis +instrumentation_class: RedisMetric +options: + prefix: web_ide + event: terminals_count + include_usage_prefix: false distribution: - ce - ee diff --git a/config/metrics/counts_all/20210216180252_web_ide_pipelines.yml b/config/metrics/counts_all/20210216180252_web_ide_pipelines.yml index ae891775bf9..bfd0b69401e 100644 --- a/config/metrics/counts_all/20210216180252_web_ide_pipelines.yml +++ b/config/metrics/counts_all/20210216180252_web_ide_pipelines.yml @@ -10,6 +10,11 @@ value_type: number status: active time_frame: all data_source: redis +instrumentation_class: RedisMetric +options: + prefix: web_ide + event: pipelines_count + include_usage_prefix: false distribution: - ce - ee diff --git a/config/metrics/counts_all/20220122022215_web_ide_previews_success.yml b/config/metrics/counts_all/20220122022215_web_ide_previews_success.yml index 203201e9174..e2d617dba16 100644 --- a/config/metrics/counts_all/20220122022215_web_ide_previews_success.yml +++ b/config/metrics/counts_all/20220122022215_web_ide_previews_success.yml @@ -10,6 +10,11 @@ value_type: number status: active time_frame: all data_source: redis +instrumentation_class: RedisMetric +options: + prefix: web_ide + event: previews_success_count + include_usage_prefix: false distribution: - ce - ee diff --git a/db/migrate/20220831182105_add_constraints_view.rb b/db/migrate/20220831182105_add_constraints_view.rb new file mode 100644 index 00000000000..03c183b6e9f --- /dev/null +++ b/db/migrate/20220831182105_add_constraints_view.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class AddConstraintsView < Gitlab::Database::Migration[2.0] + def up + execute(<<~SQL) + CREATE OR REPLACE VIEW postgres_constraints + AS + SELECT + pg_constraint.oid AS oid, + pg_constraint.conname AS name, + pg_constraint.contype AS constraint_type, + pg_constraint.convalidated AS constraint_valid, + (SELECT array_agg(attname ORDER BY ordering) + FROM unnest(pg_constraint.conkey) WITH ORDINALITY attnums(attnum, ordering) + INNER JOIN pg_attribute ON pg_attribute.attnum = attnums.attnum AND pg_attribute.attrelid = pg_class.oid + ) AS column_names, + pg_namespace.nspname::text || '.'::text || pg_class.relname::text AS table_identifier, + -- pg_constraint reports a 0 oid rather than null if the constraint is not a partition child constraint. + nullif(pg_constraint.conparentid, 0) AS parent_constraint_oid, + pg_get_constraintdef(pg_constraint.oid) AS definition + FROM pg_constraint + INNER JOIN pg_class ON pg_constraint.conrelid = pg_class.oid + INNER JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid; + SQL + end + + def down + execute(<<~SQL) + DROP VIEW postgres_constraints; + SQL + end +end diff --git a/db/schema_migrations/20220831182105 b/db/schema_migrations/20220831182105 new file mode 100644 index 00000000000..6f4b0f46ff1 --- /dev/null +++ b/db/schema_migrations/20220831182105 @@ -0,0 +1 @@ +80828666cac381dde65dc208764b6e1c7fe703b63c708410f72afdd33886fc60
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index f536bd170eb..d03a291ea6b 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -19307,6 +19307,21 @@ CREATE VIEW postgres_autovacuum_activity AS COMMENT ON VIEW postgres_autovacuum_activity IS 'Contains information about PostgreSQL backends currently performing autovacuum operations on the tables indicated here.'; +CREATE VIEW postgres_constraints AS + SELECT pg_constraint.oid, + pg_constraint.conname AS name, + pg_constraint.contype AS constraint_type, + pg_constraint.convalidated AS constraint_valid, + ( SELECT array_agg(pg_attribute.attname ORDER BY attnums.ordering) AS array_agg + FROM (unnest(pg_constraint.conkey) WITH ORDINALITY attnums(attnum, ordering) + JOIN pg_attribute ON (((pg_attribute.attnum = attnums.attnum) AND (pg_attribute.attrelid = pg_class.oid))))) AS column_names, + (((pg_namespace.nspname)::text || '.'::text) || (pg_class.relname)::text) AS table_identifier, + NULLIF(pg_constraint.conparentid, (0)::oid) AS parent_constraint_oid, + pg_get_constraintdef(pg_constraint.oid) AS definition + FROM ((pg_constraint + JOIN pg_class ON ((pg_constraint.conrelid = pg_class.oid))) + JOIN pg_namespace ON ((pg_class.relnamespace = pg_namespace.oid))); + CREATE VIEW postgres_foreign_keys AS SELECT pg_constraint.oid, pg_constraint.conname AS name, diff --git a/doc/api/discussions.md b/doc/api/discussions.md index a5610adad79..60ce10590e1 100644 --- a/doc/api/discussions.md +++ b/doc/api/discussions.md @@ -1109,7 +1109,7 @@ GET /projects/:id/repository/commits/:commit_id/discussions | Attribute | Type | Required | Description | | ------------------- | ---------------- | ---------- | ------------ | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) | -| `commit_id` | integer | yes | The ID of a commit | +| `commit_id` | string | yes | The SHA of a commit | ```json [ @@ -1237,7 +1237,7 @@ Diff comments contain also position: ```shell curl --header "PRIVATE-TOKEN: <your_access_token>"\ - "https://gitlab.example.com/api/v4/projects/5/repository/commits/11/discussions" + "https://gitlab.example.com/api/v4/projects/5/repository/commits/<commit_id>/discussions" ``` ### Get single commit discussion item @@ -1253,12 +1253,12 @@ Parameters: | Attribute | Type | Required | Description | | ------------------- | -------------- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) | -| `commit_id` | integer | yes | The ID of a commit | -| `discussion_id` | integer | yes | The ID of a discussion item | +| `commit_id` | string | yes | The SHA of a commit | +| `discussion_id` | string | yes | The ID of a discussion item | ```shell curl --header "PRIVATE-TOKEN: <your_access_token>"\ - "https://gitlab.example.com/api/v4/projects/5/repository/commits/11/discussions/<discussion_id>" + "https://gitlab.example.com/api/v4/projects/5/repository/commits/<commit_id>/discussions/<discussion_id>" ``` ### Create new commit thread @@ -1294,7 +1294,7 @@ Parameters: ```shell curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\ - "https://gitlab.example.com/api/v4/projects/5/repository/commits/11/discussions?body=comment" + "https://gitlab.example.com/api/v4/projects/5/repository/commits/<commit_id>/discussions?body=comment" ``` The rules for creating the API request are the same as when @@ -1314,15 +1314,15 @@ Parameters: | Attribute | Type | Required | Description | | ------------------- | -------------- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) | -| `commit_id` | integer | yes | The ID of a commit | -| `discussion_id` | integer | yes | The ID of a thread | +| `commit_id` | string | yes | The SHA of a commit | +| `discussion_id` | string | yes | The ID of a thread | | `note_id` | integer | yes | The ID of a thread note | | `body` | string | yes | The content of the note/reply | | `created_at` | string | no | Date time string, ISO 8601 formatted, such `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) | ```shell curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\ - "https://gitlab.example.com/api/v4/projects/5/repository/commits/11/discussions/<discussion_id>/notes?body=comment + "https://gitlab.example.com/api/v4/projects/5/repository/commits/<commit_id>/discussions/<discussion_id>/notes?body=comment ``` ### Modify an existing commit thread note @@ -1338,21 +1338,21 @@ Parameters: | Attribute | Type | Required | Description | | ------------------- | -------------- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) | -| `commit_id` | integer | yes | The ID of a commit | -| `discussion_id` | integer | yes | The ID of a thread | +| `commit_id` | string | yes | The SHA of a commit | +| `discussion_id` | string | yes | The ID of a thread | | `note_id` | integer | yes | The ID of a thread note | | `body` | string | no | The content of a note | ```shell curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>"\ - "https://gitlab.example.com/api/v4/projects/5/repository/commits/11/discussions/<discussion_id>/notes/1108?body=comment" + "https://gitlab.example.com/api/v4/projects/5/repository/commits/<commit_id>/discussions/<discussion_id>/notes/1108?body=comment" ``` Resolving a note: ```shell curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>"\ - "https://gitlab.example.com/api/v4/projects/5/repository/commits/11/discussions/<discussion_id>/notes/1108?resolved=true" + "https://gitlab.example.com/api/v4/projects/5/repository/commits/<commit_id>/discussions/<discussion_id>/notes/1108?resolved=true" ``` ### Delete a commit thread note @@ -1368,11 +1368,11 @@ Parameters: | Attribute | Type | Required | Description | | ------------------- | -------------- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) | -| `commit_id` | integer | yes | The ID of a commit | -| `discussion_id` | integer | yes | The ID of a thread | +| `commit_id` | string | yes | The SHA of a commit | +| `discussion_id` | string | yes | The ID of a thread | | `note_id` | integer | yes | The ID of a thread note | ```shell curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>"\ - "https://gitlab.example.com/api/v4/projects/5/repository/commits/11/discussions/636" + "https://gitlab.example.com/api/v4/projects/5/repository/commits/<commit_id>/discussions/<discussion_id>/notes/636" ``` diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md index f4e74973fd7..73c1874f09e 100644 --- a/doc/development/documentation/index.md +++ b/doc/development/documentation/index.md @@ -240,9 +240,9 @@ Every GitLab instance includes documentation at `/help` (`https://gitlab.example that matches the version of the instance. For example, <https://gitlab.com/help>. The documentation available online at <https://docs.gitlab.com> is deployed every -four hours from the default branch of [GitLab, Omnibus, Runner, and Charts](#source-files-and-rendered-web-locations). +hour from the default branch of [GitLab, Omnibus, Runner, and Charts](#source-files-and-rendered-web-locations). After a merge request that updates documentation is merged, it is available online -in 4 hours or less. +in an hour or less. However, it's only available at `/help` on self-managed instances in the next released version. The date an update is merged can impact which self-managed release the update diff --git a/doc/development/import_project.md b/doc/development/import_project.md index 7c55d2e2668..1f3bf860257 100644 --- a/doc/development/import_project.md +++ b/doc/development/import_project.md @@ -149,26 +149,10 @@ You might see an error like `N is out of range for ActiveModel::Type::Integer wi where `N` is the integer exceeding the 4-byte integer limit. If that's the case, you are likely hitting the issue with rebalancing of `relative_position` field of the issues. -The feature flag to enable the rebalance automatically was enabled on GitLab.com. -We intend to enable it by default on self-managed instances when the issue -[Rebalance issues FF rollout](https://gitlab.com/gitlab-org/gitlab/-/issues/343368) -is implemented. - -If the feature is not enabled by default on your GitLab version, run the following -commands in the [Rails console](../administration/operations/rails_console.md) as -a workaround. Replace the ID with the ID of your project you were trying to import: - ```ruby -# Check if the feature is enabled on your instance. If it is, rebalance should work automatically on your instance -Feature.enabled?(:rebalance_issues,Project.find(ID).root_namespace) - # Check the current maximum value of relative_position Issue.where(project_id: Project.find(ID).root_namespace.all_projects).maximum(:relative_position) -# Enable `rebalance_issues` feauture and check that it was successfully enabled -Feature.enable(:rebalance_issues,Project.find(ID).root_namespace) -Feature.enabled?(:rebalance_issues,Project.find(ID).root_namespace) - # Run the rebalancing process and check if the maximum value of relative_position has changed Issues::RelativePositionRebalancingService.new(Project.find(ID).root_namespace.all_projects).execute Issue.where(project_id: Project.find(ID).root_namespace.all_projects).maximum(:relative_position) diff --git a/doc/integration/oauth_provider.md b/doc/integration/oauth_provider.md index 55a14cb4859..21184f7b678 100644 --- a/doc/integration/oauth_provider.md +++ b/doc/integration/oauth_provider.md @@ -127,3 +127,13 @@ application can perform. Available scopes are depicted in the following table. | `email` | Grants read-only access to the user's primary email address using [OpenID Connect](openid_connect_provider.md). | At any time you can revoke any access by clicking **Revoke**. + +## Hashed OAuth application secrets + +> Introduced in GitLab 15.4 [with a flag](../administration/feature_flags.md) named `hash_oauth_secrets`. Disabled by default. + +FLAG: +On self-managed GitLab, by default, this feature is not available. To make it available, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `hash_oauth_secrets`. +On GitLab.com, this feature is not available. + +By default, OAuth application secrets are stored as plain text in the database. When enabled, OAuth application secrets are stored in the database in hashed format and are only available to users immediately after creating OAuth applications. diff --git a/doc/user/compliance/license_compliance/img/denied_licenses_v15_3.png b/doc/user/compliance/license_compliance/img/denied_licenses_v15_3.png Binary files differindex db29cc1cf13..4ed84047133 100644 --- a/doc/user/compliance/license_compliance/img/denied_licenses_v15_3.png +++ b/doc/user/compliance/license_compliance/img/denied_licenses_v15_3.png diff --git a/doc/user/group/epics/epic_boards.md b/doc/user/group/epics/epic_boards.md index 25cb88e8a7c..a8bbb0575b3 100644 --- a/doc/user/group/epics/epic_boards.md +++ b/doc/user/group/epics/epic_boards.md @@ -155,6 +155,42 @@ into another list. Learn about possible effects in [Dragging epics between lists To move a list, select its top bar, and drag it horizontally. You can't move the **Open** and **Closed** lists, but you can hide them when editing an epic board. +#### Move an epic to the start of the list + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/367473) in GitLab 15.4. + +When you have many epics, it's inconvenient to manually drag an epic from the bottom of a board list all +the way to the top. You can move epics to the top of the list with a menu shortcut. + +Your epic is moved to the top of the list even if other epics are hidden by a filter. + +Prerequisites: + +- You must at least have the Reporter role for a group. + +To move an epic to the start of the list: + +1. In an epic board, hover over the card of the epic you want to move. +1. Select the vertical ellipsis (**{ellipsis_v}**), then **Move to start of list**. + +#### Move an epic to the end of the list + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/367473) in GitLab 15.4. + +When you have many epics, it's inconvenient to manually drag an epic from the top of a board list all +the way to the bottom. You can move epics to the bottom of the list with a menu shortcut. + +Your epic is moved to the bottom of the list even if other epics are hidden by a filter. + +Prerequisites: + +- You must at least have the Reporter role for a group. + +To move an epic to the end of the list: + +1. In an epic board, hover over the card of the epic you want to move. +1. Select the vertical ellipsis (**{ellipsis_v}**), then **Move to end of list**. + #### Dragging epics between lists When you drag epics between lists, the result is different depending on the source list diff --git a/doc/user/project/issues/img/related_issue_block_v15_3.png b/doc/user/project/issues/img/related_issue_block_v15_3.png Binary files differindex 827ddeabf10..942f7a33fe0 100644 --- a/doc/user/project/issues/img/related_issue_block_v15_3.png +++ b/doc/user/project/issues/img/related_issue_block_v15_3.png diff --git a/doc/user/project/issues/img/related_issues_add_v15_3.png b/doc/user/project/issues/img/related_issues_add_v15_3.png Binary files differindex 7c6edf61427..28739c0b909 100644 --- a/doc/user/project/issues/img/related_issues_add_v15_3.png +++ b/doc/user/project/issues/img/related_issues_add_v15_3.png diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md index 4647631343b..b4edb238479 100644 --- a/doc/user/project/issues/managing_issues.md +++ b/doc/user/project/issues/managing_issues.md @@ -686,6 +686,7 @@ Up to five similar issues, sorted by most recently updated, are displayed below > - Health status of closed issues [can't be edited](https://gitlab.com/gitlab-org/gitlab/-/issues/220867) in GitLab 13.4 and later. > - Issue health status visible in issue lists [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45141) in GitLab 13.6. > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/213567) in GitLab 13.7. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218618) in GitLab 15.4: health status is visible on issue cards in issue boards. To help you track issue statuses, you can assign a status to each issue. This status marks issues as progressing as planned or needing attention to keep on schedule. @@ -704,7 +705,11 @@ To edit health status of an issue: - Needs attention (amber) - At risk (red) -You can then see the issue's status in the issues list and the epic tree. +You can see the issue’s health status in: + +- Issues list +- Epic tree +- Issue cards in issue boards After an issue is closed, its health status can't be edited and the **Edit** button becomes disabled until the issue is reopened. diff --git a/lib/gitlab/ci/pipeline/chain/assign_partition.rb b/lib/gitlab/ci/pipeline/chain/assign_partition.rb index f946041db31..4b8efe13d44 100644 --- a/lib/gitlab/ci/pipeline/chain/assign_partition.rb +++ b/lib/gitlab/ci/pipeline/chain/assign_partition.rb @@ -17,9 +17,12 @@ module Gitlab private - # TODO handle parent-child pipelines def find_partition_id - ::Ci::Pipeline.current_partition_value + if @command.creates_child_pipeline? + @command.parent_pipeline_partition_id + else + ::Ci::Pipeline.current_partition_value + end end end end diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb index 2419b039f24..14c320f77bf 100644 --- a/lib/gitlab/ci/pipeline/chain/command.rb +++ b/lib/gitlab/ci/pipeline/chain/command.rb @@ -80,6 +80,10 @@ module Gitlab bridge&.parent_pipeline end + def parent_pipeline_partition_id + parent_pipeline.partition_id if creates_child_pipeline? + end + def creates_child_pipeline? bridge&.triggers_child_pipeline? end diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml index 7d1addd1a3d..6d290a14896 100644 --- a/lib/gitlab/database/gitlab_schemas.yml +++ b/lib/gitlab/database/gitlab_schemas.yml @@ -403,6 +403,7 @@ plans: :gitlab_main pool_repositories: :gitlab_main postgres_async_indexes: :gitlab_shared postgres_autovacuum_activity: :gitlab_shared +postgres_constraints: :gitlab_shared postgres_foreign_keys: :gitlab_shared postgres_index_bloat_estimates: :gitlab_shared postgres_indexes: :gitlab_shared diff --git a/lib/gitlab/database/partitioning/convert_table_to_first_list_partition.rb b/lib/gitlab/database/partitioning/convert_table_to_first_list_partition.rb new file mode 100644 index 00000000000..f45cf02ec9b --- /dev/null +++ b/lib/gitlab/database/partitioning/convert_table_to_first_list_partition.rb @@ -0,0 +1,214 @@ +# frozen_string_literal: true + +module Gitlab + module Database + module Partitioning + class ConvertTableToFirstListPartition + UnableToPartition = Class.new(StandardError) + + include Gitlab::Database::MigrationHelpers + + SQL_STATEMENT_SEPARATOR = ";\n\n" + + attr_reader :partitioning_column, :table_name, :parent_table_name, :zero_partition_value + + def initialize(migration_context:, table_name:, parent_table_name:, partitioning_column:, zero_partition_value:) + @migration_context = migration_context + @connection = migration_context.connection + @table_name = table_name + @parent_table_name = parent_table_name + @partitioning_column = partitioning_column + @zero_partition_value = zero_partition_value + end + + def prepare_for_partitioning + assert_existing_constraints_partitionable + + add_partitioning_check_constraint + end + + def revert_preparation_for_partitioning + migration_context.remove_check_constraint(table_name, partitioning_constraint.name) + end + + def partition + assert_existing_constraints_partitionable + assert_partitioning_constraint_present + create_parent_table + attach_foreign_keys_to_parent + + migration_context.with_lock_retries(raise_on_exhaustion: true) do + migration_context.execute(sql_to_convert_table) + end + end + + def revert_partitioning + migration_context.with_lock_retries(raise_on_exhaustion: true) do + migration_context.execute(<<~SQL) + ALTER TABLE #{connection.quote_table_name(parent_table_name)} + DETACH PARTITION #{connection.quote_table_name(table_name)}; + SQL + + alter_sequences_sql = alter_sequence_statements(old_table: parent_table_name, new_table: table_name) + .join(SQL_STATEMENT_SEPARATOR) + + migration_context.execute(alter_sequences_sql) + + # This takes locks for all the foreign keys that the parent table had. + # However, those same locks were taken while detaching the partition, and we can't avoid that. + # If we dropped the foreign key before detaching the partition to avoid this locking, + # the drop would cascade to the child partitions and drop their foreign keys as well + migration_context.drop_table(parent_table_name) + end + + add_partitioning_check_constraint + end + + private + + attr_reader :connection, :migration_context + + delegate :quote_table_name, :quote_column_name, to: :connection + + def sql_to_convert_table + # The critical statement here is the attach_table_to_parent statement. + # The following statements could be run in a later transaction, + # but they acquire the same locks so it's much faster to incude them + # here. + [ + attach_table_to_parent_statement, + alter_sequence_statements(old_table: table_name, new_table: parent_table_name), + remove_constraint_statement + ].flatten.join(SQL_STATEMENT_SEPARATOR) + end + + def table_identifier + "#{connection.current_schema}.#{table_name}" + end + + def assert_existing_constraints_partitionable + violating_constraints = Gitlab::Database::PostgresConstraint + .by_table_identifier(table_identifier) + .primary_or_unique_constraints + .not_including_column(partitioning_column) + .to_a + + return if violating_constraints.empty? + + violation_messages = violating_constraints.map { |c| "#{c.name} on (#{c.column_names.join(', ')})" } + + raise UnableToPartition, <<~MSG + Constraints on #{table_name} are incompatible with partitioning on #{partitioning_column} + + All primary key and unique constraints must include the partitioning column. + Violations: + #{violation_messages.join("\n")} + MSG + end + + def partitioning_constraint + constraints_on_column = Gitlab::Database::PostgresConstraint + .by_table_identifier(table_identifier) + .check_constraints + .valid + .including_column(partitioning_column) + + constraints_on_column.to_a.find do |constraint| + constraint.definition == "CHECK ((#{partitioning_column} = #{zero_partition_value}))" + end + end + + def assert_partitioning_constraint_present + return if partitioning_constraint + + raise UnableToPartition, <<~MSG + Table #{table_name} is not ready for partitioning. + Before partitioning, a check constraint must enforce that (#{partitioning_column} = #{zero_partition_value}) + MSG + end + + def add_partitioning_check_constraint + return if partitioning_constraint.present? + + check_body = "#{partitioning_column} = #{connection.quote(zero_partition_value)}" + # Any constraint name would work. The constraint is found based on its definition before partitioning + migration_context.add_check_constraint(table_name, check_body, 'partitioning_constraint') + + raise UnableToPartition, 'Error adding partitioning constraint' unless partitioning_constraint.present? + end + + def create_parent_table + migration_context.execute(<<~SQL) + CREATE TABLE IF NOT EXISTS #{quote_table_name(parent_table_name)} ( + LIKE #{quote_table_name(table_name)} INCLUDING ALL + ) PARTITION BY LIST(#{quote_column_name(partitioning_column)}) + SQL + end + + def attach_foreign_keys_to_parent + migration_context.foreign_keys(table_name).each do |fk| + # At this point no other connection knows about the parent table. + # Thus the only contended lock in the following transaction is on fk.to_table. + # So a deadlock is impossible. + + # If we're rerunning this migration after a failure to acquire a lock, the foreign key might already exist. + # Don't try to recreate it in that case + if migration_context.foreign_keys(parent_table_name) + .any? { |p_fk| p_fk.options[:name] == fk.options[:name] } + next + end + + migration_context.with_lock_retries(raise_on_exhaustion: true) do + migration_context.add_foreign_key(parent_table_name, fk.to_table, **fk.options) + end + end + end + + def attach_table_to_parent_statement + <<~SQL + ALTER TABLE #{quote_table_name(parent_table_name)} + ATTACH PARTITION #{table_name} + FOR VALUES IN (#{zero_partition_value}) + SQL + end + + def alter_sequence_statements(old_table:, new_table:) + sequences_owned_by(old_table).map do |seq_info| + seq_name, column_name = seq_info.values_at(:name, :column_name) + <<~SQL.chomp + ALTER SEQUENCE #{quote_table_name(seq_name)} OWNED BY #{quote_table_name(new_table)}.#{quote_column_name(column_name)} + SQL + end + end + + def remove_constraint_statement + <<~SQL + ALTER TABLE #{quote_table_name(parent_table_name)} + DROP CONSTRAINT #{quote_table_name(partitioning_constraint.name)} + SQL + end + + # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/373887 + def sequences_owned_by(table_name) + sequence_data = connection.exec_query(<<~SQL, nil, [table_name]) + SELECT seq_pg_class.relname AS seq_name, + dep_pg_class.relname AS table_name, + pg_attribute.attname AS col_name + FROM pg_class seq_pg_class + INNER JOIN pg_depend ON seq_pg_class.oid = pg_depend.objid + INNER JOIN pg_class dep_pg_class ON pg_depend.refobjid = dep_pg_class.oid + INNER JOIN pg_attribute ON dep_pg_class.oid = pg_attribute.attrelid + AND pg_depend.refobjsubid = pg_attribute.attnum + WHERE seq_pg_class.relkind = 'S' + AND dep_pg_class.relname = $1 + SQL + + sequence_data.map do |seq_info| + name, column_name = seq_info.values_at('seq_name', 'col_name') + { name: name, column_name: column_name } + end + end + end + end + end +end diff --git a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb index a541ecf5316..695a5d7ec77 100644 --- a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb +++ b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb @@ -251,6 +251,54 @@ module Gitlab create_sync_trigger(source_table_name, trigger_name, function_name) end + def prepare_constraint_for_list_partitioning(table_name:, partitioning_column:, parent_table_name:, initial_partitioning_value:) + validate_not_in_transaction!(:prepare_constraint_for_list_partitioning) + + Gitlab::Database::Partitioning::ConvertTableToFirstListPartition + .new(migration_context: self, + table_name: table_name, + parent_table_name: parent_table_name, + partitioning_column: partitioning_column, + zero_partition_value: initial_partitioning_value + ).prepare_for_partitioning + end + + def revert_preparing_constraint_for_list_partitioning(table_name:, partitioning_column:, parent_table_name:, initial_partitioning_value:) + validate_not_in_transaction!(:revert_preparing_constraint_for_list_partitioning) + + Gitlab::Database::Partitioning::ConvertTableToFirstListPartition + .new(migration_context: self, + table_name: table_name, + parent_table_name: parent_table_name, + partitioning_column: partitioning_column, + zero_partition_value: initial_partitioning_value + ).revert_preparation_for_partitioning + end + + def convert_table_to_first_list_partition(table_name:, partitioning_column:, parent_table_name:, initial_partitioning_value:) + validate_not_in_transaction!(:convert_table_to_first_list_partition) + + Gitlab::Database::Partitioning::ConvertTableToFirstListPartition + .new(migration_context: self, + table_name: table_name, + parent_table_name: parent_table_name, + partitioning_column: partitioning_column, + zero_partition_value: initial_partitioning_value + ).partition + end + + def revert_converting_table_to_first_list_partition(table_name:, partitioning_column:, parent_table_name:, initial_partitioning_value:) + validate_not_in_transaction!(:revert_converting_table_to_first_list_partition) + + Gitlab::Database::Partitioning::ConvertTableToFirstListPartition + .new(migration_context: self, + table_name: table_name, + parent_table_name: parent_table_name, + partitioning_column: partitioning_column, + zero_partition_value: initial_partitioning_value + ).revert_partitioning + end + private def assert_table_is_allowed(table_name) diff --git a/lib/gitlab/database/postgres_constraint.rb b/lib/gitlab/database/postgres_constraint.rb new file mode 100644 index 00000000000..fa590914332 --- /dev/null +++ b/lib/gitlab/database/postgres_constraint.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Gitlab + module Database + # Backed by the postgres_constraints view + class PostgresConstraint < SharedModel + IDENTIFIER_REGEX = /^\w+\.\w+$/.freeze + self.primary_key = :oid + + scope :check_constraints, -> { where(constraint_type: 'c') } + scope :primary_key_constraints, -> { where(constraint_type: 'p') } + scope :unique_constraints, -> { where(constraint_type: 'u') } + scope :primary_or_unique_constraints, -> { where(constraint_type: %w[u p]) } + + scope :including_column, ->(column) { where("? = ANY(column_names)", column) } + scope :not_including_column, ->(column) { where.not("? = ANY(column_names)", column) } + + scope :valid, -> { where(constraint_valid: true) } + + scope :by_table_identifier, ->(identifier) do + unless identifier =~ IDENTIFIER_REGEX + raise ArgumentError, "Table name is not fully qualified with a schema: #{identifier}" + end + + where(table_identifier: identifier) + end + end + end +end diff --git a/lib/gitlab/doorkeeper_secret_storing/pbkdf2_sha512.rb b/lib/gitlab/doorkeeper_secret_storing/pbkdf2_sha512.rb deleted file mode 100644 index 4bfb5f9e64c..00000000000 --- a/lib/gitlab/doorkeeper_secret_storing/pbkdf2_sha512.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module DoorkeeperSecretStoring - class Pbkdf2Sha512 < ::Doorkeeper::SecretStoring::Base - STRETCHES = 20_000 - # An empty salt is used because we need to look tokens up solely by - # their hashed value. Additionally, tokens are always cryptographically - # pseudo-random and unique, therefore salting provides no - # additional security. - SALT = '' - - def self.transform_secret(plain_secret) - return plain_secret unless Feature.enabled?(:hash_oauth_tokens) - - Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.digest(plain_secret, STRETCHES, SALT) - end - - ## - # Determines whether this strategy supports restoring - # secrets from the database. This allows detecting users - # trying to use a non-restorable strategy with +reuse_access_tokens+. - def self.allows_restoring_secrets? - false - end - end - end -end diff --git a/lib/gitlab/doorkeeper_secret_storing/secret/pbkdf2_sha512.rb b/lib/gitlab/doorkeeper_secret_storing/secret/pbkdf2_sha512.rb new file mode 100644 index 00000000000..e0884557496 --- /dev/null +++ b/lib/gitlab/doorkeeper_secret_storing/secret/pbkdf2_sha512.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +module Gitlab + module DoorkeeperSecretStoring + module Secret + class Pbkdf2Sha512 < ::Doorkeeper::SecretStoring::Base + STRETCHES = 20_000 + # An empty salt is used because we need to look tokens up solely by + # their hashed value. Additionally, tokens are always cryptographically + # pseudo-random and unique, therefore salting provides no + # additional security. + SALT = '' + + def self.transform_secret(plain_secret, stored_as_hash = false) + return plain_secret if Feature.disabled?(:hash_oauth_secrets) && !stored_as_hash + + Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.digest(plain_secret, STRETCHES, SALT) + end + + ## + # Determines whether this strategy supports restoring + # secrets from the database. This allows detecting users + # trying to use a non-restorable strategy with +reuse_access_tokens+. + def self.allows_restoring_secrets? + false + end + + ## + # Securely compare the given +input+ value with a +stored+ value + # processed by +transform_secret+. + def self.secret_matches?(input, stored) + stored_as_hash = stored.starts_with?('$pbkdf2-') + transformed_input = transform_secret(input, stored_as_hash) + ActiveSupport::SecurityUtils.secure_compare transformed_input, stored + end + end + end + end +end diff --git a/lib/gitlab/doorkeeper_secret_storing/token/pbkdf2_sha512.rb b/lib/gitlab/doorkeeper_secret_storing/token/pbkdf2_sha512.rb new file mode 100644 index 00000000000..f9e6d4076f3 --- /dev/null +++ b/lib/gitlab/doorkeeper_secret_storing/token/pbkdf2_sha512.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Gitlab + module DoorkeeperSecretStoring + module Token + class Pbkdf2Sha512 < ::Doorkeeper::SecretStoring::Base + STRETCHES = 20_000 + # An empty salt is used because we need to look tokens up solely by + # their hashed value. Additionally, tokens are always cryptographically + # pseudo-random and unique, therefore salting provides no + # additional security. + SALT = '' + + def self.transform_secret(plain_secret) + return plain_secret unless Feature.enabled?(:hash_oauth_tokens) + + Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.digest(plain_secret, STRETCHES, SALT) + end + + ## + # Determines whether this strategy supports restoring + # secrets from the database. This allows detecting users + # trying to use a non-restorable strategy with +reuse_access_tokens+. + def self.allows_restoring_secrets? + false + end + end + end + end +end diff --git a/lib/gitlab/github_import/importer/protected_branches_importer.rb b/lib/gitlab/github_import/importer/protected_branches_importer.rb index be3ac3d17c1..b5be823d5ab 100644 --- a/lib/gitlab/github_import/importer/protected_branches_importer.rb +++ b/lib/gitlab/github_import/importer/protected_branches_importer.rb @@ -11,7 +11,7 @@ module Gitlab def each_object_to_import repo = project.import_source - protected_branches = client.branches(repo).select { |branch| branch.protection.enabled } + protected_branches = client.branches(repo).select { |branch| branch.protection&.enabled } protected_branches.each do |protected_branch| object = client.branch_protection(repo, protected_branch.name) next if object.nil? || already_imported?(object) diff --git a/lib/gitlab/import_export/base/relation_factory.rb b/lib/gitlab/import_export/base/relation_factory.rb index 1cbfcbdb595..bbec473d29d 100644 --- a/lib/gitlab/import_export/base/relation_factory.rb +++ b/lib/gitlab/import_export/base/relation_factory.rb @@ -31,6 +31,8 @@ module Gitlab TOKEN_RESET_MODELS = %i[Project Namespace Group Ci::Trigger Ci::Build Ci::Runner ProjectHook ErrorTracking::ProjectErrorTrackingSetting].freeze + attr_reader :relation_name, :importable + def self.create(*args, **kwargs) new(*args, **kwargs).create end diff --git a/lib/gitlab/import_export/base/relation_object_saver.rb b/lib/gitlab/import_export/base/relation_object_saver.rb index ea989487ebd..3c473449ec0 100644 --- a/lib/gitlab/import_export/base/relation_object_saver.rb +++ b/lib/gitlab/import_export/base/relation_object_saver.rb @@ -58,8 +58,19 @@ module Gitlab records.each_slice(BATCH_SIZE) do |batch| valid_records, invalid_records = batch.partition { |record| record.valid? } - invalid_subrelations << invalid_records relation_object.public_send(relation_name) << valid_records + + # Attempt to save some of the invalid subrelations, as they might be valid after all. + # For example, a merge request `Approval` validates presence of merge_request_id. + # It is not present at a time of calling `#valid?` above, since it's indeed missing. + # However, when saving such subrelation against already persisted merge request + # such validation won't fail (e.g. `merge_request.approvals << Approval.new(user_id: 1)`), + # as we're operating on a merge request that has `id` present. + invalid_records.each do |invalid_record| + relation_object.public_send(relation_name) << invalid_record + + invalid_subrelations << invalid_record unless invalid_record.persisted? + end end end end diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml index 270a5c5c258..33e4823f192 100644 --- a/lib/gitlab/import_export/project/import_export.yml +++ b/lib/gitlab/import_export/project/import_export.yml @@ -1020,6 +1020,35 @@ excluded_attributes: - :merge_request_id - :epic_id - :source_merge_request_id + iteration: + - :id + - :title + - :title_html + - :project_id + - :description_html + - :cached_markdown_version + - :iterations_cadence_id + - :sequence + resource_iteration_events: + - :id + - :issue_id + - :merge_request_id + - :iteration_id + iterations_cadence: + - :id + - :last_run_date + - :duration_in_weeks + - :iterations_in_advance + - :automatic + - :group_id + - :created_at + - :updated_at + - :start_date + - :active + - :roll_over + - :description + - :sequence + methods: notes: - :type @@ -1093,6 +1122,11 @@ ee: - epic_issue: - :epic - :issuable_sla + - iteration: + - :iterations_cadence + - resource_iteration_events: + - iteration: + - :iterations_cadence - protected_branches: - :unprotect_access_levels - protected_environments: @@ -1151,12 +1185,22 @@ ee: - :auto_fix_dependency_scanning - :auto_fix_sast project: - - :requirements_enabled - - :requirements_access_level + - :requirements_enabled + - :requirements_access_level resource_iteration_events: - :user_id - :action - :created_at + iteration: + - :iid + - :created_at + - :updated_at + - :start_date + - :due_date + - :group_id + - :description + iterations_cadence: + - :title preloads: issues: diff --git a/lib/gitlab/import_export/project/object_builder.rb b/lib/gitlab/import_export/project/object_builder.rb index bf60d115a25..50a67a746f8 100644 --- a/lib/gitlab/import_export/project/object_builder.rb +++ b/lib/gitlab/import_export/project/object_builder.rb @@ -21,7 +21,7 @@ module Gitlab end def find - return if epic? && group.nil? + return if group_relation_without_group? return find_diff_commit_user if diff_commit_user? return find_diff_commit if diff_commit? @@ -60,7 +60,7 @@ module Gitlab def prepare_attributes attributes.dup.tap do |atts| - atts.delete('group') unless epic? + atts.delete('group') unless epic? || iteration? if label? atts['type'] = 'ProjectLabel' # Always create project labels @@ -141,6 +141,10 @@ module Gitlab klass == MergeRequestDiffCommit end + def iteration? + klass == Iteration + end + # If an existing group milestone used the IID # claim the IID back and set the group milestone to use one available # This is necessary to fix situations like the following: @@ -157,7 +161,13 @@ module Gitlab milestone.ensure_project_iid! milestone.save! end + + def group_relation_without_group? + (epic? || iteration?) && group.nil? + end end end end end + +Gitlab::ImportExport::Project::ObjectBuilder.prepend_mod diff --git a/lib/gitlab/import_export/project/relation_tree_restorer.rb b/lib/gitlab/import_export/project/relation_tree_restorer.rb index 6e9548f393a..47196db6f8a 100644 --- a/lib/gitlab/import_export/project/relation_tree_restorer.rb +++ b/lib/gitlab/import_export/project/relation_tree_restorer.rb @@ -5,7 +5,7 @@ module Gitlab module Project class RelationTreeRestorer < ImportExport::Group::RelationTreeRestorer # Relations which cannot be saved at project level (and have a group assigned) - GROUP_MODELS = [GroupLabel, Milestone, Epic].freeze + GROUP_MODELS = [GroupLabel, Milestone, Epic, Iteration].freeze private diff --git a/lib/gitlab/usage/metrics/instrumentations/redis_metric.rb b/lib/gitlab/usage/metrics/instrumentations/redis_metric.rb index c9449f10cc2..26d963e2407 100644 --- a/lib/gitlab/usage/metrics/instrumentations/redis_metric.rb +++ b/lib/gitlab/usage/metrics/instrumentations/redis_metric.rb @@ -16,6 +16,8 @@ module Gitlab class RedisMetric < BaseMetric include Gitlab::UsageDataCounters::RedisCounter + USAGE_PREFIX = "USAGE_" + def initialize(time_frame:, options: {}) super @@ -31,6 +33,10 @@ module Gitlab options[:prefix] end + def include_usage_prefix? + options.fetch(:include_usage_prefix, true) + end + def value redis_usage_data do total_count(redis_key) @@ -44,7 +50,9 @@ module Gitlab private def redis_key - "USAGE_#{prefix}_#{metric_event}".upcase + key = "#{prefix}_#{metric_event}".upcase + key.prepend(USAGE_PREFIX) if include_usage_prefix? + key end end end diff --git a/lib/gitlab/usage_data_counters.rb b/lib/gitlab/usage_data_counters.rb index 27376b9d231..fa6bde8c19c 100644 --- a/lib/gitlab/usage_data_counters.rb +++ b/lib/gitlab/usage_data_counters.rb @@ -4,7 +4,6 @@ module Gitlab module UsageDataCounters COUNTERS = [ WikiPageCounter, - WebIdeCounter, NoteCounter, SnippetCounter, SearchCounter, @@ -20,7 +19,8 @@ module Gitlab ].freeze COUNTERS_MIGRATED_TO_INSTRUMENTATION_CLASSES = [ - PackageEventCounter + PackageEventCounter, + WebIdeCounter ].freeze UsageDataCounterError = Class.new(StandardError) diff --git a/lib/gitlab/usage_data_counters/base_counter.rb b/lib/gitlab/usage_data_counters/base_counter.rb index 4ab310a2519..5d2ab5eaf74 100644 --- a/lib/gitlab/usage_data_counters/base_counter.rb +++ b/lib/gitlab/usage_data_counters/base_counter.rb @@ -10,7 +10,9 @@ module Gitlab::UsageDataCounters def redis_key(event) require_known_event(event) - "USAGE_#{prefix}_#{event}".upcase + usage_prefix = Gitlab::Usage::Metrics::Instrumentations::RedisMetric::USAGE_PREFIX + + "#{usage_prefix}#{prefix}_#{event}".upcase end def count(event) diff --git a/lib/sidebars/projects/menus/packages_registries_menu.rb b/lib/sidebars/projects/menus/packages_registries_menu.rb index 30110f1a97f..2ddffe42899 100644 --- a/lib/sidebars/projects/menus/packages_registries_menu.rb +++ b/lib/sidebars/projects/menus/packages_registries_menu.rb @@ -66,7 +66,7 @@ module Sidebars end def harbor_registry__menu_item - if Feature.disabled?(:harbor_registry_integration) || context.project.harbor_integration.nil? + if Feature.disabled?(:harbor_registry_integration, context.project) || context.project.harbor_integration.nil? return ::Sidebars::NilMenuItem.new(item_id: :harbor_registry) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 87ff69fa9eb..9d4acf41ff1 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -39991,6 +39991,9 @@ msgstr "" msgid "The scan has been created." msgstr "" +msgid "The secret is only available when you first create the application." +msgstr "" + msgid "The snippet can be accessed without any authentication." msgstr "" @@ -40726,6 +40729,9 @@ msgstr "" msgid "This is the number of %{billable_users_link_start}billable users%{link_end} on your installation, and this is the minimum number you need to purchase when you renew your license." msgstr "" +msgid "This is the only time the secret is accessible. Copy the secret and store it securely." +msgstr "" + msgid "This is your current session" msgstr "" diff --git a/qa/qa/support/matchers/eventually_matcher.rb b/qa/qa/support/matchers/eventually_matcher.rb index 01d07585f57..3f451f89246 100644 --- a/qa/qa/support/matchers/eventually_matcher.rb +++ b/qa/qa/support/matchers/eventually_matcher.rb @@ -21,6 +21,7 @@ module QA eq be include + match be_truthy be_falsey be_empty diff --git a/spec/controllers/admin/applications_controller_spec.rb b/spec/controllers/admin/applications_controller_spec.rb index 6c423097e70..bf7707f177c 100644 --- a/spec/controllers/admin/applications_controller_spec.rb +++ b/spec/controllers/admin/applications_controller_spec.rb @@ -39,17 +39,43 @@ RSpec.describe Admin::ApplicationsController do end describe 'POST #create' do - it 'creates the application' do - create_params = attributes_for(:application, trusted: true, confidential: false, scopes: ['api']) + context 'with hash_oauth_secrets flag off' do + before do + stub_feature_flags(hash_oauth_secrets: false) + end - expect do - post :create, params: { doorkeeper_application: create_params } - end.to change { Doorkeeper::Application.count }.by(1) + it 'creates the application' do + create_params = attributes_for(:application, trusted: true, confidential: false, scopes: ['api']) + + expect do + post :create, params: { doorkeeper_application: create_params } + end.to change { Doorkeeper::Application.count }.by(1) - application = Doorkeeper::Application.last + application = Doorkeeper::Application.last - expect(response).to redirect_to(admin_application_path(application)) - expect(application).to have_attributes(create_params.except(:uid, :owner_type)) + expect(response).to redirect_to(admin_application_path(application)) + expect(application).to have_attributes(create_params.except(:uid, :owner_type)) + end + end + + context 'with hash_oauth_secrets flag on' do + before do + stub_feature_flags(hash_oauth_secrets: true) + end + + it 'creates the application' do + create_params = attributes_for(:application, trusted: true, confidential: false, scopes: ['api']) + + expect do + post :create, params: { doorkeeper_application: create_params } + end.to change { Doorkeeper::Application.count }.by(1) + + application = Doorkeeper::Application.last + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template :show + expect(application).to have_attributes(create_params.except(:uid, :owner_type)) + end end it 'renders the application form on errors' do @@ -62,17 +88,43 @@ RSpec.describe Admin::ApplicationsController do end context 'when the params are for a confidential application' do - it 'creates a confidential application' do - create_params = attributes_for(:application, confidential: true, scopes: ['read_user']) + context 'with hash_oauth_secrets flag off' do + before do + stub_feature_flags(hash_oauth_secrets: false) + end - expect do - post :create, params: { doorkeeper_application: create_params } - end.to change { Doorkeeper::Application.count }.by(1) + it 'creates a confidential application' do + create_params = attributes_for(:application, confidential: true, scopes: ['read_user']) - application = Doorkeeper::Application.last + expect do + post :create, params: { doorkeeper_application: create_params } + end.to change { Doorkeeper::Application.count }.by(1) - expect(response).to redirect_to(admin_application_path(application)) - expect(application).to have_attributes(create_params.except(:uid, :owner_type)) + application = Doorkeeper::Application.last + + expect(response).to redirect_to(admin_application_path(application)) + expect(application).to have_attributes(create_params.except(:uid, :owner_type)) + end + end + + context 'with hash_oauth_secrets flag on' do + before do + stub_feature_flags(hash_oauth_secrets: true) + end + + it 'creates a confidential application' do + create_params = attributes_for(:application, confidential: true, scopes: ['read_user']) + + expect do + post :create, params: { doorkeeper_application: create_params } + end.to change { Doorkeeper::Application.count }.by(1) + + application = Doorkeeper::Application.last + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template :show + expect(application).to have_attributes(create_params.except(:uid, :owner_type)) + end end end diff --git a/spec/controllers/groups/settings/applications_controller_spec.rb b/spec/controllers/groups/settings/applications_controller_spec.rb index 0804a5536e0..b9457770ed6 100644 --- a/spec/controllers/groups/settings/applications_controller_spec.rb +++ b/spec/controllers/groups/settings/applications_controller_spec.rb @@ -71,17 +71,43 @@ RSpec.describe Groups::Settings::ApplicationsController do group.add_owner(user) end - it 'creates the application' do - create_params = attributes_for(:application, trusted: false, confidential: false, scopes: ['api']) + context 'with hash_oauth_secrets flag on' do + before do + stub_feature_flags(hash_oauth_secrets: true) + end - expect do - post :create, params: { group_id: group, doorkeeper_application: create_params } - end.to change { Doorkeeper::Application.count }.by(1) + it 'creates the application' do + create_params = attributes_for(:application, trusted: false, confidential: false, scopes: ['api']) + + expect do + post :create, params: { group_id: group, doorkeeper_application: create_params } + end.to change { Doorkeeper::Application.count }.by(1) - application = Doorkeeper::Application.last + application = Doorkeeper::Application.last - expect(response).to redirect_to(group_settings_application_path(group, application)) - expect(application).to have_attributes(create_params.except(:uid, :owner_type)) + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template :show + expect(application).to have_attributes(create_params.except(:uid, :owner_type)) + end + end + + context 'with hash_oauth_secrets flag off' do + before do + stub_feature_flags(hash_oauth_secrets: false) + end + + it 'creates the application' do + create_params = attributes_for(:application, trusted: false, confidential: false, scopes: ['api']) + + expect do + post :create, params: { group_id: group, doorkeeper_application: create_params } + end.to change { Doorkeeper::Application.count }.by(1) + + application = Doorkeeper::Application.last + + expect(response).to redirect_to(group_settings_application_path(group, application)) + expect(application).to have_attributes(create_params.except(:uid, :owner_type)) + end end it 'renders the application form on errors' do @@ -94,17 +120,43 @@ RSpec.describe Groups::Settings::ApplicationsController do end context 'when the params are for a confidential application' do - it 'creates a confidential application' do - create_params = attributes_for(:application, confidential: true, scopes: ['read_user']) + context 'with hash_oauth_secrets flag off' do + before do + stub_feature_flags(hash_oauth_secrets: false) + end - expect do - post :create, params: { group_id: group, doorkeeper_application: create_params } - end.to change { Doorkeeper::Application.count }.by(1) + it 'creates a confidential application' do + create_params = attributes_for(:application, confidential: true, scopes: ['read_user']) - application = Doorkeeper::Application.last + expect do + post :create, params: { group_id: group, doorkeeper_application: create_params } + end.to change { Doorkeeper::Application.count }.by(1) - expect(response).to redirect_to(group_settings_application_path(group, application)) - expect(application).to have_attributes(create_params.except(:uid, :owner_type)) + application = Doorkeeper::Application.last + + expect(response).to redirect_to(group_settings_application_path(group, application)) + expect(application).to have_attributes(create_params.except(:uid, :owner_type)) + end + end + + context 'with hash_oauth_secrets flag on' do + before do + stub_feature_flags(hash_oauth_secrets: true) + end + + it 'creates a confidential application' do + create_params = attributes_for(:application, confidential: true, scopes: ['read_user']) + + expect do + post :create, params: { group_id: group, doorkeeper_application: create_params } + end.to change { Doorkeeper::Application.count }.by(1) + + application = Doorkeeper::Application.last + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template :show + expect(application).to have_attributes(create_params.except(:uid, :owner_type)) + end end end diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb index 5bf3b4c48bf..9b16dc9a463 100644 --- a/spec/controllers/oauth/applications_controller_spec.rb +++ b/spec/controllers/oauth/applications_controller_spec.rb @@ -113,11 +113,30 @@ RSpec.describe Oauth::ApplicationsController do subject { post :create, params: oauth_params } - it 'creates an application' do - subject + context 'when hash_oauth_tokens flag set' do + before do + stub_feature_flags(hash_oauth_secrets: true) + end - expect(response).to have_gitlab_http_status(:found) - expect(response).to redirect_to(oauth_application_path(Doorkeeper::Application.last)) + it 'creates an application' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template :show + end + end + + context 'when hash_oauth_tokens flag not set' do + before do + stub_feature_flags(hash_oauth_secrets: false) + end + + it 'creates an application' do + subject + + expect(response).to have_gitlab_http_status(:found) + expect(response).to redirect_to(oauth_application_path(Doorkeeper::Application.last)) + end end it 'redirects back to profile page if OAuth applications are disabled' do diff --git a/spec/fixtures/lib/gitlab/import_export/group/project.json b/spec/fixtures/lib/gitlab/import_export/group/project.json index e8e1e53a86a..671ff92087b 100644 --- a/spec/fixtures/lib/gitlab/import_export/group/project.json +++ b/spec/fixtures/lib/gitlab/import_export/group/project.json @@ -205,6 +205,18 @@ "iid": 1, "group_id": 100 }, + "iteration": { + "created_at": "2022-08-15T12:55:42.607Z", + "updated_at": "2022-08-15T12:56:19.269Z", + "start_date": "2022-08-15", + "due_date": "2022-08-21", + "group_id": 260, + "iid": 5, + "description": "iteration description", + "iterations_cadence": { + "title": "iterations cadence" + } + }, "epic_issue": { "id": 78, "relative_position": 1073740323, @@ -239,7 +251,26 @@ "due_date_sourcing_epic_id": null, "milestone_id": null } - } + }, + "resource_iteration_events": [ + { + "user_id": 1, + "created_at": "2022-08-17T13:04:02.495Z", + "action": "add", + "iteration": { + "created_at": "2022-08-15T12:55:42.607Z", + "updated_at": "2022-08-15T12:56:19.269Z", + "start_date": "2022-08-15", + "due_date": "2022-08-21", + "group_id": 260, + "iid": 5, + "description": "iteration description", + "iterations_cadence": { + "title": "iterations cadence" + } + } + } + ] } ], "snippets": [ diff --git a/spec/fixtures/lib/gitlab/import_export/group/tree/project/issues.ndjson b/spec/fixtures/lib/gitlab/import_export/group/tree/project/issues.ndjson index 4759e97228f..9596986dca0 100644 --- a/spec/fixtures/lib/gitlab/import_export/group/tree/project/issues.ndjson +++ b/spec/fixtures/lib/gitlab/import_export/group/tree/project/issues.ndjson @@ -1,3 +1,3 @@ {"id":1,"title":"Fugiat est minima quae maxime non similique.","assignee_id":null,"project_id":8,"author_id":1,"created_at":"2017-07-07T18:13:01.138Z","updated_at":"2017-08-15T18:37:40.807Z","branch_name":null,"description":"Quam totam fuga numquam in eveniet.","state":"opened","iid":1,"updated_by_id":1,"confidential":false,"due_date":null,"moved_to_id":null,"lock_version":null,"time_estimate":0,"closed_at":null,"last_edited_at":null,"last_edited_by_id":null,"group_milestone_id":null,"milestone":{"id":1,"title":"Project milestone","project_id":8,"description":"Project-level milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"group_id":null},"label_links":[{"id":11,"label_id":6,"target_id":1,"target_type":"Issue","created_at":"2017-08-15T18:37:40.795Z","updated_at":"2017-08-15T18:37:40.795Z","label":{"id":6,"title":"group label","color":"#A8D695","project_id":null,"created_at":"2017-08-15T18:37:19.698Z","updated_at":"2017-08-15T18:37:19.698Z","template":false,"description":"","group_id":5,"type":"GroupLabel","priorities":[]}},{"id":11,"label_id":2,"target_id":1,"target_type":"Issue","created_at":"2017-08-15T18:37:40.795Z","updated_at":"2017-08-15T18:37:40.795Z","label":{"id":6,"title":"A project label","color":"#A8D695","project_id":null,"created_at":"2017-08-15T18:37:19.698Z","updated_at":"2017-08-15T18:37:19.698Z","template":false,"description":"","group_id":5,"type":"ProjectLabel","priorities":[]}}]} {"id":2,"title":"Fugiat est minima quae maxime non similique.","assignee_id":null,"project_id":8,"author_id":1,"created_at":"2017-07-07T18:13:01.138Z","updated_at":"2017-08-15T18:37:40.807Z","branch_name":null,"description":"Quam totam fuga numquam in eveniet.","state":"closed","iid":2,"updated_by_id":1,"confidential":false,"due_date":null,"moved_to_id":null,"lock_version":null,"time_estimate":0,"closed_at":null,"last_edited_at":null,"last_edited_by_id":null,"group_milestone_id":null,"milestone":{"id":2,"title":"A group milestone","description":"Group-level milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"group_id":100},"label_links":[{"id":11,"label_id":2,"target_id":1,"target_type":"Issue","created_at":"2017-08-15T18:37:40.795Z","updated_at":"2017-08-15T18:37:40.795Z","label":{"id":2,"title":"A project label","color":"#A8D695","project_id":null,"created_at":"2017-08-15T18:37:19.698Z","updated_at":"2017-08-15T18:37:19.698Z","template":false,"description":"","group_id":5,"type":"ProjectLabel","priorities":[]}}]} -{"id":3,"title":"Issue with Epic","author_id":1,"project_id":8,"created_at":"2019-12-08T19:41:11.233Z","updated_at":"2019-12-08T19:41:53.194Z","position":0,"branch_name":null,"description":"Donec at nulla vitae sem molestie rutrum ut at sem.","state":"opened","iid":3,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"issue_assignees":[],"notes":[],"milestone":{"id":2,"title":"A group milestone","description":"Group-level milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"group_id":100},"epic_issue":{"id":78,"relative_position":1073740323,"epic":{"id":1,"group_id":5,"author_id":1,"assignee_id":null,"iid":1,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-12-08T19:37:07.098Z","updated_at":"2019-12-08T19:43:11.568Z","title":"An epic","description":null,"start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"milestone_id":null}}} +{"id":3,"title":"Issue with Epic","author_id":1,"project_id":8,"created_at":"2019-12-08T19:41:11.233Z","updated_at":"2019-12-08T19:41:53.194Z","position":0,"branch_name":null,"description":"Donec at nulla vitae sem molestie rutrum ut at sem.","state":"opened","iid":3,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"issue_assignees":[],"notes":[],"milestone":{"id":2,"title":"A group milestone","description":"Group-level milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"group_id":100},"iteration":{"created_at":"2022-08-15T12:55:42.607Z","updated_at":"2022-08-15T12:56:19.269Z","start_date":"2022-08-15","due_date":"2022-08-21","group_id":260,"iid":5,"description":"iteration description","iterations_cadence":{"title":"iterations cadence"}},"epic_issue":{"id":78,"relative_position":1073740323,"epic":{"id":1,"group_id":5,"author_id":1,"assignee_id":null,"iid":1,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-12-08T19:37:07.098Z","updated_at":"2019-12-08T19:43:11.568Z","title":"An epic","description":null,"start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"milestone_id":null}},"resource_iteration_events":[{"user_id":1,"created_at":"2022-08-17T13:04:02.495Z","action":"add","iteration":{"created_at":"2022-08-15T12:55:42.607Z","updated_at":"2022-08-15T12:56:19.269Z","start_date":"2022-08-15","due_date":"2022-08-21","group_id":260,"iid":5,"description":"iteration description","iterations_cadence":{"title":"iterations cadence"}}}]} diff --git a/spec/fixtures/security_reports/deprecated/gl-sast-report.json b/spec/fixtures/security_reports/deprecated/gl-sast-report.json index 2f7e47281e2..c5b0148fe3e 100644 --- a/spec/fixtures/security_reports/deprecated/gl-sast-report.json +++ b/spec/fixtures/security_reports/deprecated/gl-sast-report.json @@ -961,4 +961,4 @@ "url": "https://cwe.mitre.org/data/definitions/120.html", "tool": "flawfinder" } -] +]
\ No newline at end of file diff --git a/spec/fixtures/security_reports/feature-branch/gl-sast-report.json b/spec/fixtures/security_reports/feature-branch/gl-sast-report.json index f93233e0ebb..51761583c70 100644 --- a/spec/fixtures/security_reports/feature-branch/gl-sast-report.json +++ b/spec/fixtures/security_reports/feature-branch/gl-sast-report.json @@ -174,4 +174,4 @@ "start_time": "placeholder-value", "end_time": "placeholder-value" } -} +}
\ No newline at end of file diff --git a/spec/fixtures/security_reports/feature-branch/gl-secret-detection-report.json b/spec/fixtures/security_reports/feature-branch/gl-secret-detection-report.json index 538364f84a2..4862a504cec 100644 --- a/spec/fixtures/security_reports/feature-branch/gl-secret-detection-report.json +++ b/spec/fixtures/security_reports/feature-branch/gl-secret-detection-report.json @@ -2,4 +2,4 @@ "version": "14.1.2", "vulnerabilities": [], "remediations": [] -} +}
\ No newline at end of file diff --git a/spec/fixtures/security_reports/master/gl-common-scanning-report-names.json b/spec/fixtures/security_reports/master/gl-common-scanning-report-names.json index 3cfb3e51ef7..ef2ff7443d3 100644 --- a/spec/fixtures/security_reports/master/gl-common-scanning-report-names.json +++ b/spec/fixtures/security_reports/master/gl-common-scanning-report-names.json @@ -165,4 +165,4 @@ "end_time": "placeholder-value", "status": "success" } -} +}
\ No newline at end of file diff --git a/spec/fixtures/security_reports/master/gl-common-scanning-report-without-top-level-scanner.json b/spec/fixtures/security_reports/master/gl-common-scanning-report-without-top-level-scanner.json index 7f092bf5f68..417dc960aff 100644 --- a/spec/fixtures/security_reports/master/gl-common-scanning-report-without-top-level-scanner.json +++ b/spec/fixtures/security_reports/master/gl-common-scanning-report-without-top-level-scanner.json @@ -1,5 +1,6 @@ { - "vulnerabilities": [{ + "vulnerabilities": [ + { "category": "dependency_scanning", "name": "Vulnerability for remediation testing 1", "message": "This vulnerability should have ONE remediation", @@ -12,24 +13,32 @@ "name": "Gemnasium" }, "location": {}, - "identifiers": [{ - "type": "GitLab", - "name": "Foo vulnerability", - "value": "foo" - }], - "links": [{ - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2137" - }], + "identifiers": [ + { + "type": "GitLab", + "name": "Foo vulnerability", + "value": "foo" + } + ], + "links": [ + { + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2137" + } + ], "details": { "commit": { - "name": [{ - "lang": "en", - "value": "The Commit" - }], - "description": [{ - "lang": "en", - "value": "Commit where the vulnerability was identified" - }], + "name": [ + { + "lang": "en", + "value": "The Commit" + } + ], + "description": [ + { + "lang": "en", + "value": "Commit where the vulnerability was identified" + } + ], "type": "commit", "value": "41df7b7eb3be2b5be2c406c2f6d28cd6631eeb19" } @@ -38,4 +47,4 @@ ], "dependency_files": [], "version": "14.0.2" -} +}
\ No newline at end of file diff --git a/spec/fixtures/security_reports/master/gl-common-scanning-report.json b/spec/fixtures/security_reports/master/gl-common-scanning-report.json index 2065af15267..1295b44d4df 100644 --- a/spec/fixtures/security_reports/master/gl-common-scanning-report.json +++ b/spec/fixtures/security_reports/master/gl-common-scanning-report.json @@ -1,5 +1,6 @@ { - "vulnerabilities": [{ + "vulnerabilities": [ + { "category": "dependency_scanning", "name": "Vulnerability for remediation testing 1", "message": "This vulnerability should have ONE remediation", @@ -12,24 +13,32 @@ "name": "Gemnasium" }, "location": {}, - "identifiers": [{ - "type": "GitLab", - "name": "Foo vulnerability", - "value": "foo" - }], - "links": [{ - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2137" - }], + "identifiers": [ + { + "type": "GitLab", + "name": "Foo vulnerability", + "value": "foo" + } + ], + "links": [ + { + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2137" + } + ], "details": { "commit": { - "name": [{ - "lang": "en", - "value": "The Commit" - }], - "description": [{ - "lang": "en", - "value": "Commit where the vulnerability was identified" - }], + "name": [ + { + "lang": "en", + "value": "The Commit" + } + ], + "description": [ + { + "lang": "en", + "value": "Commit where the vulnerability was identified" + } + ], "type": "commit", "value": "41df7b7eb3be2b5be2c406c2f6d28cd6631eeb19" } @@ -48,24 +57,32 @@ "name": "Gemnasium" }, "location": {}, - "identifiers": [{ - "type": "GitLab", - "name": "Foo vulnerability", - "value": "foo" - }], - "links": [{ - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2138" - }], + "identifiers": [ + { + "type": "GitLab", + "name": "Foo vulnerability", + "value": "foo" + } + ], + "links": [ + { + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2138" + } + ], "details": { "commit": { - "name": [{ - "lang": "en", - "value": "The Commit" - }], - "description": [{ - "lang": "en", - "value": "Commit where the vulnerability was identified" - }], + "name": [ + { + "lang": "en", + "value": "The Commit" + } + ], + "description": [ + { + "lang": "en", + "value": "Commit where the vulnerability was identified" + } + ], "type": "commit", "value": "41df7b7eb3be2b5be2c406c2f6d28cd6631eeb19" } @@ -84,24 +101,32 @@ "name": "Gemnasium" }, "location": {}, - "identifiers": [{ - "type": "GitLab", - "name": "Foo vulnerability", - "value": "foo" - }], - "links": [{ - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2139" - }], + "identifiers": [ + { + "type": "GitLab", + "name": "Foo vulnerability", + "value": "foo" + } + ], + "links": [ + { + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2139" + } + ], "details": { "commit": { - "name": [{ - "lang": "en", - "value": "The Commit" - }], - "description": [{ - "lang": "en", - "value": "Commit where the vulnerability was identified" - }], + "name": [ + { + "lang": "en", + "value": "The Commit" + } + ], + "description": [ + { + "lang": "en", + "value": "Commit where the vulnerability was identified" + } + ], "type": "commit", "value": "41df7b7eb3be2b5be2c406c2f6d28cd6631eeb19" } @@ -120,24 +145,32 @@ "name": "Gemnasium" }, "location": {}, - "identifiers": [{ - "type": "GitLab", - "name": "Foo vulnerability", - "value": "foo" - }], - "links": [{ - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2140" - }], + "identifiers": [ + { + "type": "GitLab", + "name": "Foo vulnerability", + "value": "foo" + } + ], + "links": [ + { + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2140" + } + ], "details": { "commit": { - "name": [{ - "lang": "en", - "value": "The Commit" - }], - "description": [{ - "lang": "en", - "value": "Commit where the vulnerability was identified" - }], + "name": [ + { + "lang": "en", + "value": "The Commit" + } + ], + "description": [ + { + "lang": "en", + "value": "Commit where the vulnerability was identified" + } + ], "type": "commit", "value": "41df7b7eb3be2b5be2c406c2f6d28cd6631eeb19" } @@ -162,30 +195,37 @@ }, "summary": "The Origin header was changed to an invalid value of http://peachapisecurity.com and the response contained an Access-Control-Allow-Origin header which included this invalid Origin, indicating that the CORS configuration on the server is overly permissive.\n\n\n", "request": { - "headers": [{ - "name": "Host", - "value": "127.0.0.1:7777" - }], + "headers": [ + { + "name": "Host", + "value": "127.0.0.1:7777" + } + ], "method": "GET", "url": "http://127.0.0.1:7777/api/users", "body": "" }, "response": { - "headers": [{ - "name": "Server", - "value": "TwistedWeb/20.3.0" - }], + "headers": [ + { + "name": "Server", + "value": "TwistedWeb/20.3.0" + } + ], "reason_phrase": "OK", "status_code": 200, "body": "[{\"user_id\":1,\"user\":\"admin\",\"first\":\"Joe\",\"last\":\"Smith\",\"password\":\"Password!\"}]" }, - "supporting_messages": [{ + "supporting_messages": [ + { "name": "Origional", "request": { - "headers": [{ - "name": "Host", - "value": "127.0.0.1:7777" - }], + "headers": [ + { + "name": "Host", + "value": "127.0.0.1:7777" + } + ], "method": "GET", "url": "http://127.0.0.1:7777/api/users", "body": "" @@ -194,19 +234,23 @@ { "name": "Recorded", "request": { - "headers": [{ - "name": "Host", - "value": "127.0.0.1:7777" - }], + "headers": [ + { + "name": "Host", + "value": "127.0.0.1:7777" + } + ], "method": "GET", "url": "http://127.0.0.1:7777/api/users", "body": "" }, "response": { - "headers": [{ - "name": "Server", - "value": "TwistedWeb/20.3.0" - }], + "headers": [ + { + "name": "Server", + "value": "TwistedWeb/20.3.0" + } + ], "reason_phrase": "OK", "status_code": 200, "body": "[{\"user_id\":1,\"user\":\"admin\",\"first\":\"Joe\",\"last\":\"Smith\",\"password\":\"Password!\"}]" @@ -215,24 +259,32 @@ ] }, "location": {}, - "identifiers": [{ - "type": "GitLab", - "name": "Foo vulnerability", - "value": "foo" - }], - "links": [{ - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1020" - }], + "identifiers": [ + { + "type": "GitLab", + "name": "Foo vulnerability", + "value": "foo" + } + ], + "links": [ + { + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1020" + } + ], "details": { "commit": { - "name": [{ - "lang": "en", - "value": "The Commit" - }], - "description": [{ - "lang": "en", - "value": "Commit where the vulnerability was identified" - }], + "name": [ + { + "lang": "en", + "value": "The Commit" + } + ], + "description": [ + { + "lang": "en", + "value": "Commit where the vulnerability was identified" + } + ], "type": "commit", "value": "41df7b7eb3be2b5be2c406c2f6d28cd6631eeb19" } @@ -258,30 +310,37 @@ }, "summary": "The Origin header was changed to an invalid value of http://peachapisecurity.com and the response contained an Access-Control-Allow-Origin header which included this invalid Origin, indicating that the CORS configuration on the server is overly permissive.\n\n\n", "request": { - "headers": [{ - "name": "Host", - "value": "127.0.0.1:7777" - }], + "headers": [ + { + "name": "Host", + "value": "127.0.0.1:7777" + } + ], "method": "GET", "url": "http://127.0.0.1:7777/api/users", "body": "" }, "response": { - "headers": [{ - "name": "Server", - "value": "TwistedWeb/20.3.0" - }], + "headers": [ + { + "name": "Server", + "value": "TwistedWeb/20.3.0" + } + ], "reason_phrase": "OK", "status_code": 200, "body": "[{\"user_id\":1,\"user\":\"admin\",\"first\":\"Joe\",\"last\":\"Smith\",\"password\":\"Password!\"}]" }, - "supporting_messages": [{ + "supporting_messages": [ + { "name": "Origional", "request": { - "headers": [{ - "name": "Host", - "value": "127.0.0.1:7777" - }], + "headers": [ + { + "name": "Host", + "value": "127.0.0.1:7777" + } + ], "method": "GET", "url": "http://127.0.0.1:7777/api/users", "body": "" @@ -290,19 +349,23 @@ { "name": "Recorded", "request": { - "headers": [{ - "name": "Host", - "value": "127.0.0.1:7777" - }], + "headers": [ + { + "name": "Host", + "value": "127.0.0.1:7777" + } + ], "method": "GET", "url": "http://127.0.0.1:7777/api/users", "body": "" }, "response": { - "headers": [{ - "name": "Server", - "value": "TwistedWeb/20.3.0" - }], + "headers": [ + { + "name": "Server", + "value": "TwistedWeb/20.3.0" + } + ], "reason_phrase": "OK", "status_code": 200, "body": "[{\"user_id\":1,\"user\":\"admin\",\"first\":\"Joe\",\"last\":\"Smith\",\"password\":\"Password!\"}]" @@ -311,15 +374,19 @@ ] }, "location": {}, - "identifiers": [{ - "type": "GitLab", - "name": "Bar vulnerability", - "value": "bar" - }], - "links": [{ - "name": "CVE-1030", - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1030" - }] + "identifiers": [ + { + "type": "GitLab", + "name": "Bar vulnerability", + "value": "bar" + } + ], + "links": [ + { + "name": "CVE-1030", + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1030" + } + ] }, { "category": "dependency_scanning", @@ -338,57 +405,73 @@ "links": [] } ], - "remediations": [{ - "fixes": [{ - "cve": "CVE-2137" - }], + "remediations": [ + { + "fixes": [ + { + "cve": "CVE-2137" + } + ], "summary": "this remediates CVE-2137", "diff": "dG90YWxseSBsZWdpdCBkaWZm" }, { - "fixes": [{ - "cve": "CVE-2138" - }], + "fixes": [ + { + "cve": "CVE-2138" + } + ], "summary": "this remediates CVE-2138", "diff": "dG90YWxseSBsZWdpdCBkaWZm" }, { - "fixes": [{ - "cve": "CVE-2139" - }, { - "cve": "CVE-2140" - }], + "fixes": [ + { + "cve": "CVE-2139" + }, + { + "cve": "CVE-2140" + } + ], "summary": "this remediates CVE-2139 and CVE-2140", "diff": "dG90YWxseSBsZWdpdGltYXRlIGRpZmYsIDEwLzEwIHdvdWxkIGFwcGx5" }, { - "fixes": [{ - "cve": "CVE-1020" - }], + "fixes": [ + { + "cve": "CVE-1020" + } + ], "summary": "", "diff": "" }, { - "fixes": [{ - "cve": "CVE", - "id": "bb2fbeb1b71ea360ce3f86f001d4e84823c3ffe1a1f7d41ba7466b14cfa953d3" - }], + "fixes": [ + { + "cve": "CVE", + "id": "bb2fbeb1b71ea360ce3f86f001d4e84823c3ffe1a1f7d41ba7466b14cfa953d3" + } + ], "summary": "", "diff": "" }, { - "fixes": [{ - "cve": "CVE", - "id": "bb2fbeb1b71ea360ce3f86f001d4e84823c3ffe1a1f7d41ba7466b14cfa953d3" - }], + "fixes": [ + { + "cve": "CVE", + "id": "bb2fbeb1b71ea360ce3f86f001d4e84823c3ffe1a1f7d41ba7466b14cfa953d3" + } + ], "summary": "", "diff": "" }, { - "fixes": [{ - "id": "2134", - "cve": "CVE-1" - }], + "fixes": [ + { + "id": "2134", + "cve": "CVE-1" + } + ], "summary": "", "diff": "" } @@ -419,4 +502,4 @@ "status": "success" }, "version": "14.0.2" -} +}
\ No newline at end of file diff --git a/spec/fixtures/security_reports/master/gl-sast-missing-scanner.json b/spec/fixtures/security_reports/master/gl-sast-missing-scanner.json index ab3ee348263..fcfd9b831f4 100644 --- a/spec/fixtures/security_reports/master/gl-sast-missing-scanner.json +++ b/spec/fixtures/security_reports/master/gl-sast-missing-scanner.json @@ -799,4 +799,4 @@ "url": "https://cwe.mitre.org/data/definitions/120.html" } ] -} +}
\ No newline at end of file diff --git a/spec/fixtures/security_reports/master/gl-sast-report-bandit.json b/spec/fixtures/security_reports/master/gl-sast-report-bandit.json index a80833354ed..d0346479b85 100644 --- a/spec/fixtures/security_reports/master/gl-sast-report-bandit.json +++ b/spec/fixtures/security_reports/master/gl-sast-report-bandit.json @@ -40,4 +40,4 @@ "end_time": "2022-03-11T00:21:50", "status": "success" } -} +}
\ No newline at end of file diff --git a/spec/fixtures/security_reports/master/gl-sast-report-gosec.json b/spec/fixtures/security_reports/master/gl-sast-report-gosec.json index 42986ea1045..4c385326c8c 100644 --- a/spec/fixtures/security_reports/master/gl-sast-report-gosec.json +++ b/spec/fixtures/security_reports/master/gl-sast-report-gosec.json @@ -65,4 +65,4 @@ "end_time": "2022-03-15T20:33:17", "status": "success" } -} +}
\ No newline at end of file diff --git a/spec/fixtures/security_reports/master/gl-sast-report-minimal.json b/spec/fixtures/security_reports/master/gl-sast-report-minimal.json index 60a67453c9b..5e9273d43b1 100644 --- a/spec/fixtures/security_reports/master/gl-sast-report-minimal.json +++ b/spec/fixtures/security_reports/master/gl-sast-report-minimal.json @@ -65,4 +65,4 @@ "start_time": "placeholder-value", "end_time": "placeholder-value" } -} +}
\ No newline at end of file diff --git a/spec/fixtures/security_reports/master/gl-sast-report-semgrep-for-bandit.json b/spec/fixtures/security_reports/master/gl-sast-report-semgrep-for-bandit.json index 2a60a75366e..037b9fb8d3e 100644 --- a/spec/fixtures/security_reports/master/gl-sast-report-semgrep-for-bandit.json +++ b/spec/fixtures/security_reports/master/gl-sast-report-semgrep-for-bandit.json @@ -68,4 +68,4 @@ "end_time": "2022-03-11T18:48:22", "status": "success" } -} +}
\ No newline at end of file diff --git a/spec/fixtures/security_reports/master/gl-sast-report-semgrep-for-gosec.json b/spec/fixtures/security_reports/master/gl-sast-report-semgrep-for-gosec.json index 3d8c65d5823..f01d26a69c9 100644 --- a/spec/fixtures/security_reports/master/gl-sast-report-semgrep-for-gosec.json +++ b/spec/fixtures/security_reports/master/gl-sast-report-semgrep-for-gosec.json @@ -67,4 +67,4 @@ "end_time": "2022-03-15T20:37:05", "status": "success" } -} +}
\ No newline at end of file diff --git a/spec/fixtures/security_reports/master/gl-sast-report.json b/spec/fixtures/security_reports/master/gl-sast-report.json index 63504e6fccc..1aa8db1a65f 100644 --- a/spec/fixtures/security_reports/master/gl-sast-report.json +++ b/spec/fixtures/security_reports/master/gl-sast-report.json @@ -197,4 +197,4 @@ "start_time": "placeholder-value", "end_time": "placeholder-value" } -} +}
\ No newline at end of file diff --git a/spec/fixtures/security_reports/master/gl-secret-detection-report.json b/spec/fixtures/security_reports/master/gl-secret-detection-report.json index 9b0b2a19beb..21d4f3f1798 100644 --- a/spec/fixtures/security_reports/master/gl-secret-detection-report.json +++ b/spec/fixtures/security_reports/master/gl-secret-detection-report.json @@ -30,4 +30,4 @@ } ], "remediations": [] -} +}
\ No newline at end of file diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js index 653143a6243..01317eb5dba 100644 --- a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js @@ -30,7 +30,7 @@ describe('SignInOauthButton', () => { let store; const mockOauthMetadata = { oauth_authorize_url: 'https://gitlab.com/mockOauth', - oauth_token_url: 'https://gitlab.com/mockOauthToken', + oauth_token_path: 'https://gitlab.com/mockOauthToken', oauth_token_payload: { client_id: '543678901', }, @@ -197,7 +197,7 @@ describe('SignInOauthButton', () => { }); it('executes POST request to Oauth token endpoint', () => { - expect(fetchOAuthToken).toHaveBeenCalledWith(mockOauthMetadata.oauth_token_url, { + expect(fetchOAuthToken).toHaveBeenCalledWith(mockOauthMetadata.oauth_token_path, { code: '1234', code_verifier: 'mock-verifier', client_id: mockOauthMetadata.oauth_token_payload.client_id, diff --git a/spec/helpers/jira_connect_helper_spec.rb b/spec/helpers/jira_connect_helper_spec.rb index 4d2fc3d9ee6..97e37023c2d 100644 --- a/spec/helpers/jira_connect_helper_spec.rb +++ b/spec/helpers/jira_connect_helper_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe JiraConnectHelper do describe '#jira_connect_app_data' do + let_it_be(:installation) { create(:jira_connect_installation) } let_it_be(:subscription) { create(:jira_connect_subscription) } let(:user) { create(:user) } @@ -13,11 +14,12 @@ RSpec.describe JiraConnectHelper do stub_application_setting(jira_connect_application_key: client_id) end - subject { helper.jira_connect_app_data([subscription]) } + subject { helper.jira_connect_app_data([subscription], installation) } context 'user is not logged in' do before do allow(view).to receive(:current_user).and_return(nil) + allow(Gitlab).to receive_message_chain('config.gitlab.url') { 'http://test.host' } end it 'includes Jira Connect app attributes' do @@ -36,14 +38,14 @@ RSpec.describe JiraConnectHelper do end context 'with oauth_metadata' do - let(:oauth_metadata) { helper.jira_connect_app_data([subscription])[:oauth_metadata] } + let(:oauth_metadata) { helper.jira_connect_app_data([subscription], installation)[:oauth_metadata] } subject(:parsed_oauth_metadata) { Gitlab::Json.parse(oauth_metadata).deep_symbolize_keys } it 'assigns oauth_metadata' do expect(parsed_oauth_metadata).to include( oauth_authorize_url: start_with('http://test.host/oauth/authorize?'), - oauth_token_url: 'http://test.host/oauth/token', + oauth_token_path: '/oauth/token', state: %r/[a-z0-9.]{32}/, oauth_token_payload: hash_including( grant_type: 'authorization_code', @@ -74,6 +76,30 @@ RSpec.describe JiraConnectHelper do expect(oauth_metadata).to be_nil end end + + context 'with self-managed instance' do + let_it_be(:installation) { create(:jira_connect_installation, instance_url: 'https://gitlab.example.com') } + + it 'points urls to the self-managed instance' do + expect(parsed_oauth_metadata).to include( + oauth_authorize_url: start_with('https://gitlab.example.com/oauth/authorize?'), + oauth_token_path: '/oauth/token' + ) + end + + context 'and jira_connect_oauth_self_managed feature is disabled' do + before do + stub_feature_flags(jira_connect_oauth_self_managed: false) + end + + it 'does not point urls to the self-managed instance' do + expect(parsed_oauth_metadata).not_to include( + oauth_authorize_url: start_with('https://gitlab.example.com/oauth/authorize?'), + oauth_token_path: 'https://gitlab.example.com/oauth/token' + ) + end + end + end end it 'passes group as "skip_groups" param' do diff --git a/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb index 3f02356b41e..e780cde4ae2 100644 --- a/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb +++ b/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe BulkImports::Projects::Pipelines::MergeRequestsPipeline do let_it_be(:user) { create(:user) } + let_it_be(:another_user) { create(:user) } let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, :repository, group: group) } let_it_be(:bulk_import) { create(:bulk_import, user: user) } @@ -85,6 +86,9 @@ RSpec.describe BulkImports::Projects::Pipelines::MergeRequestsPipeline do describe '#run' do before do group.add_owner(user) + group.add_maintainer(another_user) + + ::BulkImports::UsersMapper.new(context: context).cache_source_user_id(42, another_user.id) allow_next_instance_of(BulkImports::Common::Extractors::NdjsonExtractor) do |extractor| allow(extractor).to receive(:remove_tmp_dir) @@ -293,5 +297,52 @@ RSpec.describe BulkImports::Projects::Pipelines::MergeRequestsPipeline do expect(imported_mr.milestone.title).to eq(attributes.dig('milestone', 'title')) end end + + context 'user assignments' do + let(:attributes) do + { + key => [ + { + 'user_id' => 22, + 'created_at' => '2020-01-07T11:21:21.235Z' + }, + { + 'user_id' => 42, + 'created_at' => '2020-01-08T12:21:21.235Z' + } + ] + } + end + + context 'assignees' do + let(:key) { 'merge_request_assignees' } + + it 'imports mr assignees' do + assignees = imported_mr.merge_request_assignees + + expect(assignees.pluck(:user_id)).to contain_exactly(user.id, another_user.id) + end + end + + context 'approvals' do + let(:key) { 'approvals' } + + it 'imports mr approvals' do + approvals = imported_mr.approvals + + expect(approvals.pluck(:user_id)).to contain_exactly(user.id, another_user.id) + end + end + + context 'reviewers' do + let(:key) { 'merge_request_reviewers' } + + it 'imports mr reviewers' do + reviewers = imported_mr.merge_request_reviewers + + expect(reviewers.pluck(:user_id)).to contain_exactly(user.id, another_user.id) + end + end + end end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/assign_partition_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/assign_partition_spec.rb index 7215c65d41b..15df5b2f68c 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/assign_partition_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/assign_partition_spec.rb @@ -24,5 +24,24 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::AssignPartition do it 'assigns partition_id to pipeline' do expect { subject }.to change(pipeline, :partition_id).to(current_partition_id) end + + context 'with parent-child pipelines' do + let(:bridge) do + instance_double(Ci::Bridge, + triggers_child_pipeline?: true, + parent_pipeline: instance_double(Ci::Pipeline, partition_id: 125)) + end + + let(:command) do + Gitlab::Ci::Pipeline::Chain::Command.new( + project: project, + current_user: user, + bridge: bridge) + end + + it 'assigns partition_id to pipeline' do + expect { subject }.to change(pipeline, :partition_id).to(125) + end + end end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb index de43e759193..6e8b6e40928 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb @@ -302,13 +302,13 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do context 'when bridge is present' do context 'when bridge triggers a child pipeline' do - let(:bridge) { double(:bridge, triggers_child_pipeline?: true) } + let(:bridge) { instance_double(Ci::Bridge, triggers_child_pipeline?: true) } it { is_expected.to be_truthy } end context 'when bridge triggers a multi-project pipeline' do - let(:bridge) { double(:bridge, triggers_child_pipeline?: false) } + let(:bridge) { instance_double(Ci::Bridge, triggers_child_pipeline?: false) } it { is_expected.to be_falsey } end @@ -321,6 +321,38 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do end end + describe '#parent_pipeline_partition_id' do + let(:command) { described_class.new(bridge: bridge) } + + subject { command.parent_pipeline_partition_id } + + context 'when bridge is present' do + context 'when bridge triggers a child pipeline' do + let(:pipeline) { instance_double(Ci::Pipeline, partition_id: 123) } + + let(:bridge) do + instance_double(Ci::Bridge, + triggers_child_pipeline?: true, + parent_pipeline: pipeline) + end + + it { is_expected.to eq(123) } + end + + context 'when bridge triggers a multi-project pipeline' do + let(:bridge) { instance_double(Ci::Bridge, triggers_child_pipeline?: false) } + + it { is_expected.to be_nil } + end + end + + context 'when bridge is not present' do + let(:bridge) { nil } + + it { is_expected.to be_nil } + end + end + describe '#increment_pipeline_failure_reason_counter' do let(:command) { described_class.new } let(:reason) { :size_limit_exceeded } @@ -345,7 +377,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do describe '#observe_step_duration' do context 'when ci_pipeline_creation_step_duration_tracking is enabled' do it 'adds the duration to the step duration histogram' do - histogram = double(:histogram) + histogram = instance_double(Prometheus::Client::Histogram) duration = 1.hour expect(::Gitlab::Ci::Pipeline::Metrics).to receive(:pipeline_creation_step_duration_histogram) diff --git a/spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb b/spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb new file mode 100644 index 00000000000..af7d751a404 --- /dev/null +++ b/spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb @@ -0,0 +1,246 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::Partitioning::ConvertTableToFirstListPartition do + include Gitlab::Database::DynamicModelHelpers + include Database::TableSchemaHelpers + + let(:migration_context) { Gitlab::Database::Migration[2.0].new } + + let(:connection) { migration_context.connection } + let(:table_name) { '_test_table_to_partition' } + let(:table_identifier) { "#{connection.current_schema}.#{table_name}" } + let(:partitioning_column) { :partition_number } + let(:partitioning_default) { 1 } + let(:referenced_table_name) { '_test_referenced_table' } + let(:other_referenced_table_name) { '_test_other_referenced_table' } + let(:parent_table_name) { "#{table_name}_parent" } + + let(:model) { define_batchable_model(table_name, connection: connection) } + + let(:parent_model) { define_batchable_model(parent_table_name, connection: connection) } + + let(:converter) do + described_class.new( + migration_context: migration_context, + table_name: table_name, + partitioning_column: partitioning_column, + parent_table_name: parent_table_name, + zero_partition_value: partitioning_default + ) + end + + before do + # Suppress printing migration progress + allow(migration_context).to receive(:puts) + allow(migration_context.connection).to receive(:transaction_open?).and_return(false) + + connection.execute(<<~SQL) + create table #{referenced_table_name} ( + id bigserial primary key not null + ) + SQL + + connection.execute(<<~SQL) + create table #{other_referenced_table_name} ( + id bigserial primary key not null + ) + SQL + + connection.execute(<<~SQL) + insert into #{referenced_table_name} default values; + insert into #{other_referenced_table_name} default values; + SQL + + connection.execute(<<~SQL) + create table #{table_name} ( + id bigserial not null, + #{partitioning_column} bigint not null default #{partitioning_default}, + referenced_id bigint not null references #{referenced_table_name} (id) on delete cascade, + other_referenced_id bigint not null references #{other_referenced_table_name} (id) on delete set null, + primary key (id, #{partitioning_column}) + ) + SQL + + connection.execute(<<~SQL) + insert into #{table_name} (referenced_id, other_referenced_id) + select #{referenced_table_name}.id, #{other_referenced_table_name}.id + from #{referenced_table_name}, #{other_referenced_table_name}; + SQL + end + + describe "#prepare_for_partitioning" do + subject(:prepare) { converter.prepare_for_partitioning } + + it 'adds a check constraint' do + expect { prepare }.to change { + Gitlab::Database::PostgresConstraint + .check_constraints + .by_table_identifier(table_identifier) + .count + }.from(0).to(1) + end + end + + describe '#revert_prepare_for_partitioning' do + before do + converter.prepare_for_partitioning + end + + subject(:revert_prepare) { converter.revert_preparation_for_partitioning } + + it 'removes a check constraint' do + expect { revert_prepare }.to change { + Gitlab::Database::PostgresConstraint + .check_constraints + .by_table_identifier("#{connection.current_schema}.#{table_name}") + .count + }.from(1).to(0) + end + end + + describe "#convert_to_zero_partition" do + subject(:partition) { converter.partition } + + before do + converter.prepare_for_partitioning + end + + context 'when the primary key is incorrect' do + before do + connection.execute(<<~SQL) + alter table #{table_name} drop constraint #{table_name}_pkey; + alter table #{table_name} add constraint #{table_name}_pkey PRIMARY KEY (id); + SQL + end + + it 'throws a reasonable error message' do + expect { partition }.to raise_error(described_class::UnableToPartition, /#{partitioning_column}/) + end + end + + context 'when there is not a supporting check constraint' do + before do + connection.execute(<<~SQL) + alter table #{table_name} drop constraint partitioning_constraint; + SQL + end + + it 'throws a reasonable error message' do + expect { partition }.to raise_error(described_class::UnableToPartition, /constraint /) + end + end + + it 'migrates the table to a partitioned table' do + fks_before = migration_context.foreign_keys(table_name) + + partition + + expect(Gitlab::Database::PostgresPartition.for_parent_table(parent_table_name).count).to eq(1) + expect(migration_context.foreign_keys(parent_table_name).map(&:options)).to match_array(fks_before.map(&:options)) + + connection.execute(<<~SQL) + insert into #{table_name} (referenced_id, other_referenced_id) select #{referenced_table_name}.id, #{other_referenced_table_name}.id from #{referenced_table_name}, #{other_referenced_table_name}; + SQL + + # Create a second partition + connection.execute(<<~SQL) + create table #{table_name}2 partition of #{parent_table_name} FOR VALUES IN (2) + SQL + + parent_model.create!(partitioning_column => 2, :referenced_id => 1, :other_referenced_id => 1) + expect(parent_model.pluck(:id)).to match_array([1, 2, 3]) + end + + context 'when an error occurs during the conversion' do + def fail_first_time + # We can't directly use a boolean here, as we need something that will be passed by-reference to the proc + fault_status = { faulted: false } + proc do |m, *args, **kwargs| + next m.call(*args, **kwargs) if fault_status[:faulted] + + fault_status[:faulted] = true + raise 'fault!' + end + end + + def fail_sql_matching(regex) + proc do + allow(migration_context.connection).to receive(:execute).and_call_original + allow(migration_context.connection).to receive(:execute).with(regex).and_wrap_original(&fail_first_time) + end + end + + def fail_adding_fk(from_table, to_table) + proc do + allow(migration_context.connection).to receive(:add_foreign_key).and_call_original + expect(migration_context.connection).to receive(:add_foreign_key).with(from_table, to_table, any_args) + .and_wrap_original(&fail_first_time) + end + end + + where(:case_name, :fault) do + [ + ["creating parent table", lazy { fail_sql_matching(/CREATE/i) }], + ["adding the first foreign key", lazy { fail_adding_fk(parent_table_name, referenced_table_name) }], + ["adding the second foreign key", lazy { fail_adding_fk(parent_table_name, other_referenced_table_name) }], + ["attaching table", lazy { fail_sql_matching(/ATTACH/i) }] + ] + end + + before do + # Set up the fault that we'd like to inject + fault.call + end + + with_them do + it 'recovers from a fault', :aggregate_failures do + expect { converter.partition }.to raise_error(/fault/) + expect(Gitlab::Database::PostgresPartition.for_parent_table(parent_table_name).count).to eq(0) + + expect { converter.partition }.not_to raise_error + expect(Gitlab::Database::PostgresPartition.for_parent_table(parent_table_name).count).to eq(1) + end + end + end + end + + describe '#revert_conversion_to_zero_partition' do + before do + converter.prepare_for_partitioning + converter.partition + end + + subject(:revert_conversion) { converter.revert_partitioning } + + it 'detaches the partition' do + expect { revert_conversion }.to change { + Gitlab::Database::PostgresPartition + .for_parent_table(parent_table_name).count + }.from(1).to(0) + end + + it 'does not drop the child partition' do + expect { revert_conversion }.not_to change { table_oid(table_name) } + end + + it 'removes the parent table' do + expect { revert_conversion }.to change { table_oid(parent_table_name).present? }.from(true).to(false) + end + + it 're-adds the check constraint' do + expect { revert_conversion }.to change { + Gitlab::Database::PostgresConstraint + .check_constraints + .by_table_identifier(table_identifier) + .count + }.by(1) + end + + it 'moves sequences back to the original table' do + expect { revert_conversion }.to change { converter.send(:sequences_owned_by, table_name).count }.from(0) + .and change { converter.send(:sequences_owned_by, parent_table_name).count }.to(0) + end + end +end diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb index 1026b4370a5..8bb9ad2737a 100644 --- a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb @@ -41,6 +41,76 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe allow(migration).to receive(:assert_table_is_allowed) end + context 'list partitioning conversion helpers' do + shared_examples_for 'delegates to ConvertTableToFirstListPartition' do + it 'throws an error if in a transaction' do + allow(migration).to receive(:transaction_open?).and_return(true) + expect { migrate }.to raise_error(/cannot be run inside a transaction/) + end + + it 'delegates to a method on ConvertTableToFirstListPartition' do + expect_next_instance_of(Gitlab::Database::Partitioning::ConvertTableToFirstListPartition, + migration_context: migration, + table_name: source_table, + parent_table_name: partitioned_table, + partitioning_column: partition_column, + zero_partition_value: min_date) do |converter| + expect(converter).to receive(expected_method) + end + + migrate + end + end + + describe '#convert_table_to_first_list_partition' do + it_behaves_like 'delegates to ConvertTableToFirstListPartition' do + let(:expected_method) { :partition } + let(:migrate) do + migration.convert_table_to_first_list_partition(table_name: source_table, + partitioning_column: partition_column, + parent_table_name: partitioned_table, + initial_partitioning_value: min_date) + end + end + end + + describe '#revert_converting_table_to_first_list_partition' do + it_behaves_like 'delegates to ConvertTableToFirstListPartition' do + let(:expected_method) { :revert_partitioning } + let(:migrate) do + migration.revert_converting_table_to_first_list_partition(table_name: source_table, + partitioning_column: partition_column, + parent_table_name: partitioned_table, + initial_partitioning_value: min_date) + end + end + end + + describe '#prepare_constraint_for_list_partitioning' do + it_behaves_like 'delegates to ConvertTableToFirstListPartition' do + let(:expected_method) { :prepare_for_partitioning } + let(:migrate) do + migration.prepare_constraint_for_list_partitioning(table_name: source_table, + partitioning_column: partition_column, + parent_table_name: partitioned_table, + initial_partitioning_value: min_date) + end + end + end + + describe '#revert_preparing_constraint_for_list_partitioning' do + it_behaves_like 'delegates to ConvertTableToFirstListPartition' do + let(:expected_method) { :revert_preparation_for_partitioning } + let(:migrate) do + migration.revert_preparing_constraint_for_list_partitioning(table_name: source_table, + partitioning_column: partition_column, + parent_table_name: partitioned_table, + initial_partitioning_value: min_date) + end + end + end + end + describe '#partition_table_by_date' do let(:partition_column) { 'created_at' } let(:old_primary_key) { 'id' } diff --git a/spec/lib/gitlab/database/postgres_constraint_spec.rb b/spec/lib/gitlab/database/postgres_constraint_spec.rb new file mode 100644 index 00000000000..75084a69115 --- /dev/null +++ b/spec/lib/gitlab/database/postgres_constraint_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::PostgresConstraint, type: :model do + # PostgresConstraint does not `behaves_like 'a postgres model'` because it does not correspond 1-1 with a single entry + # in pg_class + let(:schema) { ActiveRecord::Base.connection.current_schema } + let(:table_name) { '_test_table' } + let(:table_identifier) { "#{schema}.#{table_name}" } + let(:referenced_name) { '_test_referenced' } + let(:check_constraint_a_positive) { 'check_constraint_a_positive' } + let(:check_constraint_a_gt_b) { 'check_constraint_a_gt_b' } + let(:invalid_constraint_a) { 'check_constraint_b_positive_invalid' } + let(:unique_constraint_a) { "#{table_name}_a_key" } + + before do + ActiveRecord::Base.connection.execute(<<~SQL) + create table #{referenced_name} ( + id bigserial primary key not null + ); + + create table #{table_name} ( + id bigserial not null, + referenced_id bigint not null references #{referenced_name}(id), + a integer unique, + b integer, + primary key (id, referenced_id), + constraint #{check_constraint_a_positive} check (a > 0), + constraint #{check_constraint_a_gt_b} check (a > b) + ); + + alter table #{table_name} add constraint #{invalid_constraint_a} CHECK (a > 1) NOT VALID; + SQL + end + + describe '#by_table_identifier' do + subject(:constraints_for_table) { described_class.by_table_identifier(table_identifier) } + + it 'includes all constraints on the table' do + all_constraints_for_table = described_class.all.to_a.select { |c| c.table_identifier == table_identifier } + expect(all_constraints_for_table.map(&:oid)).to match_array(constraints_for_table.pluck(:oid)) + end + + it 'throws an error if the format is incorrect' do + expect { described_class.by_table_identifier('not-an-identifier') }.to raise_error(ArgumentError) + end + end + + describe '#check_constraints' do + subject(:check_constraints) { described_class.check_constraints.by_table_identifier(table_identifier) } + + it 'finds check constraints for the table' do + expect(check_constraints.map(&:name)).to contain_exactly(check_constraint_a_positive, + check_constraint_a_gt_b, + invalid_constraint_a) + end + + it 'includes columns for the check constraints', :aggregate_failures do + expect(check_constraints.find_by(name: check_constraint_a_positive).column_names).to contain_exactly('a') + expect(check_constraints.find_by(name: check_constraint_a_gt_b).column_names).to contain_exactly('a', 'b') + end + end + + describe "#valid" do + subject(:valid_constraint_names) { described_class.valid.by_table_identifier(table_identifier).pluck(:name) } + + let(:all_constraint_names) { described_class.by_table_identifier(table_identifier).pluck(:name) } + + it 'excludes invalid constraints' do + expect(valid_constraint_names).not_to include(invalid_constraint_a) + expect(valid_constraint_names).to match_array(all_constraint_names - [invalid_constraint_a]) + end + end + + describe '#primary_key_constraints' do + subject(:pk_constraints) { described_class.primary_key_constraints.by_table_identifier(table_identifier) } + + it 'finds the primary key constraint for the table' do + expect(pk_constraints.count).to eq(1) + expect(pk_constraints.first.constraint_type).to eq('p') + end + + it 'finds the columns in the primary key constraint' do + constraint = pk_constraints.first + expect(constraint.column_names).to contain_exactly('id', 'referenced_id') + end + end + + describe '#unique_constraints' do + subject(:unique_constraints) { described_class.unique_constraints.by_table_identifier(table_identifier) } + + it 'finds the unique constraints for the table' do + expect(unique_constraints.pluck(:name)).to contain_exactly(unique_constraint_a) + end + end + + describe '#primary_or_unique_constraints' do + subject(:pk_or_unique_constraints) do + described_class.primary_or_unique_constraints.by_table_identifier(table_identifier) + end + + it 'finds primary and unique constraints' do + expect(pk_or_unique_constraints.pluck(:name)).to contain_exactly("#{table_name}_pkey", unique_constraint_a) + end + end + + describe '#including_column' do + it 'only matches constraints on the given column' do + constraints_on_a = described_class.by_table_identifier(table_identifier).including_column('a').map(&:name) + expect(constraints_on_a).to contain_exactly(check_constraint_a_positive, check_constraint_a_gt_b, + unique_constraint_a, invalid_constraint_a) + end + end + + describe '#not_including_column' do + it 'only matches constraints not including the given column' do + constraints_not_on_a = described_class.by_table_identifier(table_identifier).not_including_column('a').map(&:name) + + expect(constraints_not_on_a).to contain_exactly("#{table_name}_pkey", "#{table_name}_referenced_id_fkey") + end + end +end diff --git a/spec/lib/gitlab/doorkeeper_secret_storing/secret/pbkdf2_sha512_spec.rb b/spec/lib/gitlab/doorkeeper_secret_storing/secret/pbkdf2_sha512_spec.rb new file mode 100644 index 00000000000..df17d92bb0c --- /dev/null +++ b/spec/lib/gitlab/doorkeeper_secret_storing/secret/pbkdf2_sha512_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::DoorkeeperSecretStoring::Secret::Pbkdf2Sha512 do + describe '.transform_secret' do + let(:plaintext_secret) { 'CzOBzBfU9F-HvsqfTaTXF4ivuuxYZuv3BoAK4pnvmyw' } + + it 'generates a PBKDF2+SHA512 hashed value in the correct format' do + expect(described_class.transform_secret(plaintext_secret)) + .to eq("$pbkdf2-sha512$20000$$.c0G5XJVEew1TyeJk5TrkvB0VyOaTmDzPrsdNRED9vVeZlSyuG3G90F0ow23zUCiWKAVwmNnR/ceh.nJG3MdpQ") # rubocop:disable Layout/LineLength + end + + context 'when hash_oauth_secrets is disabled' do + before do + stub_feature_flags(hash_oauth_secrets: false) + end + + it 'returns a plaintext secret' do + expect(described_class.transform_secret(plaintext_secret)).to eq(plaintext_secret) + end + end + end + + describe 'STRETCHES' do + it 'is 20_000' do + expect(described_class::STRETCHES).to eq(20_000) + end + end + + describe 'SALT' do + it 'is empty' do + expect(described_class::SALT).to be_empty + end + end + + describe '.secret_matches?' do + it "match by hashing the input if the stored value is hashed" do + stub_feature_flags(hash_oauth_secrets: false) + plain_secret = 'plain_secret' + stored_value = '$pbkdf2-sha512$20000$$/BwQRdwSpL16xkQhstavh7nvA5avCP7.4n9LLKe9AupgJDeA7M5xOAvG3N3E5XbRyGWWBbbr.BsojPVWzd1Sqg' # rubocop:disable Layout/LineLength + expect(described_class.secret_matches?(plain_secret, stored_value)).to be true + end + end +end diff --git a/spec/lib/gitlab/doorkeeper_secret_storing/pbkdf2_sha512_spec.rb b/spec/lib/gitlab/doorkeeper_secret_storing/token/pbkdf2_sha512_spec.rb index e953733c997..c73744cd481 100644 --- a/spec/lib/gitlab/doorkeeper_secret_storing/pbkdf2_sha512_spec.rb +++ b/spec/lib/gitlab/doorkeeper_secret_storing/token/pbkdf2_sha512_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::DoorkeeperSecretStoring::Pbkdf2Sha512 do +RSpec.describe Gitlab::DoorkeeperSecretStoring::Token::Pbkdf2Sha512 do describe '.transform_secret' do let(:plaintext_token) { 'CzOBzBfU9F-HvsqfTaTXF4ivuuxYZuv3BoAK4pnvmyw' } diff --git a/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb b/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb index 9266b1b0585..4e9208be985 100644 --- a/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb @@ -15,7 +15,8 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchesImporter do [ branch.new(name: 'main', protection: protection.new(enabled: false)), - branch.new(name: 'staging', protection: protection.new(enabled: true)) + branch.new(name: 'staging', protection: protection.new(enabled: true)), + branch.new(name: 'development', protection: nil) # when user has no admin right for this repo ] end @@ -154,12 +155,14 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchesImporter do let(:protection_struct) { Struct.new(:enabled, keyword_init: true) } let(:protected_branch) { branch_struct.new(name: 'main', protection: protection_struct.new(enabled: true)) } let(:unprotected_branch) { branch_struct.new(name: 'staging', protection: protection_struct.new(enabled: false)) } + # when user has no admin rights on repo + let(:unknown_protection_branch) { branch_struct.new(name: 'development', protection: nil) } let(:page_counter) { instance_double(Gitlab::GithubImport::PageCounter) } before do allow(client).to receive(:branches).with(project.import_source) - .and_return([protected_branch, unprotected_branch]) + .and_return([protected_branch, unprotected_branch, unknown_protection_branch]) allow(client).to receive(:branch_protection) .with(project.import_source, protected_branch.name).once .and_return(github_protection_rule) diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index e429fc93d65..b9c9e02625a 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -831,3 +831,17 @@ resource_state_events: - merge_request - source_merge_request - epic +iteration: + - group + - iterations_cadence + - issues + - labels + - merge_requests +resource_iteration_events: + - user + - issue + - merge_request + - iteration +iterations_cadence: + - group + - iterations diff --git a/spec/lib/gitlab/import_export/base/relation_object_saver_spec.rb b/spec/lib/gitlab/import_export/base/relation_object_saver_spec.rb index 9f1b15aa049..4ee825c71b6 100644 --- a/spec/lib/gitlab/import_export/base/relation_object_saver_spec.rb +++ b/spec/lib/gitlab/import_export/base/relation_object_saver_spec.rb @@ -79,14 +79,14 @@ RSpec.describe Gitlab::ImportExport::Base::RelationObjectSaver do let(:relation_definition) { { 'notes' => {} } } it 'saves valid subrelations and logs invalid subrelation' do - expect(relation_object.notes).to receive(:<<).and_call_original + expect(relation_object.notes).to receive(:<<).twice.and_call_original expect(Gitlab::Import::Logger) .to receive(:info) .with( message: '[Project/Group Import] Invalid subrelation', project_id: project.id, relation_key: 'issues', - error_messages: "Noteable can't be blank and Project does not match noteable project" + error_messages: "Project does not match noteable project" ) saver.execute @@ -94,9 +94,28 @@ RSpec.describe Gitlab::ImportExport::Base::RelationObjectSaver do issue = project.issues.last import_failure = project.import_failures.last + expect(invalid_note.persisted?).to eq(false) expect(issue.notes.count).to eq(5) expect(import_failure.source).to eq('RelationObjectSaver#save!') - expect(import_failure.exception_message).to eq("Noteable can't be blank and Project does not match noteable project") + expect(import_failure.exception_message).to eq('Project does not match noteable project') + end + + context 'when invalid subrelation can still be persisted' do + let(:relation_key) { 'merge_requests' } + let(:relation_definition) { { 'approvals' => {} } } + let(:approval_1) { build(:approval, merge_request_id: nil, user: create(:user)) } + let(:approval_2) { build(:approval, merge_request_id: nil, user: create(:user)) } + let(:relation_object) { build(:merge_request, source_project: project, target_project: project, approvals: [approval_1, approval_2]) } + + it 'saves the subrelation' do + expect(approval_1.valid?).to eq(false) + expect(Gitlab::Import::Logger).not_to receive(:info) + + saver.execute + + expect(project.merge_requests.first.approvals.count).to eq(2) + expect(project.merge_requests.first.approvals.first.persisted?).to eq(true) + end end context 'when importable is group' do diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 352255afc8d..e591cbd05a0 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -930,3 +930,17 @@ ResourceStateEvent: - source_commit - close_after_error_tracking_resolve - close_auto_resolve_prometheus_alert +Iteration: + - created_at + - updated_at + - start_date + - due_date + - group_id + - iid + - description +ResourceIterationEvent: + - user_id + - created_at + - action +Iterations::Cadence: + - title diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb index e228a0a7d72..80ae5c6fd21 100644 --- a/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb +++ b/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb @@ -44,4 +44,18 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisMetric, :clean_git end end end + + context "with usage prefix disabled" do + let(:expected_value) { 3 } + + before do + 3.times do + Gitlab::UsageDataCounters::WebIdeCounter.increment_merge_requests_count + end + end + + it_behaves_like 'a correct instrumented metric value', { + options: { event: 'merge_requests_count', prefix: 'web_ide', include_usage_prefix: false } + } + end end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 7226c9db15d..181351222c1 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -266,13 +266,13 @@ RSpec.describe Ci::Runner do end shared_examples '.belonging_to_parent_group_of_project' do - let!(:group1) { create(:group) } - let!(:project1) { create(:project, group: group1) } - let!(:runner1) { create(:ci_runner, :group, groups: [group1]) } + let_it_be(:group1) { create(:group) } + let_it_be(:project1) { create(:project, group: group1) } + let_it_be(:runner1) { create(:ci_runner, :group, groups: [group1]) } - let!(:group2) { create(:group) } - let!(:project2) { create(:project, group: group2) } - let!(:runner2) { create(:ci_runner, :group, groups: [group2]) } + let_it_be(:group2) { create(:group) } + let_it_be(:project2) { create(:project, group: group2) } + let_it_be(:runner2) { create(:ci_runner, :group, groups: [group2]) } let(:project_id) { project1.id } @@ -495,8 +495,8 @@ RSpec.describe Ci::Runner do describe '.active' do subject { described_class.active(active_value) } - let!(:runner1) { create(:ci_runner, :instance, active: false) } - let!(:runner2) { create(:ci_runner, :instance) } + let_it_be(:runner1) { create(:ci_runner, :instance, active: false) } + let_it_be(:runner2) { create(:ci_runner, :instance) } context 'with active_value set to false' do let(:active_value) { false } @@ -544,7 +544,7 @@ RSpec.describe Ci::Runner do end describe '#stale?', :clean_gitlab_redis_cache do - let(:runner) { create(:ci_runner, :instance) } + let(:runner) { build(:ci_runner, :instance) } subject { runner.stale? } @@ -619,7 +619,7 @@ RSpec.describe Ci::Runner do end describe '#online?', :clean_gitlab_redis_cache do - let(:runner) { create(:ci_runner, :instance) } + let(:runner) { build(:ci_runner, :instance) } subject { runner.online? } @@ -1162,13 +1162,13 @@ RSpec.describe Ci::Runner do end describe '.assignable_for' do - let(:project) { create(:project) } - let(:group) { create(:group) } - let(:another_project) { create(:project) } - let!(:unlocked_project_runner) { create(:ci_runner, :project, projects: [project]) } - let!(:locked_project_runner) { create(:ci_runner, :project, locked: true, projects: [project]) } - let!(:group_runner) { create(:ci_runner, :group, groups: [group]) } - let!(:instance_runner) { create(:ci_runner, :instance) } + let_it_be(:project) { create(:project) } + let_it_be(:group) { create(:group) } + let_it_be(:another_project) { create(:project) } + let_it_be(:unlocked_project_runner) { create(:ci_runner, :project, projects: [project]) } + let_it_be(:locked_project_runner) { create(:ci_runner, :project, locked: true, projects: [project]) } + let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group]) } + let_it_be(:instance_runner) { create(:ci_runner, :instance) } context 'with already assigned project' do subject { described_class.assignable_for(project) } @@ -1186,78 +1186,74 @@ RSpec.describe Ci::Runner do end end - describe '#owner_project' do + context 'Project-related queries' do let_it_be(:project1) { create(:project) } let_it_be(:project2) { create(:project) } - subject(:owner_project) { project_runner.owner_project } + describe '#owner_project' do + subject(:owner_project) { project_runner.owner_project } - context 'with project1 as first project associated with runner' do - let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project1, project2]) } + context 'with project1 as first project associated with runner' do + let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project1, project2]) } - it { is_expected.to eq project1 } - end + it { is_expected.to eq project1 } + end - context 'with project2 as first project associated with runner' do - let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project2, project1]) } + context 'with project2 as first project associated with runner' do + let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project2, project1]) } - it { is_expected.to eq project2 } + it { is_expected.to eq project2 } + end end - end - describe "belongs_to_one_project?" do - it "returns false if there are two projects runner assigned to" do - project1 = create(:project) - project2 = create(:project) - runner = create(:ci_runner, :project, projects: [project1, project2]) + describe "belongs_to_one_project?" do + it "returns false if there are two projects runner is assigned to" do + runner = create(:ci_runner, :project, projects: [project1, project2]) - expect(runner.belongs_to_one_project?).to be_falsey - end + expect(runner.belongs_to_one_project?).to be_falsey + end - it "returns true" do - project = create(:project) - runner = create(:ci_runner, :project, projects: [project]) + it "returns true if there is only one project runner is assigned to" do + runner = create(:ci_runner, :project, projects: [project1]) - expect(runner.belongs_to_one_project?).to be_truthy + expect(runner.belongs_to_one_project?).to be_truthy + end end - end - describe '#belongs_to_more_than_one_project?' do - context 'project runner' do - let(:project1) { create(:project) } - let(:project2) { create(:project) } + describe '#belongs_to_more_than_one_project?' do + context 'project runner' do + context 'two projects assigned to runner' do + let(:runner) { create(:ci_runner, :project, projects: [project1, project2]) } - context 'two projects assigned to runner' do - let(:runner) { create(:ci_runner, :project, projects: [project1, project2]) } + it 'returns true' do + expect(runner.belongs_to_more_than_one_project?).to be_truthy + end + end + + context 'one project assigned to runner' do + let(:runner) { create(:ci_runner, :project, projects: [project1]) } - it 'returns true' do - expect(runner.belongs_to_more_than_one_project?).to be_truthy + it 'returns false' do + expect(runner.belongs_to_more_than_one_project?).to be_falsey + end end end - context 'one project assigned to runner' do - let(:runner) { create(:ci_runner, :project, projects: [project1]) } + context 'group runner' do + let(:group) { create(:group) } + let(:runner) { create(:ci_runner, :group, groups: [group]) } it 'returns false' do expect(runner.belongs_to_more_than_one_project?).to be_falsey end end - end - - context 'group runner' do - let(:group) { create(:group) } - let(:runner) { create(:ci_runner, :group, groups: [group]) } - - it 'returns false' do - expect(runner.belongs_to_more_than_one_project?).to be_falsey - end - end - context 'shared runner' do - let(:runner) { create(:ci_runner, :instance) } + context 'shared runner' do + let(:runner) { create(:ci_runner, :instance) } - it 'returns false' do - expect(runner.belongs_to_more_than_one_project?).to be_falsey + it 'returns false' do + expect(runner.belongs_to_more_than_one_project?).to be_falsey + end end end end @@ -1318,7 +1314,7 @@ RSpec.describe Ci::Runner do end describe '.search' do - let(:runner) { create(:ci_runner, token: '123abc', description: 'test runner') } + let_it_be(:runner) { create(:ci_runner, token: '123abc', description: 'test runner') } it 'returns runners with a matching token' do expect(described_class.search(runner.token)).to eq([runner]) @@ -1346,8 +1342,9 @@ RSpec.describe Ci::Runner do end describe '#pick_build!' do + let_it_be(:runner) { create(:ci_runner) } + let(:build) { create(:ci_build) } - let(:runner) { create(:ci_runner) } context 'runner can pick the build' do it 'calls #tick_runner_queue' do @@ -1384,26 +1381,26 @@ RSpec.describe Ci::Runner do end describe '.order_by' do + let_it_be(:runner1) { create(:ci_runner, created_at: 1.year.ago, contacted_at: 1.year.ago) } + let_it_be(:runner2) { create(:ci_runner, created_at: 1.month.ago, contacted_at: 1.month.ago) } + + before do + runner1.update!(token_expires_at: 1.year.from_now) + end + it 'supports ordering by the contact date' do - runner1 = create(:ci_runner, contacted_at: 1.year.ago) - runner2 = create(:ci_runner, contacted_at: 1.month.ago) runners = described_class.order_by('contacted_asc') expect(runners).to eq([runner1, runner2]) end it 'supports ordering by the creation date' do - runner1 = create(:ci_runner, created_at: 1.year.ago) - runner2 = create(:ci_runner, created_at: 1.month.ago) runners = described_class.order_by('created_asc') expect(runners).to eq([runner2, runner1]) end it 'supports ordering by the token expiration' do - runner1 = create(:ci_runner) - runner1.update!(token_expires_at: 1.year.from_now) - runner2 = create(:ci_runner) runner3 = create(:ci_runner) runner3.update!(token_expires_at: 1.month.from_now) diff --git a/spec/models/jira_connect_installation_spec.rb b/spec/models/jira_connect_installation_spec.rb index 3d1095845aa..9c1f7c678a9 100644 --- a/spec/models/jira_connect_installation_spec.rb +++ b/spec/models/jira_connect_installation_spec.rb @@ -45,4 +45,30 @@ RSpec.describe JiraConnectInstallation do expect(subject).to contain_exactly(subscription.installation) end end + + describe '#oauth_authorization_url' do + let_it_be(:installation) { create(:jira_connect_installation) } + + subject { installation.oauth_authorization_url } + + before do + allow(Gitlab).to receive_message_chain('config.gitlab.url') { 'http://test.host' } + end + + it { is_expected.to eq('http://test.host') } + + context 'with instance_url' do + let_it_be(:installation) { create(:jira_connect_installation, instance_url: 'https://gitlab.example.com') } + + it { is_expected.to eq('https://gitlab.example.com') } + + context 'and jira_connect_oauth_self_managed feature is disabled' do + before do + stub_feature_flags(jira_connect_oauth_self_managed: false) + end + + it { is_expected.to eq('http://test.host') } + end + end + end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 5b032c53352..fefd9f71408 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -2729,6 +2729,41 @@ RSpec.describe ProjectPolicy do end end + describe 'read_milestone' do + context 'when project is public' do + let(:project) { public_project_in_group } + + context 'and issues and merge requests are private' do + before do + project.project_feature.update!( + issues_access_level: ProjectFeature::PRIVATE, + merge_requests_access_level: ProjectFeature::PRIVATE + ) + end + + context 'when user is an inherited member from the group' do + context 'and user is a guest' do + let(:current_user) { inherited_guest } + + it { is_expected.to be_allowed(:read_milestone) } + end + + context 'and user is a reporter' do + let(:current_user) { inherited_reporter } + + it { is_expected.to be_allowed(:read_milestone) } + end + + context 'and user is a developer' do + let(:current_user) { inherited_developer } + + it { is_expected.to be_allowed(:read_milestone) } + end + end + end + end + end + private def project_subject(project_type) diff --git a/spec/services/ci/create_pipeline_service/partitioning_spec.rb b/spec/services/ci/create_pipeline_service/partitioning_spec.rb index cb4f32f591f..43fbb74ede4 100644 --- a/spec/services/ci/create_pipeline_service/partitioning_spec.rb +++ b/spec/services/ci/create_pipeline_service/partitioning_spec.rb @@ -96,6 +96,47 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes end end + context 'with parent child pipelines' do + before do + allow(Ci::Pipeline) + .to receive(:current_partition_value) + .and_return(current_partition_id, 301, 302) + + allow_next_found_instance_of(Ci::Bridge) do |bridge| + allow(bridge).to receive(:yaml_for_downstream).and_return(child_config) + end + end + + let(:config) do + <<-YAML + test: + trigger: + include: child.yml + YAML + end + + let(:child_config) do + <<-YAML + test: + script: make test + YAML + end + + it 'assigns partition values to child pipelines', :aggregate_failures, :sidekiq_inline do + expect(pipeline).to be_created_successfully + expect(pipeline.child_pipelines).to all be_created_successfully + + child_partition_ids = pipeline.child_pipelines.map(&:partition_id).uniq + child_jobs = CommitStatus.where(commit_id: pipeline.child_pipelines) + + expect(pipeline.partition_id).to eq(current_partition_id) + expect(child_partition_ids).to eq([current_partition_id]) + + expect(child_jobs).to all be_a(Ci::Build) + expect(child_jobs.pluck(:partition_id).uniq).to eq([current_partition_id]) + end + end + def find_metadata(name) pipeline .processables diff --git a/spec/services/issues/relative_position_rebalancing_service_spec.rb b/spec/services/issues/relative_position_rebalancing_service_spec.rb index 20064bd7e4b..37a94e1d6a2 100644 --- a/spec/services/issues/relative_position_rebalancing_service_spec.rb +++ b/spec/services/issues/relative_position_rebalancing_service_spec.rb @@ -72,22 +72,8 @@ RSpec.describe Issues::RelativePositionRebalancingService, :clean_gitlab_redis_s end.not_to change { issues_in_position_order.map(&:id) } end - it 'does nothing if the feature flag is disabled' do - stub_feature_flags(rebalance_issues: false) - issue = project.issues.first - issue.project - issue.project.group - old_pos = issue.relative_position - - # fetching root namespace in the initializer triggers 2 queries: - # for fetching a random project from collection and fetching the root namespace. - expect { service.execute }.not_to exceed_query_limit(2) - expect(old_pos).to eq(issue.reload.relative_position) - end - it 'acts if the flag is enabled for the root namespace' do issue = create(:issue, project: project, author: user, relative_position: max_pos) - stub_feature_flags(rebalance_issues: project.root_namespace) expect { service.execute }.to change { issue.reload.relative_position } end @@ -95,7 +81,6 @@ RSpec.describe Issues::RelativePositionRebalancingService, :clean_gitlab_redis_s it 'acts if the flag is enabled for the group' do issue = create(:issue, project: project, author: user, relative_position: max_pos) project.update!(group: create(:group)) - stub_feature_flags(rebalance_issues: issue.project.group) expect { service.execute }.to change { issue.reload.relative_position } end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 68d145259b5..8a2e9ed74f7 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -304,38 +304,6 @@ RSpec.describe Issues::UpdateService, :mailer do end end - it 'does not rebalance even if needed if the flag is disabled' do - stub_feature_flags(rebalance_issues: false) - - range = described_class::NO_REBALANCING_NEEDED - issue1 = create(:issue, project: project, relative_position: range.first - 100) - issue2 = create(:issue, project: project, relative_position: range.first) - issue.update!(relative_position: RelativePositioning::START_POSITION) - - opts[:move_between_ids] = [issue1.id, issue2.id] - - expect(Issues::RebalancingWorker).not_to receive(:perform_async) - - update_issue(opts) - expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position) - end - - it 'rebalances if needed if the flag is enabled for the project' do - stub_feature_flags(rebalance_issues: project) - - range = described_class::NO_REBALANCING_NEEDED - issue1 = create(:issue, project: project, relative_position: range.first - 100) - issue2 = create(:issue, project: project, relative_position: range.first) - issue.update!(relative_position: RelativePositioning::START_POSITION) - - opts[:move_between_ids] = [issue1.id, issue2.id] - - expect(Issues::RebalancingWorker).to receive(:perform_async).with(nil, nil, project.root_namespace.id) - - update_issue(opts) - expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position) - end - it 'rebalances if needed on the left' do range = described_class::NO_REBALANCING_NEEDED issue1 = create(:issue, project: project, relative_position: range.first - 100) diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb index 7b60561432c..d8c9c5b7556 100644 --- a/spec/support/helpers/usage_data_helpers.rb +++ b/spec/support/helpers/usage_data_helpers.rb @@ -11,10 +11,6 @@ module UsageDataHelpers wiki_pages_create wiki_pages_update wiki_pages_delete - web_ide_views - web_ide_commits - web_ide_merge_requests - web_ide_previews navbar_searches cycle_analytics_views productivity_analytics_views diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb index 1d4731d9b39..fc7255a4a20 100644 --- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb @@ -6,13 +6,19 @@ RSpec.shared_context 'ProjectPolicy context' do let_it_be(:reporter) { create(:user) } let_it_be(:developer) { create(:user) } let_it_be(:maintainer) { create(:user) } + let_it_be(:inherited_guest) { create(:user) } + let_it_be(:inherited_reporter) { create(:user) } + let_it_be(:inherited_developer) { create(:user) } + let_it_be(:inherited_maintainer) { create(:user) } let_it_be(:owner) { create(:user) } let_it_be(:admin) { create(:admin) } let_it_be(:non_member) { create(:user) } + let_it_be_with_refind(:group) { create(:group, :public) } let_it_be_with_refind(:private_project) { create(:project, :private, namespace: owner.namespace) } let_it_be_with_refind(:internal_project) { create(:project, :internal, namespace: owner.namespace) } let_it_be_with_refind(:public_project) { create(:project, :public, namespace: owner.namespace) } - let_it_be_with_refind(:public_project_in_group) { create(:project, :public, namespace: create(:group, :public)) } + let_it_be_with_refind(:public_project_in_group) { create(:project, :public, namespace: group) } + let_it_be_with_refind(:private_project_in_group) { create(:project, :private, namespace: group) } let(:base_guest_permissions) do %i[ @@ -95,6 +101,11 @@ RSpec.shared_context 'ProjectPolicy context' do let(:owner_permissions) { base_owner_permissions + additional_owner_permissions } before_all do + group.add_guest(inherited_guest) + group.add_reporter(inherited_reporter) + group.add_developer(inherited_developer) + group.add_maintainer(inherited_maintainer) + [private_project, internal_project, public_project, public_project_in_group].each do |project| project.add_guest(guest) project.add_reporter(reporter) diff --git a/spec/support/shared_examples/features/manage_applications_shared_examples.rb b/spec/support/shared_examples/features/manage_applications_shared_examples.rb index 442264e7ae4..b59f3f1e27b 100644 --- a/spec/support/shared_examples/features/manage_applications_shared_examples.rb +++ b/spec/support/shared_examples/features/manage_applications_shared_examples.rb @@ -5,39 +5,87 @@ RSpec.shared_examples 'manage applications' do let_it_be(:application_name_changed) { "#{application_name} changed" } let_it_be(:application_redirect_uri) { 'https://foo.bar' } - it 'allows user to manage applications', :js do - visit new_application_path + context 'when hash_oauth_secrets flag set' do + before do + stub_feature_flags(hash_oauth_secrets: true) + end + + it 'allows user to manage applications', :js do + visit new_application_path - expect(page).to have_content 'Add new application' + expect(page).to have_content 'Add new application' - fill_in :doorkeeper_application_name, with: application_name - fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri - check :doorkeeper_application_scopes_read_user - click_on 'Save application' + fill_in :doorkeeper_application_name, with: application_name + fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri + check :doorkeeper_application_scopes_read_user + click_on 'Save application' - validate_application(application_name, 'Yes') - expect(page).to have_link('Continue', href: index_path) + validate_application(application_name, 'Yes') + expect(page).to have_content _('This is the only time the secret is accessible. Copy the secret and store it securely') + expect(page).to have_link('Continue', href: index_path) - application = Doorkeeper::Application.find_by(name: application_name) - expect(page).to have_css("button[title=\"Copy secret\"][data-clipboard-text=\"#{application.secret}\"]", text: 'Copy') + expect(page).to have_css("button[title=\"Copy secret\"]", text: 'Copy') - click_on 'Edit' + click_on 'Edit' - application_name_changed = "#{application_name} changed" + application_name_changed = "#{application_name} changed" - fill_in :doorkeeper_application_name, with: application_name_changed - uncheck :doorkeeper_application_confidential - click_on 'Save application' + fill_in :doorkeeper_application_name, with: application_name_changed + uncheck :doorkeeper_application_confidential + click_on 'Save application' + + validate_application(application_name_changed, 'No') + expect(page).not_to have_link('Continue') + expect(page).to have_content _('The secret is only available when you first create the application') + + visit_applications_path + + page.within '.oauth-applications' do + click_on 'Destroy' + end + expect(page.find('.oauth-applications')).not_to have_content 'test_changed' + end + end + + context 'when hash_oauth_secrets flag not set' do + before do + stub_feature_flags(hash_oauth_secrets: false) + end + + it 'allows user to manage applications', :js do + visit new_application_path + + expect(page).to have_content 'Add new application' + + fill_in :doorkeeper_application_name, with: application_name + fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri + check :doorkeeper_application_scopes_read_user + click_on 'Save application' + + validate_application(application_name, 'Yes') + expect(page).to have_link('Continue', href: index_path) + + application = Doorkeeper::Application.find_by(name: application_name) + expect(page).to have_css("button[title=\"Copy secret\"][data-clipboard-text=\"#{application.secret}\"]", text: 'Copy') + + click_on 'Edit' + + application_name_changed = "#{application_name} changed" + + fill_in :doorkeeper_application_name, with: application_name_changed + uncheck :doorkeeper_application_confidential + click_on 'Save application' - validate_application(application_name_changed, 'No') - expect(page).not_to have_link('Continue') + validate_application(application_name_changed, 'No') + expect(page).not_to have_link('Continue') - visit_applications_path + visit_applications_path - page.within '.oauth-applications' do - click_on 'Destroy' + page.within '.oauth-applications' do + click_on 'Destroy' + end + expect(page.find('.oauth-applications')).not_to have_content 'test_changed' end - expect(page.find('.oauth-applications')).not_to have_content 'test_changed' end context 'when scopes are blank' do diff --git a/spec/support/shared_examples/policies/project_policy_shared_examples.rb b/spec/support/shared_examples/policies/project_policy_shared_examples.rb index c4083df47e2..cfcc3615e13 100644 --- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb +++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb @@ -107,70 +107,88 @@ RSpec.shared_examples 'deploy token does not get confused with user' do end RSpec.shared_examples 'project policies as guest' do - context 'abilities for public projects' do - let(:project) { public_project } - let(:current_user) { guest } - - it do - expect_allowed(*guest_permissions) - expect_allowed(*public_permissions) - expect_disallowed(*developer_permissions) - expect_disallowed(*maintainer_permissions) - expect_disallowed(*owner_permissions) - end + let(:reporter_public_build_permissions) do + reporter_permissions - [:read_build, :read_pipeline] end - context 'abilities for non-public projects' do - let(:project) { private_project } - let(:current_user) { guest } + context 'as a direct project member' do + context 'abilities for public projects' do + let(:project) { public_project } + let(:current_user) { guest } - let(:reporter_public_build_permissions) do - reporter_permissions - [:read_build, :read_pipeline] + specify do + expect_allowed(*guest_permissions) + expect_allowed(*public_permissions) + expect_disallowed(*developer_permissions) + expect_disallowed(*maintainer_permissions) + expect_disallowed(*owner_permissions) + end end - it do - expect_allowed(*guest_permissions) - expect_disallowed(*reporter_public_build_permissions) - expect_disallowed(*team_member_reporter_permissions) - expect_disallowed(*developer_permissions) - expect_disallowed(*maintainer_permissions) - expect_disallowed(*owner_permissions) - end + context 'abilities for non-public projects' do + let(:project) { private_project } + let(:current_user) { guest } - it_behaves_like 'deploy token does not get confused with user' do - let(:user_id) { guest.id } - end + specify do + expect_allowed(*guest_permissions) + expect_disallowed(*reporter_public_build_permissions) + expect_disallowed(*team_member_reporter_permissions) + expect_disallowed(*developer_permissions) + expect_disallowed(*maintainer_permissions) + expect_disallowed(*owner_permissions) + end - it_behaves_like 'archived project policies' do - let(:regular_abilities) { guest_permissions } - end + it_behaves_like 'deploy token does not get confused with user' do + let(:user_id) { guest.id } + end - context 'public builds enabled' do - it do - expect_allowed(*guest_permissions) - expect_allowed(:read_build, :read_pipeline) + it_behaves_like 'archived project policies' do + let(:regular_abilities) { guest_permissions } end - end - context 'when public builds disabled' do - before do - project.update!(public_builds: false) + context 'public builds enabled' do + specify do + expect_allowed(*guest_permissions) + expect_allowed(:read_build, :read_pipeline) + end end - it do - expect_allowed(*guest_permissions) - expect_disallowed(:read_build, :read_pipeline) + context 'when public builds disabled' do + before do + project.update!(public_builds: false) + end + + specify do + expect_allowed(*guest_permissions) + expect_disallowed(:read_build, :read_pipeline) + end end - end - context 'when builds are disabled' do - before do - project.project_feature.update!(builds_access_level: ProjectFeature::DISABLED) + context 'when builds are disabled' do + before do + project.project_feature.update!(builds_access_level: ProjectFeature::DISABLED) + end + + specify do + expect_disallowed(:read_build) + expect_allowed(:read_pipeline) + end end + end + end - it do - expect_disallowed(:read_build) - expect_allowed(:read_pipeline) + context 'as an inherited member from the group' do + context 'abilities for private projects' do + let(:project) { private_project_in_group } + let(:current_user) { inherited_guest } + + specify do + expect_allowed(*guest_permissions) + expect_disallowed(*reporter_public_build_permissions) + expect_disallowed(*team_member_reporter_permissions) + expect_disallowed(*developer_permissions) + expect_disallowed(*maintainer_permissions) + expect_disallowed(*owner_permissions) end end end @@ -181,7 +199,7 @@ RSpec.shared_examples 'project policies as reporter' do let(:project) { private_project } let(:current_user) { reporter } - it do + specify do expect_allowed(*guest_permissions) expect_allowed(*reporter_permissions) expect_allowed(*team_member_reporter_permissions) @@ -198,6 +216,22 @@ RSpec.shared_examples 'project policies as reporter' do let(:regular_abilities) { reporter_permissions } end end + + context 'as an inherited member from the group' do + context 'abilities for private projects' do + let(:project) { private_project_in_group } + let(:current_user) { inherited_reporter } + + specify do + expect_allowed(*guest_permissions) + expect_allowed(*reporter_permissions) + expect_allowed(*team_member_reporter_permissions) + expect_disallowed(*developer_permissions) + expect_disallowed(*maintainer_permissions) + expect_disallowed(*owner_permissions) + end + end + end end RSpec.shared_examples 'project policies as developer' do @@ -205,7 +239,7 @@ RSpec.shared_examples 'project policies as developer' do let(:project) { private_project } let(:current_user) { developer } - it do + specify do expect_allowed(*guest_permissions) expect_allowed(*reporter_permissions) expect_allowed(*team_member_reporter_permissions) @@ -222,6 +256,22 @@ RSpec.shared_examples 'project policies as developer' do let(:regular_abilities) { developer_permissions } end end + + context 'as an inherited member from the group' do + context 'abilities for private projects' do + let(:project) { private_project_in_group } + let(:current_user) { inherited_developer } + + specify do + expect_allowed(*guest_permissions) + expect_allowed(*reporter_permissions) + expect_allowed(*team_member_reporter_permissions) + expect_allowed(*developer_permissions) + expect_disallowed(*maintainer_permissions) + expect_disallowed(*owner_permissions) + end + end + end end RSpec.shared_examples 'project policies as maintainer' do diff --git a/spec/support/shared_examples/requests/applications_controller_shared_examples.rb b/spec/support/shared_examples/requests/applications_controller_shared_examples.rb index 8f852d42c2c..642930dd982 100644 --- a/spec/support/shared_examples/requests/applications_controller_shared_examples.rb +++ b/spec/support/shared_examples/requests/applications_controller_shared_examples.rb @@ -11,6 +11,7 @@ RSpec.shared_examples 'applications controller - GET #show' do context 'when application is viewed after being created' do before do create_application + stub_feature_flags(hash_oauth_secrets: false) end it 'sets `@created` instance variable to `true`' do @@ -21,6 +22,10 @@ RSpec.shared_examples 'applications controller - GET #show' do end context 'when application is reviewed' do + before do + stub_feature_flags(hash_oauth_secrets: false) + end + it 'sets `@created` instance variable to `false`' do get show_path @@ -32,6 +37,7 @@ end RSpec.shared_examples 'applications controller - POST #create' do it "sets `#{OauthApplications::CREATED_SESSION_KEY}` session key to `true`" do + stub_feature_flags(hash_oauth_secrets: false) create_application expect(session[OauthApplications::CREATED_SESSION_KEY]).to eq(true) |