Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,9 @@ REDIS_STANDALONE=true
# Dashboard, the admin page, and features under the /preview path.
# BASIC_AUTH_READONLY_USERNAME=
# BASIC_AUTH_READONLY_PASSWORD=

# These credentials enable access to Tableau Cloud content which we are embedding in various pages.
# TABLEAU_USER=
# TABLEAU_CLIENT_ID=
# TABLEAU_SECRET_ID=
# TABLEAU_SECRET_VALUE=
10 changes: 10 additions & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,16 @@ config :dotcom, DotcomWeb.ViewHelpers,

config :dotcom, google_api_key: System.get_env("GOOGLE_API_KEY")

config :joken,
tableau_signer: [
signer_alg: "HS256",
key_octet: System.get_env("TABLEAU_SECRET_VALUE"),
jose_extra_headers: %{
"kid" => System.get_env("TABLEAU_SECRET_ID"),
"iss" => System.get_env("TABLEAU_CLIENT_ID")
}
]

config :recaptcha,
public_key: System.get_env("RECAPTCHA_PUBLIC_KEY"),
secret: System.get_env("RECAPTCHA_PRIVATE_KEY", "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe")
Expand Down
1 change: 1 addition & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ config :dotcom, :redix_pub_sub, Dotcom.Redix.PubSub.Mock
config :dotcom, :otp_module, OpenTripPlannerClient.Mock
config :dotcom, :req_module, Req.Mock

config :dotcom, :tableau_cloud_token_module, TableauCloudToken.Mock
config :dotcom, :trip_plan_feedback_cache, Dotcom.Cache.TestCache

# Let test requests get routed through the :secure pipeline
Expand Down
34 changes: 34 additions & 0 deletions lib/dotcom/content_rewriters/code_embed.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
defmodule Dotcom.ContentRewriters.CodeEmbed do
@moduledoc """
Code embeds should not be modified unless necessary!
"""

@tableau_cloud_token_module Application.compile_env(
:dotcom,
:tableau_cloud_token_module,
TableauCloudToken
)

@spec rewrite(Phoenix.HTML.safe()) :: Phoenix.HTML.safe()
def rewrite({:safe, content}) do
{:ok, parsed} = Floki.parse_fragment(content)

parsed
|> Enum.map(&dispatch_rewrites/1)
|> Floki.raw_html(encode: false)
|> Phoenix.HTML.raw()
end

# Tableau dashboards: add the JWT needed for authentication
@spec dispatch_rewrites(Floki.html_tree()) :: Floki.html_tree()
defp dispatch_rewrites({"tableau-viz", attrs, children}) do
attrs = [{"token", @tableau_cloud_token_module.default_token()} | attrs]
{"tableau-viz", attrs, children}
end

defp dispatch_rewrites({name, attrs, children}) when is_list(children) do
{name, attrs, Enum.map(children, &dispatch_rewrites/1)}
end

