From 8136229ad7f3e95f5ba0e924c8d636a94e2f10b7 Mon Sep 17 00:00:00 2001
From: Roger Meier <r.meier@siemens.com>
Date: Fri, 31 May 2024 07:46:21 +0000
Subject: [PATCH] Add option to add custom html header tags via gitlab.yml
 config

In some cases such as adding the EU cookie consent, custom
tags within the html header are needed.

Closes https://gitlab.com/gitlab-org/gitlab/-/issues/444193

Changelog: added
---
 app/views/layouts/_head.html.haml             |  4 ++
 config/gitlab.yml.example                     |  9 +++
 config/initializers/1_settings.rb             |  1 +
 doc/administration/configure.md               |  1 +
 doc/administration/custom_html_header_tags.md | 70 +++++++++++++++++++
 spec/views/layouts/_head.html.haml_spec.rb    | 11 +++
 6 files changed, 96 insertions(+)
 create mode 100644 doc/administration/custom_html_header_tags.md

diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 34858a5220413..73ae623b81b1c 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -111,3 +111,7 @@
   = render_if_exists "layouts/frontend_monitor"
   %meta{ name: "description", content: page_description }
   %meta{ name: 'theme-color', content: user_theme_primary_color }
+
+  - if Gitlab.config.gitlab.respond_to?(:custom_html_header_tags)
+    - unless Gitlab.config.gitlab.custom_html_header_tags.empty?
+      = Gitlab.config.gitlab.custom_html_header_tags.html_safe
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 86457f339b82c..afb7697472e38 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -139,6 +139,15 @@ production: &base
     ##   11 - Dark Mode (alpha)
     # default_theme: 1 # default: 1
 
+    ## Custom html header tags
+    # In some cases some custom header tags are needed
+    # e.g., to add the EU cookie consent
+    # Tip: you must add the externals source to the content_security_policy as
+    #      well, typically the script_src and style_src.
+    # custom_html_header_tags: |
+    #   <script src="https://example.com/cookie-consent.js"></script>
+    #   <link rel="stylesheet" href="https://example.com/cookie-consent.css"/>
+
     ## Automatic issue closing
     # If a commit message matches this regular expression, all issues referenced from the matched text will be closed.
     # This happens when the commit is pushed or merged into the default branch of a project.
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index feda8a86af685..1a37895853c4b 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -187,6 +187,7 @@
 # `default_can_create_group` is deprecated since GitLab 15.5 in favour of the `can_create_group` column on `ApplicationSetting`.
 Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil?
 Settings.gitlab['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil?
+Settings.gitlab['custom_html_header_tags'] ||= Settings.gitlab['custom_html_header_tags'] || ''
 Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost'
 Settings.gitlab['cdn_host'] ||= ENV['GITLAB_CDN_HOST'].presence
 Settings.gitlab['ssh_host'] ||= Settings.gitlab.host
diff --git a/doc/administration/configure.md b/doc/administration/configure.md
index 7ea3b19ea33cc..df6f280e11d01 100644
--- a/doc/administration/configure.md
+++ b/doc/administration/configure.md
@@ -47,3 +47,4 @@ Customize and configure your self-managed GitLab installation.
 - [Issue closing pattern](../administration/issue_closing_pattern.md)
 - [Snippets](../administration/snippets/index.md)
 - [Host the product documentation](../administration/docs_self_host.md)
+- [Custom HTML header tags](../administration/custom_html_header_tags.md)
diff --git a/doc/administration/custom_html_header_tags.md b/doc/administration/custom_html_header_tags.md
new file mode 100644
index 0000000000000..6a8b509411fa9
--- /dev/null
+++ b/doc/administration/custom_html_header_tags.md
@@ -0,0 +1,70 @@
+---
+stage: Govern
+group: Compliance
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
+description: Learn how to modify the HTML header tags of your GitLab instance.
+---
+
+# Custom HTML header tags
+
+DETAILS:
+**Tier:** Free, Premium, Ultimate
+**Offering:** Self-managed
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/153877) in GitLab 17.1.
+
+If you self-manage a GitLab instance in the EU, or any jurisdiction that
+requires a cookie consent banner, additional HTML header tags are needed to
+add scripts and stylesheets.
+
+## Security implications
+
+Before enabling this feature, you should understand the security implications this might have.
+
+A previously legit external resource could end up being compromised and then used to extract
+pretty much any data from any user in the GitLab instance. For that reason,
+you should never add resources from untrusted external sources. If possible, you should always
+use integrity checks like [Subresource Integrity](https://www.w3.org/TR/SRI/) with third-party
+resources to confirm the authenticity of the resources that are loaded.
+
+Limit the functionality you are adding by using HTML header tags to the minimum.
+Otherwise, it could cause also stability or functionality issues if you, for example,
+interact with other application code from GitLab.
+
+## Add a custom HTML header tag
+
+You must add the externals sources to the Content Security Policy which is
+available in the `content_security_policy` option. For the following example, you
+must extend the `script_src` and `style_src`.
+
+To add a custom HTML header tag:
+
+::Tabs
+
+:::TabTitle Self-compiled
+
+1. Edit `/home/git/gitlab/config/gitlab.yml`:
+
+   ```yaml
+   production: &base
+     gitlab:
+       custom_html_header_tags: |
+         <script src="https://example.com/cookie-consent.js" integrity="sha384-Li9vy3DqF8tnTXuiaAJuML3ky+er10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7"         crossorigin="anonymous"></script>
+         <link rel="stylesheet" href="https://example.com/cookie-consent.css" integrity="sha384-+/M6kredJcxdsqkczBUjMLvqyHb1K/JThDXWsBVxMEeZHEaMKEOEct339VItX1zB"        crossorigin="anonymous">
+       content_security_policy:
+         directives:
+           script_src: "'self' 'unsafe-eval' https://example.com http://localhost:* https://www.google.com/recaptcha/ https://www.recaptcha.net/ https://www.gstatic.com/recaptcha/ https://apis.google.com"
+           style_src: "'self' 'unsafe-inline' https://example.com"
+   ```
+
+1. Save the file and restart GitLab:
+
+   ```shell
+   # For systems running systemd
+   sudo systemctl restart gitlab.target
+
+   # For systems running SysV init
+   sudo service gitlab restart
+   ```
+
+::EndTabs
diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb
index 5ef25bdbde42e..992edcf95f321 100644
--- a/spec/views/layouts/_head.html.haml_spec.rb
+++ b/spec/views/layouts/_head.html.haml_spec.rb
@@ -95,6 +95,17 @@
     end
   end
 
+  context 'when custom_html_header_tags are set' do
+    before do
+      allow(Gitlab.config.gitlab).to receive(:custom_html_header_tags).and_return('<script src="https://example.com/cookie-consent.js"></script>')
+    end
+
+    it 'adds the custom html header tag' do
+      render
+      expect(rendered).to match('<script src="https://example.com/cookie-consent.js"></script>')
+    end
+  end
+
   context 'when an asset_host is set and snowplow url is set', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/346542' do
     let(:asset_host) { 'http://test.host' }
     let(:snowplow_collector_hostname) { 'www.snow.plow' }
-- 
GitLab