Skip to content

Commit

Permalink
Merge pull request #2643 from alphagov/webchat-csp
Browse files Browse the repository at this point in the history
Handle webchat CSP modifications in application
  • Loading branch information
kevindew authored Dec 29, 2022
2 parents 1175a1a + 76071c7 commit a83dbed
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 39 deletions.
10 changes: 10 additions & 0 deletions app/controllers/content_items_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ class ContentItemsController < ApplicationController

attr_accessor :content_item, :taxonomy_navigation

content_security_policy do |p|
p.connect_src(*p.connect_src, -> { csp_connect_src })
end

def show
load_content_item

Expand Down Expand Up @@ -212,4 +216,10 @@ def set_account_vary_header
# variation, rather than caching pages per user
response.headers["Vary"] = [response.headers["Vary"], "GOVUK-Account-Session-Exists", "GOVUK-Account-Session-Flash"].compact.join(", ")
end

def csp_connect_src
return if !@content_item || !@content_item.respond_to?(:csp_connect_src)

@content_item.csp_connect_src
end
end
11 changes: 6 additions & 5 deletions app/models/webchat.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ class Webchat

validates :base_path, :open_url, :availability_url, presence: true

attr_reader :base_path, :open_url, :availability_url, :open_url_redirect
attr_reader :base_path, :open_url, :availability_url, :open_url_redirect, :csp_connect_src

def initialize(attrs)
@base_path = attrs["base_path"] if attrs["base_path"].present?
@open_url = attrs["open_url"] if attrs["open_url"].present?
@availability_url = attrs["availability_url"] if attrs["availability_url"].present?
@open_url_redirect = attrs.fetch("open_url_redirect", nil)
@base_path = attrs["base_path"]
@open_url = attrs["open_url"]
@availability_url = attrs["availability_url"]
@open_url_redirect = attrs["open_url_redirect"]
@csp_connect_src = attrs["csp_connect_src"]
validate!
end

Expand Down
4 changes: 4 additions & 0 deletions app/presenters/contact_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ def webchat_body
content_item.dig("details", "more_info_webchat").try(:html_safe)
end

def csp_connect_src
webchat&.csp_connect_src
end

private

def phone_numbers_in_group(group)
Expand Down
49 changes: 15 additions & 34 deletions docs/webchat.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,4 @@
# Webchat Component

This Webchat can represent four 'busy', 'unavailable', 'available', 'error'.

## Supporting backend API Proxy

