Skip to content

Add secondary indexes and Foreign Key to partitioned web_hook_logs

What does this MR do?

Related issue: #323676 (closed)

This is the third step for partitioning the web_hook_logs table (&5558 (closed)), and the last one necessary to bring the schema of the partitioned web_hook_logs (web_hook_logs_part_0c5294f417) on par with the regular web_hook_logs before we can swap them in the next milestone.

It adds the foreign key and the secondary indexes defined for web_hook_logs to the partitioned table.

  • Foreign key added:
    • fk_rails_666826e111 FOREIGN KEY (web_hook_id) REFERENCES web_hooks(id) ON DELETE CASCADE
  • Indexes added:
    • index_web_hook_logs_on_created_at_and_web_hook_id btree (created_at, web_hook_id)
    • index_web_hook_logs_on_web_hook_id btree (web_hook_id)

Issue with Foreign Keys on Partitioned Tables

Starting on PG11, we can add Foreign Keys on Partitioned Tables, referencing other non-partitioned tables.

But there is a catch on all current versions of PostgreSQL (up to the current PG13):

https://www.postgresql.org/docs/11/sql-altertable.html

ADD table_constraint [ NOT VALID ]

[..] Also, foreign key constraints on partitioned tables may not be declared NOT VALID at present.

So, using add_concurrent_foreign_key (even with validate: true) results to an error:

PG::WrongObjectType: ERROR:  cannot add NOT VALID foreign key on partitioned table "web_hook_logs_part_0c5294f417" 
                             referencing relation "web_hooks"
DETAIL:  This feature is not yet supported on partitioned tables.

The reason for that is that we set the constraint to NOT VALID and THEN validate it --> this is not supported for partitioned tables

Also, PostgreSQL will currently ignore NOT VALID constraints on partitions when adding a valid FK to the partitioned table, so they have to also be validated before we can add the final FK. This is the error we get if we try to add a valid FK on a partitioned table while it has been added as a NOT VALID one on the partitions:

-- add_foreign_key(:web_hook_logs_part_0c5294f417, :web_hooks, {:column=>:web_hook_id, :on_delete=>:cascade, :name=>"fk_rails_bb3355782d", :validate=>true})
rake aborted!
StandardError: An error has occurred, all later migrations canceled:

PG::UniqueViolation: ERROR:  duplicate key value violates unique constraint "pg_constraint_conrelid_contypid_conname_index"
DETAIL:  Key (conrelid, contypid, conname)=(8047040, 0, fk_rails_bb3355782d) already exists.

So our only option would be to add the foreign key using add_foreign_key. The problem with that is the known issue with add_foreign_key, as it locks and validates the tables when the FK is added.

For that reason, we have also added a new Partitioning Migration Helper (add_concurrent_partitioned_foreign_key) which:

  • Adds the foreign key first to each partition by using add_concurrent_foreign_key and validating it
  • Once all partitions have a foreign key, add it also to the partitioned table using the simple add_foreign_key helper
  • For the reasons mentioned above, this method does not include an option to delay the validation, we have to enforce validate: true.

With respect to structure.sql, the end result is exactly the same as using add_foreign_key directly on the partitioned table.

Migrations

db:migrate
== 20210412183800 AddPartitionedWebHookLogIndexes: migrating ==================
-- index_name_exists?(:web_hook_logs_part_0c5294f417, "index_web_hook_logs_part_on_created_at_and_web_hook_id")
   -> 0.0007s
-- transaction_open?()
   -> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_000000", [:created_at, :web_hook_id], {:name=>"index_eecfac613f", :algorithm=>:concurrently})
   -> 0.0009s
-- execute("SET statement_timeout TO 0")
   -> 0.0006s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_000000", [:created_at, :web_hook_id], {:name=>"index_eecfac613f", :algorithm=>:concurrently})
   -> 0.0035s
-- execute("RESET ALL")
   -> 0.0007s
