diff options
34 files changed, 1062 insertions, 71 deletions
diff --git a/.eslintrc.yml b/.eslintrc.yml index 17459804a7d..549f1771593 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -95,8 +95,7 @@ rules: order: ignore overrides: - files: - - 'ee/spec/frontend*/**/*' - - 'spec/frontend*/**/*' + - '{,ee/,jh/}spec/frontend*/**/*' rules: '@gitlab/require-i18n-strings': off '@gitlab/no-runtime-template-compiler': off diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 9e9323fc86a..60d99a6d795 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -13.23.0 +13.23.1 diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_title.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_title.vue index 2a479c65d0c..9bab08b8548 100644 --- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_title.vue +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_title.vue @@ -21,13 +21,17 @@ export default { }, }, computed: { - showModuleCount() { - return Number.isInteger(this.count); + hasModules() { + return Number.isInteger(this.count) && this.count > 0; }, moduleAmountText() { return n__(`%d Module`, `%d Modules`, this.count); }, infoMessages() { + if (!this.hasModules) { + return []; + } + return [{ text: this.$options.i18n.LIST_INTRO_TEXT, link: this.helpUrl }]; }, }, @@ -43,11 +47,7 @@ export default { <template> <title-area :title="$options.i18n.LIST_TITLE_TEXT" :info-messages="infoMessages"> <template #metadata-amount> - <metadata-item - v-if="showModuleCount" - icon="infrastructure-registry" - :text="moduleAmountText" - /> + <metadata-item v-if="hasModules" icon="infrastructure-registry" :text="moduleAmountText" /> </template> </title-area> </template> diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue index 462618a7f12..184a24047eb 100644 --- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue @@ -99,7 +99,7 @@ export default { <template> <div> <infrastructure-title :help-url="packageHelpUrl" :count="packagesCount" /> - <infrastructure-search @update="requestPackagesList" /> + <infrastructure-search v-if="packagesCount > 0" @update="requestPackagesList" /> <package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest"> <template #empty-state> diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 4acbb0482f3..f96e080ecf8 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -38,6 +38,8 @@ class GroupsController < Groups::ApplicationController before_action :check_export_rate_limit!, only: [:export, :download_export] + before_action :track_experiment_event, only: [:new] + helper_method :captcha_required? skip_cross_project_access_check :index, :new, :create, :edit, :update, @@ -378,6 +380,12 @@ class GroupsController < Groups::ApplicationController def captcha_required? captcha_enabled? && !params[:parent_id] end + + def track_experiment_event + return if params[:parent_id] + + experiment(:require_verification_for_namespace_creation, user: current_user).track(:start_create_group) + end end GroupsController.prepend_mod_with('GroupsController') diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 89e87c4345e..c459afbbcf6 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -71,6 +71,7 @@ class Projects::IssuesController < Projects::ApplicationController ] feature_category :service_desk, [:service_desk] + urgency :low, [:service_desk] feature_category :importers, [:import_csv, :export_csv] attr_accessor :vulnerability_id diff --git a/app/controllers/projects/service_desk_controller.rb b/app/controllers/projects/service_desk_controller.rb index 1fb07c3a903..aa0e70121df 100644 --- a/app/controllers/projects/service_desk_controller.rb +++ b/app/controllers/projects/service_desk_controller.rb @@ -4,6 +4,7 @@ class Projects::ServiceDeskController < Projects::ApplicationController before_action :authorize_admin_project! feature_category :service_desk + urgency :low def show json_response diff --git a/app/experiments/require_verification_for_namespace_creation_experiment.rb b/app/experiments/require_verification_for_namespace_creation_experiment.rb index 1cadac7e7d4..78390ddd099 100644 --- a/app/experiments/require_verification_for_namespace_creation_experiment.rb +++ b/app/experiments/require_verification_for_namespace_creation_experiment.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true class RequireVerificationForNamespaceCreationExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass + exclude :existing_user + + EXPERIMENT_START_DATE = Date.new(2022, 1, 31) + def control_behavior false end @@ -24,4 +28,10 @@ class RequireVerificationForNamespaceCreationExperiment < ApplicationExperiment def subject context.value[:user] end + + def existing_user + return false unless user_or_actor + + user_or_actor.created_at < EXPERIMENT_START_DATE + end end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 7296560a450..3fbea0c0472 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -139,7 +139,7 @@ module GroupsHelper {} end - def require_verification_for_group_creation_enabled? + def require_verification_for_namespace_creation_enabled? # overridden in EE false end diff --git a/app/views/groups/settings/_export.html.haml b/app/views/groups/settings/_export.html.haml index 090cfc652ee..81403fd88b2 100644 --- a/app/views/groups/settings/_export.html.haml +++ b/app/views/groups/settings/_export.html.haml @@ -27,10 +27,10 @@ %li= _('Runner tokens') %li= _('SAML discovery tokens') - if group.export_file_exists? - = link_to _('Regenerate export'), export_group_path(group), - method: :post, class: 'btn gl-button btn-default', data: { qa_selector: 'regenerate_export_group_link' } = link_to _('Download export'), download_export_group_path(group), rel: 'nofollow', method: :get, class: 'btn gl-button btn-default', data: { qa_selector: 'download_export_link' } + = link_to _('Regenerate export'), export_group_path(group), + method: :post, class: 'btn gl-button btn-default', data: { qa_selector: 'regenerate_export_group_link' } - else = link_to _('Export group'), export_group_path(group), method: :post, class: 'btn gl-button btn-default', data: { qa_selector: 'export_group_link' } diff --git a/config/feature_flags/development/rate_limit_gitlab_shell.yml b/config/feature_flags/development/rate_limit_gitlab_shell.yml index ceb9e86b01c..3c29a71af6e 100644 --- a/config/feature_flags/development/rate_limit_gitlab_shell.yml +++ b/config/feature_flags/development/rate_limit_gitlab_shell.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350465 milestone: '14.7' type: development group: group::source code -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/experiment/require_verification_for_group_creation.yml b/config/feature_flags/experiment/require_verification_for_group_creation.yml deleted file mode 100644 index 767d5f55bce..00000000000 --- a/config/feature_flags/experiment/require_verification_for_group_creation.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: require_verification_for_group_creation -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77569 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/349857 -milestone: '14.7' -type: experiment -group: group::activation -default_enabled: false diff --git a/data/deprecations/15-0-instance-statistics-graphql-node-removal.yml b/data/deprecations/15-0-instance-statistics-graphql-node-removal.yml new file mode 100644 index 00000000000..c617a6cc478 --- /dev/null +++ b/data/deprecations/15-0-instance-statistics-graphql-node-removal.yml @@ -0,0 +1,13 @@ +- name: "Querying Usage Trends via the `instanceStatisticsMeasurements` GraphQL node" + announcement_milestone: "14.8" + announcement_date: "2022-02-22" + removal_milestone: "15.0" + removal_date: "2022-05-22" + breaking_change: true + body: | # Do not modify this line, instead modify the lines below. + The `instanceStatisticsMeasurements` GraphQL node has been renamed to `usageTrendsMeasurements` in 13.10 and the old field name has been marked as deprecated. To fix the existing GraphQL queries, replace `instanceStatisticsMeasurements` with `usageTrendsMeasurements`. +# The following items are not published on the docs page, but may be used in the future. + stage: Manage + tiers: [FREE] + issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/332323 + documentation_url: https://docs.gitlab.com/ee/api/graphql/reference/index.html#queryusagetrendsmeasurements diff --git a/db/migrate/20220106111958_add_insert_or_update_vulnerability_reads_trigger.rb b/db/migrate/20220106111958_add_insert_or_update_vulnerability_reads_trigger.rb new file mode 100644 index 00000000000..0049f4e00a2 --- /dev/null +++ b/db/migrate/20220106111958_add_insert_or_update_vulnerability_reads_trigger.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +class AddInsertOrUpdateVulnerabilityReadsTrigger < Gitlab::Database::Migration[1.0] + include Gitlab::Database::SchemaHelpers + + FUNCTION_NAME = 'insert_or_update_vulnerability_reads' + TRIGGER_NAME = 'trigger_insert_or_update_vulnerability_reads_from_occurrences' + + def up + execute(<<~SQL) + CREATE OR REPLACE FUNCTION #{FUNCTION_NAME}() + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ + DECLARE + severity smallint; + state smallint; + report_type smallint; + resolved_on_default_branch boolean; + BEGIN + IF (NEW.vulnerability_id IS NULL AND (TG_OP = 'INSERT' OR TG_OP = 'UPDATE')) THEN + RETURN NULL; + END IF; + + IF (TG_OP = 'UPDATE' AND OLD.vulnerability_id IS NOT NULL AND NEW.vulnerability_id IS NOT NULL) THEN + RETURN NULL; + END IF; + + SELECT + vulnerabilities.severity, vulnerabilities.state, vulnerabilities.report_type, vulnerabilities.resolved_on_default_branch + INTO + severity, state, report_type, resolved_on_default_branch + FROM + vulnerabilities + WHERE + vulnerabilities.id = NEW.vulnerability_id; + + INSERT INTO vulnerability_reads (vulnerability_id, project_id, scanner_id, report_type, severity, state, resolved_on_default_branch, uuid, location_image, cluster_agent_id) + VALUES (NEW.vulnerability_id, NEW.project_id, NEW.scanner_id, report_type, severity, state, resolved_on_default_branch, NEW.uuid::uuid, NEW.location->>'image', NEW.location->'kubernetes_resource'->>'agent_id') + ON CONFLICT(vulnerability_id) DO NOTHING; + RETURN NULL; + END + $$; + SQL + + execute(<<~SQL) + CREATE TRIGGER #{TRIGGER_NAME} + AFTER INSERT OR UPDATE ON vulnerability_occurrences + FOR EACH ROW + EXECUTE PROCEDURE #{FUNCTION_NAME}(); + SQL + end + + def down + drop_trigger(:vulnerability_occurrences, TRIGGER_NAME) + drop_function(FUNCTION_NAME) + end +end diff --git a/db/migrate/20220106112043_add_update_vulnerability_reads_trigger.rb b/db/migrate/20220106112043_add_update_vulnerability_reads_trigger.rb new file mode 100644 index 00000000000..940ec638924 --- /dev/null +++ b/db/migrate/20220106112043_add_update_vulnerability_reads_trigger.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class AddUpdateVulnerabilityReadsTrigger < Gitlab::Database::Migration[1.0] + include Gitlab::Database::SchemaHelpers + + TRIGGER_NAME = 'trigger_update_vulnerability_reads_on_vulnerability_update' + FUNCTION_NAME = 'update_vulnerability_reads_from_vulnerability' + + def up + create_trigger_function(FUNCTION_NAME, replace: true) do + <<~SQL + UPDATE + vulnerability_reads + SET + severity = NEW.severity, + state = NEW.state, + resolved_on_default_branch = NEW.resolved_on_default_branch + WHERE vulnerability_id = NEW.id; + RETURN NULL; + SQL + end + + execute(<<~SQL) + CREATE TRIGGER #{TRIGGER_NAME} + AFTER UPDATE ON vulnerabilities + FOR EACH ROW + WHEN ( + OLD.severity IS DISTINCT FROM NEW.severity OR + OLD.state IS DISTINCT FROM NEW.state OR + OLD.resolved_on_default_branch IS DISTINCT FROM NEW.resolved_on_default_branch + ) + EXECUTE PROCEDURE #{FUNCTION_NAME}(); + SQL + end + + def down + drop_trigger(:vulnerabilities, TRIGGER_NAME) + drop_function(FUNCTION_NAME) + end +end diff --git a/db/migrate/20220106112085_add_update_vulnerability_reads_location_trigger.rb b/db/migrate/20220106112085_add_update_vulnerability_reads_location_trigger.rb new file mode 100644 index 00000000000..a863fe8b7b8 --- /dev/null +++ b/db/migrate/20220106112085_add_update_vulnerability_reads_location_trigger.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +class AddUpdateVulnerabilityReadsLocationTrigger < Gitlab::Database::Migration[1.0] + include Gitlab::Database::SchemaHelpers + + TRIGGER_NAME = 'trigger_update_location_on_vulnerability_occurrences_update' + FUNCTION_NAME = 'update_location_from_vulnerability_occurrences' + + def up + create_trigger_function(FUNCTION_NAME, replace: true) do + <<~SQL + UPDATE + vulnerability_reads + SET + location_image = NEW.location->>'image', + cluster_agent_id = NEW.location->'kubernetes_resource'->>'agent_id' + WHERE + vulnerability_id = NEW.vulnerability_id; + RETURN NULL; + SQL + end + + execute(<<~SQL) + CREATE TRIGGER #{TRIGGER_NAME} + AFTER UPDATE ON vulnerability_occurrences + FOR EACH ROW + WHEN ( + NEW.report_type IN (2, 7) AND ( + OLD.location->>'image' IS DISTINCT FROM NEW.location->>'image' OR + OLD.location->'kubernetes_resource'->>'agent_id' IS DISTINCT FROM NEW.location->'kubernetes_resource'->>'agent_id' + ) + ) + EXECUTE PROCEDURE #{FUNCTION_NAME}(); + SQL + end + + def down + drop_trigger(:vulnerability_occurrences, TRIGGER_NAME) + drop_function(FUNCTION_NAME) + end +end diff --git a/db/migrate/20220106163326_add_has_issues_on_vulnerability_reads_trigger.rb b/db/migrate/20220106163326_add_has_issues_on_vulnerability_reads_trigger.rb new file mode 100644 index 00000000000..b3023a1f915 --- /dev/null +++ b/db/migrate/20220106163326_add_has_issues_on_vulnerability_reads_trigger.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +class AddHasIssuesOnVulnerabilityReadsTrigger < Gitlab::Database::Migration[1.0] + include Gitlab::Database::SchemaHelpers + + TRIGGER_ON_INSERT = 'trigger_update_has_issues_on_vulnerability_issue_links_update' + INSERT_FUNCTION_NAME = 'set_has_issues_on_vulnerability_reads' + + TRIGGER_ON_DELETE = 'trigger_update_has_issues_on_vulnerability_issue_links_delete' + DELETE_FUNCTION_NAME = 'unset_has_issues_on_vulnerability_reads' + + def up + create_trigger_function(INSERT_FUNCTION_NAME, replace: true) do + <<~SQL + UPDATE + vulnerability_reads + SET + has_issues = true + WHERE + vulnerability_id = NEW.vulnerability_id AND has_issues IS FALSE; + RETURN NULL; + SQL + end + + execute(<<~SQL) + CREATE OR REPLACE FUNCTION #{DELETE_FUNCTION_NAME}() + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ + DECLARE + has_issue_links integer; + BEGIN + PERFORM 1 + FROM + vulnerability_reads + WHERE + vulnerability_id = OLD.vulnerability_id + FOR UPDATE; + + SELECT 1 INTO has_issue_links FROM vulnerability_issue_links WHERE vulnerability_id = OLD.vulnerability_id LIMIT 1; + + IF (has_issue_links = 1) THEN + RETURN NULL; + END IF; + + UPDATE + vulnerability_reads + SET + has_issues = false + WHERE + vulnerability_id = OLD.vulnerability_id; + + RETURN NULL; + END + $$; + SQL + + execute(<<~SQL) + CREATE TRIGGER #{TRIGGER_ON_INSERT} + AFTER INSERT ON vulnerability_issue_links + FOR EACH ROW + EXECUTE FUNCTION #{INSERT_FUNCTION_NAME}(); + SQL + + execute(<<~SQL) + CREATE TRIGGER #{TRIGGER_ON_DELETE} + AFTER DELETE ON vulnerability_issue_links + FOR EACH ROW + EXECUTE FUNCTION #{DELETE_FUNCTION_NAME}(); + SQL + end + + def down + drop_trigger(:vulnerability_issue_links, TRIGGER_ON_INSERT) + drop_function(INSERT_FUNCTION_NAME) + drop_trigger(:vulnerability_issue_links, TRIGGER_ON_DELETE) + drop_function(DELETE_FUNCTION_NAME) + end +end diff --git a/db/schema_migrations/20220106111958 b/db/schema_migrations/20220106111958 new file mode 100644 index 00000000000..954db532950 --- /dev/null +++ b/db/schema_migrations/20220106111958 @@ -0,0 +1 @@ +c1af9546bdfa0f32c3c2faf362062cd300800514e5b1efd1fa8a1770753d00e5
\ No newline at end of file diff --git a/db/schema_migrations/20220106112043 b/db/schema_migrations/20220106112043 new file mode 100644 index 00000000000..34c8c5152da --- /dev/null +++ b/db/schema_migrations/20220106112043 @@ -0,0 +1 @@ +8b51ae2b13066a56d2131efb7ea746335513031e751fb231e43121552d6f2b1d
\ No newline at end of file diff --git a/db/schema_migrations/20220106112085 b/db/schema_migrations/20220106112085 new file mode 100644 index 00000000000..171f893a0ab --- /dev/null +++ b/db/schema_migrations/20220106112085 @@ -0,0 +1 @@ +f385631d0317630661d487011a228501a6cbc71de25ca457d75e6a815c598045
\ No newline at end of file diff --git a/db/schema_migrations/20220106163326 b/db/schema_migrations/20220106163326 new file mode 100644 index 00000000000..dbfb9079dc1 --- /dev/null +++ b/db/schema_migrations/20220106163326 @@ -0,0 +1 @@ +4726d84ff42e64b1c47c5ba454ff5be05f434a86bb2af4bbe27dd00e5e3da5cb
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 6e0d49078e2..085ce257403 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -45,6 +45,39 @@ RETURN NULL; END $$; +CREATE FUNCTION insert_or_update_vulnerability_reads() RETURNS trigger + LANGUAGE plpgsql + AS $$ +DECLARE + severity smallint; + state smallint; + report_type smallint; + resolved_on_default_branch boolean; +BEGIN + IF (NEW.vulnerability_id IS NULL AND (TG_OP = 'INSERT' OR TG_OP = 'UPDATE')) THEN + RETURN NULL; + END IF; + + IF (TG_OP = 'UPDATE' AND OLD.vulnerability_id IS NOT NULL AND NEW.vulnerability_id IS NOT NULL) THEN + RETURN NULL; + END IF; + + SELECT + vulnerabilities.severity, vulnerabilities.state, vulnerabilities.report_type, vulnerabilities.resolved_on_default_branch + INTO + severity, state, report_type, resolved_on_default_branch + FROM + vulnerabilities + WHERE + vulnerabilities.id = NEW.vulnerability_id; + + INSERT INTO vulnerability_reads (vulnerability_id, project_id, scanner_id, report_type, severity, state, resolved_on_default_branch, uuid, location_image, cluster_agent_id) + VALUES (NEW.vulnerability_id, NEW.project_id, NEW.scanner_id, report_type, severity, state, resolved_on_default_branch, NEW.uuid::uuid, NEW.location->>'image', NEW.location->'kubernetes_resource'->>'agent_id') + ON CONFLICT(vulnerability_id) DO NOTHING; + RETURN NULL; +END +$$; + CREATE FUNCTION insert_projects_sync_event() RETURNS trigger LANGUAGE plpgsql AS $$ @@ -107,6 +140,83 @@ RETURN NULL; END $$; +CREATE FUNCTION set_has_issues_on_vulnerability_reads() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN +UPDATE + vulnerability_reads +SET + has_issues = true +WHERE + vulnerability_id = NEW.vulnerability_id AND has_issues IS FALSE; +RETURN NULL; + +END +$$; + +CREATE FUNCTION unset_has_issues_on_vulnerability_reads() RETURNS trigger + LANGUAGE plpgsql + AS $$ +DECLARE + has_issue_links integer; +BEGIN + PERFORM 1 + FROM + vulnerability_reads + WHERE + vulnerability_id = OLD.vulnerability_id + FOR UPDATE; + + SELECT 1 INTO has_issue_links FROM vulnerability_issue_links WHERE vulnerability_id = OLD.vulnerability_id LIMIT 1; + + IF (has_issue_links = 1) THEN + RETURN NULL; + END IF; + + UPDATE + vulnerability_reads + SET + has_issues = false + WHERE + vulnerability_id = OLD.vulnerability_id; + + RETURN NULL; +END +$$; + +CREATE FUNCTION update_location_from_vulnerability_occurrences() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN +UPDATE + vulnerability_reads +SET + location_image = NEW.location->>'image', + cluster_agent_id = NEW.location->'kubernetes_resource'->>'agent_id' +WHERE + vulnerability_id = NEW.vulnerability_id; +RETURN NULL; + +END +$$; + +CREATE FUNCTION update_vulnerability_reads_from_vulnerability() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN +UPDATE + vulnerability_reads +SET + severity = NEW.severity, + state = NEW.state, + resolved_on_default_branch = NEW.resolved_on_default_branch +WHERE vulnerability_id = NEW.id; +RETURN NULL; + +END +$$; + CREATE TABLE audit_events ( id bigint NOT NULL, author_id integer NOT NULL, @@ -29163,6 +29273,8 @@ CREATE TRIGGER trigger_has_external_wiki_on_type_new_updated AFTER UPDATE OF typ CREATE TRIGGER trigger_has_external_wiki_on_update AFTER UPDATE ON integrations FOR EACH ROW WHEN (((new.type_new = 'Integrations::ExternalWiki'::text) AND (old.active <> new.active) AND (new.project_id IS NOT NULL))) EXECUTE FUNCTION set_has_external_wiki(); +CREATE TRIGGER trigger_insert_or_update_vulnerability_reads_from_occurrences AFTER INSERT OR UPDATE ON vulnerability_occurrences FOR EACH ROW EXECUTE FUNCTION insert_or_update_vulnerability_reads(); + CREATE TRIGGER trigger_namespaces_parent_id_on_insert AFTER INSERT ON namespaces FOR EACH ROW EXECUTE FUNCTION insert_namespaces_sync_event(); CREATE TRIGGER trigger_namespaces_parent_id_on_update AFTER UPDATE ON namespaces FOR EACH ROW WHEN ((old.parent_id IS DISTINCT FROM new.parent_id)) EXECUTE FUNCTION insert_namespaces_sync_event(); @@ -29173,6 +29285,14 @@ CREATE TRIGGER trigger_projects_parent_id_on_update AFTER UPDATE ON projects FOR CREATE TRIGGER trigger_type_new_on_insert AFTER INSERT ON integrations FOR EACH ROW EXECUTE FUNCTION integrations_set_type_new(); +CREATE TRIGGER trigger_update_has_issues_on_vulnerability_issue_links_delete AFTER DELETE ON vulnerability_issue_links FOR EACH ROW EXECUTE FUNCTION unset_has_issues_on_vulnerability_reads(); + +CREATE TRIGGER trigger_update_has_issues_on_vulnerability_issue_links_update AFTER INSERT ON vulnerability_issue_links FOR EACH ROW EXECUTE FUNCTION set_has_issues_on_vulnerability_reads(); + +CREATE TRIGGER trigger_update_location_on_vulnerability_occurrences_update AFTER UPDATE ON vulnerability_occurrences FOR EACH ROW WHEN (((new.report_type = ANY (ARRAY[2, 7])) AND (((old.location ->> 'image'::text) IS DISTINCT FROM (new.location ->> 'image'::text)) OR (((old.location -> 'kubernetes_resource'::text) ->> 'agent_id'::text) IS DISTINCT FROM ((new.location -> 'kubernetes_resource'::text) ->> 'agent_id'::text))))) EXECUTE FUNCTION update_location_from_vulnerability_occurrences(); + +CREATE TRIGGER trigger_update_vulnerability_reads_on_vulnerability_update AFTER UPDATE ON vulnerabilities FOR EACH ROW WHEN (((old.severity IS DISTINCT FROM new.severity) OR (old.state IS DISTINCT FROM new.state) OR (old.resolved_on_default_branch IS DISTINCT FROM new.resolved_on_default_branch))) EXECUTE FUNCTION update_vulnerability_reads_from_vulnerability(); + CREATE TRIGGER users_loose_fk_trigger AFTER DELETE ON users REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records(); ALTER TABLE ONLY chat_names diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md index da304b7154b..c57d24986c6 100644 --- a/doc/update/deprecations.md +++ b/doc/update/deprecations.md @@ -721,6 +721,18 @@ The `merged_by` field in the [merge request API](https://docs.gitlab.com/ee/api/ ## 14.8 +### Querying Usage Trends via the `instanceStatisticsMeasurements` GraphQL node + +WARNING: +This feature will be changed or removed in 15.0 +as a [breaking change](https://docs.gitlab.com/ee/development/contributing/#breaking-changes). +Before updating GitLab, review the details carefully to determine if you need to make any +changes to your code, settings, or workflow. + +The `instanceStatisticsMeasurements` GraphQL node has been renamed to `usageTrendsMeasurements` in 13.10 and the old field name has been marked as deprecated. To fix the existing GraphQL queries, replace `instanceStatisticsMeasurements` with `usageTrendsMeasurements`. + +**Planned removal milestone: 15.0 (2022-05-22)** + ### REST and GraphQL API Runner usage of `active` replaced by `paused` WARNING: diff --git a/doc/user/project/working_with_projects.md b/doc/user/project/working_with_projects.md index bfc1097d5b4..6e62e533b1e 100644 --- a/doc/user/project/working_with_projects.md +++ b/doc/user/project/working_with_projects.md @@ -297,16 +297,16 @@ To delete a project: 1. Select **Delete project** 1. Confirm this action by completing the field. -## Projects pending deletion **(PREMIUM SELF)** +## Projects pending deletion **(PREMIUM)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37014) in GitLab 13.3 for Administrators. > - [Tab renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/347468) from **Deleted projects** in GitLab 14.6. -> - [Available to all users](https://gitlab.com/gitlab-org/gitlab/-/issues/346976) in GitLab 14.8 [with a flag](../../administration/feature_flags.md) named `project_owners_list_project_pending_deletion`. Disabled by default. +> - [Available to all users](https://gitlab.com/gitlab-org/gitlab/-/issues/346976) in GitLab 14.8 [with a flag](../../administration/feature_flags.md) named `project_owners_list_project_pending_deletion`. Enabled by default. FLAG: -On self-managed GitLab, by default this feature is available to administrators only. To make it available to all users, -ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `project_owners_list_project_pending_deletion`. -On GitLab.com, this feature is available to GitLab.com administrators only. The feature being used by all users is not ready for production use. +On self-managed GitLab, by default this feature is available to all users. To make it available for administrators only, +ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `project_owners_list_project_pending_deletion`. +On GitLab.com, this feature is available to all users. When delayed project deletion is [enabled for a group](../group/index.md#enable-delayed-project-deletion), projects within that group are not deleted immediately, but only after a delay. To access a list of all projects that are pending deletion: diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 62171528695..a82c5681911 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -132,6 +132,29 @@ RSpec.describe GroupsController, factory_default: :keep do end end end + + describe 'require_verification_for_namespace_creation experiment', :experiment do + before do + sign_in(owner) + stub_experiments(require_verification_for_namespace_creation: :candidate) + end + + it 'tracks a "start_create_group" event' do + expect(experiment(:require_verification_for_namespace_creation)).to track( + :start_create_group + ).on_next_instance.with_context(user: owner) + + get :new + end + + context 'when creating a sub-group' do + it 'does not track a "start_create_group" event' do + expect(experiment(:require_verification_for_namespace_creation)).not_to track(:start_create_group) + + get :new, params: { parent_id: group.id } + end + end + end end describe 'GET #activity' do diff --git a/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb b/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb index 87417fe1637..269b6222020 100644 --- a/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb +++ b/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb @@ -5,7 +5,8 @@ require 'spec_helper' RSpec.describe RequireVerificationForNamespaceCreationExperiment, :experiment do subject(:experiment) { described_class.new(user: user) } - let_it_be(:user) { create(:user) } + let(:user_created_at) { RequireVerificationForNamespaceCreationExperiment::EXPERIMENT_START_DATE + 1.hour } + let(:user) { create(:user, created_at: user_created_at) } describe '#candidate?' do context 'when experiment subject is candidate' do @@ -56,4 +57,21 @@ RSpec.describe RequireVerificationForNamespaceCreationExperiment, :experiment do end end end + + describe 'exclusions' do + context 'when user is new' do + it 'is not excluded' do + expect(subject).not_to exclude(user: user) + end + end + + context 'when user is NOT new' do + let(:user_created_at) { RequireVerificationForNamespaceCreationExperiment::EXPERIMENT_START_DATE - 1.day } + let(:user) { create(:user, created_at: user_created_at) } + + it 'is excluded' do + expect(subject).to exclude(user: user) + end + end + end end diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap index 7cdf21dde46..a6f3e00fde1 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap @@ -3,6 +3,7 @@ exports[`packages_list_app renders 1`] = ` <div> <infrastructure-title-stub + count="1" helpurl="foo" /> diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js index b0e586f189a..72d08d5683b 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js @@ -10,7 +10,9 @@ describe('Infrastructure Title', () => { const findTitleArea = () => wrapper.find(TitleArea); const findMetadataItem = () => wrapper.find(MetadataItem); - const mountComponent = (propsData = { helpUrl: 'foo' }) => { + const exampleProps = { helpUrl: 'http://example.gitlab.com/help' }; + + const mountComponent = (propsData = exampleProps) => { wrapper = shallowMount(component, { store, propsData, @@ -26,23 +28,36 @@ describe('Infrastructure Title', () => { }); describe('title area', () => { - it('exists', () => { + beforeEach(() => { mountComponent(); + }); + it('exists', () => { expect(findTitleArea().exists()).toBe(true); }); - it('has the correct props', () => { - mountComponent(); + it('has the correct title', () => { + expect(findTitleArea().props('title')).toBe('Infrastructure Registry'); + }); + + describe('with no modules', () => { + it('has no info message', () => { + expect(findTitleArea().props('infoMessages')).toStrictEqual([]); + }); + }); + + describe('with at least one module', () => { + beforeEach(() => { + mountComponent({ ...exampleProps, count: 1 }); + }); - expect(findTitleArea().props()).toMatchObject({ - title: 'Infrastructure Registry', - infoMessages: [ + it('has an info message', () => { + expect(findTitleArea().props('infoMessages')).toStrictEqual([ { text: 'Publish and share your modules. %{docLinkStart}More information%{docLinkEnd}', - link: 'foo', + link: exampleProps.helpUrl, }, - ], + ]); }); }); }); @@ -51,15 +66,15 @@ describe('Infrastructure Title', () => { count | exist | text ${null} | ${false} | ${''} ${undefined} | ${false} | ${''} - ${0} | ${true} | ${'0 Modules'} + ${0} | ${false} | ${''} ${1} | ${true} | ${'1 Module'} ${2} | ${true} | ${'2 Modules'} `('when count is $count metadata item', ({ count, exist, text }) => { beforeEach(() => { - mountComponent({ count, helpUrl: 'foo' }); + mountComponent({ ...exampleProps, count }); }); - it(`is ${exist} that it exists`, () => { + it(exist ? 'exists' : 'does not exist', () => { expect(findMetadataItem().exists()).toBe(exist); }); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js index 4c536b6d56a..31616e0b2f5 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js @@ -35,7 +35,7 @@ describe('packages_list_app', () => { const findListComponent = () => wrapper.find(PackageList); const findInfrastructureSearch = () => wrapper.find(InfrastructureSearch); - const createStore = (filter = []) => { + const createStore = ({ filter = [], packageCount = 0 } = {}) => { store = new Vuex.Store({ state: { isLoading: false, @@ -46,6 +46,9 @@ describe('packages_list_app', () => { packageHelpUrl: 'foo', }, filter, + pagination: { + total: packageCount, + }, }, }); store.dispatch = jest.fn(); @@ -68,6 +71,7 @@ describe('packages_list_app', () => { beforeEach(() => { createStore(); jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue({}); + mountComponent(); }); afterEach(() => { @@ -75,30 +79,26 @@ describe('packages_list_app', () => { }); it('renders', () => { + createStore({ packageCount: 1 }); mountComponent(); + expect(wrapper.element).toMatchSnapshot(); }); - it('call requestPackagesList on page:changed', () => { - mountComponent(); - store.dispatch.mockClear(); - + it('calls requestPackagesList on page:changed', () => { const list = findListComponent(); list.vm.$emit('page:changed', 1); expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList', { page: 1 }); }); - it('call requestDeletePackage on package:delete', () => { - mountComponent(); - + it('calls requestDeletePackage on package:delete', () => { const list = findListComponent(); list.vm.$emit('package:delete', 'foo'); + expect(store.dispatch).toHaveBeenCalledWith('requestDeletePackage', 'foo'); }); - it('does call requestPackagesList only one time on render', () => { - mountComponent(); - + it('calls requestPackagesList only once on render', () => { expect(store.dispatch).toHaveBeenCalledTimes(3); expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', expect.any(Object)); expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', expect.any(Array)); @@ -113,9 +113,12 @@ describe('packages_list_app', () => { orderBy: 'created', }; - it('calls setSorting with the query string based sorting', () => { + beforeEach(() => { + createStore(); jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue(defaultQueryParamsMock); + }); + it('calls setSorting with the query string based sorting', () => { mountComponent(); expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', { @@ -125,8 +128,6 @@ describe('packages_list_app', () => { }); it('calls setFilter with the query string based filters', () => { - jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue(defaultQueryParamsMock); - mountComponent(); expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', [ @@ -150,8 +151,6 @@ describe('packages_list_app', () => { describe('empty state', () => { it('generate the correct empty list link', () => { - mountComponent(); - const link = findListComponent().find(GlLink); expect(link.attributes('href')).toBe(emptyListHelpUrl); @@ -159,8 +158,6 @@ describe('packages_list_app', () => { }); it('includes the right content on the default tab', () => { - mountComponent(); - const heading = findEmptyState().find('h1'); expect(heading.text()).toBe('There are no packages yet'); @@ -169,7 +166,7 @@ describe('packages_list_app', () => { describe('filter without results', () => { beforeEach(() => { - createStore([{ type: 'something' }]); + createStore({ filter: [{ type: 'something' }] }); mountComponent(); }); @@ -181,20 +178,30 @@ describe('packages_list_app', () => { }); }); - describe('Search', () => { - it('exists', () => { - mountComponent(); - - expect(findInfrastructureSearch().exists()).toBe(true); + describe('search', () => { + describe('with no packages', () => { + it('does not exist', () => { + expect(findInfrastructureSearch().exists()).toBe(false); + }); }); - it('on update fetches data from the store', () => { - mountComponent(); - store.dispatch.mockClear(); + describe('with packages', () => { + beforeEach(() => { + createStore({ packageCount: 1 }); + mountComponent(); + }); - findInfrastructureSearch().vm.$emit('update'); + it('exists', () => { + expect(findInfrastructureSearch().exists()).toBe(true); + }); - expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList'); + it('on update fetches data from the store', () => { + store.dispatch.mockClear(); + + findInfrastructureSearch().vm.$emit('update'); + + expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList'); + }); }); }); diff --git a/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb b/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb index 7214225c32c..f6f4a3f6115 100644 --- a/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb +++ b/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb @@ -87,7 +87,7 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveDuplicateVulnerabilitiesFindin let!(:unrelated_finding) do create_finding!( id: 9999999, - uuid: "unreleated_finding", + uuid: Gitlab::UUID.v5(SecureRandom.hex), vulnerability_id: nil, report_type: 1, location_fingerprint: 'random_location_fingerprint', diff --git a/spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb b/spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb new file mode 100644 index 00000000000..3e450546315 --- /dev/null +++ b/spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb @@ -0,0 +1,151 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe AddInsertOrUpdateVulnerabilityReadsTrigger do + let(:migration) { described_class.new } + let(:vulnerabilities) { table(:vulnerabilities) } + let(:vulnerability_reads) { table(:vulnerability_reads) } + let(:vulnerabilities_findings) { table(:vulnerability_occurrences) } + let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } + let(:user) { table(:users).create!(id: 13, email: 'author@example.com', username: 'author', projects_limit: 10) } + let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) } + let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') } + + let(:vulnerability) do + create_vulnerability!( + project_id: project.id, + author_id: user.id + ) + end + + let(:vulnerability2) do + create_vulnerability!( + project_id: project.id, + author_id: user.id + ) + end + + let(:identifier) do + table(:vulnerability_identifiers).create!( + project_id: project.id, + external_type: 'uuid-v5', + external_id: 'uuid-v5', + fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a', + name: 'Identifier for UUIDv5') + end + + let(:finding) do + create_finding!( + project_id: project.id, + scanner_id: scanner.id, + primary_identifier_id: identifier.id + ) + end + + describe '#up' do + before do + migrate! + end + + describe 'UPDATE trigger' do + context 'when vulnerability_id is updated' do + it 'creates a new vulnerability_reads row' do + expect do + finding.update!(vulnerability_id: vulnerability.id) + end.to change { vulnerability_reads.count }.from(0).to(1) + end + end + + context 'when vulnerability_id is not updated' do + it 'does not create a new vulnerability_reads row' do + finding.update!(vulnerability_id: nil) + + expect do + finding.update!(location: '') + end.not_to change { vulnerability_reads.count } + end + end + end + + describe 'INSERT trigger' do + context 'when vulnerability_id is set' do + it 'creates a new vulnerability_reads row' do + expect do + create_finding!( + vulnerability_id: vulnerability2.id, + project_id: project.id, + scanner_id: scanner.id, + primary_identifier_id: identifier.id + ) + end.to change { vulnerability_reads.count }.from(0).to(1) + end + end + + context 'when vulnerability_id is not set' do + it 'does not create a new vulnerability_reads row' do + expect do + create_finding!( + project_id: project.id, + scanner_id: scanner.id, + primary_identifier_id: identifier.id + ) + end.not_to change { vulnerability_reads.count } + end + end + end + end + + describe '#down' do + before do + migration.up + migration.down + end + + it 'drops the trigger' do + expect do + finding.update!(vulnerability_id: vulnerability.id) + end.not_to change { vulnerability_reads.count } + end + end + + private + + def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0) + vulnerabilities.create!( + project_id: project_id, + author_id: author_id, + title: title, + severity: severity, + confidence: confidence, + report_type: report_type + ) + end + + # rubocop:disable Metrics/ParameterLists + def create_finding!( + vulnerability_id: nil, project_id:, scanner_id:, primary_identifier_id:, + name: "test", severity: 7, confidence: 7, report_type: 0, + project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test', + metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid) + vulnerabilities_findings.create!( + vulnerability_id: vulnerability_id, + project_id: project_id, + name: name, + severity: severity, + confidence: confidence, + report_type: report_type, + project_fingerprint: project_fingerprint, + scanner_id: scanner_id, + primary_identifier_id: primary_identifier_id, + location: location, + location_fingerprint: location_fingerprint, + metadata_version: metadata_version, + raw_metadata: raw_metadata, + uuid: uuid + ) + end + # rubocop:enable Metrics/ParameterLists +end diff --git a/spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb b/spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb new file mode 100644 index 00000000000..d988b1e42b9 --- /dev/null +++ b/spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe AddUpdateVulnerabilityReadsTrigger do + let(:migration) { described_class.new } + let(:vulnerability_reads) { table(:vulnerability_reads) } + let(:issue_links) { table(:vulnerability_issue_links) } + let(:vulnerabilities) { table(:vulnerabilities) } + let(:vulnerabilities_findings) { table(:vulnerability_occurrences) } + + let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } + let(:user) { table(:users).create!(id: 13, email: 'author@example.com', username: 'author', projects_limit: 10) } + let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) } + let(:issue) { table(:issues).create!(description: '1234', state_id: 1, project_id: project.id) } + let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') } + + let(:vulnerability) do + create_vulnerability!( + project_id: project.id, + report_type: 7, + author_id: user.id + ) + end + + let(:identifier) do + table(:vulnerability_identifiers).create!( + project_id: project.id, + external_type: 'uuid-v5', + external_id: 'uuid-v5', + fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a', + name: 'Identifier for UUIDv5') + end + + describe '#up' do + before do + migrate! + end + + describe 'UPDATE trigger' do + before do + create_finding!( + vulnerability_id: vulnerability.id, + project_id: project.id, + scanner_id: scanner.id, + report_type: 7, + primary_identifier_id: identifier.id + ) + end + + context 'when vulnerability attributes are updated' do + it 'updates vulnerability attributes in vulnerability_reads' do + expect do + vulnerability.update!(severity: 6) + end.to change { vulnerability_reads.first.severity }.from(7).to(6) + end + end + + context 'when vulnerability attributes are not updated' do + it 'does not update vulnerability attributes in vulnerability_reads' do + expect do + vulnerability.update!(title: "New vulnerability") + end.not_to change { vulnerability_reads.first } + end + end + end + end + + describe '#down' do + before do + migration.up + migration.down + create_finding!( + vulnerability_id: vulnerability.id, + project_id: project.id, + scanner_id: scanner.id, + report_type: 7, + primary_identifier_id: identifier.id + ) + end + + it 'drops the trigger' do + expect do + vulnerability.update!(severity: 6) + end.not_to change { vulnerability_reads.first.severity } + end + end + + private + + def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0) + vulnerabilities.create!( + project_id: project_id, + author_id: author_id, + title: title, + severity: severity, + confidence: confidence, + report_type: report_type + ) + end + + # rubocop:disable Metrics/ParameterLists + def create_finding!( + vulnerability_id: nil, project_id:, scanner_id:, primary_identifier_id:, + name: "test", severity: 7, confidence: 7, report_type: 0, + project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test', + metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid) + vulnerabilities_findings.create!( + vulnerability_id: vulnerability_id, + project_id: project_id, + name: name, + severity: severity, + confidence: confidence, + report_type: report_type, + project_fingerprint: project_fingerprint, + scanner_id: scanner_id, + primary_identifier_id: primary_identifier_id, + location: location, + location_fingerprint: location_fingerprint, + metadata_version: metadata_version, + raw_metadata: raw_metadata, + uuid: uuid + ) + end + # rubocop:enable Metrics/ParameterLists +end diff --git a/spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb b/spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb new file mode 100644 index 00000000000..901f1cf6041 --- /dev/null +++ b/spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe AddUpdateVulnerabilityReadsLocationTrigger do + let(:migration) { described_class.new } + let(:vulnerability_reads) { table(:vulnerability_reads) } + let(:issue_links) { table(:vulnerability_issue_links) } + let(:vulnerabilities) { table(:vulnerabilities) } + let(:vulnerabilities_findings) { table(:vulnerability_occurrences) } + + let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } + let(:user) { table(:users).create!(id: 13, email: 'author@example.com', username: 'author', projects_limit: 10) } + let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) } + let(:issue) { table(:issues).create!(description: '1234', state_id: 1, project_id: project.id) } + let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') } + + let(:vulnerability) do + create_vulnerability!( + project_id: project.id, + report_type: 7, + author_id: user.id + ) + end + + let(:identifier) do + table(:vulnerability_identifiers).create!( + project_id: project.id, + external_type: 'uuid-v5', + external_id: 'uuid-v5', + fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a', + name: 'Identifier for UUIDv5') + end + + describe '#up' do + before do + migrate! + end + + describe 'UPDATE trigger' do + context 'when image is updated' do + it 'updates location_image in vulnerability_reads' do + finding = create_finding!( + vulnerability_id: vulnerability.id, + project_id: project.id, + scanner_id: scanner.id, + report_type: 7, + location: { "image" => "alpine:3.4" }, + primary_identifier_id: identifier.id + ) + + expect do + finding.update!(location: { "image" => "alpine:4", "kubernetes_resource" => { "agent_id" => "1234" } }) + end.to change { vulnerability_reads.first.location_image }.from("alpine:3.4").to("alpine:4") + end + end + + context 'when image is not updated' do + it 'updates location_image in vulnerability_reads' do + finding = create_finding!( + vulnerability_id: vulnerability.id, + project_id: project.id, + scanner_id: scanner.id, + report_type: 7, + location: { "image" => "alpine:3.4", "kubernetes_resource" => { "agent_id" => "1234" } }, + primary_identifier_id: identifier.id + ) + + expect do + finding.update!(project_fingerprint: "123qweasdzx") + end.not_to change { vulnerability_reads.first.location_image } + end + end + end + end + + describe '#down' do + before do + migration.up + migration.down + end + + it 'drops the trigger' do + finding = create_finding!( + vulnerability_id: vulnerability.id, + project_id: project.id, + scanner_id: scanner.id, + primary_identifier_id: identifier.id + ) + + expect do + finding.update!(location: '{"image":"alpine:4"}') + end.not_to change { vulnerability_reads.first.location_image } + end + end + + private + + def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0) + vulnerabilities.create!( + project_id: project_id, + author_id: author_id, + title: title, + severity: severity, + confidence: confidence, + report_type: report_type + ) + end + + # rubocop:disable Metrics/ParameterLists + def create_finding!( + vulnerability_id: nil, project_id:, scanner_id:, primary_identifier_id:, + name: "test", severity: 7, confidence: 7, report_type: 0, + project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test', + metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid) + vulnerabilities_findings.create!( + vulnerability_id: vulnerability_id, + project_id: project_id, + name: name, + severity: severity, + confidence: confidence, + report_type: report_type, + project_fingerprint: project_fingerprint, + scanner_id: scanner_id, + primary_identifier_id: primary_identifier_id, + location: location, + location_fingerprint: location_fingerprint, + metadata_version: metadata_version, + raw_metadata: raw_metadata, + uuid: uuid + ) + end + # rubocop:enable Metrics/ParameterLists +end diff --git a/spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb b/spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb new file mode 100644 index 00000000000..8e50b74eb9c --- /dev/null +++ b/spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe AddHasIssuesOnVulnerabilityReadsTrigger do + let(:migration) { described_class.new } + let(:vulnerability_reads) { table(:vulnerability_reads) } + let(:issue_links) { table(:vulnerability_issue_links) } + let(:vulnerabilities) { table(:vulnerabilities) } + let(:vulnerabilities_findings) { table(:vulnerability_occurrences) } + + let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } + let(:user) { table(:users).create!(id: 13, email: 'author@example.com', username: 'author', projects_limit: 10) } + let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) } + let(:issue) { table(:issues).create!(description: '1234', state_id: 1, project_id: project.id) } + let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') } + + let(:vulnerability) do + create_vulnerability!( + project_id: project.id, + author_id: user.id + ) + end + + let(:identifier) do + table(:vulnerability_identifiers).create!( + project_id: project.id, + external_type: 'uuid-v5', + external_id: 'uuid-v5', + fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a', + name: 'Identifier for UUIDv5') + end + + before do + create_finding!( + vulnerability_id: vulnerability.id, + project_id: project.id, + scanner_id: scanner.id, + primary_identifier_id: identifier.id + ) + + @vulnerability_read = vulnerability_reads.first + end + + describe '#up' do + before do + migrate! + end + + describe 'INSERT trigger' do + it 'updates has_issues in vulnerability_reads' do + expect do + issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue.id) + end.to change { @vulnerability_read.reload.has_issues }.from(false).to(true) + end + end + + describe 'DELETE trigger' do + let(:issue2) { table(:issues).create!(description: '1234', state_id: 1, project_id: project.id) } + + it 'does not change has_issues when there exists another issue' do + issue_link1 = issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue.id) + issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue2.id) + + expect do + issue_link1.delete + end.not_to change { @vulnerability_read.reload.has_issues } + end + + it 'unsets has_issues when all issues are deleted' do + issue_link1 = issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue.id) + issue_link2 = issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue2.id) + + expect do + issue_link1.delete + issue_link2.delete + end.to change { @vulnerability_read.reload.has_issues }.from(true).to(false) + end + end + end + + describe '#down' do + before do + migration.up + migration.down + end + + it 'drops the trigger' do + expect do + issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue.id) + end.not_to change { @vulnerability_read.has_issues } + end + end + + private + + def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0) + vulnerabilities.create!( + project_id: project_id, + author_id: author_id, + title: title, + severity: severity, + confidence: confidence, + report_type: report_type + ) + end + + # rubocop:disable Metrics/ParameterLists + def create_finding!( + vulnerability_id: nil, project_id:, scanner_id:, primary_identifier_id:, + name: "test", severity: 7, confidence: 7, report_type: 0, + project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test', + metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid) + vulnerabilities_findings.create!( + vulnerability_id: vulnerability_id, + project_id: project_id, + name: name, + severity: severity, + confidence: confidence, + report_type: report_type, + project_fingerprint: project_fingerprint, + scanner_id: scanner_id, + primary_identifier_id: primary_identifier_id, + location: location, + location_fingerprint: location_fingerprint, + metadata_version: metadata_version, + raw_metadata: raw_metadata, + uuid: uuid + ) + end + # rubocop:enable Metrics/ParameterLists +end |