See the [reference implementation](https://github.com/alphagov/reference-webchat-proxy) for the backend API that is used alongside this component.

## Usage

The Webchat should be accessed through a shared [shared partial](https://github.com/alphagov/government-frontend/blob/main/app/views/shared/_webchat.html.erb) in which you need to pass the locals `webchat_availability_url` and `webchat_open_url`.

`webchat_availability_url` will be polled at an interval to check for the webchat instances' avaliability.


`webchat_open_url` is the page that will be opened when a user clicks the webchat call-to-action shown in the 'avaliable' state.


Finally once you have your partial rendering on the page, you will need to make sure the [library](https://github.com/alphagov/government-frontend/blob/main/app/assets/javascripts/webchat/library.js) is included on your page and this in your initialisation code.

```javascript
var webchats = document.querySelectorAll('.js-webchat')
for (var i = 0; i < webchats.length; i++) {
new GOVUK.Webchat(webchats[i])
}
```
a fuller example can be seen [here](https://github.com/alphagov/government-frontend/blob/main/app/assets/javascripts/webchat.js) that has an implementation of the normalisation as noted below

### Note regarding response Normalisation
As can be seen in the fuller example above, we currently have the option to do the normalisation in JavaScript, this is deprecated, and is shim code until all current users of Wbchat have their own proxies up and running.

Once this shim is removed we can move this component into Static as a GOV.UK Publishing Component
# Webchat

## How to add a new webchat provider

Expand All @@ -39,14 +8,18 @@ Once this shim is removed we can move this component into Static as a GOV.UK Pub
- base_path: /government/contact/my-amazing-service
open_url: https://www.my-amazing-webchat.com/007/open-chat
availability_url: https://www.my-amazing-webchat.com/007/check-availability
csp_connect_src: https://www.my-amazing-webchat.com
```
3. Deploy changes
4. Go to https://www.gov.uk/government/contact/my-amazing-service
5. Finished
## CORS considerations
To avoid CORS and CSP issues a new provider would need to be added to the [Content Security Policy](https://docs.publishing.service.gov.uk/manual/content-security-policy.html)
## Content Security Policy considerations
For a webchat provider to integrate with GOV.UK it needs permissions from the [Content Security Policy](https://docs.publishing.service.gov.uk/manual/content-security-policy.html).
This will be set-up for a provider by the `csp_connect_src` option which configures the `connect-src` directive, however other providers may need additional configuration, such as `script-src`. This configuration should be done in the same manner as `csp_connect_src` to only affect resources that embed webchat.

## Required configuration

Expand Down Expand Up @@ -84,3 +57,11 @@ The default response from the api as used by HMRC webchat provider is JSONP. To
```yaml
availability_payload_format: json
```

### CSP connect-src

Updates the Content Security Policy for pages that embed webchat to grant permission to make requests to the host specified. This should be in the form of a hostname, ideally with a scheme. For more information see, [connect-src](https://content-security-policy.com/connect-src/).

```yaml
csp_connect_src: https://webchat.host
```
1 change: 1 addition & 0 deletions lib/webchat.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
- base_path: /government/organisations/hm-passport-office/contact/hm-passport-office-webchat
open_url: https://omni.eckoh.uk/v03.5/launcherV3.php?p=HMPO&d=717&ch=CH&psk=chat_a1&iid=STC&srbp=0&fcl=0&r=Chat&s=https://omni.eckoh.uk/v03.5&u=&wo=&uh=&pid=2&iif=0
availability_url: https://omni.eckoh.uk/v03.5/providers/HMPO/api/availability.php
csp_connect_src: https://omni.eckoh.uk
open_url_redirect: false
28 changes: 28 additions & 0 deletions test/integration/contact_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,32 @@ class ContactTest < ActionDispatch::IntegrationTest
setup_and_visit_content_item("contact")
assert_not page.has_css?(".gem-c-single-page-notification-button")
end

# Ideally this would be tested at the Controller level however the
# ActionController::TestCase does not integrate with CSP, so tested at a level
# which does.
test "the content security policy is updated for webchat hosts" do
# Need to use Rack as Selenium, the default driver, doesn't provide header access
Capybara.current_driver = :rack_test

webchat = Webchat.new({
"base_path" => "/content",
"open_url" => "https://webchat.host/open",
"availability_url" => "https://webchat.host/avaiable",
"csp_connect_src" => "https://webchat.host",
})

Webchat.stubs(:find).returns(webchat)

setup_and_visit_content_item("contact")
parsed_csp = page.response_headers["Content-Security-Policy"]
.split(";")
.map { |directive| directive.strip.split(" ") }
.each_with_object({}) { |directive, memo| memo[directive.first] = directive[1..] }

assert_includes parsed_csp["connect-src"], "https://webchat.host"

# reset back to default driver
Capybara.use_default_driver
end
end
2 changes: 2 additions & 0 deletions test/models/webchat_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class WebchatTest < ActiveSupport::TestCase
"base_path" => "/government/contact/my-amazing-service",
"open_url" => "https://www.tax.service.gov.uk/csp-partials/open/1023",
"availability_url" => "https://www.tax.service.gov.uk/csp-partials/availability/1023",
"csp_connect_src" => "https://www.tax.service.gov.uk",
}

test "should create instance correctly" do
Expand All @@ -14,6 +15,7 @@ class WebchatTest < ActiveSupport::TestCase
assert_equal(instance.base_path, webchat_config["base_path"])
assert_equal(instance.open_url, webchat_config["open_url"])
assert_equal(instance.availability_url, webchat_config["availability_url"])
assert_equal(instance.csp_connect_src, webchat_config["csp_connect_src"])
end

test "should return error if config is invalid" do
Expand Down
18 changes: 18 additions & 0 deletions test/presenters/contact_presenter_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,5 +99,23 @@ def schema_name
assert_equal presented.webchat.availability_url, availability_url
assert_equal presented.webchat.open_url, open_url
end

test "returns csp_connect_src if a webchat is configured" do
webchat = Webchat.new({
"base_path" => "/content",
"open_url" => "https://webchat.host/open",
"availability_url" => "https://webchat.host/avaiable",
"csp_connect_src" => "https://webchat.host",
})

Webchat.stubs(:find).returns(webchat)
assert_equal "https://webchat.host", presented_item.csp_connect_src
end

test "returns a csp_connect_src of nil if webchat isn't configured" do
Webchat.stubs(:find).returns(nil)

assert_nil presented_item.csp_connect_src
end
end
end

0 comments on commit a83dbed

Please sign in to comment.