-- transaction_open?()
   -> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202104", [:created_at, :web_hook_id], {:name=>"index_5dc0dc60d8", :algorithm=>:concurrently})
   -> 0.0008s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202104", [:created_at, :web_hook_id], {:name=>"index_5dc0dc60d8", :algorithm=>:concurrently})
   -> 0.0026s
-- transaction_open?()
   -> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202105", [:created_at, :web_hook_id], {:name=>"index_b7bc5cf1cc", :algorithm=>:concurrently})
   -> 0.0011s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202105", [:created_at, :web_hook_id], {:name=>"index_b7bc5cf1cc", :algorithm=>:concurrently})
   -> 0.0023s
-- transaction_open?()
   -> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202106", [:created_at, :web_hook_id], {:name=>"index_b7d0090183", :algorithm=>:concurrently})
   -> 0.0007s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202106", [:created_at, :web_hook_id], {:name=>"index_b7d0090183", :algorithm=>:concurrently})
   -> 0.0028s
-- transaction_open?()
   -> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202107", [:created_at, :web_hook_id], {:name=>"index_78c85c2c10", :algorithm=>:concurrently})
   -> 0.0006s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202107", [:created_at, :web_hook_id], {:name=>"index_78c85c2c10", :algorithm=>:concurrently})
   -> 0.0028s
-- transaction_open?()
   -> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202108", [:created_at, :web_hook_id], {:name=>"index_d0cba9e188", :algorithm=>:concurrently})
   -> 0.0006s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202108", [:created_at, :web_hook_id], {:name=>"index_d0cba9e188", :algorithm=>:concurrently})
   -> 0.0027s
-- transaction_open?()
   -> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202109", [:created_at, :web_hook_id], {:name=>"index_b1292a72b5", :algorithm=>:concurrently})
   -> 0.0006s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202109", [:created_at, :web_hook_id], {:name=>"index_b1292a72b5", :algorithm=>:concurrently})
   -> 0.0030s
-- transaction_open?()
   -> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202110", [:created_at, :web_hook_id], {:name=>"index_9cd16f1883", :algorithm=>:concurrently})
   -> 0.0008s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202110", [:created_at, :web_hook_id], {:name=>"index_9cd16f1883", :algorithm=>:concurrently})
   -> 0.0030s
-- add_index(:web_hook_logs_part_0c5294f417, [:created_at, :web_hook_id], {:name=>"index_web_hook_logs_part_on_created_at_and_web_hook_id"})
   -> 0.0031s
-- index_name_exists?(:web_hook_logs_part_0c5294f417, "index_web_hook_logs_part_on_web_hook_id")
   -> 0.0006s
-- transaction_open?()
   -> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_000000", :web_hook_id, {:name=>"index_80bf138a51", :algorithm=>:concurrently})
   -> 0.0012s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_000000", :web_hook_id, {:name=>"index_80bf138a51", :algorithm=>:concurrently})
   -> 0.0033s
-- transaction_open?()
   -> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202104", :web_hook_id, {:name=>"index_d6756f0be7", :algorithm=>:concurrently})
   -> 0.0015s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202104", :web_hook_id, {:name=>"index_d6756f0be7", :algorithm=>:concurrently})
   -> 0.0027s
-- transaction_open?()
   -> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202105", :web_hook_id, {:name=>"index_a0034b7fa9", :algorithm=>:concurrently})
   -> 0.0011s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202105", :web_hook_id, {:name=>"index_a0034b7fa9", :algorithm=>:concurrently})
   -> 0.0028s
-- transaction_open?()
   -> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202106", :web_hook_id, {:name=>"index_b0d3af5ca1", :algorithm=>:concurrently})
   -> 0.0015s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202106", :web_hook_id, {:name=>"index_b0d3af5ca1", :algorithm=>:concurrently})
   -> 0.0036s
-- transaction_open?()
   -> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202107", :web_hook_id, {:name=>"index_37c3fb440a", :algorithm=>:concurrently})
   -> 0.0013s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202107", :web_hook_id, {:name=>"index_37c3fb440a", :algorithm=>:concurrently})
   -> 0.0028s
