diff --git a/elixir/apps/api/lib/api/client/channel.ex b/elixir/apps/api/lib/api/client/channel.ex index 3c46aecb5..290c5a353 100644 --- a/elixir/apps/api/lib/api/client/channel.ex +++ b/elixir/apps/api/lib/api/client/channel.ex @@ -34,6 +34,10 @@ defmodule API.Client.Channel do defp schedule_expiration(%{assigns: %{subject: %{expires_at: expires_at}}} = socket) do expires_in = DateTime.diff(expires_at, DateTime.utc_now(), :millisecond) + # Protect from race conditions where the token might have expired during code execution + expires_in = max(0, expires_in) + # Expiration time is capped at 31 days even if IdP returns really long lived tokens + expires_in = min(expires_in, 2_678_400_000) if expires_in > 0 do Process.send_after(self(), :token_expired, expires_in) diff --git a/elixir/apps/api/test/api/client/channel_test.exs b/elixir/apps/api/test/api/client/channel_test.exs index e243cb13c..24c7766dc 100644 --- a/elixir/apps/api/test/api/client/channel_test.exs +++ b/elixir/apps/api/test/api/client/channel_test.exs @@ -113,6 +113,31 @@ defmodule API.Client.ChannelTest do assert is_number(online_at) end + test "does not crash when subject expiration is too large", %{ + client: client, + subject: subject + } do + expires_at = DateTime.utc_now() |> DateTime.add(100_000_000_000, :millisecond) + subject = %{subject | expires_at: expires_at} + + # We need to trap exits to avoid test process termination + # because it is linked to the created test channel process + Process.flag(:trap_exit, true) + + {:ok, _reply, _socket} = + API.Client.Socket + |> socket("client:#{client.id}", %{ + opentelemetry_ctx: OpenTelemetry.Ctx.new(), + opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"), + client: client, + subject: subject + }) + |> subscribe_and_join(API.Client.Channel, "client") + + refute_receive {:EXIT, _pid, _} + refute_receive {:socket_close, _pid, _} + end + test "expires the channel when token is expired", %{client: client, subject: subject} do expires_at = DateTime.utc_now() |> DateTime.add(25, :millisecond) subject = %{subject | expires_at: expires_at} diff --git a/elixir/apps/domain/lib/domain/flows/flow/query.ex b/elixir/apps/domain/lib/domain/flows/flow/query.ex index 5774092b2..db9c86c13 100644 --- a/elixir/apps/domain/lib/domain/flows/flow/query.ex +++ b/elixir/apps/domain/lib/domain/flows/flow/query.ex @@ -97,7 +97,7 @@ defmodule Domain.Flows.Flow.Query do @impl Domain.Repo.Query def cursor_fields, do: [ - {:flows, :asc, :inserted_at}, + {:flows, :desc, :inserted_at}, {:flows, :asc, :id} ] end diff --git a/elixir/apps/web/lib/web/live/flows/show.ex b/elixir/apps/web/lib/web/live/flows/show.ex index 58426760e..fa3d0f42b 100644 --- a/elixir/apps/web/lib/web/live/flows/show.ex +++ b/elixir/apps/web/lib/web/live/flows/show.ex @@ -32,7 +32,7 @@ defmodule Web.Flows.Show do {:ok, socket} else - {:error, _reason} -> raise Web.LiveErrors.NotFoundError + _other -> raise Web.LiveErrors.NotFoundError end end diff --git a/elixir/apps/web/lib/web/live/resources/show.ex b/elixir/apps/web/lib/web/live/resources/show.ex index 212dd8fb9..7f1cb0a63 100644 --- a/elixir/apps/web/lib/web/live/resources/show.ex +++ b/elixir/apps/web/lib/web/live/resources/show.ex @@ -20,6 +20,7 @@ defmodule Web.Resources.Show do socket, page_title: "Resource #{resource.name}", traffic_filters_enabled?: Accounts.traffic_filters_enabled?(socket.assigns.account), + flow_activities_enabled?: Accounts.flow_activities_enabled?(socket.assigns.account), resource: resource, actor_groups_peek: Map.fetch!(actor_groups_peek, resource.id), params: Map.take(params, ["site_id"]) @@ -298,7 +299,7 @@ defmodule Web.Resources.Show do (<%= flow.gateway_remote_ip %>) - <:col :let={flow} label="ACTIVITY"> + <:col :let={flow} :if={@flow_activities_enabled?} label="ACTIVITY"> <.link navigate={~p"/#{@account}/flows/#{flow.id}"} class={[link_style()]}> Show diff --git a/elixir/apps/web/lib/web/live/sign_in/email.ex b/elixir/apps/web/lib/web/live/sign_in/email.ex index 364fe5e80..9c2e19b55 100644 --- a/elixir/apps/web/lib/web/live/sign_in/email.ex +++ b/elixir/apps/web/lib/web/live/sign_in/email.ex @@ -28,6 +28,10 @@ defmodule Web.SignIn.Email do {:ok, socket, temporary_assigns: [form: %Phoenix.HTML.Form{}]} end + def mount(_params, _session, _socket) do + raise Web.LiveErrors.NotFoundError + end + def render(assigns) do ~H"""
diff --git a/elixir/apps/web/lib/web/live/sign_in/success.ex b/elixir/apps/web/lib/web/live/sign_in/success.ex index b7fd4a111..cb29e2050 100644 --- a/elixir/apps/web/lib/web/live/sign_in/success.ex +++ b/elixir/apps/web/lib/web/live/sign_in/success.ex @@ -1,7 +1,16 @@ defmodule Web.SignIn.Success do use Web, {:live_view, layout: {Web.Layouts, :public}} - def mount(params, _session, socket) do + def mount( + %{ + "fragment" => _, + "state" => _, + "actor_name" => _, + "identity_provider_identifier" => _ + } = params, + _session, + socket + ) do if connected?(socket) do Process.send_after(self(), :redirect_client, 100) end @@ -16,6 +25,10 @@ defmodule Web.SignIn.Success do {:ok, socket} end + def mount(_params, _session, _socket) do + raise Web.LiveErrors.InvalidParamsError + end + def render(assigns) do ~H"""
diff --git a/elixir/apps/web/lib/web/live/sign_up.ex b/elixir/apps/web/lib/web/live/sign_up.ex index e5d17d6c9..384565241 100644 --- a/elixir/apps/web/lib/web/live/sign_up.ex +++ b/elixir/apps/web/lib/web/live/sign_up.ex @@ -22,7 +22,8 @@ defmodule Web.SignUp do %Registration{} |> Ecto.Changeset.cast(attrs, [:email]) |> Ecto.Changeset.validate_required([:email]) - |> Ecto.Changeset.validate_format(:email, ~r/.+@.+/) + |> Domain.Repo.Changeset.trim_change(:email) + |> Domain.Repo.Changeset.validate_email(:email) |> validate_email_allowed(whitelisted_domains) |> Ecto.Changeset.validate_confirmation(:email, required: true, diff --git a/elixir/apps/web/lib/web/live_errors.ex b/elixir/apps/web/lib/web/live_errors.ex index 501d82b67..e9ca49872 100644 --- a/elixir/apps/web/lib/web/live_errors.ex +++ b/elixir/apps/web/lib/web/live_errors.ex @@ -7,4 +7,15 @@ defmodule Web.LiveErrors do def actions(_exception), do: [] end end + + # this is not a styled error because only security scanners that + # try to manipulate the request will see it + defmodule InvalidParamsError do + defexception message: "Unprocessable Entity" + + defimpl Plug.Exception do + def status(_exception), do: 422 + def actions(_exception), do: [] + end + end end diff --git a/elixir/apps/web/test/web/live/flows/show_test.exs b/elixir/apps/web/test/web/live/flows/show_test.exs index 50eebf582..b4aeab2fe 100644 --- a/elixir/apps/web/test/web/live/flows/show_test.exs +++ b/elixir/apps/web/test/web/live/flows/show_test.exs @@ -36,6 +36,26 @@ defmodule Web.Live.Flows.ShowTest do }}} end + test "renders 404 error when flow activities are not enabled", %{ + account: account, + identity: identity, + flow: flow, + conn: conn + } do + {:ok, account} = + Domain.Accounts.update_account(account, %{ + features: %{ + flow_activities: false + } + }) + + assert_raise Web.LiveErrors.NotFoundError, fn -> + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/flows/#{flow}") =~ "404" + end + end + test "renders breadcrumbs item", %{ account: account, flow: flow, diff --git a/elixir/apps/web/test/web/live/sign_in/success_test.exs b/elixir/apps/web/test/web/live/sign_in/success_test.exs index 2f14d04e4..c260ed2ac 100644 --- a/elixir/apps/web/test/web/live/sign_in/success_test.exs +++ b/elixir/apps/web/test/web/live/sign_in/success_test.exs @@ -34,4 +34,13 @@ defmodule Web.SignIn.SuccessTest do uri = URI.parse(path) assert URI.decode_query(uri.query) == expected_query_params end + + test "returns 422 error when params are missing", %{ + account: account, + conn: conn + } do + assert_raise Web.LiveErrors.InvalidParamsError, fn -> + live(conn, ~p"/#{account}/sign_in/success") + end + end end diff --git a/elixir/apps/web/test/web/live/sign_up_test.exs b/elixir/apps/web/test/web/live/sign_up_test.exs index f0e2c99fc..e955ae2b4 100644 --- a/elixir/apps/web/test/web/live/sign_up_test.exs +++ b/elixir/apps/web/test/web/live/sign_up_test.exs @@ -75,6 +75,42 @@ defmodule Web.Live.SignUpTest do end) end + test "trims the user email", %{conn: conn} do + Domain.Config.put_env_override(:outbound_email_adapter_configured?, true) + + account_name = "FooBar" + + {:ok, lv, _html} = live(conn, ~p"/sign_up") + + email = Fixtures.Auth.email() + + attrs = %{ + account: %{name: account_name}, + actor: %{name: "John Doe"}, + email: " " <> email <> " " + } + + Bypass.open() + |> Domain.Mocks.Stripe.mock_create_customer_endpoint(%{ + id: Ecto.UUID.generate(), + name: account_name + }) + |> Domain.Mocks.Stripe.mock_create_subscription_endpoint() + + lv + |> form("form", registration: attrs) + |> render_submit() + + account = Repo.one(Domain.Accounts.Account) + assert account.name == account_name + assert account.metadata.stripe.customer_id + assert account.metadata.stripe.billing_email == email + + identity = Repo.one(Domain.Auth.Identity) + assert identity.account_id == account.id + assert identity.provider_identifier == email + end + test "allows whitelisted domains to create new account", %{conn: conn} do whitelisted_domain = "example.com" Domain.Config.put_env_override(:outbound_email_adapter_configured?, true) @@ -175,7 +211,7 @@ defmodule Web.Live.SignUpTest do |> form("form", registration: attrs) |> render_submit() |> form_validation_errors() == %{ - "registration[email]" => ["has invalid format"] + "registration[email]" => ["is an invalid email address"] } end