defp dispatch_rewrites(element), do: element
end
2 changes: 2 additions & 0 deletions lib/dotcom_web/plugs/content_security_policy.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ defmodule DotcomWeb.Plugs.ContentSecurityPolicy do
'self'
#{@tile_server_url}
*.arcgis.com
*.tableau.com
analytics.google.com
cdn.mbta.com
px.ads.linkedin.com
Expand All @@ -27,6 +28,7 @@ defmodule DotcomWeb.Plugs.ContentSecurityPolicy do
frame_src: ~w[
'self'
*.arcgis.com
*.tableau.com
*.soundcloud.com
*.vimeo.com
cdn.knightlab.com
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@
<%= ContentRewriter.rewrite(@content.header.text, @conn) %>
<% end %>

<%# Output raw CMS source HTML %>
<%= @content.body %>
<%= Dotcom.ContentRewriters.CodeEmbed.rewrite(@content.body) %>
33 changes: 33 additions & 0 deletions lib/tableau_cloud_token.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
defmodule TableauCloudToken do
@moduledoc """
Configuration for using JSON Web Tokens with our Tableau Cloud server.

https://help.tableau.com/current/online/en-us/connected_apps_direct.htm#step-3-configure-the-jwt
"""
use Joken.Config, default_signer: :tableau_signer

@impl Joken.Config
def token_config do
default_claims(
aud: "tableau",
iss: System.get_env("TABLEAU_CLIENT_ID"),
default_exp: 300
)
|> add_claim("kid", fn -> System.get_env("TABLEAU_SECRET_ID") end)
|> add_claim("sub", fn -> System.get_env("TABLEAU_USER") end)
|> add_claim("scp", fn -> ["tableau:views:embed", "tableau:metrics:embed"] end)
end

@behaviour __MODULE__
@callback default_token() :: Joken.bearer_token() | {:error, Joken.error_reason()}
@doc """
Returns a valid token for using with embedding Tableau Cloud visualizations
"""
@impl __MODULE__
def default_token do
with {:ok, token, _} <- generate_and_sign(),
{:ok, _claims} <- verify_and_validate(token) do
token
end
end
end
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ defmodule DotCom.Mixfile do
{:httpoison, "2.2.2"},
{:inflex, "2.1.0"},
{:jason, "1.4.4", override: true},
{:joken, "2.6.2"},
{:kino_live_component, "0.0.5"},
{:logster, "1.1.1"},
# reverted from 0.4
Expand Down
2 changes: 2 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
"inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"},
"iso8601": {:hex, :iso8601, "1.3.4", "7b1f095f86f6cf65e1e5a77872e8e8bf69bd58d4c3a415b3f77d9cc9423ecbb9", [:rebar3], [], "hexpm", "a334469c07f1c219326bc891a95f5eec8eb12dd8071a3fff56a7843cb20fae34"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"},
"jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"},
"jsx": {:hex, :jsx, "3.1.0", "d12516baa0bb23a59bb35dccaf02a1bd08243fcbb9efe24f2d9d056ccff71268", [:rebar3], [], "hexpm", "0c5cc8fdc11b53cc25cf65ac6705ad39e54ecc56d1c22e4adb8f5a53fb9427f3"},
"kino": {:hex, :kino, "0.15.3", "c99e21fc3e5d89513120295b91efc3efd18f7c1fb83875edced9d06ada13a2c0", [:mix], [{:fss, "~> 0.1.0", [hex: :fss, repo: "hexpm", optional: false]}, {:nx, "~> 0.1", [hex: :nx, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:table, "~> 0.1.2", [hex: :table, repo: "hexpm", optional: false]}], "hexpm", "11f62457ce6ac97ad377db9fcde168361fcf0de7db2a47b6f570607dc7897753"},
"kino_live_component": {:hex, :kino_live_component, "0.0.5", "0d9a222b296a568dce6db646219800ddae75e4e7bc8bfb70e7d4d7e759321d19", [:mix], [{:bandit, "~> 1.6", [hex: :bandit, repo: "hexpm", optional: false]}, {:cors_plug, ">= 3.0.0", [hex: :cors_plug, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:kino, "~> 0.14", [hex: :kino, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.2", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.16", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "041ee6bc09e284bfd10882be6bd7000774259d677023385ebbfd15267598ed47"},
Expand Down
30 changes: 30 additions & 0 deletions test/dotcom/content_rewriters/code_embed_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
defmodule Dotcom.ContentRewriters.CodeEmbedTest do
use ExUnit.Case

import Dotcom.ContentRewriters.CodeEmbed
import Mox

setup :verify_on_exit!

describe "rewrite/1" do
test "finds <tableau-viz> and adds a token attribute" do
token = Faker.String.base64(50)
expect(TableauCloudToken.Mock, :default_token, fn -> token end)

content =
{:safe,
~s(<script type='module' src='https://us-east-1.online.tableau.com/javascripts/api/tableau.embedding.3.latest.min.js'></script><tableau-viz id='tableau-viz' src='https://us-east-1.online.tableau.com/t/mbta-public/views/EmbedTestDashboard/TestingDashboard' width='1730' height='965' hide-tabs toolbar='bottom'></tableau-viz>)}

{:safe, rewritten} = rewrite(content)
{:ok, fragment} = Floki.parse_fragment(rewritten)
[{_, attrs, _}] = Floki.find(fragment, "tableau-viz")
assert Enum.find(attrs, fn {name, ^token} -> name == "token" end)
end

test "passes through other code embeds" do
input_code = ~s(<a href="#{Faker.File.file_name()}"><kbd>ABCD</kbd></a>)
{:safe, rewritten} = rewrite({:safe, input_code})
assert rewritten == input_code
end
end
end
1 change: 1 addition & 0 deletions test/support/mocks.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Mox.defmock(OpenTripPlannerClient.Mock, for: OpenTripPlannerClient.Behaviour)
Mox.defmock(Predictions.Phoenix.PubSub.Mock, for: Phoenix.Channel)
Mox.defmock(Predictions.PubSub.Mock, for: [GenServer, Predictions.PubSub.Behaviour])
Mox.defmock(Predictions.Store.Mock, for: Predictions.Store.Behaviour)
Mox.defmock(TableauCloudToken.Mock, for: TableauCloudToken)

# Repos
Mox.defmock(Alerts.Repo.Mock, for: Alerts.Repo.Behaviour)
Expand Down
Loading