-- transaction_open?()
   -> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202108", :web_hook_id, {:name=>"index_9325dbf04d", :algorithm=>:concurrently})
   -> 0.0011s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202108", :web_hook_id, {:name=>"index_9325dbf04d", :algorithm=>:concurrently})
   -> 0.0025s
-- transaction_open?()
   -> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202109", :web_hook_id, {:name=>"index_4a63751fa3", :algorithm=>:concurrently})
   -> 0.0014s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202109", :web_hook_id, {:name=>"index_4a63751fa3", :algorithm=>:concurrently})
   -> 0.0029s
-- transaction_open?()
   -> 0.0000s
-- index_exists?("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202110", :web_hook_id, {:name=>"index_fb2dcc655b", :algorithm=>:concurrently})
   -> 0.0017s
-- add_index("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202110", :web_hook_id, {:name=>"index_fb2dcc655b", :algorithm=>:concurrently})
   -> 0.0032s
-- add_index(:web_hook_logs_part_0c5294f417, :web_hook_id, {:name=>"index_web_hook_logs_part_on_web_hook_id"})
   -> 0.0034s
== 20210412183800 AddPartitionedWebHookLogIndexes: migrated (0.1508s) =========

== 20210412183810 AddPartitionedWebHookLogFk: migrating =======================
-- foreign_keys(:web_hook_logs_part_0c5294f417)
   -> 0.0027s
-- transaction_open?()
   -> 0.0000s
-- foreign_keys("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_000000")
   -> 0.0019s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_000000\nADD CONSTRAINT fk_rails_bb3355782d\nFOREIGN KEY (web_hook_id)\nREFERENCES web_hooks (id)\nON DELETE CASCADE\nNOT VALID;\n")
   -> 0.0019s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_000000 VALIDATE CONSTRAINT fk_rails_bb3355782d;")
   -> 0.0017s
-- transaction_open?()
   -> 0.0000s
-- foreign_keys("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202104")
   -> 0.0019s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202104\nADD CONSTRAINT fk_rails_bb3355782d\nFOREIGN KEY (web_hook_id)\nREFERENCES web_hooks (id)\nON DELETE CASCADE\nNOT VALID;\n")
   -> 0.0011s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202104 VALIDATE CONSTRAINT fk_rails_bb3355782d;")
   -> 0.0013s
-- transaction_open?()
   -> 0.0000s
-- foreign_keys("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202105")
   -> 0.0025s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202105\nADD CONSTRAINT fk_rails_bb3355782d\nFOREIGN KEY (web_hook_id)\nREFERENCES web_hooks (id)\nON DELETE CASCADE\nNOT VALID;\n")
   -> 0.0011s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202105 VALIDATE CONSTRAINT fk_rails_bb3355782d;")
   -> 0.0012s
-- transaction_open?()
   -> 0.0000s
-- foreign_keys("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202106")
   -> 0.0017s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202106\nADD CONSTRAINT fk_rails_bb3355782d\nFOREIGN KEY (web_hook_id)\nREFERENCES web_hooks (id)\nON DELETE CASCADE\nNOT VALID;\n")
   -> 0.0009s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202106 VALIDATE CONSTRAINT fk_rails_bb3355782d;")
   -> 0.0013s
-- transaction_open?()
   -> 0.0000s
-- foreign_keys("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202107")
   -> 0.0022s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202107\nADD CONSTRAINT fk_rails_bb3355782d\nFOREIGN KEY (web_hook_id)\nREFERENCES web_hooks (id)\nON DELETE CASCADE\nNOT VALID;\n")
   -> 0.0013s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202107 VALIDATE CONSTRAINT fk_rails_bb3355782d;")
   -> 0.0013s
-- transaction_open?()
   -> 0.0000s
-- foreign_keys("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202108")
   -> 0.0018s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202108\nADD CONSTRAINT fk_rails_bb3355782d\nFOREIGN KEY (web_hook_id)\nREFERENCES web_hooks (id)\nON DELETE CASCADE\nNOT VALID;\n")
   -> 0.0014s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202108 VALIDATE CONSTRAINT fk_rails_bb3355782d;")
   -> 0.0015s
-- transaction_open?()
   -> 0.0000s
-- foreign_keys("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202109")
   -> 0.0021s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202109\nADD CONSTRAINT fk_rails_bb3355782d\nFOREIGN KEY (web_hook_id)\nREFERENCES web_hooks (id)\nON DELETE CASCADE\nNOT VALID;\n")
   -> 0.0010s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202109 VALIDATE CONSTRAINT fk_rails_bb3355782d;")
   -> 0.0013s
-- transaction_open?()
   -> 0.0000s
-- foreign_keys("gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202110")
   -> 0.0019s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202110\nADD CONSTRAINT fk_rails_bb3355782d\nFOREIGN KEY (web_hook_id)\nREFERENCES web_hooks (id)\nON DELETE CASCADE\nNOT VALID;\n")
   -> 0.0010s
-- execute("ALTER TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202110 VALIDATE CONSTRAINT fk_rails_bb3355782d;")
   -> 0.0014s
-- add_foreign_key(:web_hook_logs_part_0c5294f417, :web_hooks, {:column=>:web_hook_id, :on_delete=>:cascade, :name=>"fk_rails_bb3355782d", :validate=>true})
   -> 0.0031s
== 20210412183810 AddPartitionedWebHookLogFk: migrated (0.0873s) ==============
db:rollback
== 20210412183810 AddPartitionedWebHookLogFk: reverting =======================
-- foreign_keys(:web_hook_logs_part_0c5294f417)
   -> 0.0024s
-- remove_foreign_key(:web_hook_logs_part_0c5294f417, {:column=>:web_hook_id})
   -> 0.0036s
== 20210412183810 AddPartitionedWebHookLogFk: reverted (0.0112s) ==============

== 20210412183800 AddPartitionedWebHookLogIndexes: reverting ==================
-- index_name_exists?(:web_hook_logs_part_0c5294f417, "index_web_hook_logs_part_on_web_hook_id")
   -> 0.0009s
-- remove_index(:web_hook_logs_part_0c5294f417, {:name=>"index_web_hook_logs_part_on_web_hook_id"})
   -> 0.0014s
-- index_name_exists?(:web_hook_logs_part_0c5294f417, "index_web_hook_logs_part_on_created_at_and_web_hook_id")
   -> 0.0006s
-- remove_index(:web_hook_logs_part_0c5294f417, {:name=>"index_web_hook_logs_part_on_created_at_and_web_hook_id"})
   -> 0.0014s
== 20210412183800 AddPartitionedWebHookLogIndexes: reverted (0.0423s) =========

Additional Checks that everything went as planned

Double Checking the table schemas:

details
gitlabhq_development=# \d web_hook_logs
                                              Table "public.web_hook_logs"
... ... ...
Indexes:
    "web_hook_logs_pkey" PRIMARY KEY, btree (id)
    "index_web_hook_logs_on_created_at_and_web_hook_id" btree (created_at, web_hook_id)
    "index_web_hook_logs_on_web_hook_id" btree (web_hook_id)
Foreign-key constraints:
    "fk_rails_666826e111" FOREIGN KEY (web_hook_id) REFERENCES web_hooks(id) ON DELETE CASCADE
Triggers:
    table_sync_trigger_b99eb6998c AFTER INSERT OR DELETE OR UPDATE ON web_hook_logs FOR EACH ROW EXECUTE PROCEDURE table_sync_function_29bc99d6db()


gitlabhq_development=# \d web_hook_logs_part_0c5294f417
                     Table "public.web_hook_logs_part_0c5294f417"
... ... ...
Partition key: RANGE (created_at)
Indexes:
    "web_hook_logs_part_0c5294f417_pkey" PRIMARY KEY, btree (id, created_at)
    "index_web_hook_logs_part_on_created_at_and_web_hook_id" btree (created_at, web_hook_id)
    "index_web_hook_logs_part_on_web_hook_id" btree (web_hook_id)
Foreign-key constraints:
    "fk_rails_bb3355782d" FOREIGN KEY (web_hook_id) REFERENCES web_hooks(id) ON DELETE CASCADE


gitlabhq_development=# \d gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202104
        Table "gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202104"
... ... ...
Partition of: web_hook_logs_part_0c5294f417 FOR VALUES FROM ('2021-04-01 00:00:00') TO ('2021-05-01 00:00:00')
Indexes:
    "web_hook_logs_part_0c5294f417_202104_pkey" PRIMARY KEY, btree (id, created_at)
    "index_5dc0dc60d8" btree (created_at, web_hook_id)
    "index_d6756f0be7" btree (web_hook_id)
Foreign-key constraints:
    "fk_rails_bb3355782d" FOREIGN KEY (web_hook_id) REFERENCES web_hooks(id) ON DELETE CASCADE

Adding a new partition:

details
gitlabhq_development=# CREATE TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202111 
                       PARTITION OF web_hook_logs_part_0c5294f417
                       FOR VALUES FROM ('2021-11-01') TO ('2021-12-01');

gitlabhq_development=# \d gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202111
        Table "gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202111"
... ... ...
Partition of: web_hook_logs_part_0c5294f417 FOR VALUES FROM ('2021-11-01 00:00:00') TO ('2021-12-01 00:00:00')
Indexes:
    "web_hook_logs_part_0c5294f417_202111_pkey" PRIMARY KEY, btree (id, created_at)
    "web_hook_logs_part_0c5294f417_202111_created_at_web_hook_id_idx" btree (created_at, web_hook_id)
    "web_hook_logs_part_0c5294f417_202111_web_hook_id_idx" btree (web_hook_id)
Foreign-key constraints:
    "fk_rails_bb3355782d" FOREIGN KEY (web_hook_id) REFERENCES web_hooks(id) ON DELETE CASCADE

Testing the PartitionCreator:

details
gitlabhq_development=# DROP TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202110;
gitlabhq_development=# DROP TABLE gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202111;

pry(main)> Gitlab::Database::Partitioning::PartitionCreator.new.create_partitions
... ... ...
   (5.3ms)  CREATE TABLE IF NOT EXISTS "gitlab_partitions_dynamic"."web_hook_logs_part_0c5294f417_202110"
... ... ...

gitlabhq_development=# \d gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202110
        Table "gitlab_partitions_dynamic.web_hook_logs_part_0c5294f417_202110"
... ... ...
Partition of: web_hook_logs_part_0c5294f417 FOR VALUES FROM ('2021-10-01 00:00:00') TO ('2021-11-01 00:00:00')
Indexes:
    "web_hook_logs_part_0c5294f417_202110_pkey" PRIMARY KEY, btree (id, created_at)
    "web_hook_logs_part_0c5294f417_202110_created_at_web_hook_id_idx" btree (created_at, web_hook_id)
    "web_hook_logs_part_0c5294f417_202110_web_hook_id_idx" btree (web_hook_id)
Foreign-key constraints:
    "fk_rails_bb3355782d" FOREIGN KEY (web_hook_id) REFERENCES web_hooks(id) ON DELETE CASCADE

Does this MR meet the acceptance criteria?

Conformity

Availability and Testing

Security

If this MR contains changes to processing or storing of credentials or tokens, authorization and authentication methods and other items described in the security review guidelines:

  • [-] Label as security and @ mention @gitlab-com/gl-security/appsec
  • [-] The MR includes necessary changes to maintain consistency between UI, API, email, or other methods
  • [-] Security reports checked/validated by a reviewer from the AppSec team
Edited by Yannis Roussos

Merge request reports