diff --git a/docker-compose.yml b/docker-compose.yml index 643c672a9..fa5be3ade 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -83,10 +83,6 @@ services: # Secrets TOKENS_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" TOKENS_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" - RELAYS_AUTH_TOKEN_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" - RELAYS_AUTH_TOKEN_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" - GATEWAYS_AUTH_TOKEN_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" - GATEWAYS_AUTH_TOKEN_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" SECRET_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" LIVE_VIEW_SIGNING_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" COOKIE_SIGNING_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" @@ -153,7 +149,7 @@ services: healthcheck: test: ["CMD-SHELL", "cat /proc/net/dev | grep tun-firezone"] environment: - FIREZONE_TOKEN: "SFMyNTY.g2gDaAJtAAAAJDNjZWYwNTY2LWFkZmQtNDhmZS1hMGYxLTU4MDY3OTYwOGY2Zm0AAABAamp0enhSRkpQWkdCYy1vQ1o5RHkyRndqd2FIWE1BVWRwenVScjJzUnJvcHg3NS16bmhfeHBfNWJUNU9uby1yYm4GAIC98hKNAWIAAVGA.-0Shqu5DAwS2pN9EZ5aIcMK08vSVFqA_kuXsLWxJ__o" + FIREZONE_TOKEN: ".SFMyNTY.g2gDaANtAAAAJGM4OWJjYzhjLTkzOTItNGRhZS1hNDBkLTg4OGFlZjZkMjhlMG0AAAAkMjI3NDU2MGItZTk3Yi00NWU0LThiMzQtNjc5Yzc2MTdlOThkbQAAADhPMDJMN1VTMkozVklOT01QUjlKNklMODhRSVFQNlVPOEFRVk82VTVJUEwwVkpDMjJKR0gwPT09PW4GAF3gLBONAWIAAVGA.DCT0Qv80qzF5OQ6CccLKXPLgzC3Rzx5DqzDAh9mWAww" RUST_LOG: firezone_gateway=trace,wire=trace,connlib_gateway_shared=trace,firezone_tunnel=trace,connlib_shared=trace,warn FIREZONE_ENABLE_MASQUERADE: 1 FIREZONE_API_URL: ws://api:8081 @@ -209,9 +205,9 @@ services: LOWEST_PORT: 55555 HIGHEST_PORT: 55666 # Token for self-hosted Relay - #FIREZONE_TOKEN: "SFMyNTY.g2gDaAJtAAAAJDcyODZiNTNkLTA3M2UtNGM0MS05ZmYxLWNjODQ1MWRhZDI5OW0AAABARVg3N0dhMEhLSlVWTGdjcE1yTjZIYXRkR25mdkFEWVFyUmpVV1d5VHFxdDdCYVVkRVUzbzktRmJCbFJkSU5JS24GAFSzb0KJAWIAAVGA.waeGE26tbgkgIcMrWyck0ysv9SHIoHr0zqoM3wao84M" + # FIREZONE_TOKEN: ".SFMyNTY.g2gDaANtAAAAJGM4OWJjYzhjLTkzOTItNGRhZS1hNDBkLTg4OGFlZjZkMjhlMG0AAAAkNTQ5YzQxMDctMTQ5Mi00ZjhmLWE0ZWMtYTlkMmE2NmQ4YWE5bQAAADhQVTVBSVRFMU84VkRWTk1ITU9BQzc3RElLTU9HVERJQTY3MlM2RzFBQjAyT1MzNEg1TUUwPT09PW4GAEngLBONAWIAAVGA.E-f2MFdGMX7JTL2jwoHBdWcUd2G3UNz2JRZLbQrlf0k" # Token for global Relay - FIREZONE_TOKEN: "SFMyNTY.g2gDaAJtAAAAJGMxMDM4ZTIyLTAyMTUtNDk3Ny05ZjZjLWY2NTYyMWUwMDA4Zm0AAABWT2JubmIzN2RCdE5RY2NDVS1mQll1MWg4TmFmQXAwS3lvT3dsbzJUVEl5NjBvZm9rSWxWNjBzcGExMkc1cElHLVJWS2o1cXdIVkVoMWs5bjh4QmNmOUFuBgAqiFHuiwFiAAFRgA.VyV9cW06PCZyxTefBwIlSCFTDBFEOSRQ2gfJXtMplVE" + FIREZONE_TOKEN: ".SFMyNTY.g2gDaAN3A25pbG0AAAAkZTgyZmNkYzEtMDU3YS00MDE1LWI5MGItM2IxOGYwZjI4MDUzbQAAADhDMTROR0E4N0VKUlIwM0c0UVBSMDdBOUM2Rzc4NFRTU1RIU0Y0VEk1VDBHRDhENkwwVlJHPT09PW4GADXgLBONAWIAAVGA.dShU17FgnvO2GLcTSnBBTDoqQ2tScuG7qjiyKhhlq8s" RUST_LOG: "debug" RUST_BACKTRACE: 1 FIREZONE_API_URL: ws://api:8081 @@ -279,10 +275,6 @@ services: # Secrets TOKENS_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" TOKENS_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" - RELAYS_AUTH_TOKEN_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" - RELAYS_AUTH_TOKEN_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" - GATEWAYS_AUTH_TOKEN_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" - GATEWAYS_AUTH_TOKEN_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" SECRET_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" LIVE_VIEW_SIGNING_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" COOKIE_SIGNING_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" @@ -344,10 +336,6 @@ services: # Secrets TOKENS_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" TOKENS_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" - RELAYS_AUTH_TOKEN_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" - RELAYS_AUTH_TOKEN_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" - GATEWAYS_AUTH_TOKEN_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" - GATEWAYS_AUTH_TOKEN_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" SECRET_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" LIVE_VIEW_SIGNING_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" COOKIE_SIGNING_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" diff --git a/elixir/README.md b/elixir/README.md index ce640de21..1debce696 100644 --- a/elixir/README.md +++ b/elixir/README.md @@ -63,7 +63,7 @@ Now you can verify that it's working by connecting to a websocket: ```bash # Note: The token value below is an example. The token value you will need is generated and printed out when seeding the database, as described earlier in the document. -❯ export GATEWAY_TOKEN_FROM_SEEDS="SFMyNTY.g2gDaAJtAAAAJDNjZWYwNTY2LWFkZmQtNDhmZS1hMGYxLTU4MDY3OTYwOGY2Zm0AAABAamp0enhSRkpQWkdCYy1vQ1o5RHkyRndqd2FIWE1BVWRwenVScjJzUnJvcHg3NS16bmhfeHBfNWJUNU9uby1yYm4GAJXr4emIAWIAAVGA.jz0s-NohxgdAXeRMjIQ9kLBOyd7CmKXWi2FHY-Op8GM" +❯ export GATEWAY_TOKEN_FROM_SEEDS=".SFMyNTY.g2gDaANtAAAAJGM4OWJjYzhjLTkzOTItNGRhZS1hNDBkLTg4OGFlZjZkMjhlMG0AAAAkMjI3NDU2MGItZTk3Yi00NWU0LThiMzQtNjc5Yzc2MTdlOThkbQAAADhPMDJMN1VTMkozVklOT01QUjlKNklMODhRSVFQNlVPOEFRVk82VTVJUEwwVkpDMjJKR0gwPT09PW4GAF3gLBONAWIAAVGA.DCT0Qv80qzF5OQ6CccLKXPLgzC3Rzx5DqzDAh9mWAww" ❯ websocat --header="User-Agent: iOS/12.7 (iPhone) connlib/0.7.412" "ws://127.0.0.1:13000/gateway/websocket?token=${GATEWAY_TOKEN_FROM_SEEDS}&external_id=thisisrandomandpersistent&name=kkX1&public_key=kceI60D6PrwOIiGoVz6hD7VYCgD1H57IVQlPJTTieUE=" @@ -81,7 +81,7 @@ Now you can verify that it's working by connecting to a websocket: ```bash # Note: The token value below is an example. The token value you will need is generated and printed out when seeding the database, as described earlier in the document. -❯ export RELAY_TOKEN_FROM_SEEDS="SFMyNTY.g2gDaAJtAAAAJDcyODZiNTNkLTA3M2UtNGM0MS05ZmYxLWNjODQ1MWRhZDI5OW0AAABARVg3N0dhMEhLSlVWTGdjcE1yTjZIYXRkR25mdkFEWVFyUmpVV1d5VHFxdDdCYVVkRVUzbzktRmJCbFJkSU5JS24GAMDq4emIAWIAAVGA.fLlZsUMS0VJ4RCN146QzUuINmGubpsxoyIf3uhRHdiQ" +❯ export RELAY_TOKEN_FROM_SEEDS=".SFMyNTY.g2gDaAN3A25pbG0AAAAkZTgyZmNkYzEtMDU3YS00MDE1LWI5MGItM2IxOGYwZjI4MDUzbQAAADhDMTROR0E4N0VKUlIwM0c0UVBSMDdBOUM2Rzc4NFRTU1RIU0Y0VEk1VDBHRDhENkwwVlJHPT09PW4GADXgLBONAWIAAVGA.dShU17FgnvO2GLcTSnBBTDoqQ2tScuG7qjiyKhhlq8s" ❯ websocat --header="User-Agent: Linux/5.2.6 (Debian; x86_64) relay/0.7.412" "ws://127.0.0.1:8081/relay/websocket?token=${RELAY_TOKEN_FROM_SEEDS}&ipv4=24.12.79.100&ipv6=4d36:aa7f:473c:4c61:6b9e:2416:9917:55cc" @@ -327,10 +327,12 @@ iex(web@web-3vmw.us-east1-d.c.firezone-staging.internal)3> {:ok, actor} = Domain iex(web@web-3vmw.us-east1-d.c.firezone-staging.internal)4> {:ok, identity} = Domain.Auth.upsert_identity(actor, magic_link_provider, %{provider_identifier: "a@firezone.dev", provider_identifier_confirmation: "a@firezone.dev"}) ... -iex(web@web-3vmw.us-east1-d.c.firezone-staging.internal)5> {:ok, identity} = Domain.Auth.Adapters.Email.request_sign_in_token(identity) +iex(web@web-3vmw.us-east1-d.c.firezone-staging.internal)5> context = %Domain.Auth.Context{type: :browser, user_agent: "User-Agent: iOS/12.7 (iPhone) connlib/0.7.412", remote_ip: {127, 0, 0, 1}} + +iex(web@web-3vmw.us-east1-d.c.firezone-staging.internal)6> {:ok, identity} = Domain.Auth.Adapters.Email.request_sign_in_token(identity, context) {:ok, ...} -iex(web@web-3vmw.us-east1-d.c.firezone-staging.internal)6> Web.Mailer.AuthEmail.sign_in_link_email(identity) |> Web.Mailer.deliver() +iex(web@web-3vmw.us-east1-d.c.firezone-staging.internal)7> Web.Mailer.AuthEmail.sign_in_link_email(identity) |> Web.Mailer.deliver() {:ok, %{id: "d24dbe9a-d0f5-4049-ac0d-0df793725a80"}} ``` @@ -371,9 +373,7 @@ iex(web@web-2f4j.us-east1-d.c.firezone-staging.internal)7> {:ok, subject} = Doma iex(web@web-xxxx.us-east1-d.c.firezone-staging.internal)1> # select group to update ... -iex(web@web-xxxx.us-east1-d.c.firezone-staging.internal)2> {:ok, %{tokens: [token]}} = %{group | tokens: []} |> Domain.Repo.preload(:account) |> Domain.Relays.Group.Changeset.update(%{tokens: [%{}]}) |> Domain.Repo.update() - -iex(web@web-xxxx.us-east1-d.c.firezone-staging.internal)3> Domain.Relays.encode_token!(token) +iex(web@web-xxxx.us-east1-d.c.firezone-staging.internal)2> {:ok, token} = Domain.Relays.create_token(group, %{}, subject) ... ``` diff --git a/elixir/apps/api/lib/api/client/channel.ex b/elixir/apps/api/lib/api/client/channel.ex index 88e802bfc..911f24ae7 100644 --- a/elixir/apps/api/lib/api/client/channel.ex +++ b/elixir/apps/api/lib/api/client/channel.ex @@ -24,26 +24,34 @@ defmodule API.Client.Channel do opentelemetry_ctx = OpenTelemetry.Ctx.get_current() opentelemetry_span_ctx = OpenTelemetry.Tracer.current_span_ctx() - expires_in = - DateTime.diff(socket.assigns.subject.expires_at, DateTime.utc_now(), :millisecond) + socket = + assign(socket, + opentelemetry_ctx: opentelemetry_ctx, + opentelemetry_span_ctx: opentelemetry_span_ctx + ) - if expires_in > 0 do - Process.send_after(self(), :token_expired, expires_in) + with {:ok, socket} <- schedule_expiration(socket) do send(self(), {:after_join, {opentelemetry_ctx, opentelemetry_span_ctx}}) - - socket = - assign(socket, - opentelemetry_ctx: opentelemetry_ctx, - opentelemetry_span_ctx: opentelemetry_span_ctx - ) - {:ok, socket} - else - {:error, %{"reason" => "token_expired"}} end end end + defp schedule_expiration(%{assigns: %{subject: %{expires_at: nil}}} = socket) do + {:ok, socket} + end + + defp schedule_expiration(%{assigns: %{subject: %{expires_at: expires_at}}} = socket) do + expires_in = DateTime.diff(expires_at, DateTime.utc_now(), :millisecond) + + if expires_in > 0 do + Process.send_after(self(), :token_expired, expires_in) + {:ok, socket} + else + {:error, %{"reason" => "token_expired"}} + end + end + @impl true def handle_info({:after_join, {opentelemetry_ctx, opentelemetry_span_ctx}}, socket) do OpenTelemetry.Ctx.attach(opentelemetry_ctx) diff --git a/elixir/apps/api/lib/api/client/socket.ex b/elixir/apps/api/lib/api/client/socket.ex index c938ac0cb..43de1d423 100644 --- a/elixir/apps/api/lib/api/client/socket.ex +++ b/elixir/apps/api/lib/api/client/socket.ex @@ -15,33 +15,17 @@ defmodule API.Client.Socket do :otel_propagator_text_map.extract(connect_info.trace_context_headers) OpenTelemetry.Tracer.with_span "client.connect" do - %{ - user_agent: user_agent, - x_headers: x_headers, - peer_data: peer_data - } = connect_info - - real_ip = API.Sockets.real_ip(x_headers, peer_data) - - {location_region, location_city, {location_lat, location_lon}} = - API.Sockets.load_balancer_ip_location(x_headers) - - context = %Auth.Context{ - type: :client, - user_agent: user_agent, - remote_ip: real_ip, - remote_ip_location_region: location_region, - remote_ip_location_city: location_city, - remote_ip_location_lat: location_lat, - remote_ip_location_lon: location_lon - } + context = API.Sockets.auth_context(connect_info, :client) with {:ok, subject} <- Auth.authenticate(token, context), {:ok, client} <- Clients.upsert_client(attrs, subject) do + :ok = API.Endpoint.subscribe("sessions:#{subject.token_id}") + OpenTelemetry.Tracer.set_attributes(%{ client_id: client.id, lat: client.last_seen_remote_ip_location_lat, lon: client.last_seen_remote_ip_location_lon, + version: client.last_seen_version, account_id: subject.account.id }) diff --git a/elixir/apps/api/lib/api/gateway/channel.ex b/elixir/apps/api/lib/api/gateway/channel.ex index 85bc779be..720632e4b 100644 --- a/elixir/apps/api/lib/api/gateway/channel.ex +++ b/elixir/apps/api/lib/api/gateway/channel.ex @@ -44,11 +44,15 @@ defmodule API.Gateway.Channel do :ok = Gateways.connect_gateway(socket.assigns.gateway) :ok = API.Endpoint.subscribe("gateway:#{socket.assigns.gateway.id}") + config = Domain.Config.fetch_env!(:domain, Domain.Gateways) + ipv4_masquerade_enabled = Keyword.fetch!(config, :gateway_ipv4_masquerade) + ipv6_masquerade_enabled = Keyword.fetch!(config, :gateway_ipv6_masquerade) + push(socket, "init", %{ interface: Views.Interface.render(socket.assigns.gateway), # TODO: move to settings - ipv4_masquerade_enabled: true, - ipv6_masquerade_enabled: true + ipv4_masquerade_enabled: ipv4_masquerade_enabled, + ipv6_masquerade_enabled: ipv6_masquerade_enabled }) {:noreply, socket} diff --git a/elixir/apps/api/lib/api/gateway/socket.ex b/elixir/apps/api/lib/api/gateway/socket.ex index 8a1c599b5..da21d1023 100644 --- a/elixir/apps/api/lib/api/gateway/socket.ex +++ b/elixir/apps/api/lib/api/gateway/socket.ex @@ -11,49 +11,33 @@ defmodule API.Gateway.Socket do ## Authentication @impl true - def connect(%{"token" => encrypted_secret} = attrs, socket, connect_info) do + def connect(%{"token" => encoded_token} = attrs, socket, connect_info) do :otel_propagator_text_map.extract(connect_info.trace_context_headers) OpenTelemetry.Tracer.with_span "gateway.connect" do - %{ - user_agent: user_agent, - x_headers: x_headers, - peer_data: peer_data - } = connect_info + context = API.Sockets.auth_context(connect_info, :gateway_group) + attrs = Map.take(attrs, ~w[external_id name public_key]) - real_ip = API.Sockets.real_ip(x_headers, peer_data) + with {:ok, group} <- Gateways.authenticate(encoded_token, context), + {:ok, gateway} <- Gateways.upsert_gateway(group, attrs, context) do + :ok = API.Endpoint.subscribe("gateway_group_sessions:#{group.id}") - {location_region, location_city, {location_lat, location_lon}} = - API.Sockets.load_balancer_ip_location(x_headers) - - attrs = - attrs - |> Map.take(~w[external_id name public_key]) - |> Map.put("last_seen_user_agent", user_agent) - |> Map.put("last_seen_remote_ip", real_ip) - |> Map.put("last_seen_remote_ip_location_region", location_region) - |> Map.put("last_seen_remote_ip_location_city", location_city) - |> Map.put("last_seen_remote_ip_location_lat", location_lat) - |> Map.put("last_seen_remote_ip_location_lon", location_lon) - - with {:ok, token} <- Gateways.authorize_gateway(encrypted_secret), - {:ok, gateway} <- Gateways.upsert_gateway(token, attrs), - {:ok, gateway_group} <- Gateways.fetch_group_by_id(gateway.group_id) do OpenTelemetry.Tracer.set_attributes(%{ gateway_id: gateway.id, - account_id: gateway.account_id + account_id: gateway.account_id, + version: gateway.last_seen_version }) socket = socket + |> assign(:gateway_group, group) |> assign(:gateway, gateway) - |> assign(:gateway_group, gateway_group) |> assign(:opentelemetry_span_ctx, OpenTelemetry.Tracer.current_span_ctx()) |> assign(:opentelemetry_ctx, OpenTelemetry.Ctx.get_current()) {:ok, socket} else - {:error, :invalid_token} -> + {:error, :unauthorized} -> OpenTelemetry.Tracer.set_status(:error, "invalid_token") {:error, :invalid_token} diff --git a/elixir/apps/api/lib/api/relay/socket.ex b/elixir/apps/api/lib/api/relay/socket.ex index 6cadf744a..1a2013153 100644 --- a/elixir/apps/api/lib/api/relay/socket.ex +++ b/elixir/apps/api/lib/api/relay/socket.ex @@ -11,36 +11,21 @@ defmodule API.Relay.Socket do ## Authentication @impl true - def connect(%{"token" => encrypted_secret} = attrs, socket, connect_info) do + def connect(%{"token" => encoded_token} = attrs, socket, connect_info) do :otel_propagator_text_map.extract(connect_info.trace_context_headers) OpenTelemetry.Tracer.with_span "relay.connect" do - %{ - user_agent: user_agent, - x_headers: x_headers, - peer_data: peer_data - } = connect_info + context = API.Sockets.auth_context(connect_info, :relay_group) + attrs = Map.take(attrs, ~w[ipv4 ipv6 name]) - real_ip = API.Sockets.real_ip(x_headers, peer_data) + with {:ok, group} <- Relays.authenticate(encoded_token, context), + {:ok, relay} <- Relays.upsert_relay(group, attrs, context) do + :ok = API.Endpoint.subscribe("relay_group_sessions:#{group.id}") - {location_region, location_city, {location_lat, location_lon}} = - API.Sockets.load_balancer_ip_location(x_headers) - - attrs = - attrs - |> Map.take(~w[ipv4 ipv6 name]) - |> Map.put("last_seen_user_agent", user_agent) - |> Map.put("last_seen_remote_ip", real_ip) - |> Map.put("last_seen_remote_ip_location_region", location_region) - |> Map.put("last_seen_remote_ip_location_city", location_city) - |> Map.put("last_seen_remote_ip_location_lat", location_lat) - |> Map.put("last_seen_remote_ip_location_lon", location_lon) - - with {:ok, token} <- Relays.authorize_relay(encrypted_secret), - {:ok, relay} <- Relays.upsert_relay(token, attrs) do OpenTelemetry.Tracer.set_attributes(%{ - gateway_id: relay.id, - account_id: relay.account_id + relay_id: relay.id, + account_id: relay.account_id, + version: relay.last_seen_version }) socket = @@ -51,7 +36,7 @@ defmodule API.Relay.Socket do {:ok, socket} else - {:error, :invalid_token} -> + {:error, :unauthorized} -> OpenTelemetry.Tracer.set_status(:error, "invalid_token") {:error, :invalid_token} diff --git a/elixir/apps/api/lib/api/sockets.ex b/elixir/apps/api/lib/api/sockets.ex index 342ca2496..b8893103e 100644 --- a/elixir/apps/api/lib/api/sockets.ex +++ b/elixir/apps/api/lib/api/sockets.ex @@ -99,4 +99,27 @@ defmodule API.Sockets do def get_header(x_headers, key) do List.keyfind(x_headers, key, 0) end + + def auth_context(connect_info, type) do + %{ + user_agent: user_agent, + x_headers: x_headers, + peer_data: peer_data + } = connect_info + + real_ip = API.Sockets.real_ip(x_headers, peer_data) + + {location_region, location_city, {location_lat, location_lon}} = + API.Sockets.load_balancer_ip_location(x_headers) + + %Domain.Auth.Context{ + type: type, + user_agent: user_agent, + remote_ip: real_ip, + remote_ip_location_region: location_region, + remote_ip_location_city: location_city, + remote_ip_location_lat: location_lat, + remote_ip_location_lon: location_lon + } + end end diff --git a/elixir/apps/api/test/api/client/socket_test.exs b/elixir/apps/api/test/api/client/socket_test.exs index 2574e3017..06a5b84c9 100644 --- a/elixir/apps/api/test/api/client/socket_test.exs +++ b/elixir/apps/api/test/api/client/socket_test.exs @@ -77,11 +77,10 @@ defmodule API.Client.SocketTest do actor = Fixtures.Actors.create_actor(type: :service_account, account: account) subject = Fixtures.Auth.create_subject(account: account, actor: [type: :account_admin_user]) + in_one_minute = DateTime.utc_now() |> DateTime.add(60, :second) {:ok, encoded_token} = - Domain.Auth.create_service_account_token(actor, subject, %{ - "expires_at" => DateTime.utc_now() |> DateTime.add(60, :second) - }) + Domain.Auth.create_service_account_token(actor, %{"expires_at" => in_one_minute}, subject) attrs = connect_attrs(token: encoded_token) diff --git a/elixir/apps/api/test/api/gateway/socket_test.exs b/elixir/apps/api/test/api/gateway/socket_test.exs index d610a7b8b..abe88f3b6 100644 --- a/elixir/apps/api/test/api/gateway/socket_test.exs +++ b/elixir/apps/api/test/api/gateway/socket_test.exs @@ -2,7 +2,6 @@ defmodule API.Gateway.SocketTest do use API.ChannelCase, async: true import API.Gateway.Socket, except: [connect: 3] alias API.Gateway.Socket - alias Domain.Gateways @connlib_version "0.1.1" @@ -25,7 +24,7 @@ defmodule API.Gateway.SocketTest do test "creates a new gateway" do token = Fixtures.Gateways.create_token() - encrypted_secret = Gateways.encode_token!(token) + encrypted_secret = Domain.Tokens.encode_fragment!(token) attrs = connect_attrs(token: encrypted_secret) @@ -45,7 +44,7 @@ defmodule API.Gateway.SocketTest do test "uses region code to put default coordinates" do token = Fixtures.Gateways.create_token() - encrypted_secret = Gateways.encode_token!(token) + encrypted_secret = Domain.Tokens.encode_fragment!(token) attrs = connect_attrs(token: encrypted_secret) @@ -61,7 +60,7 @@ defmodule API.Gateway.SocketTest do test "propagates trace context" do token = Fixtures.Gateways.create_token() - encrypted_secret = Gateways.encode_token!(token) + encrypted_secret = Domain.Tokens.encode_fragment!(token) attrs = connect_attrs(token: encrypted_secret) span_ctx = OpenTelemetry.Tracer.start_span("test") @@ -78,11 +77,13 @@ defmodule API.Gateway.SocketTest do end test "updates existing gateway" do - token = Fixtures.Gateways.create_token() - existing_gateway = Fixtures.Gateways.create_gateway(token: token) - encrypted_secret = Gateways.encode_token!(token) + account = Fixtures.Accounts.create_account() + group = Fixtures.Gateways.create_group(account: account) + gateway = Fixtures.Gateways.create_gateway(account: account, group: group) + token = Fixtures.Gateways.create_token(account: account, group: group) + encrypted_secret = Domain.Tokens.encode_fragment!(token) - attrs = connect_attrs(token: encrypted_secret, external_id: existing_gateway.external_id) + attrs = connect_attrs(token: encrypted_secret, external_id: gateway.external_id) assert {:ok, socket} = connect(Socket, attrs, connect_info: @connect_info) assert gateway = Repo.one(Domain.Gateways.Gateway) diff --git a/elixir/apps/api/test/api/relay/socket_test.exs b/elixir/apps/api/test/api/relay/socket_test.exs index 822e6c755..cff6597d5 100644 --- a/elixir/apps/api/test/api/relay/socket_test.exs +++ b/elixir/apps/api/test/api/relay/socket_test.exs @@ -2,7 +2,6 @@ defmodule API.Relay.SocketTest do use API.ChannelCase, async: true import API.Relay.Socket, except: [connect: 3] alias API.Relay.Socket - alias Domain.Relays @connlib_version "0.1.1" @@ -25,7 +24,7 @@ defmodule API.Relay.SocketTest do test "creates a new relay" do token = Fixtures.Relays.create_token() - encrypted_secret = Relays.encode_token!(token) + encrypted_secret = Domain.Tokens.encode_fragment!(token) attrs = connect_attrs(token: encrypted_secret) @@ -45,7 +44,7 @@ defmodule API.Relay.SocketTest do test "creates a new named relay" do token = Fixtures.Relays.create_token() - encrypted_secret = Relays.encode_token!(token) + encrypted_secret = Domain.Tokens.encode_fragment!(token) attrs = connect_attrs(token: encrypted_secret) @@ -58,7 +57,7 @@ defmodule API.Relay.SocketTest do test "uses region code to put default coordinates" do token = Fixtures.Relays.create_token() - encrypted_secret = Relays.encode_token!(token) + encrypted_secret = Domain.Tokens.encode_fragment!(token) attrs = connect_attrs(token: encrypted_secret) @@ -74,7 +73,7 @@ defmodule API.Relay.SocketTest do test "propagates trace context" do token = Fixtures.Relays.create_token() - encrypted_secret = Relays.encode_token!(token) + encrypted_secret = Domain.Tokens.encode_fragment!(token) attrs = connect_attrs(token: encrypted_secret) span_ctx = OpenTelemetry.Tracer.start_span("test") @@ -91,11 +90,13 @@ defmodule API.Relay.SocketTest do end test "updates existing relay" do - token = Fixtures.Relays.create_token() - existing_relay = Fixtures.Relays.create_relay(token: token) - encrypted_secret = Relays.encode_token!(token) + account = Fixtures.Accounts.create_account() + group = Fixtures.Relays.create_group(account: account) + relay = Fixtures.Relays.create_relay(account: account, group: group) + token = Fixtures.Relays.create_token(account: account, group: group) + encrypted_secret = Domain.Tokens.encode_fragment!(token) - attrs = connect_attrs(token: encrypted_secret, ipv4: existing_relay.ipv4) + attrs = connect_attrs(token: encrypted_secret, ipv4: relay.ipv4) assert {:ok, socket} = connect(Socket, attrs, connect_info: @connect_info) assert relay = Repo.one(Domain.Relays.Relay) diff --git a/elixir/apps/domain/lib/domain/accounts/account.ex b/elixir/apps/domain/lib/domain/accounts/account.ex index a2da2433c..31eb5f6af 100644 --- a/elixir/apps/domain/lib/domain/accounts/account.ex +++ b/elixir/apps/domain/lib/domain/accounts/account.ex @@ -27,11 +27,9 @@ defmodule Domain.Accounts.Account do has_many :gateways, Domain.Gateways.Gateway, where: [deleted_at: nil] has_many :gateway_groups, Domain.Gateways.Group, where: [deleted_at: nil] - has_many :gateway_tokens, Domain.Gateways.Token, where: [deleted_at: nil] has_many :relays, Domain.Relays.Relay, where: [deleted_at: nil] has_many :relay_groups, Domain.Relays.Group, where: [deleted_at: nil] - has_many :relay_tokens, Domain.Relays.Token, where: [deleted_at: nil] has_many :tokens, Domain.Tokens.Token, where: [deleted_at: nil] diff --git a/elixir/apps/domain/lib/domain/actors/actor.ex b/elixir/apps/domain/lib/domain/actors/actor.ex index 4f8a07545..042f6cbfa 100644 --- a/elixir/apps/domain/lib/domain/actors/actor.ex +++ b/elixir/apps/domain/lib/domain/actors/actor.ex @@ -2,7 +2,8 @@ defmodule Domain.Actors.Actor do use Domain, :schema schema "actors" do - field :type, Ecto.Enum, values: [:account_user, :account_admin_user, :service_account] + field :type, Ecto.Enum, + values: [:account_user, :account_admin_user, :service_account, :api_client] field :name, :string @@ -12,6 +13,8 @@ defmodule Domain.Actors.Actor do where: [deleted_at: nil], preload_order: [desc: :last_seen_at] + has_many :tokens, Domain.Tokens.Token, where: [deleted_at: nil] + has_many :memberships, Domain.Actors.Membership, on_replace: :delete # TODO: where doesn't work on join tables so soft-deleted records will be preloaded, # ref https://github.com/firezone/firezone/issues/2162 diff --git a/elixir/apps/domain/lib/domain/auth.ex b/elixir/apps/domain/lib/domain/auth.ex index 5c67453f4..218a9c93a 100644 --- a/elixir/apps/domain/lib/domain/auth.ex +++ b/elixir/apps/domain/lib/domain/auth.ex @@ -324,6 +324,7 @@ defmodule Domain.Auth do end end + # TODO: can be replaced with peek for consistency def fetch_identities_count_grouped_by_provider_id(%Subject{} = subject) do with :ok <- ensure_has_permissions(subject, Authorizer.manage_identities_permission()) do {:ok, identities} = @@ -497,7 +498,8 @@ defmodule Domain.Auth do |> Identity.Query.by_id_or_provider_identifier(id_or_provider_identifier) with {:ok, identity} <- Repo.fetch(identity_queryable), - {:ok, identity, expires_at} <- Adapters.verify_secret(provider, identity, secret), + {:ok, identity, expires_at} <- + Adapters.verify_secret(provider, identity, context, secret), identity = Repo.preload(identity, :actor), {:ok, token} <- create_token(identity, context, token_nonce, expires_at) do {:ok, identity, Tokens.encode_fragment!(token)} @@ -603,13 +605,13 @@ defmodule Domain.Auth do def create_service_account_token( %Actors.Actor{type: :service_account, account_id: account_id} = actor, - %Subject{account: %{id: account_id}} = subject, - attrs + attrs, + %Subject{account: %{id: account_id}} = subject ) do attrs = Map.merge(attrs, %{ "type" => :client, - "secret_fragment" => Domain.Crypto.random_token(32), + "secret_fragment" => Domain.Crypto.random_token(32, encoder: :hex32), "account_id" => actor.account_id, "actor_id" => actor.id, "created_by_user_agent" => subject.context.user_agent, @@ -622,6 +624,27 @@ defmodule Domain.Auth do end end + def create_api_client_token( + %Actors.Actor{type: :api_client, account_id: account_id} = actor, + attrs, + %Subject{account: %{id: account_id}} = subject + ) do + attrs = + Map.merge(attrs, %{ + "type" => :api_client, + "secret_fragment" => Domain.Crypto.random_token(32, encoder: :hex32), + "account_id" => actor.account_id, + "actor_id" => actor.id, + "created_by_user_agent" => subject.context.user_agent, + "created_by_remote_ip" => subject.context.remote_ip + }) + + with :ok <- ensure_has_permissions(subject, Authorizer.manage_api_clients_permission()), + {:ok, token} <- Tokens.create_token(attrs, subject) do + {:ok, Tokens.encode_fragment!(token)} + end + end + # Authentication def authenticate(encoded_token, %Context{} = context) @@ -657,7 +680,7 @@ defmodule Domain.Auth do # used in tests and seeds @doc false def build_subject(%Tokens.Token{type: type} = token, %Context{} = context) - when type in [:browser, :client] do + when type in [:browser, :client, :api_client] do account = Accounts.fetch_account_by_id!(token.account_id) with {:ok, actor} <- Actors.fetch_actor_by_id(token.actor_id) do @@ -680,6 +703,10 @@ defmodule Domain.Auth do {:ok, subject} end + defp maybe_fetch_subject_identity(%{actor: %{type: :api_client}} = subject, _token) do + {:ok, subject} + end + defp maybe_fetch_subject_identity(_subject, %{identity_id: nil}) do {:error, :not_found} end diff --git a/elixir/apps/domain/lib/domain/auth/adapter.ex b/elixir/apps/domain/lib/domain/auth/adapter.ex index ae064c85c..8084c442a 100644 --- a/elixir/apps/domain/lib/domain/auth/adapter.ex +++ b/elixir/apps/domain/lib/domain/auth/adapter.ex @@ -1,5 +1,5 @@ defmodule Domain.Auth.Adapter do - alias Domain.Auth.{Provider, Identity} + alias Domain.Auth.{Provider, Identity, Context} @typedoc """ This type defines which kind of provisioners are enabled for IdP adapter. @@ -74,7 +74,7 @@ defmodule Domain.Auth.Adapter do Used by secret-based providers, eg.: UserPass, Email. """ - @callback verify_secret(%Identity{}, secret :: term()) :: + @callback verify_secret(%Identity{}, %Context{}, secret :: term()) :: {:ok, %Identity{}, expires_at :: %DateTime{} | nil} | {:error, :invalid_secret} | {:error, :expired_secret} diff --git a/elixir/apps/domain/lib/domain/auth/adapters.ex b/elixir/apps/domain/lib/domain/auth/adapters.ex index a1ce388bf..c672d6e45 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters.ex @@ -1,6 +1,6 @@ defmodule Domain.Auth.Adapters do use Supervisor - alias Domain.Auth.{Provider, Identity} + alias Domain.Auth.{Provider, Identity, Context} @adapters %{ email: Domain.Auth.Adapters.Email, @@ -79,10 +79,10 @@ defmodule Domain.Auth.Adapters do adapter.sign_out(provider, identity, redirect_url) end - def verify_secret(%Provider{} = provider, %Identity{} = identity, secret) do + def verify_secret(%Provider{} = provider, %Identity{} = identity, %Context{} = context, secret) do adapter = fetch_provider_adapter!(provider) - case adapter.verify_secret(identity, secret) do + case adapter.verify_secret(identity, context, secret) do {:ok, %Identity{} = identity, expires_at} -> {:ok, identity, expires_at} {:error, :invalid_secret} -> {:error, :invalid_secret} {:error, :expired_secret} -> {:error, :expired_secret} diff --git a/elixir/apps/domain/lib/domain/auth/adapters/email.ex b/elixir/apps/domain/lib/domain/auth/adapters/email.ex index 57fbfd6b7..b931627dd 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/email.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/email.ex @@ -1,7 +1,8 @@ defmodule Domain.Auth.Adapters.Email do use Supervisor alias Domain.Repo - alias Domain.Auth.{Identity, Provider, Adapter} + alias Domain.Tokens + alias Domain.Auth.{Identity, Provider, Adapter, Context} @behaviour Adapter @behaviour Adapter.Local @@ -30,9 +31,7 @@ defmodule Domain.Auth.Adapters.Email do end @impl true - def identity_changeset(%Provider{} = provider, %Ecto.Changeset{} = changeset) do - {state, virtual_state} = identity_create_state(provider) - + def identity_changeset(%Provider{}, %Ecto.Changeset{} = changeset) do changeset |> Domain.Validator.trim_change(:provider_identifier) |> Domain.Validator.validate_email(:provider_identifier) @@ -40,8 +39,8 @@ defmodule Domain.Auth.Adapters.Email do required: true, message: "email does not match" ) - |> Ecto.Changeset.put_change(:provider_state, state) - |> Ecto.Changeset.put_change(:provider_virtual_state, virtual_state) + |> Ecto.Changeset.put_change(:provider_state, %{}) + |> Ecto.Changeset.put_change(:provider_virtual_state, %{}) end @impl true @@ -68,111 +67,51 @@ defmodule Domain.Auth.Adapters.Email do {:ok, identity, redirect_url} end - defp identity_create_state(%Provider{} = _provider) do - email_token = Domain.Crypto.random_token(5, encoder: :user_friendly) - nonce = Domain.Crypto.random_token(27) - sign_in_token = String.downcase(email_token) <> nonce + def request_sign_in_token(%Identity{} = identity, %Context{} = context) do + nonce = String.downcase(Domain.Crypto.random_token(5, encoder: :user_friendly)) + expires_at = DateTime.utc_now() |> DateTime.add(@sign_in_token_expiration_seconds, :second) - salt = Domain.Crypto.random_token(16) + {:ok, _count} = Tokens.delete_all_tokens_by_type_and_assoc(:email, identity) - { - %{ - "sign_in_token_salt" => salt, - "sign_in_token_hash" => Domain.Crypto.hash(:argon2, sign_in_token <> salt), - "sign_in_token_created_at" => DateTime.utc_now() - }, - %{ - sign_in_token: sign_in_token - } + {:ok, token} = + Tokens.create_token(%{ + type: :email, + secret_fragment: Domain.Crypto.random_token(27), + secret_nonce: nonce, + account_id: identity.account_id, + actor_id: identity.actor_id, + identity_id: identity.id, + remaining_attempts: @sign_in_token_max_attempts, + expires_at: expires_at, + created_by_user_agent: context.user_agent, + created_by_remote_ip: context.remote_ip + }) + + fragment = Tokens.encode_fragment!(token) + + state = %{ + "last_created_token_id" => token.id, + "token_created_at" => token.inserted_at } - end - def request_sign_in_token(%Identity{} = identity) do - identity = Repo.preload(identity, :provider) - {state, virtual_state} = identity_create_state(identity.provider) + virtual_state = %{nonce: nonce, fragment: fragment} Identity.Mutator.update_provider_state(identity, state, virtual_state) end @impl true - def verify_secret(%Identity{} = identity, token) do - consume_sign_in_token(identity, token) - end + def verify_secret(%Identity{} = identity, %Context{} = context, encoded_token) do + with {:ok, token} <- Tokens.use_token(encoded_token, %{context | type: :email}) do + {:ok, identity} = + Identity.Changeset.update_identity_provider_state(identity, %{ + last_used_token_id: token.id + }) + |> Repo.update() - defp consume_sign_in_token(%Identity{} = identity, token) when is_binary(token) do - Identity.Query.by_id(identity.id) - |> Repo.fetch_and_update( - with: fn identity -> - sign_in_token_hash = - identity.provider_state["sign_in_token_hash"] || - identity.provider_state[:sign_in_token_hash] + {:ok, _count} = Tokens.delete_all_tokens_by_type_and_assoc(:email, identity) - sign_in_token_created_at = - identity.provider_state["sign_in_token_created_at"] || - identity.provider_state[:sign_in_token_created_at] - - sign_in_token_salt = - identity.provider_state["sign_in_token_salt"] || - identity.provider_state[:sign_in_token_salt] - - cond do - is_nil(sign_in_token_hash) -> - :invalid_secret - - is_nil(sign_in_token_salt) -> - :invalid_secret - - is_nil(sign_in_token_created_at) -> - :invalid_secret - - sign_in_token_expired?(sign_in_token_created_at) -> - :expired_secret - - not Domain.Crypto.equal?(:argon2, token <> sign_in_token_salt, sign_in_token_hash) -> - track_failed_attempt!(identity) - - true -> - Identity.Changeset.update_identity_provider_state(identity, %{}, %{}) - end - end - ) - |> case do - {:ok, identity} -> {:ok, identity, nil} - {:error, reason} -> {:error, reason} - end - end - - defp track_failed_attempt!(%Identity{} = identity) do - attempts = identity.provider_state["sign_in_failed_attempts"] || 0 - attempt = attempts + 1 - - {error, provider_state} = - if attempt > @sign_in_token_max_attempts do - {:invalid_secret, %{}} - else - provider_state = Map.put(identity.provider_state, "sign_in_failed_attempts", attempts + 1) - {:invalid_secret, provider_state} - end - - Identity.Changeset.update_identity_provider_state(identity, provider_state, %{}) - |> Repo.update!() - - error - end - - defp sign_in_token_expired?(%DateTime{} = sign_in_token_created_at) do - now = DateTime.utc_now() - DateTime.diff(now, sign_in_token_created_at, :second) > @sign_in_token_expiration_seconds - end - - defp sign_in_token_expired?(sign_in_token_created_at) do - now = DateTime.utc_now() - - case DateTime.from_iso8601(sign_in_token_created_at) do - {:ok, sign_in_token_created_at, 0} -> - DateTime.diff(now, sign_in_token_created_at, :second) > @sign_in_token_expiration_seconds - - {:error, _reason} -> - true + {:ok, identity, nil} + else + {:error, :invalid_or_expired_token} -> {:error, :invalid_secret} end end end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/openid_connect.ex b/elixir/apps/domain/lib/domain/auth/adapters/openid_connect.ex index 5ed190e85..8ca5b3101 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/openid_connect.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/openid_connect.ex @@ -218,6 +218,9 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do {:error, {:invalid_jwt, _reason}} -> {:error, :invalid_token} + {:error, {400, _reason}} -> + {:error, :invalid_token} + {:error, other} -> Logger.error("Failed to connect OpenID Connect provider", provider_id: provider.id, diff --git a/elixir/apps/domain/lib/domain/auth/adapters/userpass.ex b/elixir/apps/domain/lib/domain/auth/adapters/userpass.ex index d071d008f..f65459933 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/userpass.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/userpass.ex @@ -5,7 +5,7 @@ defmodule Domain.Auth.Adapters.UserPass do """ use Supervisor alias Domain.Repo - alias Domain.Auth.{Identity, Provider, Adapter} + alias Domain.Auth.{Identity, Provider, Adapter, Context} alias Domain.Auth.Adapters.UserPass.Password @behaviour Adapter @@ -93,7 +93,8 @@ defmodule Domain.Auth.Adapters.UserPass do end @impl true - def verify_secret(%Identity{} = identity, password) when is_binary(password) do + def verify_secret(%Identity{} = identity, %Context{} = _context, password) + when is_binary(password) do Identity.Query.by_id(identity.id) |> Repo.fetch_and_update( with: fn identity -> diff --git a/elixir/apps/domain/lib/domain/auth/authorizer.ex b/elixir/apps/domain/lib/domain/auth/authorizer.ex index 083d29574..f1dae9625 100644 --- a/elixir/apps/domain/lib/domain/auth/authorizer.ex +++ b/elixir/apps/domain/lib/domain/auth/authorizer.ex @@ -32,16 +32,17 @@ defmodule Domain.Auth.Authorizer do %Auth.Permission{resource: resource, action: action} end - # TODO: is this the best place for this? def manage_providers_permission, do: build(Auth.Provider, :manage) def manage_identities_permission, do: build(Auth.Identity, :manage) def manage_service_accounts_permission, do: build(Auth, :manage_service_accounts) + def manage_api_clients_permission, do: build(Auth, :manage_api_clients) def manage_own_identities_permission, do: build(Auth.Identity, :manage_own) def list_permissions_for_role(:account_admin_user) do [ manage_providers_permission(), manage_service_accounts_permission(), + manage_api_clients_permission(), manage_own_identities_permission(), manage_identities_permission() ] diff --git a/elixir/apps/domain/lib/domain/auth/identity.ex b/elixir/apps/domain/lib/domain/auth/identity.ex index 02d474520..5f3fc5286 100644 --- a/elixir/apps/domain/lib/domain/auth/identity.ex +++ b/elixir/apps/domain/lib/domain/auth/identity.ex @@ -24,6 +24,8 @@ defmodule Domain.Auth.Identity do has_many :clients, Domain.Clients.Client, where: [deleted_at: nil] + has_many :tokens, Domain.Tokens.Token, foreign_key: :identity_id, where: [deleted_at: nil] + field :deleted_at, :utc_datetime_usec timestamps(updated_at: false) end diff --git a/elixir/apps/domain/lib/domain/auth/roles.ex b/elixir/apps/domain/lib/domain/auth/roles.ex index 17fb770ea..a1b8dda8a 100644 --- a/elixir/apps/domain/lib/domain/auth/roles.ex +++ b/elixir/apps/domain/lib/domain/auth/roles.ex @@ -1,13 +1,6 @@ defmodule Domain.Auth.Roles do alias Domain.Auth.Role - def list_roles do - [ - build(:account_admin_user), - build(:account_user) - ] - end - defp list_authorizers do [ Domain.Accounts.Authorizer, diff --git a/elixir/apps/domain/lib/domain/config/definitions.ex b/elixir/apps/domain/lib/domain/config/definitions.ex index 9c3e8d83b..640575770 100644 --- a/elixir/apps/domain/lib/domain/config/definitions.ex +++ b/elixir/apps/domain/lib/domain/config/definitions.ex @@ -82,10 +82,6 @@ defmodule Domain.Config.Definitions do [ :tokens_key_base, :tokens_salt, - :relays_auth_token_key_base, - :relays_auth_token_salt, - :gateways_auth_token_key_base, - :gateways_auth_token_salt, :secret_key_base, :live_view_signing_salt, :cookie_signing_salt, @@ -381,38 +377,6 @@ defmodule Domain.Config.Definitions do changeset: &Domain.Validator.validate_base64/2 ) - @doc """ - Secret which is used to encode and sign relays auth tokens. - """ - defconfig(:relays_auth_token_key_base, :string, - sensitive: true, - changeset: &Domain.Validator.validate_base64/2 - ) - - @doc """ - Salt which is used to encode and sign relays auth tokens. - """ - defconfig(:relays_auth_token_salt, :string, - sensitive: true, - changeset: &Domain.Validator.validate_base64/2 - ) - - @doc """ - Secret which is used to encode and sign gateways auth tokens. - """ - defconfig(:gateways_auth_token_key_base, :string, - sensitive: true, - changeset: &Domain.Validator.validate_base64/2 - ) - - @doc """ - Salt which is used to encode and sign gateways auth tokens. - """ - defconfig(:gateways_auth_token_salt, :string, - sensitive: true, - changeset: &Domain.Validator.validate_base64/2 - ) - @doc """ Primary secret key base for the Phoenix application. """ @@ -596,7 +560,6 @@ defmodule Domain.Config.Definitions do Adapter configuration, for list of options see [Swoosh Adapters](https://github.com/swoosh/swoosh#adapters). """ defconfig(:outbound_email_adapter_opts, :map, - # TODO: validate opts are present if adapter is not NOOP one default: %{}, sensitive: true, dump: fn map -> diff --git a/elixir/apps/domain/lib/domain/flows/flow/changeset.ex b/elixir/apps/domain/lib/domain/flows/flow/changeset.ex index e6fc2eee3..d333edd5a 100644 --- a/elixir/apps/domain/lib/domain/flows/flow/changeset.ex +++ b/elixir/apps/domain/lib/domain/flows/flow/changeset.ex @@ -7,7 +7,7 @@ defmodule Domain.Flows.Flow.Changeset do client_remote_ip client_user_agent gateway_remote_ip expires_at]a - @required_fields @fields + @required_fields @fields -- ~w[expires_at]a def create(attrs) do %Flow{} diff --git a/elixir/apps/domain/lib/domain/gateways.ex b/elixir/apps/domain/lib/domain/gateways.ex index c6a3224ed..0795db647 100644 --- a/elixir/apps/domain/lib/domain/gateways.ex +++ b/elixir/apps/domain/lib/domain/gateways.ex @@ -1,8 +1,8 @@ defmodule Domain.Gateways do use Supervisor alias Domain.{Repo, Auth, Validator, Geo} - alias Domain.{Accounts, Resources} - alias Domain.Gateways.{Authorizer, Gateway, Group, Token, Presence} + alias Domain.{Accounts, Resources, Tokens} + alias Domain.Gateways.{Authorizer, Gateway, Group, Presence} def start_link(opts) do Supervisor.start_link(__MODULE__, opts, name: __MODULE__) @@ -150,35 +150,42 @@ defmodule Domain.Gateways do |> Authorizer.for_subject(subject) |> Repo.fetch_and_update( with: fn group -> - :ok = - Token.Query.by_group_id(group.id) - |> Repo.all() - |> Enum.each(fn token -> - Token.Changeset.delete(token) - |> Repo.update!() - end) - - group - |> Group.Changeset.delete() + {:ok, _count} = Tokens.delete_tokens_for(group, subject) + Group.Changeset.delete(group) end ) end end - def use_token_by_id_and_secret(id, secret) do - if Validator.valid_uuid?(id) do - Token.Query.by_id(id) - |> Repo.fetch_and_update( - with: fn token -> - if Domain.Crypto.equal?(:argon2, secret, token.hash) do - Token.Changeset.use(token) - else - :not_found - end - end - ) + def create_token( + %Group{account_id: account_id} = group, + attrs, + %Auth.Subject{account: %{id: account_id}} = subject + ) do + attrs = + Map.merge(attrs, %{ + "type" => :gateway_group, + "secret_fragment" => Domain.Crypto.random_token(32, encoder: :hex32), + "account_id" => group.account_id, + "gateway_group_id" => group.id, + "created_by_user_agent" => subject.context.user_agent, + "created_by_remote_ip" => subject.context.remote_ip + }) + + with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_gateways_permission()), + {:ok, token} <- Tokens.create_token(attrs, subject) do + {:ok, Tokens.encode_fragment!(token)} + end + end + + def authenticate(encoded_token, %Auth.Context{} = context) when is_binary(encoded_token) do + with {:ok, token} <- Tokens.use_token(encoded_token, context), + queryable = Group.Query.by_id(token.gateway_group_id), + {:ok, group} <- Repo.fetch(queryable) do + {:ok, group} else - {:error, :not_found} + {:error, :invalid_or_expired_token} -> {:error, :unauthorized} + {:error, :not_found} -> {:error, :unauthorized} end end @@ -341,8 +348,8 @@ defmodule Domain.Gateways do Gateway.Changeset.update(gateway, attrs) end - def upsert_gateway(%Token{} = token, attrs) do - changeset = Gateway.Changeset.upsert(token, attrs) + def upsert_gateway(%Group{} = group, attrs, %Auth.Context{} = context) do + changeset = Gateway.Changeset.upsert(group, attrs, context) Ecto.Multi.new() |> Ecto.Multi.insert(:gateway, changeset, @@ -445,29 +452,6 @@ defmodule Domain.Gateways do end end - def encode_token!(%Token{value: value} = token) when not is_nil(value) do - body = {token.id, token.value} - config = fetch_config!() - key_base = Keyword.fetch!(config, :key_base) - salt = Keyword.fetch!(config, :salt) - Plug.Crypto.sign(key_base, salt, body) - end - - def authorize_gateway(encrypted_secret) do - config = fetch_config!() - key_base = Keyword.fetch!(config, :key_base) - salt = Keyword.fetch!(config, :salt) - - with {:ok, {id, secret}} <- - Plug.Crypto.verify(key_base, salt, encrypted_secret, max_age: :infinity), - {:ok, token} <- use_token_by_id_and_secret(id, secret) do - {:ok, token} - else - {:error, :invalid} -> {:error, :invalid_token} - {:error, :not_found} -> {:error, :invalid_token} - end - end - def connect_gateway(%Gateway{} = gateway) do meta = %{online_at: System.system_time(:second)} @@ -485,10 +469,6 @@ defmodule Domain.Gateways do Phoenix.PubSub.subscribe(Domain.PubSub, "gateway_groups:#{group.id}") end - defp fetch_config! do - Domain.Config.fetch_env!(:domain, __MODULE__) - end - # Finds the most strict routing strategy for a given list of gateway groups. def relay_strategy(gateway_groups) when is_list(gateway_groups) do strictness = [ diff --git a/elixir/apps/domain/lib/domain/gateways/gateway.ex b/elixir/apps/domain/lib/domain/gateways/gateway.ex index b4bd6f96c..2caaf1be7 100644 --- a/elixir/apps/domain/lib/domain/gateways/gateway.ex +++ b/elixir/apps/domain/lib/domain/gateways/gateway.ex @@ -24,7 +24,6 @@ defmodule Domain.Gateways.Gateway do belongs_to :account, Domain.Accounts.Account belongs_to :group, Domain.Gateways.Group - belongs_to :token, Domain.Gateways.Token field :deleted_at, :utc_datetime_usec timestamps() diff --git a/elixir/apps/domain/lib/domain/gateways/gateway/changeset.ex b/elixir/apps/domain/lib/domain/gateways/gateway/changeset.ex index a4a1bc9fb..1bd340654 100644 --- a/elixir/apps/domain/lib/domain/gateways/gateway/changeset.ex +++ b/elixir/apps/domain/lib/domain/gateways/gateway/changeset.ex @@ -1,6 +1,6 @@ defmodule Domain.Gateways.Gateway.Changeset do use Domain, :changeset - alias Domain.Version + alias Domain.{Version, Auth} alias Domain.Gateways @upsert_fields ~w[external_id name public_key @@ -22,8 +22,7 @@ defmodule Domain.Gateways.Gateway.Changeset do last_seen_at updated_at]a @update_fields ~w[name]a - @required_fields ~w[external_id name public_key - last_seen_user_agent last_seen_remote_ip]a + @required_fields ~w[external_id name public_key]a # WireGuard base64-encoded string length @key_length 44 @@ -33,7 +32,7 @@ defmodule Domain.Gateways.Gateway.Changeset do def upsert_on_conflict, do: {:replace, @conflict_replace_fields} - def upsert(%Gateways.Token{} = token, attrs) do + def upsert(%Gateways.Group{} = group, attrs, %Auth.Context{} = context) do %Gateways.Gateway{} |> cast(attrs, @upsert_fields) |> put_default_value(:name, fn -> @@ -47,11 +46,15 @@ defmodule Domain.Gateways.Gateway.Changeset do |> unique_constraint(:ipv4) |> unique_constraint(:ipv6) |> put_change(:last_seen_at, DateTime.utc_now()) + |> put_change(:last_seen_user_agent, context.user_agent) + |> put_change(:last_seen_remote_ip, context.remote_ip) + |> put_change(:last_seen_remote_ip_location_region, context.remote_ip_location_region) + |> put_change(:last_seen_remote_ip_location_city, context.remote_ip_location_city) + |> put_change(:last_seen_remote_ip_location_lat, context.remote_ip_location_lat) + |> put_change(:last_seen_remote_ip_location_lon, context.remote_ip_location_lon) |> put_gateway_version() - |> put_change(:account_id, token.account_id) - |> put_change(:group_id, token.group_id) - |> put_change(:token_id, token.id) - |> assoc_constraint(:token) + |> put_change(:account_id, group.account_id) + |> put_change(:group_id, group.id) end def finalize_upsert(%Gateways.Gateway{} = gateway, ipv4, ipv6) do diff --git a/elixir/apps/domain/lib/domain/gateways/group.ex b/elixir/apps/domain/lib/domain/gateways/group.ex index c601f0613..be0987868 100644 --- a/elixir/apps/domain/lib/domain/gateways/group.ex +++ b/elixir/apps/domain/lib/domain/gateways/group.ex @@ -7,7 +7,10 @@ defmodule Domain.Gateways.Group do belongs_to :account, Domain.Accounts.Account has_many :gateways, Domain.Gateways.Gateway, foreign_key: :group_id, where: [deleted_at: nil] - has_many :tokens, Domain.Gateways.Token, foreign_key: :group_id, where: [deleted_at: nil] + + has_many :tokens, Domain.Tokens.Token, + foreign_key: :gateway_group_id, + where: [deleted_at: nil] has_many :connections, Domain.Resources.Connection, foreign_key: :gateway_group_id diff --git a/elixir/apps/domain/lib/domain/gateways/group/changeset.ex b/elixir/apps/domain/lib/domain/gateways/group/changeset.ex index e6ca44b54..ef60fa4a6 100644 --- a/elixir/apps/domain/lib/domain/gateways/group/changeset.ex +++ b/elixir/apps/domain/lib/domain/gateways/group/changeset.ex @@ -11,21 +11,10 @@ defmodule Domain.Gateways.Group.Changeset do |> put_change(:account_id, account.id) |> put_change(:created_by, :identity) |> put_change(:created_by_identity_id, subject.identity.id) - |> cast_assoc(:tokens, - with: fn _token, _attrs -> - Gateways.Token.Changeset.create(account, subject) - end, - required: true - ) end - def update(%Gateways.Group{} = group, attrs, %Auth.Subject{} = subject) do + def update(%Gateways.Group{} = group, attrs, %Auth.Subject{}) do changeset(group, attrs) - |> cast_assoc(:tokens, - with: fn _token, _attrs -> - Gateways.Token.Changeset.create(group.account, subject) - end - ) end def update(%Gateways.Group{} = group, attrs) do diff --git a/elixir/apps/domain/lib/domain/gateways/token.ex b/elixir/apps/domain/lib/domain/gateways/token.ex deleted file mode 100644 index edbb33f67..000000000 --- a/elixir/apps/domain/lib/domain/gateways/token.ex +++ /dev/null @@ -1,17 +0,0 @@ -defmodule Domain.Gateways.Token do - use Domain, :schema - - schema "gateway_tokens" do - field :value, :string, virtual: true - field :hash, :string - - belongs_to :account, Domain.Accounts.Account - belongs_to :group, Domain.Gateways.Group - - field :created_by, Ecto.Enum, values: ~w[identity]a - belongs_to :created_by_identity, Domain.Auth.Identity - - field :deleted_at, :utc_datetime_usec - timestamps(updated_at: false) - end -end diff --git a/elixir/apps/domain/lib/domain/gateways/token/changeset.ex b/elixir/apps/domain/lib/domain/gateways/token/changeset.ex deleted file mode 100644 index e6d1adb8d..000000000 --- a/elixir/apps/domain/lib/domain/gateways/token/changeset.ex +++ /dev/null @@ -1,34 +0,0 @@ -defmodule Domain.Gateways.Token.Changeset do - use Domain, :changeset - alias Domain.Auth - alias Domain.Accounts - alias Domain.Gateways - - def create(%Accounts.Account{} = account, %Auth.Subject{} = subject) do - %Gateways.Token{} - |> change() - |> put_change(:account_id, account.id) - |> put_change(:value, Domain.Crypto.random_token(64)) - |> put_hash(:value, :argon2, to: :hash) - |> assoc_constraint(:group) - |> check_constraint(:hash, name: :hash_not_null, message: "can't be blank") - |> put_change(:created_by, :identity) - |> put_change(:created_by_identity_id, subject.identity.id) - end - - def use(%Gateways.Token{} = token) do - # TODO: While we don't have token rotation implemented, the tokens are all multi-use - # delete(token) - - token - |> change() - end - - def delete(%Gateways.Token{} = token) do - token - |> change() - |> put_default_value(:deleted_at, DateTime.utc_now()) - |> put_change(:hash, nil) - |> check_constraint(:hash, name: :hash_not_null, message: "must be blank") - end -end diff --git a/elixir/apps/domain/lib/domain/gateways/token/query.ex b/elixir/apps/domain/lib/domain/gateways/token/query.ex deleted file mode 100644 index c38d22509..000000000 --- a/elixir/apps/domain/lib/domain/gateways/token/query.ex +++ /dev/null @@ -1,16 +0,0 @@ -defmodule Domain.Gateways.Token.Query do - use Domain, :query - - def not_deleted do - from(token in Domain.Gateways.Token, as: :token) - |> where([token: token], is_nil(token.deleted_at)) - end - - def by_id(queryable \\ not_deleted(), id) do - where(queryable, [token: token], token.id == ^id) - end - - def by_group_id(queryable \\ not_deleted(), group_id) do - where(queryable, [token: token], token.group_id == ^group_id) - end -end diff --git a/elixir/apps/domain/lib/domain/relays.ex b/elixir/apps/domain/lib/domain/relays.ex index 2b420ead5..c6b31cc3a 100644 --- a/elixir/apps/domain/lib/domain/relays.ex +++ b/elixir/apps/domain/lib/domain/relays.ex @@ -1,8 +1,8 @@ defmodule Domain.Relays do use Supervisor alias Domain.{Repo, Auth, Validator, Geo} - alias Domain.{Accounts, Resources} - alias Domain.Relays.{Authorizer, Relay, Group, Token, Presence} + alias Domain.{Accounts, Resources, Tokens} + alias Domain.Relays.{Authorizer, Relay, Group, Presence} def start_link(opts) do Supervisor.start_link(__MODULE__, opts, name: __MODULE__) @@ -150,35 +150,55 @@ defmodule Domain.Relays do |> Group.Query.by_account_id(subject.account.id) |> Repo.fetch_and_update( with: fn group -> - :ok = - Token.Query.by_group_id(group.id) - |> Repo.all() - |> Enum.each(fn token -> - Token.Changeset.delete(token) - |> Repo.update!() - end) - - group - |> Group.Changeset.delete() + {:ok, _count} = Tokens.delete_tokens_for(group, subject) + Group.Changeset.delete(group) end ) end end - def use_token_by_id_and_secret(id, secret) do - if Validator.valid_uuid?(id) do - Token.Query.by_id(id) - |> Repo.fetch_and_update( - with: fn token -> - if Domain.Crypto.equal?(:argon2, secret, token.hash) do - Token.Changeset.use(token) - else - :not_found - end - end - ) + def create_token(%Group{account_id: nil} = group, attrs) do + attrs = + Map.merge(attrs, %{ + "type" => :relay_group, + "secret_fragment" => Domain.Crypto.random_token(32, encoder: :hex32), + "relay_group_id" => group.id + }) + + with {:ok, token} <- Tokens.create_token(attrs) do + {:ok, Tokens.encode_fragment!(token)} + end + end + + def create_token( + %Group{account_id: account_id} = group, + attrs, + %Auth.Subject{account: %{id: account_id}} = subject + ) do + attrs = + Map.merge(attrs, %{ + "type" => :relay_group, + "secret_fragment" => Domain.Crypto.random_token(32, encoder: :hex32), + "account_id" => group.account_id, + "relay_group_id" => group.id, + "created_by_user_agent" => subject.context.user_agent, + "created_by_remote_ip" => subject.context.remote_ip + }) + + with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_relays_permission()), + {:ok, token} <- Tokens.create_token(attrs, subject) do + {:ok, Tokens.encode_fragment!(token)} + end + end + + def authenticate(encoded_token, %Auth.Context{} = context) when is_binary(encoded_token) do + with {:ok, token} <- Tokens.use_token(encoded_token, context), + queryable = Group.Query.by_id(token.relay_group_id), + {:ok, group} <- Repo.fetch(queryable) do + {:ok, group} else - {:error, :not_found} + {:error, :invalid_or_expired_token} -> {:error, :unauthorized} + {:error, :not_found} -> {:error, :unauthorized} end end @@ -303,12 +323,12 @@ defmodule Domain.Relays do |> Base.encode64(padding: false) end - def upsert_relay(%Token{} = token, attrs) do - changeset = Relay.Changeset.upsert(token, attrs) + def upsert_relay(%Group{} = group, attrs, %Auth.Context{} = context) do + changeset = Relay.Changeset.upsert(group, attrs, context) Ecto.Multi.new() |> Ecto.Multi.insert(:relay, changeset, - conflict_target: Relay.Changeset.upsert_conflict_target(token), + conflict_target: Relay.Changeset.upsert_conflict_target(group), on_conflict: Relay.Changeset.upsert_on_conflict(), returning: true ) @@ -356,29 +376,6 @@ defmodule Domain.Relays do |> Enum.map(&Enum.random(elem(&1, 1))) end - def encode_token!(%Token{value: value} = token) when not is_nil(value) do - body = {token.id, token.value} - config = fetch_config!() - key_base = Keyword.fetch!(config, :key_base) - salt = Keyword.fetch!(config, :salt) - Plug.Crypto.sign(key_base, salt, body) - end - - def authorize_relay(encrypted_secret) do - config = fetch_config!() - key_base = Keyword.fetch!(config, :key_base) - salt = Keyword.fetch!(config, :salt) - - with {:ok, {id, secret}} <- - Plug.Crypto.verify(key_base, salt, encrypted_secret, max_age: :infinity), - {:ok, token} <- use_token_by_id_and_secret(id, secret) do - {:ok, token} - else - {:error, :invalid} -> {:error, :invalid_token} - {:error, :not_found} -> {:error, :invalid_token} - end - end - def connect_relay(%Relay{} = relay, secret) do scope = if relay.account_id do @@ -406,8 +403,4 @@ defmodule Domain.Relays do def subscribe_for_relays_presence_in_group(%Group{} = group) do Phoenix.PubSub.subscribe(Domain.PubSub, "relay_groups:#{group.id}") end - - defp fetch_config! do - Domain.Config.fetch_env!(:domain, __MODULE__) - end end diff --git a/elixir/apps/domain/lib/domain/relays/group.ex b/elixir/apps/domain/lib/domain/relays/group.ex index 37b1f1bac..ba5f3d7b9 100644 --- a/elixir/apps/domain/lib/domain/relays/group.ex +++ b/elixir/apps/domain/lib/domain/relays/group.ex @@ -6,7 +6,7 @@ defmodule Domain.Relays.Group do belongs_to :account, Domain.Accounts.Account has_many :relays, Domain.Relays.Relay, foreign_key: :group_id, where: [deleted_at: nil] - has_many :tokens, Domain.Relays.Token, foreign_key: :group_id, where: [deleted_at: nil] + has_many :tokens, Domain.Tokens.Token, foreign_key: :relay_group_id, where: [deleted_at: nil] field :created_by, Ecto.Enum, values: ~w[system identity]a belongs_to :created_by_identity, Domain.Auth.Identity diff --git a/elixir/apps/domain/lib/domain/relays/group/changeset.ex b/elixir/apps/domain/lib/domain/relays/group/changeset.ex index 533726ad4..2812538ea 100644 --- a/elixir/apps/domain/lib/domain/relays/group/changeset.ex +++ b/elixir/apps/domain/lib/domain/relays/group/changeset.ex @@ -1,7 +1,6 @@ defmodule Domain.Relays.Group.Changeset do use Domain, :changeset - alias Domain.Auth - alias Domain.Accounts + alias Domain.{Auth, Accounts} alias Domain.Relays @fields ~w[name]a @@ -9,46 +8,23 @@ defmodule Domain.Relays.Group.Changeset do def create(attrs) do %Relays.Group{} |> changeset(attrs) - |> cast_assoc(:tokens, - with: fn _token, _attrs -> - Relays.Token.Changeset.create() - end, - required: true - ) |> put_change(:created_by, :system) end def create(%Accounts.Account{} = account, attrs, %Auth.Subject{} = subject) do %Relays.Group{account: account} |> changeset(attrs) - |> cast_assoc(:tokens, - with: fn _token, _attrs -> - Relays.Token.Changeset.create(account, subject) - end, - required: true - ) |> put_change(:account_id, account.id) |> put_change(:created_by, :identity) |> put_change(:created_by_identity_id, subject.identity.id) end - def update(%Relays.Group{} = group, attrs, %Auth.Subject{} = subject) do + def update(%Relays.Group{} = group, attrs, %Auth.Subject{}) do changeset(group, attrs) - |> cast_assoc(:tokens, - with: fn _token, _attrs -> - Relays.Token.Changeset.create(group.account, subject) - end - ) end def update(%Relays.Group{} = group, attrs) do changeset(group, attrs) - |> cast_assoc(:tokens, - with: fn _token, _attrs -> - Relays.Token.Changeset.create() - end, - required: true - ) end defp changeset(group, attrs) do diff --git a/elixir/apps/domain/lib/domain/relays/relay.ex b/elixir/apps/domain/lib/domain/relays/relay.ex index 50cb3a8f7..e60d7303f 100644 --- a/elixir/apps/domain/lib/domain/relays/relay.ex +++ b/elixir/apps/domain/lib/domain/relays/relay.ex @@ -24,7 +24,6 @@ defmodule Domain.Relays.Relay do belongs_to :account, Domain.Accounts.Account belongs_to :group, Domain.Relays.Group - belongs_to :token, Domain.Relays.Token field :deleted_at, :utc_datetime_usec timestamps() diff --git a/elixir/apps/domain/lib/domain/relays/relay/changeset.ex b/elixir/apps/domain/lib/domain/relays/relay/changeset.ex index 2e3ac205b..65083732f 100644 --- a/elixir/apps/domain/lib/domain/relays/relay/changeset.ex +++ b/elixir/apps/domain/lib/domain/relays/relay/changeset.ex @@ -1,6 +1,6 @@ defmodule Domain.Relays.Relay.Changeset do use Domain, :changeset - alias Domain.Version + alias Domain.{Version, Auth} alias Domain.Relays @upsert_fields ~w[ipv4 ipv6 port name @@ -32,10 +32,9 @@ defmodule Domain.Relays.Relay.Changeset do def upsert_on_conflict, do: {:replace, @conflict_replace_fields} - def upsert(%Relays.Token{} = token, attrs) do + def upsert(%Relays.Group{} = group, attrs, %Auth.Context{} = context) do %Relays.Relay{} |> cast(attrs, @upsert_fields) - |> validate_required(~w[last_seen_user_agent last_seen_remote_ip]a) |> validate_required_one_of(~w[ipv4 ipv6]a) |> validate_length(:name, min: 1, max: 255) |> validate_number(:port, greater_than_or_equal_to: 1, less_than_or_equal_to: 65_535) @@ -44,11 +43,15 @@ defmodule Domain.Relays.Relay.Changeset do |> unique_constraint(:ipv4, name: :global_relays_unique_address_index) |> unique_constraint(:ipv6, name: :global_relays_unique_address_index) |> put_change(:last_seen_at, DateTime.utc_now()) + |> put_change(:last_seen_user_agent, context.user_agent) + |> put_change(:last_seen_remote_ip, context.remote_ip) + |> put_change(:last_seen_remote_ip_location_region, context.remote_ip_location_region) + |> put_change(:last_seen_remote_ip_location_city, context.remote_ip_location_city) + |> put_change(:last_seen_remote_ip_location_lat, context.remote_ip_location_lat) + |> put_change(:last_seen_remote_ip_location_lon, context.remote_ip_location_lon) |> put_relay_version() - |> put_change(:account_id, token.account_id) - |> put_change(:group_id, token.group_id) - |> put_change(:token_id, token.id) - |> assoc_constraint(:token) + |> put_change(:account_id, group.account_id) + |> put_change(:group_id, group.id) end def delete(%Relays.Relay{} = relay) do diff --git a/elixir/apps/domain/lib/domain/relays/token.ex b/elixir/apps/domain/lib/domain/relays/token.ex deleted file mode 100644 index 2d6f072f6..000000000 --- a/elixir/apps/domain/lib/domain/relays/token.ex +++ /dev/null @@ -1,17 +0,0 @@ -defmodule Domain.Relays.Token do - use Domain, :schema - - schema "relay_tokens" do - field :value, :string, virtual: true - field :hash, :string - - belongs_to :account, Domain.Accounts.Account - belongs_to :group, Domain.Relays.Group - - field :created_by, Ecto.Enum, values: ~w[system identity]a - belongs_to :created_by_identity, Domain.Auth.Identity - - field :deleted_at, :utc_datetime_usec - timestamps(updated_at: false) - end -end diff --git a/elixir/apps/domain/lib/domain/relays/token/changeset.ex b/elixir/apps/domain/lib/domain/relays/token/changeset.ex deleted file mode 100644 index c7c2dfaf4..000000000 --- a/elixir/apps/domain/lib/domain/relays/token/changeset.ex +++ /dev/null @@ -1,39 +0,0 @@ -defmodule Domain.Relays.Token.Changeset do - use Domain, :changeset - alias Domain.Auth - alias Domain.Accounts - alias Domain.Relays - - def create do - %Relays.Token{} - |> change() - |> put_change(:value, Domain.Crypto.random_token(64)) - |> put_hash(:value, :argon2, to: :hash) - |> assoc_constraint(:group) - |> check_constraint(:hash, name: :hash_not_null, message: "can't be blank") - |> put_change(:created_by, :system) - end - - def create(%Accounts.Account{} = account, %Auth.Subject{} = subject) do - create() - |> put_change(:account_id, account.id) - |> put_change(:created_by, :identity) - |> put_change(:created_by_identity_id, subject.identity.id) - end - - def use(%Relays.Token{} = token) do - # TODO: While we don't have token rotation implemented, the tokens are all multi-use - # delete(token) - - token - |> change() - end - - def delete(%Relays.Token{} = token) do - token - |> change() - |> put_default_value(:deleted_at, DateTime.utc_now()) - |> put_change(:hash, nil) - |> check_constraint(:hash, name: :hash_not_null, message: "must be blank") - end -end diff --git a/elixir/apps/domain/lib/domain/relays/token/query.ex b/elixir/apps/domain/lib/domain/relays/token/query.ex deleted file mode 100644 index d7d7d92f9..000000000 --- a/elixir/apps/domain/lib/domain/relays/token/query.ex +++ /dev/null @@ -1,16 +0,0 @@ -defmodule Domain.Relays.Token.Query do - use Domain, :query - - def not_deleted do - from(token in Domain.Relays.Token, as: :token) - |> where([token: token], is_nil(token.deleted_at)) - end - - def by_id(queryable \\ not_deleted(), id) do - where(queryable, [token: token], token.id == ^id) - end - - def by_group_id(queryable \\ not_deleted(), group_id) do - where(queryable, [token: token], token.group_id == ^group_id) - end -end diff --git a/elixir/apps/domain/lib/domain/tokens.ex b/elixir/apps/domain/lib/domain/tokens.ex index c04e44d48..0db97fc83 100644 --- a/elixir/apps/domain/lib/domain/tokens.ex +++ b/elixir/apps/domain/lib/domain/tokens.ex @@ -1,7 +1,7 @@ defmodule Domain.Tokens do use Supervisor alias Domain.{Repo, Validator} - alias Domain.{Auth, Actors} + alias Domain.{Auth, Actors, Relays, Gateways} alias Domain.Tokens.{Token, Authorizer, Jobs} require Ecto.Query @@ -102,12 +102,7 @@ defmodule Domain.Tokens do def use_token(encoded_token, %Auth.Context{} = context) do with {:ok, {account_id, id, nonce, secret}} <- peek_token(encoded_token, context), - queryable = - Token.Query.by_id(id) - |> Token.Query.by_account_id(account_id) - |> Token.Query.by_type(context.type) - |> Token.Query.not_expired(), - {:ok, token} <- Repo.fetch(queryable), + {:ok, token} <- fetch_token_for_use(id, account_id, context.type), true <- Domain.Crypto.equal?( :sha3_256, @@ -125,6 +120,36 @@ defmodule Domain.Tokens do end end + defp fetch_token_for_use(id, account_id, context_type) do + Token.Query.by_id(id) + |> Token.Query.by_account_id(account_id) + |> Token.Query.by_type(context_type) + |> Token.Query.not_expired() + |> Ecto.Query.update([tokens: tokens], + set: [ + remaining_attempts: + fragment( + "CASE WHEN ? IS NOT NULL THEN ? - 1 ELSE NULL END", + tokens.remaining_attempts, + tokens.remaining_attempts + ), + expires_at: + fragment( + "CASE WHEN ? - 1 = 0 THEN COALESCE(?, NOW()) ELSE ? END", + tokens.remaining_attempts, + tokens.expires_at, + tokens.expires_at + ) + ] + ) + |> Ecto.Query.select([tokens: tokens], tokens) + |> Repo.update_all([]) + |> case do + {1, [token]} -> {:ok, token} + {0, []} -> {:error, :not_found} + end + end + @doc false def peek_token(encoded_token, %Auth.Context{} = context) do with [nonce, encoded_fragment] <- String.split(encoded_token, ".", parts: 2), @@ -133,13 +158,12 @@ defmodule Domain.Tokens do end end - defp verify_token(encrypted_token, %Auth.Context{} = context) do + defp verify_token(encoded_fragment, %Auth.Context{} = context) do config = fetch_config!() key_base = Keyword.fetch!(config, :key_base) shared_salt = Keyword.fetch!(config, :salt) salt = shared_salt <> to_string(context.type) - - Plug.Crypto.verify(key_base, salt, encrypted_token, max_age: :infinity) + Plug.Crypto.verify(key_base, salt, encoded_fragment, max_age: :infinity) end def delete_token(%Token{} = token, %Auth.Subject{} = subject) do @@ -181,26 +205,67 @@ defmodule Domain.Tokens do end end + def delete_tokens_for(%Relays.Group{} = group, %Auth.Subject{} = subject) do + with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_tokens_permission()) do + Token.Query.by_relay_group_id(group.id) + |> Authorizer.for_subject(subject) + |> delete_tokens() + end + end + + def delete_tokens_for(%Gateways.Group{} = group, %Auth.Subject{} = subject) do + with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_tokens_permission()) do + Token.Query.by_gateway_group_id(group.id) + |> Authorizer.for_subject(subject) + |> delete_tokens() + end + end + + def delete_all_tokens_by_type_and_assoc(:email, %Auth.Identity{} = identity) do + Token.Query.by_type(:email) + |> Token.Query.by_account_id(identity.account_id) + |> Token.Query.by_identity_id(identity.id) + |> delete_tokens() + end + def delete_expired_tokens do Token.Query.expired() |> delete_tokens() end defp delete_tokens(queryable) do - {count, ids} = + {count, tokens} = queryable - |> Ecto.Query.select([tokens: tokens], tokens.id) + |> Ecto.Query.select([tokens: tokens], tokens) |> Repo.update_all(set: [deleted_at: DateTime.utc_now()]) - :ok = - Enum.each(ids, fn id -> - # TODO: use Domain.PubSub once it's in the codebase - Phoenix.PubSub.broadcast(Domain.PubSub, "sessions:#{id}", "disconnect") - end) + :ok = Enum.each(tokens, &broadcast_disconnect_message/1) {:ok, count} end + defp broadcast_disconnect_message(%{type: :gateway_group, gateway_group_id: id}) do + Phoenix.PubSub.broadcast(Domain.PubSub, "gateway_groups:#{id}", "disconnect") + end + + defp broadcast_disconnect_message(%{type: :relay_group, relay_group_id: id}) do + Phoenix.PubSub.broadcast(Domain.PubSub, "relay_groups:#{id}", "disconnect") + end + + defp broadcast_disconnect_message(%{type: :client, id: id}) do + # TODO: use Domain.PubSub once it's in the codebase + Phoenix.PubSub.broadcast(Domain.PubSub, "client:#{id}", "disconnect") + Phoenix.PubSub.broadcast(Domain.PubSub, "sessions:#{id}", "disconnect") + end + + defp broadcast_disconnect_message(%{type: :browser, id: id}) do + Phoenix.PubSub.broadcast(Domain.PubSub, "sessions:#{id}", "disconnect") + end + + defp broadcast_disconnect_message(%{type: :email}) do + :ok + end + def delete_token_by_id(token_id) do if Validator.valid_uuid?(token_id) do Token.Query.by_id(token_id) diff --git a/elixir/apps/domain/lib/domain/tokens/token.ex b/elixir/apps/domain/lib/domain/tokens/token.ex index c18b6c26e..6c69d634f 100644 --- a/elixir/apps/domain/lib/domain/tokens/token.ex +++ b/elixir/apps/domain/lib/domain/tokens/token.ex @@ -1,9 +1,16 @@ -# TODO: service accounts auth as clients and as API clients? defmodule Domain.Tokens.Token do use Domain, :schema schema "tokens" do - field :type, Ecto.Enum, values: [:browser, :client, :relay, :gateway, :email, :api_client] + field :type, Ecto.Enum, + values: [ + :browser, + :client, + :api_client, + :relay_group, + :gateway_group, + :email + ] field :name, :string @@ -11,8 +18,10 @@ defmodule Domain.Tokens.Token do belongs_to :identity, Domain.Auth.Identity # set for browser and client tokens belongs_to :actor, Domain.Actors.Actor - # belongs_to :relay_group, Domain.Relays.Group - # belongs_to :gateway_group, Domain.Relays.Group + # set for relay tokens + belongs_to :relay_group, Domain.Relays.Group + # set for gateway tokens + belongs_to :gateway_group, Domain.Gateways.Group # we store just hash(nonce+fragment+salt) field :secret_nonce, :string, virtual: true, redact: true @@ -20,6 +29,9 @@ defmodule Domain.Tokens.Token do field :secret_salt, :string, redact: true field :secret_hash, :string, redact: true + # Limits how many times invalid secret can be used for a token + field :remaining_attempts, :integer + belongs_to :account, Domain.Accounts.Account field :last_seen_user_agent, :string diff --git a/elixir/apps/domain/lib/domain/tokens/token/changeset.ex b/elixir/apps/domain/lib/domain/tokens/token/changeset.ex index a5a8f9bee..eecf84ae6 100644 --- a/elixir/apps/domain/lib/domain/tokens/token/changeset.ex +++ b/elixir/apps/domain/lib/domain/tokens/token/changeset.ex @@ -5,19 +5,27 @@ defmodule Domain.Tokens.Token.Changeset do @required_attrs ~w[ type - account_id - created_by_user_agent created_by_remote_ip - expires_at ]a - @create_attrs ~w[name identity_id actor_id secret_fragment secret_nonce]a ++ @required_attrs - @update_attrs ~w[name expires_at]a + @create_attrs ~w[ + name + account_id identity_id actor_id relay_group_id gateway_group_id + secret_fragment secret_nonce + remaining_attempts + created_by_user_agent created_by_remote_ip + expires_at + ]a ++ @required_attrs + + @update_attrs ~w[ + name + expires_at + ]a def create(attrs) do %Token{} |> cast(attrs, @create_attrs) |> validate_required(@required_attrs) - |> validate_inclusion(:type, [:email, :browser, :client]) + |> validate_inclusion(:type, [:email, :browser, :client, :relay_group]) |> changeset() |> put_change(:created_by, :system) end @@ -27,7 +35,16 @@ defmodule Domain.Tokens.Token.Changeset do |> cast(attrs, @create_attrs) |> put_change(:account_id, subject.account.id) |> validate_required(@required_attrs) - |> validate_inclusion(:type, [:client, :relay, :gateway, :api_client]) + |> put_change(:created_by_user_agent, subject.context.user_agent) + |> put_change(:created_by_remote_ip, subject.context.remote_ip) + |> validate_required([:created_by_user_agent, :created_by_remote_ip]) + |> validate_inclusion(:type, [ + :client, + :relay_group, + :gateway_group, + :api_client, + :service_account_client + ]) |> changeset() |> put_change(:created_by, :identity) |> put_change(:created_by_identity_id, subject.identity.id) @@ -56,17 +73,35 @@ defmodule Domain.Tokens.Token.Changeset do case fetch_field(changeset, :context) do {_data_or_changes, :browser} -> changeset + |> validate_required(:actor_id) |> validate_required(:identity_id) - |> assoc_constraint(:identity) + |> validate_required(:expires_at) {_data_or_changes, :client} -> changeset + |> validate_required(:actor_id) + + {_data_or_changes, :api_client} -> + changeset + |> validate_required(:actor_id) + |> validate_required(:name) + + {_data_or_changes, :relay_group} -> + changeset + |> validate_required(:relay_group_id) + + {_data_or_changes, :gateway_group} -> + changeset + |> validate_required(:gateway_group_id) + + {_data_or_changes, :email} -> + changeset + |> validate_required(:actor_id) |> validate_required(:identity_id) - |> assoc_constraint(:identity) + |> validate_required(:expires_at) + |> validate_required(:remaining_attempts) - # TODO: relay, gateway, api_client - - _ -> + :error -> changeset end end diff --git a/elixir/apps/domain/lib/domain/tokens/token/query.ex b/elixir/apps/domain/lib/domain/tokens/token/query.ex index 0101b47d3..5271145b9 100644 --- a/elixir/apps/domain/lib/domain/tokens/token/query.ex +++ b/elixir/apps/domain/lib/domain/tokens/token/query.ex @@ -11,7 +11,11 @@ defmodule Domain.Tokens.Token.Query do end def not_expired(queryable \\ not_deleted()) do - where(queryable, [tokens: tokens], tokens.expires_at > ^DateTime.utc_now()) + where( + queryable, + [tokens: tokens], + tokens.expires_at > ^DateTime.utc_now() or is_nil(tokens.expires_at) + ) end def expired(queryable \\ not_deleted()) do @@ -26,7 +30,13 @@ defmodule Domain.Tokens.Token.Query do where(queryable, [tokens: tokens], tokens.type == ^type) end - def by_account_id(queryable \\ not_deleted(), account_id) do + def by_account_id(queryable \\ not_deleted(), account_id) + + def by_account_id(queryable, nil) do + where(queryable, [tokens: tokens], is_nil(tokens.account_id)) + end + + def by_account_id(queryable, account_id) do where(queryable, [tokens: tokens], tokens.account_id == ^account_id) end @@ -34,6 +44,18 @@ defmodule Domain.Tokens.Token.Query do where(queryable, [tokens: tokens], tokens.actor_id == ^actor_id) end + def by_identity_id(queryable \\ not_deleted(), identity_id) do + where(queryable, [tokens: tokens], tokens.identity_id == ^identity_id) + end + + def by_relay_group_id(queryable \\ not_deleted(), relay_group_id) do + where(queryable, [tokens: tokens], tokens.relay_group_id == ^relay_group_id) + end + + def by_gateway_group_id(queryable \\ not_deleted(), gateway_group_id) do + where(queryable, [tokens: tokens], tokens.gateway_group_id == ^gateway_group_id) + end + def with_joined_account(queryable \\ not_deleted()) do with_named_binding(queryable, :account, fn queryable, binding -> join(queryable, :inner, [tokens: tokens], account in assoc(tokens, ^binding), as: ^binding) diff --git a/elixir/apps/domain/lib/domain/types/ip.ex b/elixir/apps/domain/lib/domain/types/ip.ex index e7d15f816..2d0c088cd 100644 --- a/elixir/apps/domain/lib/domain/types/ip.ex +++ b/elixir/apps/domain/lib/domain/types/ip.ex @@ -27,6 +27,8 @@ defmodule Domain.Types.IP do def cast(_), do: :error def dump(%Postgrex.INET{} = inet), do: {:ok, inet} + def dump(tuple) when tuple_size(tuple) == 4, do: {:ok, %Postgrex.INET{address: tuple}} + def dump(tuple) when tuple_size(tuple) == 8, do: {:ok, %Postgrex.INET{address: tuple}} def dump(_), do: :error def load(%Postgrex.INET{} = inet), do: {:ok, inet} diff --git a/elixir/apps/domain/priv/repo/migrations/20240110175212_add_tokens_api_clients.exs b/elixir/apps/domain/priv/repo/migrations/20240110175212_add_tokens_api_clients.exs new file mode 100644 index 000000000..f5a726e5c --- /dev/null +++ b/elixir/apps/domain/priv/repo/migrations/20240110175212_add_tokens_api_clients.exs @@ -0,0 +1,33 @@ +defmodule Domain.Repo.Migrations.AddTokensApiClients do + use Ecto.Migration + + def change do + drop( + constraint(:tokens, :assoc_not_null, + check: """ + (type = 'browser' AND identity_id IS NOT NULL) + OR (type = 'client' AND ( + (identity_id IS NOT NULL AND actor_id IS NOT NULL) + OR actor_id IS NOT NULL) + ) + OR (type IN ('relay', 'gateway', 'email', 'api_client')) + """ + ) + ) + + create( + constraint(:tokens, :assoc_not_null, + check: """ + (type = 'browser' AND actor_id IS NOT NULL AND identity_id IS NOT NULL) + OR (type = 'email' AND actor_id IS NOT NULL AND identity_id IS NOT NULL) + OR (type = 'client' AND ( + (identity_id IS NOT NULL AND actor_id IS NOT NULL) + OR actor_id IS NOT NULL) + ) + OR (type = 'api_client' AND actor_id IS NOT NULL) + OR (type IN ('relay', 'gateway')) + """ + ) + ) + end +end diff --git a/elixir/apps/domain/priv/repo/migrations/20240111160704_use_tokens_for_relay_and_gateway_groups.exs b/elixir/apps/domain/priv/repo/migrations/20240111160704_use_tokens_for_relay_and_gateway_groups.exs new file mode 100644 index 000000000..6e0d55c97 --- /dev/null +++ b/elixir/apps/domain/priv/repo/migrations/20240111160704_use_tokens_for_relay_and_gateway_groups.exs @@ -0,0 +1,64 @@ +defmodule Domain.Repo.Migrations.UseTokensForRelayAndGatewayGroups do + use Ecto.Migration + + def change do + alter table(:relays) do + remove(:token_id) + end + + drop(table(:relay_tokens)) + + alter table(:gateways) do + remove(:token_id) + end + + drop(table(:gateway_tokens)) + + execute("ALTER TABLE tokens ALTER COLUMN expires_at DROP NOT NULL") + execute("ALTER TABLE tokens ALTER COLUMN account_id DROP NOT NULL") + execute("ALTER TABLE tokens ALTER COLUMN created_by_user_agent DROP NOT NULL") + execute("ALTER TABLE tokens ALTER COLUMN created_by_remote_ip DROP NOT NULL") + + alter table(:tokens) do + add( + :relay_group_id, + references(:relay_groups, type: :binary_id, on_delete: :delete_all) + ) + + add( + :gateway_group_id, + references(:gateway_groups, type: :binary_id, on_delete: :delete_all) + ) + + add(:remaining_attempts, :integer) + end + + drop( + constraint(:tokens, :assoc_not_null, + check: """ + (type = 'browser' AND actor_id IS NOT NULL AND identity_id IS NOT NULL) + OR (type = 'email' AND actor_id IS NOT NULL AND identity_id IS NOT NULL) + OR (type = 'client' AND ( + (identity_id IS NOT NULL AND actor_id IS NOT NULL) + OR actor_id IS NOT NULL) + ) + OR (type = 'api_client' AND actor_id IS NOT NULL) + OR (type IN ('relay', 'gateway')) + """ + ) + ) + + create( + constraint(:tokens, :assoc_not_null, + check: """ + (type = 'browser' AND actor_id IS NOT NULL AND identity_id IS NOT NULL) + OR (type = 'client' AND actor_id IS NOT NULL) + OR (type = 'email' AND actor_id IS NOT NULL AND identity_id IS NOT NULL) + OR (type = 'api_client' AND actor_id IS NOT NULL) + OR (type = 'relay_group' AND relay_group_id IS NOT NULL) + OR (type = 'gateway_group' AND gateway_group_id IS NOT NULL) + """ + ) + ) + end +end diff --git a/elixir/apps/domain/priv/repo/seeds.exs b/elixir/apps/domain/priv/repo/seeds.exs index 179483b1d..4ce87bd6a 100644 --- a/elixir/apps/domain/priv/repo/seeds.exs +++ b/elixir/apps/domain/priv/repo/seeds.exs @@ -1,4 +1,4 @@ -alias Domain.{Repo, Accounts, Auth, Actors, Relays, Gateways, Resources, Policies, Flows} +alias Domain.{Repo, Accounts, Auth, Actors, Relays, Gateways, Resources, Policies, Flows, Tokens} # Seeds can be run both with MIX_ENV=prod and MIX_ENV=test, for test env we don't have # an adapter configured and creation of email provider will fail, so we will override it here. @@ -170,11 +170,6 @@ other_admin_actor_email = "other@localhost" } }) -unprivileged_actor_email_token = - unprivileged_actor_email_identity.provider_virtual_state.sign_in_token - -admin_actor_email_token = admin_actor_email_identity.provider_virtual_state.sign_in_token - unprivileged_actor_context = %Auth.Context{ type: :browser, user_agent: "Debian/11.0.0 connlib/0.1.0", @@ -210,10 +205,34 @@ admin_actor_context = %Auth.Context{ Auth.build_subject(admin_actor_token, admin_actor_context) {:ok, service_account_actor_encoded_token} = - Auth.create_service_account_token(service_account_actor, admin_subject, %{ - "name" => "tok-#{Ecto.UUID.generate()}", - "expires_at" => DateTime.utc_now() |> DateTime.add(365, :day) - }) + Auth.create_service_account_token( + service_account_actor, + %{ + "name" => "tok-#{Ecto.UUID.generate()}", + "expires_at" => DateTime.utc_now() |> DateTime.add(365, :day) + }, + admin_subject + ) + +{:ok, unprivileged_actor_email_identity} = + Domain.Auth.Adapters.Email.request_sign_in_token( + unprivileged_actor_email_identity, + unprivileged_actor_context + ) + +unprivileged_actor_email_token = + unprivileged_actor_email_identity.provider_virtual_state.nonce <> + unprivileged_actor_email_identity.provider_virtual_state.fragment + +{:ok, admin_actor_email_identity} = + Domain.Auth.Adapters.Email.request_sign_in_token( + admin_actor_email_identity, + admin_actor_context + ) + +admin_actor_email_token = + admin_actor_email_identity.provider_virtual_state.nonce <> + admin_actor_email_identity.provider_virtual_state.fragment IO.puts("Created users: ") @@ -297,43 +316,57 @@ all_group IO.puts("") {:ok, global_relay_group} = - Relays.create_global_group(%{ - name: "fz-global-relays", - tokens: [%{}] + Relays.create_global_group(%{name: "fz-global-relays"}) + +{:ok, global_relay_group_token} = + Tokens.create_token(%{ + "type" => :relay_group, + "secret_fragment" => Domain.Crypto.random_token(32, encoder: :hex32), + "relay_group_id" => global_relay_group.id }) -global_relay_group_token = hd(global_relay_group.tokens) - global_relay_group_token = - maybe_repo_update.(global_relay_group_token, - id: "c1038e22-0215-4977-9f6c-f65621e0008f", - hash: - "$argon2id$v=19$m=65536,t=3,p=4$XBzQrgdRFH5XhiTfWFcGWA$PTTy4D7xtahPbvGTgZLgGS8qHnfd8LJKWAnTdhB4yww", - value: - "Obnnb37dBtNQccCU-fBYu1h8NafAp0KyoOwlo2TTIy60ofokIlV60spa12G5pIG-RVKj5qwHVEh1k9n8xBcf9A" + global_relay_group_token + |> maybe_repo_update.( + id: "e82fcdc1-057a-4015-b90b-3b18f0f28053", + secret_salt: "lZWUdgh-syLGVDsZEu_29A", + secret_fragment: "C14NGA87EJRR03G4QPR07A9C6G784TSSTHSF4TI5T0GD8D6L0VRG====", + secret_hash: "c3c9a031ae98f111ada642fddae546de4e16ceb85214ab4f1c9d0de1fc472797" ) +global_relay_group_encoded_token = Tokens.encode_fragment!(global_relay_group_token) + IO.puts("Created global relay groups:") -IO.puts(" #{global_relay_group.name} token: #{Relays.encode_token!(global_relay_group_token)}") +IO.puts(" #{global_relay_group.name} token: #{global_relay_group_encoded_token}") IO.puts("") +relay_context = %Auth.Context{ + type: :relay_group, + user_agent: "Ubuntu/14.04 connlib/0.7.412", + remote_ip: {100, 64, 100, 58} +} + {:ok, global_relay} = - Relays.upsert_relay(global_relay_group_token, %{ - ipv4: {189, 172, 72, 111}, - ipv6: {0, 0, 0, 0, 0, 0, 0, 1}, - last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", - last_seen_remote_ip: %Postgrex.INET{address: {189, 172, 72, 111}} - }) + Relays.upsert_relay( + global_relay_group, + %{ + ipv4: {189, 172, 72, 111}, + ipv6: {0, 0, 0, 0, 0, 0, 0, 1} + }, + relay_context + ) for i <- 1..5 do {:ok, _global_relay} = - Relays.upsert_relay(global_relay_group_token, %{ - ipv4: {189, 172, 72, 111 + i}, - ipv6: {0, 0, 0, 0, 0, 0, 0, i}, - last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", - last_seen_remote_ip: %Postgrex.INET{address: {189, 172, 72, 111 + i}} - }) + Relays.upsert_relay( + global_relay_group, + %{ + ipv4: {189, 172, 72, 111 + i}, + ipv6: {0, 0, 0, 0, 0, 0, 0, i} + }, + %{relay_context | remote_ip: %Postgrex.INET{address: {189, 172, 72, 111 + i}}} + ) end IO.puts("Created global relays:") @@ -343,42 +376,60 @@ IO.puts("") relay_group = account - |> Relays.Group.Changeset.create( - %{name: "mycorp-aws-relays", tokens: [%{}]}, - admin_subject - ) + |> Relays.Group.Changeset.create(%{name: "mycorp-aws-relays"}, admin_subject) |> Repo.insert!() -relay_group_token = hd(relay_group.tokens) +{:ok, relay_group_token} = + Tokens.create_token(%{ + "type" => :relay_group, + "secret_fragment" => Domain.Crypto.random_token(32, encoder: :hex32), + "account_id" => admin_subject.account.id, + "relay_group_id" => global_relay_group.id + }) relay_group_token = - maybe_repo_update.(relay_group_token, - id: "7286b53d-073e-4c41-9ff1-cc8451dad299", - hash: - "$argon2id$v=19$m=131072,t=8,p=4$aSw/NA3z0vGJjvF3ukOcyg$5MPWPXLETM3iZ19LTihItdVGb7ou/i4/zhpozMrpCFg", - value: "EX77Ga0HKJUVLgcpMrN6HatdGnfvADYQrRjUWWyTqqt7BaUdEU3o9-FbBlRdINIK" + relay_group_token + |> maybe_repo_update.( + id: "549c4107-1492-4f8f-a4ec-a9d2a66d8aa9", + secret_salt: "jaJwcwTRhzQr15SgzTB2LA", + secret_fragment: "PU5AITE1O8VDVNMHMOAC77DIKMOGTDIA672S6G1AB02OS34H5ME0====", + secret_hash: "af133f7efe751ca978ec3e5fadf081ce9ab50138ff52862395858c3d2c11c0c5" ) +relay_group_encoded_token = Tokens.encode_fragment!(relay_group_token) + IO.puts("Created relay groups:") -IO.puts(" #{relay_group.name} token: #{Relays.encode_token!(relay_group_token)}") +IO.puts(" #{relay_group.name} token: #{relay_group_encoded_token}") IO.puts("") {:ok, relay} = - Relays.upsert_relay(relay_group_token, %{ - ipv4: {189, 172, 73, 111}, - ipv6: {0, 0, 0, 0, 0, 0, 0, 1}, - last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", - last_seen_remote_ip: %Postgrex.INET{address: {189, 172, 73, 111}} - }) + Relays.upsert_relay( + relay_group, + %{ + ipv4: {189, 172, 73, 111}, + ipv6: {0, 0, 0, 0, 0, 0, 0, 1} + }, + %Auth.Context{ + type: :relay_group, + user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", + remote_ip: %Postgrex.INET{address: {189, 172, 73, 111}} + } + ) for i <- 1..5 do {:ok, _relay} = - Relays.upsert_relay(relay_group_token, %{ - ipv4: {189, 172, 73, 111 + i}, - ipv6: {0, 0, 0, 0, 0, 0, 0, i}, - last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", - last_seen_remote_ip: %Postgrex.INET{address: {189, 172, 73, 111}} - }) + Relays.upsert_relay( + relay_group, + %{ + ipv4: {189, 172, 73, 111 + i}, + ipv6: {0, 0, 0, 0, 0, 0, 0, i} + }, + %Auth.Context{ + type: :relay_group, + user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", + remote_ip: %Postgrex.INET{address: {189, 172, 73, 111}} + } + ) end IO.puts("Created relays:") @@ -394,48 +445,77 @@ gateway_group = ) |> Repo.insert!() -gateway_group_token = hd(gateway_group.tokens) - -gateway_group_token = - maybe_repo_update.( - gateway_group_token, - id: "3cef0566-adfd-48fe-a0f1-580679608f6f", - hash: - "$argon2id$v=19$m=131072,t=8,p=4$w0aXBd0iv/OTizWGBRTKiw$m6J0YXRsFCO95Q8LeVvH+CxFTy0Li7Lrcs3NDJRykCA", - value: "jjtzxRFJPZGBc-oCZ9Dy2FwjwaHXMAUdpzuRr2sRropx75-znh_xp_5bT5Ono-rb" +{:ok, gateway_group_token} = + Tokens.create_token( + %{ + "type" => :gateway_group, + "secret_fragment" => Domain.Crypto.random_token(32, encoder: :hex32), + "account_id" => admin_subject.account.id, + "gateway_group_id" => gateway_group.id + }, + admin_subject ) +gateway_group_token = + gateway_group_token + |> maybe_repo_update.( + id: "2274560b-e97b-45e4-8b34-679c7617e98d", + secret_salt: "uQyisyqrvYIIitMXnSJFKQ", + secret_fragment: "O02L7US2J3VINOMPR9J6IL88QIQP6UO8AQVO6U5IPL0VJC22JGH0====", + secret_hash: "876f20e8d4de25d5ffac40733f280782a7d8097347d77415ab6e4e548f13d2ee" + ) + +gateway_group_encoded_token = Tokens.encode_fragment!(gateway_group_token) + IO.puts("Created gateway groups:") -IO.puts(" #{gateway_group.name} token: #{Gateways.encode_token!(gateway_group_token)}") +IO.puts(" #{gateway_group.name} token: #{gateway_group_encoded_token}") IO.puts("") {:ok, gateway1} = - Gateways.upsert_gateway(gateway_group_token, %{ - external_id: Ecto.UUID.generate(), - name: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}", - public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(), - last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", - last_seen_remote_ip: %Postgrex.INET{address: {189, 172, 73, 153}} - }) + Gateways.upsert_gateway( + gateway_group, + %{ + external_id: Ecto.UUID.generate(), + name: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}", + public_key: :crypto.strong_rand_bytes(32) |> Base.encode64() + }, + %Auth.Context{ + type: :gateway_group, + user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", + remote_ip: %Postgrex.INET{address: {189, 172, 73, 153}} + } + ) {:ok, gateway2} = - Gateways.upsert_gateway(gateway_group_token, %{ - external_id: Ecto.UUID.generate(), - name: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}", - public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(), - last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", - last_seen_remote_ip: %Postgrex.INET{address: {164, 112, 78, 62}} - }) + Gateways.upsert_gateway( + gateway_group, + %{ + external_id: Ecto.UUID.generate(), + name: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}", + public_key: :crypto.strong_rand_bytes(32) |> Base.encode64() + }, + %Auth.Context{ + type: :gateway_group, + user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", + remote_ip: %Postgrex.INET{address: {164, 112, 78, 62}} + } + ) for i <- 1..10 do {:ok, _gateway} = - Gateways.upsert_gateway(gateway_group_token, %{ - external_id: Ecto.UUID.generate(), - name: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}", - public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(), - last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", - last_seen_remote_ip: %Postgrex.INET{address: {164, 112, 78, 62 + i}} - }) + Gateways.upsert_gateway( + gateway_group, + %{ + external_id: Ecto.UUID.generate(), + name: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}", + public_key: :crypto.strong_rand_bytes(32) |> Base.encode64() + }, + %Auth.Context{ + type: :gateway_group, + user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", + remote_ip: %Postgrex.INET{address: {164, 112, 78, 62 + i}} + } + ) end IO.puts("Created gateways:") diff --git a/elixir/apps/domain/test/domain/auth/adapters/email_test.exs b/elixir/apps/domain/test/domain/auth/adapters/email_test.exs index e19610f8c..032a3468c 100644 --- a/elixir/apps/domain/test/domain/auth/adapters/email_test.exs +++ b/elixir/apps/domain/test/domain/auth/adapters/email_test.exs @@ -18,23 +18,13 @@ defmodule Domain.Auth.Adapters.EmailTest do } end - test "puts default provider state", %{provider: provider, changeset: changeset} do + test "puts empty provider state by default", %{provider: provider, changeset: changeset} do assert %Ecto.Changeset{} = changeset = identity_changeset(provider, changeset) - assert %{ - provider_state: %{ - "sign_in_token_created_at" => %DateTime{}, - "sign_in_token_hash" => sign_in_token_hash, - "sign_in_token_salt" => sign_in_token_salt - }, - provider_virtual_state: %{sign_in_token: sign_in_token} - } = changeset.changes - - assert Domain.Crypto.equal?( - :argon2, - sign_in_token <> sign_in_token_salt, - sign_in_token_hash - ) + assert changeset.changes == %{ + provider_state: %{}, + provider_virtual_state: %{} + } end test "trims provider identifier", %{provider: provider, changeset: changeset} do @@ -97,89 +87,111 @@ defmodule Domain.Auth.Adapters.EmailTest do end describe "request_sign_in_token/1" do - test "returns identity with updated sign-in token" do + test "returns identity with valid token components" do identity = Fixtures.Auth.create_identity() + context = Fixtures.Auth.build_context(type: :email) - assert {:ok, identity} = request_sign_in_token(identity) + assert {:ok, identity} = request_sign_in_token(identity, context) assert %{ - "sign_in_token_created_at" => sign_in_token_created_at, - "sign_in_token_hash" => sign_in_token_hash, - "sign_in_token_salt" => sign_in_token_salt + "last_created_token_id" => token_id } = identity.provider_state assert %{ - sign_in_token: sign_in_token + nonce: nonce, + fragment: fragment } = identity.provider_virtual_state - assert Domain.Crypto.equal?( - :argon2, - sign_in_token <> sign_in_token_salt, - sign_in_token_hash - ) + token = Repo.get(Domain.Tokens.Token, token_id) + assert token.type == :email + assert token.account_id == identity.account_id + assert token.actor_id == identity.actor_id + assert token.identity_id == identity.id + assert token.remaining_attempts == 5 - assert %DateTime{} = sign_in_token_created_at + assert {:ok, token} = Domain.Tokens.use_token(nonce <> fragment, context) + assert token.id == token_id + assert token.remaining_attempts == 4 + end + + test "deletes previous sign in tokens" do + identity = Fixtures.Auth.create_identity() + context = Fixtures.Auth.build_context(type: :email) + + assert {:ok, identity} = request_sign_in_token(identity, context) + assert %{"last_created_token_id" => first_token_id} = identity.provider_state + + assert {:ok, identity} = request_sign_in_token(identity, context) + assert %{"last_created_token_id" => second_token_id} = identity.provider_state + + assert Repo.get(Domain.Tokens.Token, first_token_id).deleted_at + refute Repo.get(Domain.Tokens.Token, second_token_id).deleted_at end end - describe "verify_secret/2" do + describe "verify_secret/3" do setup do + context = Fixtures.Auth.build_context(type: :email) Domain.Config.put_env_override(:outbound_email_adapter_configured?, true) account = Fixtures.Accounts.create_account() provider = Fixtures.Auth.create_email_provider(account: account) identity = Fixtures.Auth.create_identity(account: account, provider: provider) - token = identity.provider_virtual_state.sign_in_token + {:ok, identity} = request_sign_in_token(identity, context) - %{account: account, provider: provider, identity: identity, token: token} + nonce = identity.provider_virtual_state.nonce + fragment = identity.provider_virtual_state.fragment + + %{ + account: account, + provider: provider, + identity: identity, + token: nonce <> fragment, + context: context + } end - test "removes token after it's used", %{ + test "removes all pending tokens after one is used", %{ + account: account, identity: identity, + context: context, token: token } do - assert {:ok, identity, nil} = verify_secret(identity, token) + other_token = + Fixtures.Tokens.create_token( + type: :email, + account: account, + identity: identity + ) - assert identity.provider_state == %{} + assert {:ok, identity, nil} = verify_secret(identity, context, token) + + assert %{last_used_token_id: token_id} = identity.provider_state assert identity.provider_virtual_state == %{} - end - test "removes token after 5 failed attempts", %{ - identity: identity - } do - for i <- 1..5 do - assert verify_secret(identity, "foo") == {:error, :invalid_secret} - assert %{"sign_in_failed_attempts" => ^i} = Repo.one(Auth.Identity).provider_state - end + token = Repo.get(Domain.Tokens.Token, token_id) + assert token.deleted_at - assert verify_secret(identity, "foo") == {:error, :invalid_secret} - assert Repo.one(Auth.Identity).provider_state == %{} + token = Repo.get(Domain.Tokens.Token, other_token.id) + assert token.deleted_at end test "returns error when token is expired", %{ - account: account, - provider: provider + context: context, + identity: identity, + token: token } do - forty_seconds_ago = DateTime.utc_now() |> DateTime.add(-1 * 15 * 60 - 1, :second) + Repo.get(Domain.Tokens.Token, identity.provider_state["last_created_token_id"]) + |> Fixtures.Tokens.expire_token() - identity = - Fixtures.Auth.create_identity( - account: account, - provider: provider, - provider_state: %{ - "sign_in_token_hash" => Domain.Crypto.hash(:argon2, "dummy_token" <> "salty"), - "sign_in_token_created_at" => DateTime.to_iso8601(forty_seconds_ago), - "sign_in_token_salt" => "salty" - } - ) - - assert verify_secret(identity, "dummy_token") == {:error, :expired_secret} + assert verify_secret(identity, context, token) == {:error, :invalid_secret} end test "returns error when token is invalid", %{ + context: context, identity: identity } do - assert verify_secret(identity, "foo") == {:error, :invalid_secret} + assert verify_secret(identity, context, "foo") == {:error, :invalid_secret} end end end diff --git a/elixir/apps/domain/test/domain/auth/adapters/userpass_test.exs b/elixir/apps/domain/test/domain/auth/adapters/userpass_test.exs index 34e8d24c6..58856f678 100644 --- a/elixir/apps/domain/test/domain/auth/adapters/userpass_test.exs +++ b/elixir/apps/domain/test/domain/auth/adapters/userpass_test.exs @@ -109,7 +109,7 @@ defmodule Domain.Auth.Adapters.UserPassTest do end end - describe "verify_secret/2" do + describe "verify_secret/3" do setup do account = Fixtures.Accounts.create_account() provider = Fixtures.Auth.create_userpass_provider(account: account) @@ -124,19 +124,22 @@ defmodule Domain.Auth.Adapters.UserPassTest do } ) + context = Fixtures.Auth.build_context() + %{ account: account, provider: provider, - identity: identity + identity: identity, + context: context } end - test "returns :invalid_secret on invalid password", %{identity: identity} do - assert verify_secret(identity, "FirezoneInvalid") == {:error, :invalid_secret} + test "returns :invalid_secret on invalid password", %{identity: identity, context: context} do + assert verify_secret(identity, context, "FirezoneInvalid") == {:error, :invalid_secret} end - test "returns :ok on valid password", %{identity: identity} do - assert {:ok, verified_identity, nil} = verify_secret(identity, "Firezone1234") + test "returns :ok on valid password", %{identity: identity, context: context} do + assert {:ok, verified_identity, nil} = verify_secret(identity, context, "Firezone1234") assert verified_identity.provider_state["password_hash"] == identity.provider_state["password_hash"] diff --git a/elixir/apps/domain/test/domain/auth_test.exs b/elixir/apps/domain/test/domain/auth_test.exs index f9ff9ebf2..1f3fff13f 100644 --- a/elixir/apps/domain/test/domain/auth_test.exs +++ b/elixir/apps/domain/test/domain/auth_test.exs @@ -1656,10 +1656,8 @@ defmodule Domain.AuthTest do assert identity.provider_identifier == provider_identifier assert identity.actor_id == actor.id - assert %{"sign_in_token_created_at" => _, "sign_in_token_hash" => _} = - identity.provider_state - - assert %{sign_in_token: _} = identity.provider_virtual_state + assert identity.provider_state == %{} + assert identity.provider_virtual_state == %{} assert identity.account_id == provider.account_id assert is_nil(identity.deleted_at) end @@ -1671,14 +1669,13 @@ defmodule Domain.AuthTest do } do provider_identifier = Fixtures.Auth.random_provider_identifier(provider) - identity = - Fixtures.Auth.create_identity( - account: account, - provider: provider, - provider_identifier: provider_identifier, - actor: actor, - provider_virtual_state: %{"foo" => "bar"} - ) + Fixtures.Auth.create_identity( + account: account, + provider: provider, + provider_identifier: provider_identifier, + actor: actor, + provider_virtual_state: %{"foo" => "bar"} + ) attrs = %{ provider_identifier: provider_identifier, @@ -1689,8 +1686,8 @@ defmodule Domain.AuthTest do assert Repo.one(Auth.Identity).id == updated_identity.id - assert updated_identity.provider_virtual_state != identity.provider_virtual_state - assert updated_identity.provider_state != identity.provider_state + assert updated_identity.provider_virtual_state == %{} + assert updated_identity.provider_state == %{} end test "returns error when identifier is invalid", %{ @@ -1818,10 +1815,8 @@ defmodule Domain.AuthTest do assert identity.provider_identifier == provider_identifier assert identity.actor_id == actor.id - assert %{"sign_in_token_created_at" => _, "sign_in_token_hash" => _} = - identity.provider_state - - assert %{sign_in_token: _} = identity.provider_virtual_state + assert identity.provider_state == %{} + assert identity.provider_virtual_state == %{} assert identity.account_id == provider.account_id assert is_nil(identity.deleted_at) end @@ -2054,10 +2049,8 @@ defmodule Domain.AuthTest do assert new_identity.provider_id == identity.provider_id assert new_identity.actor_id == identity.actor_id - assert %{"sign_in_token_created_at" => _, "sign_in_token_hash" => _} = - new_identity.provider_state - - assert %{sign_in_token: _} = new_identity.provider_virtual_state + assert new_identity.provider_state == %{} + assert new_identity.provider_virtual_state == %{} assert new_identity.account_id == identity.account_id assert is_nil(new_identity.deleted_at) @@ -2331,12 +2324,13 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do - identity = Fixtures.Auth.create_identity(account: account, provider: provider) - secret = "foo" - nonce = "nonce" + nonce = "foo" context = %Auth.Context{type: :browser, user_agent: user_agent, remote_ip: remote_ip} - assert sign_in(provider, identity.provider_identifier, nonce, secret, context) == + identity = Fixtures.Auth.create_identity(account: account, provider: provider) + {:ok, identity} = Domain.Auth.Adapters.Email.request_sign_in_token(identity, context) + + assert sign_in(provider, identity.provider_identifier, nonce, "foo", context) == {:error, :unauthorized} end @@ -2346,12 +2340,14 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do - identity = Fixtures.Auth.create_identity(account: account, provider: provider) - secret = identity.provider_virtual_state.sign_in_token - nonce = "!.=" - context = %Auth.Context{type: :browser, user_agent: user_agent, remote_ip: remote_ip} + identity = Fixtures.Auth.create_identity(account: account, provider: provider) + {:ok, identity} = Domain.Auth.Adapters.Email.request_sign_in_token(identity, context) + + secret = identity.provider_virtual_state.nonce <> identity.provider_virtual_state.fragment + nonce = "!.=" + assert sign_in(provider, identity.provider_identifier, nonce, secret, context) == {:error, :malformed_request} end @@ -2362,12 +2358,13 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do - identity = Fixtures.Auth.create_identity(account: account, provider: provider) - secret = identity.provider_virtual_state.sign_in_token - nonce = "nonce" - + nonce = "foo" context = %Auth.Context{type: :browser, user_agent: user_agent, remote_ip: remote_ip} + identity = Fixtures.Auth.create_identity(account: account, provider: provider) + {:ok, identity} = Domain.Auth.Adapters.Email.request_sign_in_token(identity, context) + secret = identity.provider_virtual_state.nonce <> identity.provider_virtual_state.fragment + assert {:ok, token_identity, fragment} = sign_in(provider, identity.provider_identifier, nonce, secret, context) @@ -2397,11 +2394,13 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do - identity = Fixtures.Auth.create_identity(account: account, provider: provider) - secret = identity.provider_virtual_state.sign_in_token - nonce = "nonce" + nonce = "foo" context = %Auth.Context{type: :browser, user_agent: user_agent, remote_ip: remote_ip} + identity = Fixtures.Auth.create_identity(account: account, provider: provider) + {:ok, identity} = Domain.Auth.Adapters.Email.request_sign_in_token(identity, context) + secret = identity.provider_virtual_state.nonce <> identity.provider_virtual_state.fragment + assert {:ok, _token_identity, _fragment} = sign_in(provider, identity.id, nonce, secret, context) end @@ -2412,15 +2411,18 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do - identity = Fixtures.Auth.create_identity(account: account, provider: provider) - secret = identity.provider_virtual_state.sign_in_token - nonce = "nonce" + nonce = "foo" context = %Auth.Context{type: :client, user_agent: user_agent, remote_ip: remote_ip} - assert {:ok, token_identity, _fragment} = + identity = Fixtures.Auth.create_identity(account: account, provider: provider) + {:ok, identity} = Domain.Auth.Adapters.Email.request_sign_in_token(identity, context) + secret = identity.provider_virtual_state.nonce <> identity.provider_virtual_state.fragment + + assert {:ok, token_identity, fragment} = sign_in(provider, identity.id, nonce, secret, context) - assert token = Repo.one(Domain.Tokens.Token) + {:ok, {_account_id, id, _nonce, _secret}} = Tokens.peek_token(fragment, context) + assert token = Repo.get(Domain.Tokens.Token, id) assert token.type == context.type assert token.identity_id == token_identity.id end @@ -2431,13 +2433,15 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do - nonce = "nonce" + nonce = "foo" - for type <- [:relay, :gateway, :api_client] do - identity = Fixtures.Auth.create_identity(account: account, provider: provider) - secret = identity.provider_virtual_state.sign_in_token + for type <- [:relay, :gateway, :api_client, :email] do context = %Auth.Context{type: type, user_agent: user_agent, remote_ip: remote_ip} + identity = Fixtures.Auth.create_identity(account: account, provider: provider) + {:ok, identity} = Domain.Auth.Adapters.Email.request_sign_in_token(identity, context) + secret = identity.provider_virtual_state.nonce <> identity.provider_virtual_state.fragment + assert_raise FunctionClauseError, fn -> sign_in(provider, identity.id, nonce, secret, context) end @@ -2450,7 +2454,7 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do - nonce = "nonce" + nonce = "foo" # Browser session context = %Auth.Context{type: :browser, user_agent: user_agent, remote_ip: remote_ip} @@ -2459,7 +2463,8 @@ defmodule Domain.AuthTest do ## Admin actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) - secret = identity.provider_virtual_state.sign_in_token + {:ok, identity} = Domain.Auth.Adapters.Email.request_sign_in_token(identity, context) + secret = identity.provider_virtual_state.nonce <> identity.provider_virtual_state.fragment assert {:ok, identity, fragment} = sign_in(provider, identity.provider_identifier, nonce, secret, context) @@ -2472,7 +2477,8 @@ defmodule Domain.AuthTest do ## Regular user actor = Fixtures.Actors.create_actor(type: :account_user, account: account) identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) - secret = identity.provider_virtual_state.sign_in_token + {:ok, identity} = Domain.Auth.Adapters.Email.request_sign_in_token(identity, context) + secret = identity.provider_virtual_state.nonce <> identity.provider_virtual_state.fragment assert {:ok, identity, fragment} = sign_in(provider, identity.provider_identifier, nonce, secret, context) @@ -2488,7 +2494,8 @@ defmodule Domain.AuthTest do ## Admin actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) - secret = identity.provider_virtual_state.sign_in_token + {:ok, identity} = Domain.Auth.Adapters.Email.request_sign_in_token(identity, context) + secret = identity.provider_virtual_state.nonce <> identity.provider_virtual_state.fragment assert {:ok, identity, fragment} = sign_in(provider, identity.provider_identifier, nonce, secret, context) @@ -2500,7 +2507,8 @@ defmodule Domain.AuthTest do ## Regular user actor = Fixtures.Actors.create_actor(type: :account_user, account: account) identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) - secret = identity.provider_virtual_state.sign_in_token + {:ok, identity} = Domain.Auth.Adapters.Email.request_sign_in_token(identity, context) + secret = identity.provider_virtual_state.nonce <> identity.provider_virtual_state.fragment assert {:ok, identity, fragment} = sign_in(provider, identity.provider_identifier, nonce, secret, context) @@ -2516,15 +2524,17 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do + nonce = "foo" actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) subject = Fixtures.Auth.create_subject(identity: identity) {:ok, _provider} = disable_provider(provider, subject) identity = Fixtures.Auth.create_identity(account: account, provider: provider) - secret = identity.provider_virtual_state.sign_in_token - nonce = "nonce" context = %Auth.Context{type: :browser, user_agent: user_agent, remote_ip: remote_ip} + {:ok, identity} = Domain.Auth.Adapters.Email.request_sign_in_token(identity, context) + + secret = identity.provider_virtual_state.nonce <> identity.provider_virtual_state.fragment assert sign_in(provider, identity.provider_identifier, nonce, secret, context) == {:error, :unauthorized} @@ -2536,13 +2546,16 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do + nonce = "foo" actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) - secret = identity.provider_virtual_state.sign_in_token - nonce = "nonce" - subject = Fixtures.Auth.create_subject(identity: identity) - {:ok, identity} = delete_identity(identity, subject) + context = %Auth.Context{type: :browser, user_agent: user_agent, remote_ip: remote_ip} + {:ok, identity} = Domain.Auth.Adapters.Email.request_sign_in_token(identity, context) + secret = identity.provider_virtual_state.nonce <> identity.provider_virtual_state.fragment + + subject = Fixtures.Auth.create_subject(identity: identity) + {:ok, _identity} = delete_identity(identity, subject) assert sign_in(provider, identity.provider_identifier, nonce, secret, context) == {:error, :unauthorized} @@ -2554,14 +2567,17 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do + nonce = "foo" + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) |> Fixtures.Actors.disable() identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) - secret = identity.provider_virtual_state.sign_in_token - nonce = "nonce" context = %Auth.Context{type: :browser, user_agent: user_agent, remote_ip: remote_ip} + {:ok, identity} = Domain.Auth.Adapters.Email.request_sign_in_token(identity, context) + + secret = identity.provider_virtual_state.nonce <> identity.provider_virtual_state.fragment assert sign_in(provider, identity.provider_identifier, nonce, secret, context) == {:error, :unauthorized} @@ -2573,14 +2589,17 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do + nonce = "foo" + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) |> Fixtures.Actors.delete() identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) - secret = identity.provider_virtual_state.sign_in_token - nonce = "nonce" context = %Auth.Context{type: :browser, user_agent: user_agent, remote_ip: remote_ip} + {:ok, identity} = Domain.Auth.Adapters.Email.request_sign_in_token(identity, context) + + secret = identity.provider_virtual_state.nonce <> identity.provider_virtual_state.fragment assert sign_in(provider, identity.provider_identifier, nonce, secret, context) == {:error, :unauthorized} @@ -2592,17 +2611,18 @@ defmodule Domain.AuthTest do user_agent: user_agent, remote_ip: remote_ip } do + nonce = "foo" + actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account) identity = Fixtures.Auth.create_identity(account: account, provider: provider, actor: actor) subject = Fixtures.Auth.create_subject(identity: identity) {:ok, _provider} = delete_provider(provider, subject) - identity = Fixtures.Auth.create_identity(account: account, provider: provider) - secret = identity.provider_virtual_state.sign_in_token - nonce = "nonce" context = %Auth.Context{type: :browser, user_agent: user_agent, remote_ip: remote_ip} + {:ok, identity} = Domain.Auth.Adapters.Email.request_sign_in_token(identity, context) + secret = identity.provider_virtual_state.nonce <> identity.provider_virtual_state.fragment - assert sign_in(provider, identity.provider_identifier, secret, nonce, context) == + assert sign_in(provider, identity.provider_identifier, nonce, secret, context) == {:error, :unauthorized} end end @@ -2744,9 +2764,10 @@ defmodule Domain.AuthTest do context = %Auth.Context{type: :client, user_agent: user_agent, remote_ip: remote_ip} - assert {:ok, token_identity, _fragment} = sign_in(provider, nonce, payload, context) + assert {:ok, token_identity, fragment} = sign_in(provider, nonce, payload, context) - assert token = Repo.one(Domain.Tokens.Token) + {:ok, {_account_id, id, _nonce, _secret}} = Tokens.peek_token(fragment, context) + assert token = Repo.get(Domain.Tokens.Token, id) assert token.type == context.type assert token.identity_id == token_identity.id end @@ -3128,10 +3149,14 @@ defmodule Domain.AuthTest do actor = Fixtures.Actors.create_actor(type: :service_account, account: account) assert {:ok, encoded_token} = - create_service_account_token(actor, subject, %{ - "name" => "foo", - "expires_at" => one_day - }) + create_service_account_token( + actor, + %{ + "name" => "foo", + "expires_at" => one_day + }, + subject + ) assert {:ok, sa_subject} = authenticate(encoded_token, context) assert sa_subject.account.id == account.id @@ -3161,7 +3186,7 @@ defmodule Domain.AuthTest do actor = Fixtures.Actors.create_actor(type: :service_account) assert_raise FunctionClauseError, fn -> - create_service_account_token(actor, subject, %{}) + create_service_account_token(actor, %{}, subject) end end @@ -3172,7 +3197,7 @@ defmodule Domain.AuthTest do actor = Fixtures.Actors.create_actor(type: :account_user, account: account) assert_raise FunctionClauseError, fn -> - create_service_account_token(actor, subject, %{}) + create_service_account_token(actor, %{}, subject) end end @@ -3183,7 +3208,7 @@ defmodule Domain.AuthTest do actor = Fixtures.Actors.create_actor(type: :service_account, account: account) subject = Fixtures.Auth.remove_permissions(subject) - assert create_service_account_token(actor, subject, %{}) == + assert create_service_account_token(actor, %{}, subject) == {:error, {:unauthorized, reason: :missing_permissions, @@ -3191,6 +3216,120 @@ defmodule Domain.AuthTest do end end + describe "create_api_client_token/3" do + setup do + account = Fixtures.Accounts.create_account() + Domain.Config.put_env_override(:outbound_email_adapter_configured?, true) + provider = Fixtures.Auth.create_email_provider(account: account) + user_agent = Fixtures.Auth.user_agent() + remote_ip = Fixtures.Auth.remote_ip() + + identity = + Fixtures.Auth.create_identity( + actor: [type: :account_admin_user], + account: account, + provider: provider, + user_agent: user_agent, + remote_ip: remote_ip + ) + + subject = Fixtures.Auth.create_subject(identity: identity) + + %{ + account: account, + provider: provider, + identity: identity, + subject: subject, + user_agent: user_agent, + remote_ip: remote_ip, + context: %Auth.Context{ + type: :api_client, + remote_ip: remote_ip, + remote_ip_location_region: "UA", + remote_ip_location_city: "Kyiv", + remote_ip_location_lat: 50.4501, + remote_ip_location_lon: 30.5234, + user_agent: user_agent + } + } + end + + test "returns valid client token for a given service account identity", %{ + account: account, + context: context, + subject: subject + } do + one_day = DateTime.utc_now() |> DateTime.add(1, :day) |> DateTime.truncate(:second) + actor = Fixtures.Actors.create_actor(type: :api_client, account: account) + + assert {:ok, encoded_token} = + create_api_client_token( + actor, + %{ + "name" => "foo", + "expires_at" => one_day + }, + subject + ) + + assert {:ok, api_subject} = authenticate(encoded_token, context) + assert api_subject.account.id == account.id + assert api_subject.actor.id == actor.id + refute api_subject.identity + assert api_subject.context.type == context.type + assert api_subject.permissions == fetch_type_permissions!(:api_client) + + assert token = Repo.get(Tokens.Token, api_subject.token_id) + assert token.name == "foo" + assert token.type == context.type + assert token.account_id == account.id + refute token.identity_id + assert token.actor_id == actor.id + assert token.created_by == :identity + assert token.created_by_identity_id == subject.identity.id + assert token.created_by_user_agent == context.user_agent + assert token.created_by_remote_ip.address == context.remote_ip + + assert api_subject.expires_at == token.expires_at + assert DateTime.truncate(api_subject.expires_at, :second) == one_day + end + + test "raises an error when trying to create a token for a different account", %{ + subject: subject + } do + actor = Fixtures.Actors.create_actor(type: :api_client) + + assert_raise FunctionClauseError, fn -> + create_api_client_token(actor, %{}, subject) + end + end + + test "raises an error when trying to create a token not for a service account", %{ + account: account, + subject: subject + } do + actor = Fixtures.Actors.create_actor(type: :account_user, account: account) + + assert_raise FunctionClauseError, fn -> + create_api_client_token(actor, %{}, subject) + end + end + + test "returns error on missing permissions", %{ + account: account, + subject: subject + } do + actor = Fixtures.Actors.create_actor(type: :api_client, account: account) + subject = Fixtures.Auth.remove_permissions(subject) + + assert create_api_client_token(actor, %{}, subject) == + {:error, + {:unauthorized, + reason: :missing_permissions, + missing_permissions: [Authorizer.manage_api_clients_permission()]}} + end + end + describe "authenticate/2" do setup do account = Fixtures.Accounts.create_account() @@ -3360,13 +3499,9 @@ defmodule Domain.AuthTest do client_context: context, client_subject: subject } do - one_day = DateTime.utc_now() |> DateTime.add(1, :day) actor = Fixtures.Actors.create_actor(type: :service_account, account: account) - assert {:ok, encoded_token} = - create_service_account_token(actor, subject, %{ - "expires_at" => one_day - }) + assert {:ok, encoded_token} = create_service_account_token(actor, %{}, subject) assert {:ok, reconstructed_subject} = authenticate(encoded_token, context) refute reconstructed_subject.identity @@ -3376,8 +3511,7 @@ defmodule Domain.AuthTest do assert reconstructed_subject.permissions == fetch_type_permissions!(:service_account) assert reconstructed_subject.context.remote_ip == context.remote_ip assert reconstructed_subject.context.user_agent == context.user_agent - - assert reconstructed_subject.expires_at == one_day + refute reconstructed_subject.expires_at end test "client token is not bound to remote ip and user agent", %{ diff --git a/elixir/apps/domain/test/domain/clients_test.exs b/elixir/apps/domain/test/domain/clients_test.exs index f56faf457..6e4cecaec 100644 --- a/elixir/apps/domain/test/domain/clients_test.exs +++ b/elixir/apps/domain/test/domain/clients_test.exs @@ -460,7 +460,8 @@ defmodule Domain.ClientsTest do end test "allows service account to create a client for self", %{account: account} do - subject = Fixtures.Auth.create_subject(account: account, actor: [type: :service_account]) + actor = Fixtures.Actors.create_actor(type: :service_account, account: account) + subject = Fixtures.Auth.create_subject(account: account, actor: actor) attrs = Fixtures.Clients.client_attrs() assert {:ok, client} = upsert_client(attrs, subject) diff --git a/elixir/apps/domain/test/domain/flows_test.exs b/elixir/apps/domain/test/domain/flows_test.exs index cf55aa49f..e8684ced5 100644 --- a/elixir/apps/domain/test/domain/flows_test.exs +++ b/elixir/apps/domain/test/domain/flows_test.exs @@ -108,12 +108,19 @@ defmodule Domain.FlowsTest do test "creates a network flow for service accounts", %{ account: account, - client: client, + actor_group: actor_group, gateway: gateway, resource: resource, - policy: policy, - subject: subject + policy: policy } do + actor = Fixtures.Actors.create_actor(type: :service_account, account: account) + Fixtures.Actors.create_membership(account: account, actor: actor, group: actor_group) + + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + subject = Fixtures.Auth.create_subject(identity: identity) + + client = Fixtures.Clients.create_client(account: account, actor: actor, identity: identity) + assert {:ok, _fetched_resource, %Flows.Flow{} = flow} = authorize_flow(client, gateway, resource.id, subject) diff --git a/elixir/apps/domain/test/domain/gateways_test.exs b/elixir/apps/domain/test/domain/gateways_test.exs index c1c653152..05ec3bb72 100644 --- a/elixir/apps/domain/test/domain/gateways_test.exs +++ b/elixir/apps/domain/test/domain/gateways_test.exs @@ -1,7 +1,7 @@ defmodule Domain.GatewaysTest do use Domain.DataCase, async: true import Domain.Gateways - alias Domain.Gateways + alias Domain.{Gateways, Tokens} setup do account = Fixtures.Accounts.create_account() @@ -156,7 +156,7 @@ defmodule Domain.GatewaysTest do describe "create_group/2" do test "returns error on empty attrs", %{subject: subject} do assert {:error, changeset} = create_group(%{}, subject) - assert errors_on(changeset) == %{tokens: ["can't be blank"], routing: ["can't be blank"]} + assert errors_on(changeset) == %{routing: ["can't be blank"]} end test "returns error on invalid attrs", %{account: account, subject: subject} do @@ -167,13 +167,12 @@ defmodule Domain.GatewaysTest do assert {:error, changeset} = create_group(attrs, subject) assert errors_on(changeset) == %{ - tokens: ["can't be blank"], name: ["should be at most 64 character(s)"], routing: ["can't be blank"] } Fixtures.Gateways.create_group(account: account, name: "foo") - attrs = %{name: "foo", tokens: [%{}], routing: "managed"} + attrs = %{name: "foo", routing: "managed"} assert {:error, changeset} = create_group(attrs, subject) assert "has already been taken" in errors_on(changeset).name end @@ -181,8 +180,7 @@ defmodule Domain.GatewaysTest do test "returns error on invalid routing value", %{subject: subject} do attrs = %{ name_prefix: "foo", - routing: "foo", - tokens: [%{}] + routing: "foo" } assert {:error, changeset} = create_group(attrs, subject) @@ -195,8 +193,7 @@ defmodule Domain.GatewaysTest do test "creates a group", %{subject: subject} do attrs = %{ name: "foo", - routing: "managed", - tokens: [%{}] + routing: "managed" } assert {:ok, group} = create_group(attrs, subject) @@ -207,10 +204,6 @@ defmodule Domain.GatewaysTest do assert group.created_by_identity_id == subject.identity.id assert group.routing == :managed - - assert [%Gateways.Token{} = token] = group.tokens - assert token.created_by == :identity - assert token.created_by_identity_id == subject.identity.id end test "returns error when subject has no permission to manage groups", %{ @@ -320,17 +313,19 @@ defmodule Domain.GatewaysTest do test "deletes all tokens when group is deleted", %{account: account, subject: subject} do group = Fixtures.Gateways.create_group(account: account) - Fixtures.Gateways.create_token(group: group) - Fixtures.Gateways.create_token(group: [account: account]) + Fixtures.Gateways.create_token(account: account, group: group) + Fixtures.Gateways.create_token(account: account, group: [account: account]) assert {:ok, deleted} = delete_group(group, subject) assert deleted.deleted_at tokens = - Gateways.Token + Domain.Tokens.Token.Query.all() + |> Domain.Tokens.Token.Query.by_gateway_group_id(group.id) |> Repo.all() - |> Enum.filter(fn token -> token.group_id == group.id end) + |> Enum.filter(fn token -> token.gateway_group_id == group.id end) + assert length(tokens) > 0 assert Enum.all?(tokens, & &1.deleted_at) end @@ -349,35 +344,110 @@ defmodule Domain.GatewaysTest do end end - describe "use_token_by_id_and_secret/2" do - test "returns token when secret is valid" do - token = Fixtures.Gateways.create_token() - assert {:ok, token} = use_token_by_id_and_secret(token.id, token.value) - assert is_nil(token.value) - # TODO: While we don't have token rotation implemented, the tokens are all multi-use - # assert is_nil(token.hash) - # refute is_nil(token.deleted_at) + describe "create_token/3" do + setup do + user_agent = Fixtures.Auth.user_agent() + remote_ip = Fixtures.Auth.remote_ip() + + %{ + context: %Domain.Auth.Context{ + type: :gateway_group, + remote_ip: remote_ip, + remote_ip_location_region: "UA", + remote_ip_location_city: "Kyiv", + remote_ip_location_lat: 50.4501, + remote_ip_location_lon: 30.5234, + user_agent: user_agent + } + } end - # TODO: While we don't have token rotation implemented, the tokens are all multi-use - # test "returns error when secret was already used" do - # token = Fixtures.Gateways.create_token() + test "returns valid token for a gateway group", %{ + account: account, + context: context, + subject: subject + } do + group = Fixtures.Gateways.create_group(account: account) - # assert {:ok, _token} = use_token_by_id_and_secret(token.id, token.value) - # assert use_token_by_id_and_secret(token.id, token.value) == {:error, :not_found} - # end + assert {:ok, encoded_token} = create_token(group, %{}, subject) - test "returns error when id is invalid" do - assert use_token_by_id_and_secret("foo", "bar") == {:error, :not_found} + assert {:ok, fetched_group} = authenticate(encoded_token, context) + assert fetched_group.id == group.id + + assert token = Repo.get_by(Tokens.Token, gateway_group_id: fetched_group.id) + assert token.type == :gateway_group + assert token.account_id == account.id + assert token.gateway_group_id == group.id + assert token.created_by == :identity + assert token.created_by_identity_id == subject.identity.id + assert token.created_by_user_agent == context.user_agent + assert token.created_by_remote_ip.address == context.remote_ip + refute token.expires_at end - test "returns error when id is not found" do - assert use_token_by_id_and_secret(Ecto.UUID.generate(), "bar") == {:error, :not_found} + test "returns error on missing permissions", %{ + account: account, + subject: subject + } do + group = Fixtures.Gateways.create_group(account: account) + subject = Fixtures.Auth.remove_permissions(subject) + + assert create_token(group, %{}, subject) == + {:error, + {:unauthorized, + reason: :missing_permissions, + missing_permissions: [Gateways.Authorizer.manage_gateways_permission()]}} + end + end + + describe "authenticate/2" do + setup do + user_agent = Fixtures.Auth.user_agent() + remote_ip = Fixtures.Auth.remote_ip() + + %{ + context: %Domain.Auth.Context{ + type: :gateway_group, + remote_ip: remote_ip, + remote_ip_location_region: "UA", + remote_ip_location_city: "Kyiv", + remote_ip_location_lat: 50.4501, + remote_ip_location_lon: 30.5234, + user_agent: user_agent + } + } end - test "returns error when secret is invalid" do - token = Fixtures.Gateways.create_token() - assert use_token_by_id_and_secret(token.id, "bar") == {:error, :not_found} + test "returns error when token is invalid", %{ + context: context + } do + assert authenticate(".foo", context) == {:error, :unauthorized} + assert authenticate("foo", context) == {:error, :unauthorized} + end + + test "returns error when context is invalid", %{ + account: account, + context: context, + subject: subject + } do + group = Fixtures.Gateways.create_group(account: account) + assert {:ok, encoded_token} = create_token(group, %{}, subject) + context = %{context | type: :client} + + assert authenticate(encoded_token, context) == {:error, :unauthorized} + end + + test "returns group when token is valid", %{ + account: account, + context: context, + subject: subject + } do + group = Fixtures.Gateways.create_group(account: account) + assert {:ok, encoded_token} = create_token(group, %{}, subject) + + assert {:ok, fetched_group} = authenticate(encoded_token, context) + assert fetched_group.id == group.id + assert fetched_group.account_id == account.id end end @@ -593,126 +663,124 @@ defmodule Domain.GatewaysTest do end describe "upsert_gateway/3" do - setup context do - token = Fixtures.Gateways.create_token(account: context.account) + setup %{account: account} do + group = Fixtures.Gateways.create_group(account: account) - context - |> Map.put(:token, token) - |> Map.put(:group, token.group) + user_agent = Fixtures.Auth.user_agent() + remote_ip = Fixtures.Auth.remote_ip() + + %{ + group: group, + context: %Domain.Auth.Context{ + type: :gateway_group, + remote_ip: remote_ip, + remote_ip_location_region: "UA", + remote_ip_location_city: "Kyiv", + remote_ip_location_lat: 50.4501, + remote_ip_location_lon: 30.5234, + user_agent: user_agent + } + } end test "returns errors on invalid attrs", %{ - token: token + context: context, + group: group } do attrs = %{ external_id: nil, - public_key: "x", - last_seen_user_agent: "foo", - last_seen_remote_ip: {256, 0, 0, 0}, - last_seen_remote_ip_location_region: -1, - last_seen_remote_ip_location_city: -1, - last_seen_remote_ip_location_lat: :x, - last_seen_remote_ip_location_lon: :x + public_key: "x" } - assert {:error, changeset} = upsert_gateway(token, attrs) + assert {:error, changeset} = upsert_gateway(group, attrs, context) assert errors_on(changeset) == %{ public_key: ["should be 44 character(s)", "must be a base64-encoded string"], - external_id: ["can't be blank"], - last_seen_user_agent: ["is invalid"], - last_seen_remote_ip_location_region: ["is invalid"], - last_seen_remote_ip_location_city: ["is invalid"], - last_seen_remote_ip_location_lat: ["is invalid"], - last_seen_remote_ip_location_lon: ["is invalid"] + external_id: ["can't be blank"] } end test "allows creating gateway with just required attributes", %{ - token: token + context: context, + group: group } do attrs = Fixtures.Gateways.gateway_attrs() |> Map.delete(:name) - assert {:ok, gateway} = upsert_gateway(token, attrs) + assert {:ok, gateway} = upsert_gateway(group, attrs, context) assert gateway.name assert gateway.public_key == attrs.public_key - assert gateway.token_id == token.id - assert gateway.group_id == token.group_id + assert gateway.group_id == group.id refute is_nil(gateway.ipv4) refute is_nil(gateway.ipv6) - assert gateway.last_seen_remote_ip == attrs.last_seen_remote_ip - assert gateway.last_seen_user_agent == attrs.last_seen_user_agent + assert gateway.last_seen_remote_ip.address == context.remote_ip + assert gateway.last_seen_user_agent == context.user_agent assert gateway.last_seen_version == "0.7.412" assert gateway.last_seen_at - - assert gateway.last_seen_remote_ip_location_region == - attrs.last_seen_remote_ip_location_region - - assert gateway.last_seen_remote_ip_location_city == attrs.last_seen_remote_ip_location_city - assert gateway.last_seen_remote_ip_location_lat == attrs.last_seen_remote_ip_location_lat - assert gateway.last_seen_remote_ip_location_lon == attrs.last_seen_remote_ip_location_lon + assert gateway.last_seen_remote_ip_location_region == context.remote_ip_location_region + assert gateway.last_seen_remote_ip_location_city == context.remote_ip_location_city + assert gateway.last_seen_remote_ip_location_lat == context.remote_ip_location_lat + assert gateway.last_seen_remote_ip_location_lon == context.remote_ip_location_lon end test "updates gateway when it already exists", %{ - token: token + account: account, + context: context, + group: group } do - gateway = Fixtures.Gateways.create_gateway(token: token) + gateway = Fixtures.Gateways.create_gateway(account: account, group: group) + attrs = Fixtures.Gateways.gateway_attrs(external_id: gateway.external_id) - attrs = - Fixtures.Gateways.gateway_attrs( - external_id: gateway.external_id, - last_seen_remote_ip: {100, 64, 100, 101}, - last_seen_user_agent: "iOS/12.5 (iPhone) connlib/0.7.411", - last_seen_remote_ip_location_region: "Mexico", - last_seen_remote_ip_location_city: "Merida", - last_seen_remote_ip_location_lat: 7.7758, - last_seen_remote_ip_location_lon: -2.4128 - ) + context = %{ + context + | remote_ip: {100, 64, 100, 158}, + user_agent: "iOS/12.5 (iPhone) connlib/0.7.413" + } - assert {:ok, updated_gateway} = upsert_gateway(token, attrs) + assert {:ok, updated_gateway} = upsert_gateway(group, attrs, context) assert Repo.aggregate(Gateways.Gateway, :count, :id) == 1 assert updated_gateway.name != gateway.name - assert updated_gateway.last_seen_remote_ip.address == attrs.last_seen_remote_ip + assert updated_gateway.last_seen_remote_ip.address == context.remote_ip assert updated_gateway.last_seen_remote_ip != gateway.last_seen_remote_ip - assert updated_gateway.last_seen_user_agent == attrs.last_seen_user_agent + assert updated_gateway.last_seen_user_agent == context.user_agent assert updated_gateway.last_seen_user_agent != gateway.last_seen_user_agent - assert updated_gateway.last_seen_version == "0.7.411" + assert updated_gateway.last_seen_version == "0.7.413" assert updated_gateway.last_seen_at assert updated_gateway.last_seen_at != gateway.last_seen_at assert updated_gateway.public_key != gateway.public_key assert updated_gateway.public_key == attrs.public_key - assert updated_gateway.token_id == token.id - assert updated_gateway.group_id == token.group_id + assert updated_gateway.group_id == group.id assert updated_gateway.ipv4 == gateway.ipv4 assert updated_gateway.ipv6 == gateway.ipv6 assert updated_gateway.last_seen_remote_ip_location_region == - attrs.last_seen_remote_ip_location_region + context.remote_ip_location_region assert updated_gateway.last_seen_remote_ip_location_city == - attrs.last_seen_remote_ip_location_city + context.remote_ip_location_city assert updated_gateway.last_seen_remote_ip_location_lat == - attrs.last_seen_remote_ip_location_lat + context.remote_ip_location_lat assert updated_gateway.last_seen_remote_ip_location_lon == - attrs.last_seen_remote_ip_location_lon + context.remote_ip_location_lon end test "does not reserve additional addresses on update", %{ - token: token + account: account, + context: context, + group: group } do - gateway = Fixtures.Gateways.create_gateway(token: token) + gateway = Fixtures.Gateways.create_gateway(account: account, group: group) attrs = Fixtures.Gateways.gateway_attrs( @@ -721,7 +789,7 @@ defmodule Domain.GatewaysTest do last_seen_remote_ip: %Postgrex.INET{address: {100, 64, 100, 100}} ) - assert {:ok, updated_gateway} = upsert_gateway(token, attrs) + assert {:ok, updated_gateway} = upsert_gateway(group, attrs, context) addresses = Domain.Network.Address @@ -737,10 +805,11 @@ defmodule Domain.GatewaysTest do test "does not allow to reuse IP addresses", %{ account: account, - token: token + context: context, + group: group } do attrs = Fixtures.Gateways.gateway_attrs() - assert {:ok, gateway} = upsert_gateway(token, attrs) + assert {:ok, gateway} = upsert_gateway(group, attrs, context) addresses = Domain.Network.Address @@ -878,14 +947,18 @@ defmodule Domain.GatewaysTest do test "prioritizes gateways with known location" do gateway_1 = Fixtures.Gateways.create_gateway( - last_seen_remote_ip_location_lat: 33.2029, - last_seen_remote_ip_location_lon: -80.0131 + context: [ + remote_ip_location_lat: 33.2029, + remote_ip_location_lon: -80.0131 + ] ) gateway_2 = Fixtures.Gateways.create_gateway( - last_seen_remote_ip_location_lat: nil, - last_seen_remote_ip_location_lon: nil + context: [ + remote_ip_location_lat: nil, + remote_ip_location_lon: nil + ] ) gateways = [ @@ -899,10 +972,22 @@ defmodule Domain.GatewaysTest do test "prioritizes gateways of more recent version" do gateway_1 = - Fixtures.Gateways.create_gateway(last_seen_user_agent: "iOS/12.7 (iPhone) connlib/1.99") + Fixtures.Gateways.create_gateway( + context: [ + remote_ip_location_lat: 33.2029, + remote_ip_location_lon: -80.0131, + user_agent: "iOS/12.7 (iPhone) connlib/1.99" + ] + ) gateway_2 = - Fixtures.Gateways.create_gateway(last_seen_user_agent: "iOS/12.7 (iPhone) connlib/2.3") + Fixtures.Gateways.create_gateway( + context: [ + remote_ip_location_lat: 33.2029, + remote_ip_location_lon: -80.0131, + user_agent: "iOS/12.7 (iPhone) connlib/2.3" + ] + ) gateways = [ gateway_1, @@ -917,40 +1002,52 @@ defmodule Domain.GatewaysTest do # Moncks Corner, South Carolina gateway_us_east_1 = Fixtures.Gateways.create_gateway( - last_seen_remote_ip_location_lat: 33.2029, - last_seen_remote_ip_location_lon: -80.0131 + context: [ + remote_ip_location_lat: 33.2029, + remote_ip_location_lon: -80.0131 + ] ) gateway_us_east_2 = Fixtures.Gateways.create_gateway( - last_seen_remote_ip_location_lat: 33.2029, - last_seen_remote_ip_location_lon: -80.0131 + context: [ + remote_ip_location_lat: 33.2029, + remote_ip_location_lon: -80.0131 + ] ) gateway_us_east_3 = Fixtures.Gateways.create_gateway( - last_seen_remote_ip_location_lat: 33.2029, - last_seen_remote_ip_location_lon: -80.0131 + context: [ + remote_ip_location_lat: 33.2029, + remote_ip_location_lon: -80.0131 + ] ) # The Dalles, Oregon gateway_us_west_1 = Fixtures.Gateways.create_gateway( - last_seen_remote_ip_location_lat: 45.5946, - last_seen_remote_ip_location_lon: -121.1787 + context: [ + remote_ip_location_lat: 45.5946, + remote_ip_location_lon: -121.1787 + ] ) gateway_us_west_2 = Fixtures.Gateways.create_gateway( - last_seen_remote_ip_location_lat: 45.5946, - last_seen_remote_ip_location_lon: -121.1787 + context: [ + remote_ip_location_lat: 45.5946, + remote_ip_location_lon: -121.1787 + ] ) # Council Bluffs, Iowa gateway_us_central_1 = Fixtures.Gateways.create_gateway( - last_seen_remote_ip_location_lat: 41.2619, - last_seen_remote_ip_location_lon: -95.8608 + context: [ + remote_ip_location_lat: 41.2619, + remote_ip_location_lon: -95.8608 + ] ) gateways = [ @@ -1009,34 +1106,6 @@ defmodule Domain.GatewaysTest do end end - describe "encode_token!/1" do - test "returns encoded token" do - token = Fixtures.Gateways.create_token() - assert encrypted_secret = encode_token!(token) - - config = Application.fetch_env!(:domain, Domain.Gateways) - key_base = Keyword.fetch!(config, :key_base) - salt = Keyword.fetch!(config, :salt) - - assert Plug.Crypto.verify(key_base, salt, encrypted_secret) == - {:ok, {token.id, token.value}} - end - end - - describe "authorize_gateway/1" do - test "returns token when encoded secret is valid" do - token = Fixtures.Gateways.create_token() - encoded_token = encode_token!(token) - assert {:ok, fetched_token} = authorize_gateway(encoded_token) - assert fetched_token.id == token.id - assert is_nil(fetched_token.value) - end - - test "returns error when secret is invalid" do - assert authorize_gateway(Ecto.UUID.generate()) == {:error, :invalid_token} - end - end - describe "relay_strategy/1" do test "managed strategy" do group = Fixtures.Gateways.create_group(routing: :managed) diff --git a/elixir/apps/domain/test/domain/relays_test.exs b/elixir/apps/domain/test/domain/relays_test.exs index e665481a6..b53b7c916 100644 --- a/elixir/apps/domain/test/domain/relays_test.exs +++ b/elixir/apps/domain/test/domain/relays_test.exs @@ -1,7 +1,7 @@ defmodule Domain.RelaysTest do use Domain.DataCase, async: true import Domain.Relays - alias Domain.Relays + alias Domain.{Relays, Tokens} setup do account = Fixtures.Accounts.create_account() @@ -144,9 +144,8 @@ defmodule Domain.RelaysTest do end describe "create_group/2" do - test "returns error on empty attrs", %{subject: subject} do - assert {:error, changeset} = create_group(%{}, subject) - assert errors_on(changeset) == %{tokens: ["can't be blank"]} + test "returns group on empty attrs", %{subject: subject} do + assert {:ok, _group} = create_group(%{}, subject) end test "returns error on invalid attrs", %{account: account, subject: subject} do @@ -157,32 +156,24 @@ defmodule Domain.RelaysTest do assert {:error, changeset} = create_group(attrs, subject) assert errors_on(changeset) == %{ - tokens: ["can't be blank"], name: ["should be at most 64 character(s)"] } Fixtures.Relays.create_group(account: account, name: "foo") - attrs = %{name: "foo", tokens: [%{}]} + attrs = %{name: "foo"} assert {:error, changeset} = create_group(attrs, subject) assert "has already been taken" in errors_on(changeset).name end test "creates a group", %{subject: subject} do - attrs = %{ - name: "foo", - tokens: [%{}] - } + attrs = %{name: Ecto.UUID.generate()} assert {:ok, group} = create_group(attrs, subject) assert group.id - assert group.name == "foo" + assert group.name == attrs.name assert group.created_by == :identity assert group.created_by_identity_id == subject.identity.id - - assert [%Relays.Token{} = token] = group.tokens - assert token.created_by == :identity - assert token.created_by_identity_id == subject.identity.id end test "returns error when subject has no permission to manage groups", %{ @@ -199,9 +190,8 @@ defmodule Domain.RelaysTest do end describe "create_global_group/1" do - test "returns error on empty attrs" do - assert {:error, changeset} = create_global_group(%{}) - assert errors_on(changeset) == %{tokens: ["can't be blank"]} + test "returns group on empty attrs" do + assert {:ok, _group} = create_global_group(%{}) end test "returns error on invalid attrs" do @@ -212,32 +202,25 @@ defmodule Domain.RelaysTest do assert {:error, changeset} = create_global_group(attrs) assert errors_on(changeset) == %{ - tokens: ["can't be blank"], name: ["should be at most 64 character(s)"] } - Fixtures.Relays.create_global_group(name: "foo") - attrs = %{name: "foo", tokens: [%{}]} + name = Ecto.UUID.generate() + Fixtures.Relays.create_global_group(name: name) + attrs = %{name: name} assert {:error, changeset} = create_global_group(attrs) assert "has already been taken" in errors_on(changeset).name end test "creates a group" do - attrs = %{ - name: "foo", - tokens: [%{}] - } + attrs = %{name: Ecto.UUID.generate()} assert {:ok, group} = create_global_group(attrs) assert group.id - assert group.name == "foo" + assert group.name == attrs.name assert group.created_by == :system assert is_nil(group.created_by_identity_id) - - assert [%Relays.Token{} = token] = group.tokens - assert token.created_by == :system - assert is_nil(token.created_by_identity_id) end end @@ -342,17 +325,18 @@ defmodule Domain.RelaysTest do test "deletes all tokens when group is deleted", %{account: account, subject: subject} do group = Fixtures.Relays.create_group(account: account) - Fixtures.Relays.create_token(group: group) - Fixtures.Relays.create_token(group: [account: account]) + Fixtures.Relays.create_token(account: account, group: group) + Fixtures.Relays.create_token(account: account, group: [account: account]) assert {:ok, deleted} = delete_group(group, subject) assert deleted.deleted_at tokens = - Relays.Token + Domain.Tokens.Token.Query.all() + |> Domain.Tokens.Token.Query.by_relay_group_id(group.id) |> Repo.all() - |> Enum.filter(fn token -> token.group_id == group.id end) + assert length(tokens) > 0 assert Enum.all?(tokens, & &1.deleted_at) end @@ -371,35 +355,182 @@ defmodule Domain.RelaysTest do end end - describe "use_token_by_id_and_secret/2" do - test "returns token when secret is valid" do - token = Fixtures.Relays.create_token() - assert {:ok, token} = use_token_by_id_and_secret(token.id, token.value) - assert is_nil(token.value) - # TODO: While we don't have token rotation implemented, the tokens are all multi-use - # assert is_nil(token.hash) - # refute is_nil(token.deleted_at) + describe "create_token/2" do + setup do + user_agent = Fixtures.Auth.user_agent() + remote_ip = Fixtures.Auth.remote_ip() + + %{ + context: %Domain.Auth.Context{ + type: :relay_group, + remote_ip: remote_ip, + remote_ip_location_region: "UA", + remote_ip_location_city: "Kyiv", + remote_ip_location_lat: 50.4501, + remote_ip_location_lon: 30.5234, + user_agent: user_agent + } + } end - # TODO: While we don't have token rotation implemented, the tokens are all multi-use - # test "returns error when secret was already used" do - # token = Fixtures.Relays.create_token() + test "returns valid token for a relay group", %{ + account: account, + context: context, + subject: subject + } do + group = Fixtures.Relays.create_group(account: account) - # assert {:ok, _token} = use_token_by_id_and_secret(token.id, token.value) - # assert use_token_by_id_and_secret(token.id, token.value) == {:error, :not_found} - # end + assert {:ok, encoded_token} = create_token(group, %{}, subject) - test "returns error when id is invalid" do - assert use_token_by_id_and_secret("foo", "bar") == {:error, :not_found} + assert {:ok, fetched_group} = authenticate(encoded_token, context) + assert fetched_group.id == group.id + + assert token = Repo.get_by(Tokens.Token, relay_group_id: fetched_group.id) + assert token.type == :relay_group + assert token.account_id == account.id + assert token.relay_group_id == group.id + assert token.created_by == :identity + assert token.created_by_identity_id == subject.identity.id + assert token.created_by_user_agent == subject.context.user_agent + assert token.created_by_remote_ip.address == subject.context.remote_ip + refute token.expires_at end - test "returns error when id is not found" do - assert use_token_by_id_and_secret(Ecto.UUID.generate(), "bar") == {:error, :not_found} + test "returns valid token for a global relay group", %{ + context: context + } do + group = Fixtures.Relays.create_global_group() + + assert {:ok, encoded_token} = create_token(group, %{}) + + assert {:ok, fetched_group} = authenticate(encoded_token, context) + assert fetched_group.id == group.id + + assert token = Repo.get_by(Tokens.Token, relay_group_id: fetched_group.id) + assert token.type == :relay_group + refute token.account_id + assert token.relay_group_id == group.id + assert token.created_by == :system + refute token.created_by_identity_id + refute token.created_by_user_agent + refute token.created_by_remote_ip + refute token.expires_at + end + end + + describe "create_token/3" do + setup do + user_agent = Fixtures.Auth.user_agent() + remote_ip = Fixtures.Auth.remote_ip() + + %{ + context: %Domain.Auth.Context{ + type: :relay_group, + remote_ip: remote_ip, + remote_ip_location_region: "UA", + remote_ip_location_city: "Kyiv", + remote_ip_location_lat: 50.4501, + remote_ip_location_lon: 30.5234, + user_agent: user_agent + } + } end - test "returns error when secret is invalid" do - token = Fixtures.Relays.create_token() - assert use_token_by_id_and_secret(token.id, "bar") == {:error, :not_found} + test "returns valid token for a given relay group", %{ + account: account, + context: context, + subject: subject + } do + group = Fixtures.Relays.create_group(account: account) + + assert {:ok, encoded_token} = create_token(group, %{}, subject) + + assert {:ok, fetched_group} = authenticate(encoded_token, context) + assert fetched_group.id == group.id + + assert token = Repo.get_by(Tokens.Token, relay_group_id: fetched_group.id) + assert token.type == :relay_group + assert token.account_id == account.id + assert token.relay_group_id == group.id + assert token.created_by == :identity + assert token.created_by_identity_id == subject.identity.id + assert token.created_by_user_agent == context.user_agent + assert token.created_by_remote_ip.address == context.remote_ip + refute token.expires_at + end + + test "returns error on missing permissions", %{ + account: account, + subject: subject + } do + group = Fixtures.Relays.create_group(account: account) + subject = Fixtures.Auth.remove_permissions(subject) + + assert create_token(group, %{}, subject) == + {:error, + {:unauthorized, + reason: :missing_permissions, + missing_permissions: [Relays.Authorizer.manage_relays_permission()]}} + end + end + + describe "authenticate/2" do + setup do + user_agent = Fixtures.Auth.user_agent() + remote_ip = Fixtures.Auth.remote_ip() + + %{ + context: %Domain.Auth.Context{ + type: :relay_group, + remote_ip: remote_ip, + remote_ip_location_region: "UA", + remote_ip_location_city: "Kyiv", + remote_ip_location_lat: 50.4501, + remote_ip_location_lon: 30.5234, + user_agent: user_agent + } + } + end + + test "returns error when token is invalid", %{ + context: context + } do + assert authenticate(".foo", context) == {:error, :unauthorized} + assert authenticate("foo", context) == {:error, :unauthorized} + end + + test "returns error when context is invalid", %{ + context: context + } do + group = Fixtures.Relays.create_global_group() + assert {:ok, encoded_token} = create_token(group, %{}) + context = %{context | type: :client} + + assert authenticate(encoded_token, context) == {:error, :unauthorized} + end + + test "returns global group when token is valid", %{ + context: context + } do + group = Fixtures.Relays.create_global_group() + assert {:ok, encoded_token} = create_token(group, %{}) + + assert {:ok, fetched_group} = authenticate(encoded_token, context) + assert fetched_group.id == group.id + refute fetched_group.account_id + end + + test "returns group when token is valid", %{ + account: account, + context: context, + subject: subject + } do + group = Fixtures.Relays.create_group(account: account) + assert {:ok, encoded_token} = create_token(group, %{}, subject) + + assert {:ok, fetched_group} = authenticate(encoded_token, context) + assert fetched_group.id == group.id + assert fetched_group.account_id == account.id end end @@ -579,73 +710,70 @@ defmodule Domain.RelaysTest do end describe "upsert_relay/3" do - setup context do - token = Fixtures.Relays.create_token(account: context.account) + setup %{account: account} do + group = Fixtures.Relays.create_group(account: account) - context - |> Map.put(:token, token) - |> Map.put(:group, token.group) + user_agent = Fixtures.Auth.user_agent() + remote_ip = Fixtures.Auth.remote_ip() + + %{ + group: group, + context: %Domain.Auth.Context{ + type: :relay_group, + remote_ip: remote_ip, + remote_ip_location_region: "UA", + remote_ip_location_city: "Kyiv", + remote_ip_location_lat: 50.4501, + remote_ip_location_lon: 30.5234, + user_agent: user_agent + } + } end test "returns errors on invalid attrs", %{ - token: token + context: context, + group: group } do attrs = %{ ipv4: "1.1.1.256", ipv6: "fd01::10000", - last_seen_user_agent: "foo", - last_seen_remote_ip: -1, - last_seen_remote_ip_location_region: -1, - last_seen_remote_ip_location_city: -1, - last_seen_remote_ip_location_lat: :x, - last_seen_remote_ip_location_lon: :x, port: -1 } - assert {:error, changeset} = upsert_relay(token, attrs) + assert {:error, changeset} = upsert_relay(group, attrs, context) assert errors_on(changeset) == %{ ipv4: ["one of these fields must be present: ipv4, ipv6", "is invalid"], ipv6: ["one of these fields must be present: ipv4, ipv6", "is invalid"], - last_seen_user_agent: ["is invalid"], - last_seen_remote_ip: ["is invalid"], - last_seen_remote_ip_location_region: ["is invalid"], - last_seen_remote_ip_location_city: ["is invalid"], - last_seen_remote_ip_location_lat: ["is invalid"], - last_seen_remote_ip_location_lon: ["is invalid"], port: ["must be greater than or equal to 1"] } attrs = %{port: 100_000} - assert {:error, changeset} = upsert_relay(token, attrs) + assert {:error, changeset} = upsert_relay(group, attrs, context) assert "must be less than or equal to 65535" in errors_on(changeset).port end test "allows creating relay with just required attributes", %{ - token: token + context: context, + group: group } do attrs = Fixtures.Relays.relay_attrs() |> Map.delete(:name) - assert {:ok, relay} = upsert_relay(token, attrs) + assert {:ok, relay} = upsert_relay(group, attrs, context) - assert relay.token_id == token.id - assert relay.group_id == token.group_id + assert relay.group_id == group.id assert relay.ipv4.address == attrs.ipv4 assert relay.ipv6.address == attrs.ipv6 - assert relay.last_seen_remote_ip.address == attrs.last_seen_remote_ip - - assert relay.last_seen_remote_ip_location_region == - attrs.last_seen_remote_ip_location_region - - assert relay.last_seen_remote_ip_location_city == attrs.last_seen_remote_ip_location_city - assert relay.last_seen_remote_ip_location_lat == attrs.last_seen_remote_ip_location_lat - assert relay.last_seen_remote_ip_location_lon == attrs.last_seen_remote_ip_location_lon - - assert relay.last_seen_user_agent == attrs.last_seen_user_agent + assert relay.last_seen_remote_ip.address == context.remote_ip + assert relay.last_seen_remote_ip_location_region == context.remote_ip_location_region + assert relay.last_seen_remote_ip_location_city == context.remote_ip_location_city + assert relay.last_seen_remote_ip_location_lat == context.remote_ip_location_lat + assert relay.last_seen_remote_ip_location_lon == context.remote_ip_location_lon + assert relay.last_seen_user_agent == context.user_agent assert relay.last_seen_version == "0.7.412" assert relay.last_seen_at assert relay.port == 3478 @@ -654,47 +782,39 @@ defmodule Domain.RelaysTest do end test "allows creating ipv6-only relays", %{ - token: token + context: context, + group: group } do attrs = Fixtures.Relays.relay_attrs() |> Map.drop([:name, :ipv4]) - assert {:ok, _relay} = upsert_relay(token, attrs) - assert {:ok, _relay} = upsert_relay(token, attrs) + assert {:ok, _relay} = upsert_relay(group, attrs, context) + assert {:ok, _relay} = upsert_relay(group, attrs, context) assert Repo.one(Relays.Relay) end test "updates ipv4 relay when it already exists", %{ - token: token + group: group, + context: context } do - relay = Fixtures.Relays.create_relay(token: token) + relay = Fixtures.Relays.create_relay(group: group) + attrs = Fixtures.Relays.relay_attrs(ipv4: relay.ipv4) + context = %{context | user_agent: "iOS/12.5 (iPhone) connlib/0.7.411"} - attrs = - Fixtures.Relays.relay_attrs( - ipv4: relay.ipv4, - last_seen_remote_ip: relay.ipv4, - last_seen_user_agent: "iOS/12.5 (iPhone) connlib/0.7.411", - last_seen_remote_ip_location_region: "Mexico", - last_seen_remote_ip_location_city: "Merida", - last_seen_remote_ip_location_lat: 7.7758, - last_seen_remote_ip_location_lon: -2.4128 - ) - - assert {:ok, updated_relay} = upsert_relay(token, attrs) + assert {:ok, updated_relay} = upsert_relay(group, attrs, context) assert Repo.aggregate(Relays.Relay, :count, :id) == 1 - assert updated_relay.last_seen_remote_ip.address == attrs.last_seen_remote_ip.address - assert updated_relay.last_seen_user_agent == attrs.last_seen_user_agent + assert updated_relay.last_seen_remote_ip.address == context.remote_ip + assert updated_relay.last_seen_user_agent == context.user_agent assert updated_relay.last_seen_user_agent != relay.last_seen_user_agent assert updated_relay.last_seen_version == "0.7.411" assert updated_relay.last_seen_at assert updated_relay.last_seen_at != relay.last_seen_at - assert updated_relay.token_id == token.id - assert updated_relay.group_id == token.group_id + assert updated_relay.group_id == group.id assert updated_relay.ipv4 == relay.ipv4 assert updated_relay.ipv6.address == attrs.ipv6 @@ -702,46 +822,38 @@ defmodule Domain.RelaysTest do assert updated_relay.port == 3478 assert updated_relay.last_seen_remote_ip_location_region == - attrs.last_seen_remote_ip_location_region + context.remote_ip_location_region - assert updated_relay.last_seen_remote_ip_location_city == - attrs.last_seen_remote_ip_location_city - - assert updated_relay.last_seen_remote_ip_location_lat == - attrs.last_seen_remote_ip_location_lat - - assert updated_relay.last_seen_remote_ip_location_lon == - attrs.last_seen_remote_ip_location_lon + assert updated_relay.last_seen_remote_ip_location_city == context.remote_ip_location_city + assert updated_relay.last_seen_remote_ip_location_lat == context.remote_ip_location_lat + assert updated_relay.last_seen_remote_ip_location_lon == context.remote_ip_location_lon assert Repo.aggregate(Domain.Network.Address, :count) == 0 end test "updates ipv6 relay when it already exists", %{ - token: token + context: context, + group: group } do - relay = Fixtures.Relays.create_relay(ipv4: nil, token: token) + relay = Fixtures.Relays.create_relay(ipv4: nil, group: group) attrs = Fixtures.Relays.relay_attrs( ipv4: nil, - ipv6: relay.ipv6, - last_seen_remote_ip: relay.ipv6, - last_seen_user_agent: "iOS/12.5 (iPhone) connlib/0.7.411" + ipv6: relay.ipv6 ) - assert {:ok, updated_relay} = upsert_relay(token, attrs) + assert {:ok, updated_relay} = upsert_relay(group, attrs, context) assert Repo.aggregate(Relays.Relay, :count, :id) == 1 - assert updated_relay.last_seen_remote_ip.address == attrs.last_seen_remote_ip.address - assert updated_relay.last_seen_user_agent == attrs.last_seen_user_agent - assert updated_relay.last_seen_user_agent != relay.last_seen_user_agent - assert updated_relay.last_seen_version == "0.7.411" + assert updated_relay.last_seen_remote_ip.address == context.remote_ip + assert updated_relay.last_seen_user_agent == context.user_agent + assert updated_relay.last_seen_version == "0.7.412" assert updated_relay.last_seen_at assert updated_relay.last_seen_at != relay.last_seen_at - assert updated_relay.token_id == token.id - assert updated_relay.group_id == token.group_id + assert updated_relay.group_id == group.id assert updated_relay.ipv4 == nil assert updated_relay.ipv6.address == attrs.ipv6.address @@ -750,31 +862,24 @@ defmodule Domain.RelaysTest do assert Repo.aggregate(Domain.Network.Address, :count) == 0 end - test "updates global relay when it already exists" do + test "updates global relay when it already exists", %{context: context} do group = Fixtures.Relays.create_global_group() - token = hd(group.tokens) - relay = Fixtures.Relays.create_relay(group: group, token: token) + relay = Fixtures.Relays.create_relay(group: group) + context = %{context | user_agent: "iOS/12.5 (iPhone) connlib/0.7.411"} + attrs = Fixtures.Relays.relay_attrs(ipv4: relay.ipv4) - attrs = - Fixtures.Relays.relay_attrs( - ipv4: relay.ipv4, - last_seen_remote_ip: relay.ipv4, - last_seen_user_agent: "iOS/12.5 (iPhone) connlib/0.7.411" - ) - - assert {:ok, updated_relay} = upsert_relay(token, attrs) + assert {:ok, updated_relay} = upsert_relay(group, attrs, context) assert Repo.aggregate(Relays.Relay, :count, :id) == 1 - assert updated_relay.last_seen_remote_ip.address == attrs.last_seen_remote_ip.address - assert updated_relay.last_seen_user_agent == attrs.last_seen_user_agent + assert updated_relay.last_seen_remote_ip.address == context.remote_ip + assert updated_relay.last_seen_user_agent == context.user_agent assert updated_relay.last_seen_user_agent != relay.last_seen_user_agent assert updated_relay.last_seen_version == "0.7.411" assert updated_relay.last_seen_at assert updated_relay.last_seen_at != relay.last_seen_at - assert updated_relay.token_id == token.id - assert updated_relay.group_id == token.group_id + assert updated_relay.group_id == group.id assert updated_relay.ipv4 == relay.ipv4 assert updated_relay.ipv6.address == attrs.ipv6 @@ -834,14 +939,18 @@ defmodule Domain.RelaysTest do test "prioritizes relays with known location" do relay_1 = Fixtures.Relays.create_relay( - last_seen_remote_ip_location_lat: 33.2029, - last_seen_remote_ip_location_lon: -80.0131 + context: [ + remote_ip_location_lat: 33.2029, + remote_ip_location_lon: -80.0131 + ] ) relay_2 = Fixtures.Relays.create_relay( - last_seen_remote_ip_location_lat: nil, - last_seen_remote_ip_location_lon: nil + context: [ + remote_ip_location_lat: nil, + remote_ip_location_lon: nil + ] ) relays = [ @@ -858,14 +967,18 @@ defmodule Domain.RelaysTest do # Moncks Corner, South Carolina relay_us_east_1 = Fixtures.Relays.create_relay( - last_seen_remote_ip_location_lat: 33.2029, - last_seen_remote_ip_location_lon: -80.0131 + context: [ + remote_ip_location_lat: 33.2029, + remote_ip_location_lon: -80.0131 + ] ) relay_us_east_2 = Fixtures.Relays.create_relay( - last_seen_remote_ip_location_lat: 33.2029, - last_seen_remote_ip_location_lon: -80.0131 + context: [ + remote_ip_location_lat: 33.2029, + remote_ip_location_lon: -80.0131 + ] ) relays = [ @@ -881,40 +994,52 @@ defmodule Domain.RelaysTest do # Moncks Corner, South Carolina relay_us_east_1 = Fixtures.Relays.create_relay( - last_seen_remote_ip_location_lat: 33.2029, - last_seen_remote_ip_location_lon: -80.0131 + context: [ + remote_ip_location_lat: 33.2029, + remote_ip_location_lon: -80.0131 + ] ) relay_us_east_2 = Fixtures.Relays.create_relay( - last_seen_remote_ip_location_lat: 33.2029, - last_seen_remote_ip_location_lon: -80.0131 + context: [ + remote_ip_location_lat: 33.2029, + remote_ip_location_lon: -80.0131 + ] ) relay_us_east_3 = Fixtures.Relays.create_relay( - last_seen_remote_ip_location_lat: 33.2029, - last_seen_remote_ip_location_lon: -80.0131 + context: [ + remote_ip_location_lat: 33.2029, + remote_ip_location_lon: -80.0131 + ] ) # The Dalles, Oregon relay_us_west_1 = Fixtures.Relays.create_relay( - last_seen_remote_ip_location_lat: 45.5946, - last_seen_remote_ip_location_lon: -121.1787 + context: [ + remote_ip_location_lat: 45.5946, + remote_ip_location_lon: -121.1787 + ] ) relay_us_west_2 = Fixtures.Relays.create_relay( - last_seen_remote_ip_location_lat: 45.5946, - last_seen_remote_ip_location_lon: -121.1787 + context: [ + remote_ip_location_lat: 45.5946, + remote_ip_location_lon: -121.1787 + ] ) # Council Bluffs, Iowa relay_us_central_1 = Fixtures.Relays.create_relay( - last_seen_remote_ip_location_lat: 41.2619, - last_seen_remote_ip_location_lon: -95.8608 + context: [ + remote_ip_location_lat: 41.2619, + remote_ip_location_lon: -95.8608 + ] ) relays = [ @@ -944,34 +1069,6 @@ defmodule Domain.RelaysTest do end end - describe "encode_token!/1" do - test "returns encoded token" do - token = Fixtures.Relays.create_token() - assert encrypted_secret = encode_token!(token) - - config = Application.fetch_env!(:domain, Domain.Relays) - key_base = Keyword.fetch!(config, :key_base) - salt = Keyword.fetch!(config, :salt) - - assert Plug.Crypto.verify(key_base, salt, encrypted_secret) == - {:ok, {token.id, token.value}} - end - end - - describe "authorize_relay/1" do - test "returns token when encoded secret is valid" do - token = Fixtures.Relays.create_token() - encoded_token = encode_token!(token) - assert {:ok, fetched_token} = authorize_relay(encoded_token) - assert fetched_token.id == token.id - assert is_nil(fetched_token.value) - end - - test "returns error when secret is invalid" do - assert authorize_relay(Ecto.UUID.generate()) == {:error, :invalid_token} - end - end - describe "connect_relay/2" do test "does not allow duplicate presence", %{account: account} do relay = Fixtures.Relays.create_relay(account: account) diff --git a/elixir/apps/domain/test/domain/tokens_test.exs b/elixir/apps/domain/test/domain/tokens_test.exs index f4b6490a9..2cb300492 100644 --- a/elixir/apps/domain/test/domain/tokens_test.exs +++ b/elixir/apps/domain/test/domain/tokens_test.exs @@ -121,18 +121,14 @@ defmodule Domain.TokensTest do assert errors_on(changeset) == %{ type: ["can't be blank"], - account_id: ["can't be blank"], - expires_at: ["can't be blank"], secret_fragment: ["can't be blank"], - secret_hash: ["can't be blank"], - created_by_remote_ip: ["can't be blank"], - created_by_user_agent: ["can't be blank"] + secret_hash: ["can't be blank"] } end test "returns errors on invalid attrs" do attrs = %{ - type: :relay, + type: :foo, secret_nonce: -1, secret_fragment: -1, expires_at: DateTime.utc_now(), @@ -198,11 +194,8 @@ defmodule Domain.TokensTest do assert errors_on(changeset) == %{ type: ["can't be blank"], - expires_at: ["can't be blank"], secret_fragment: ["can't be blank"], - secret_hash: ["can't be blank"], - created_by_remote_ip: ["can't be blank"], - created_by_user_agent: ["can't be blank"] + secret_hash: ["can't be blank"] } end @@ -239,8 +232,6 @@ defmodule Domain.TokensTest do nonce = "nonce" fragment = Domain.Crypto.random_token(32) expires_at = DateTime.utc_now() |> DateTime.add(1, :day) - user_agent = Fixtures.Tokens.user_agent() - remote_ip = Fixtures.Tokens.remote_ip() attrs = %{ type: type, @@ -248,17 +239,15 @@ defmodule Domain.TokensTest do secret_fragment: fragment, actor_id: actor.id, identity_id: identity.id, - expires_at: expires_at, - created_by_user_agent: user_agent, - created_by_remote_ip: remote_ip + expires_at: expires_at } assert {:ok, %Tokens.Token{} = token} = create_token(attrs, subject) assert token.type == type assert token.expires_at == expires_at - assert token.created_by_user_agent == user_agent - assert token.created_by_remote_ip.address == remote_ip + assert token.created_by_user_agent == subject.context.user_agent + assert token.created_by_remote_ip == subject.context.remote_ip assert token.secret_fragment == fragment refute token.secret_nonce @@ -304,6 +293,7 @@ defmodule Domain.TokensTest do assert token.last_seen_remote_ip_location_lat == context.remote_ip_location_lat assert token.last_seen_remote_ip_location_lon == context.remote_ip_location_lon assert token.last_seen_at + refute token.remaining_attempts end test "returns error when secret is invalid", %{account: account} do @@ -316,6 +306,50 @@ defmodule Domain.TokensTest do {:error, :invalid_or_expired_token} end + test "reduces attempts count when secret is invalid", %{account: account} do + nonce = "nonce" + + token = + Fixtures.Tokens.create_token( + remaining_attempts: 3, + expires_at: nil, + account: account, + secret_nonce: nonce + ) + + context = Fixtures.Auth.build_context(type: token.type) + encoded_fragment = encode_fragment!(%{token | secret_fragment: "bar"}) + + assert use_token(nonce <> encoded_fragment, context) == + {:error, :invalid_or_expired_token} + + token = Repo.get(Tokens.Token, token.id) + assert token.remaining_attempts == 2 + refute token.expires_at + end + + test "expires token when attempts limit is exceeded", %{account: account} do + nonce = "nonce" + + token = + Fixtures.Tokens.create_token( + remaining_attempts: 1, + expires_at: nil, + account: account, + secret_nonce: nonce + ) + + context = Fixtures.Auth.build_context(type: token.type) + encoded_fragment = encode_fragment!(%{token | secret_fragment: "bar"}) + + assert use_token(nonce <> encoded_fragment, context) == + {:error, :invalid_or_expired_token} + + token = Repo.get(Tokens.Token, token.id) + assert token.remaining_attempts == 0 + assert token.expires_at + end + test "returns error when nonce is invalid", %{account: account} do token = Fixtures.Tokens.create_token(account: account) context = Fixtures.Auth.build_context(type: token.type) @@ -497,10 +531,10 @@ defmodule Domain.TokensTest do end describe "delete_tokens_for/2" do - test "deletes tokens for given actor", %{account: account, subject: subject} do + test "deletes browser tokens for given actor", %{account: account, subject: subject} do actor = Fixtures.Actors.create_actor(account: account) identity = Fixtures.Auth.create_identity(account: account, actor: actor) - token = Fixtures.Tokens.create_token(account: account, identity: identity) + token = Fixtures.Tokens.create_token(type: :browser, account: account, identity: identity) Phoenix.PubSub.subscribe(Domain.PubSub, "sessions:#{token.id}") assert delete_tokens_for(actor, subject) == {:ok, 1} @@ -509,6 +543,40 @@ defmodule Domain.TokensTest do assert_receive "disconnect" end + test "deletes client tokens for given actor", %{account: account, subject: subject} do + actor = Fixtures.Actors.create_actor(account: account) + identity = Fixtures.Auth.create_identity(account: account, actor: actor) + token = Fixtures.Tokens.create_token(type: :client, account: account, identity: identity) + Phoenix.PubSub.subscribe(Domain.PubSub, "sessions:#{token.id}") + + assert delete_tokens_for(actor, subject) == {:ok, 1} + + assert Repo.get(Tokens.Token, token.id).deleted_at + assert_receive "disconnect" + end + + test "deletes gateway group tokens", %{account: account, subject: subject} do + group = Fixtures.Gateways.create_group(account: account) + token = Fixtures.Gateways.create_token(account: account, group: group) + Phoenix.PubSub.subscribe(Domain.PubSub, "gateway_groups:#{group.id}") + + assert delete_tokens_for(group, subject) == {:ok, 1} + + assert Repo.get(Tokens.Token, token.id).deleted_at + assert_receive "disconnect" + end + + test "deletes relay group tokens", %{account: account, subject: subject} do + group = Fixtures.Relays.create_group(account: account) + token = Fixtures.Relays.create_token(account: account, group: group) + Phoenix.PubSub.subscribe(Domain.PubSub, "relay_groups:#{group.id}") + + assert delete_tokens_for(group, subject) == {:ok, 1} + + assert Repo.get(Tokens.Token, token.id).deleted_at + assert_receive "disconnect" + end + test "returns error when subject does not have required permissions", %{ actor: actor, subject: subject diff --git a/elixir/apps/domain/test/support/fixtures/auth.ex b/elixir/apps/domain/test/support/fixtures/auth.ex index a7dc45f6b..4cacbf01d 100644 --- a/elixir/apps/domain/test/support/fixtures/auth.ex +++ b/elixir/apps/domain/test/support/fixtures/auth.ex @@ -273,12 +273,12 @@ defmodule Domain.Fixtures.Auth do end) {remote_ip_location_lat, attrs} = - Map.pop_lazy(attrs, :remote_ip_location_city, fn -> + Map.pop_lazy(attrs, :remote_ip_location_lat, fn -> Enum.random([37.7758, 40.7128]) end) {remote_ip_location_lon, _attrs} = - Map.pop_lazy(attrs, :remote_ip_location_city, fn -> + Map.pop_lazy(attrs, :remote_ip_location_lon, fn -> Enum.random([-122.4128, -74.0060]) end) @@ -348,14 +348,18 @@ defmodule Domain.Fixtures.Auth do {identity, attrs} = pop_assoc_fixture(attrs, :identity, fn assoc_attrs -> - assoc_attrs - |> Enum.into(%{ - actor: actor, - account: account, - provider: provider, - provider_identifier: provider_identifier - }) - |> create_identity() + if actor.type == :service_account do + nil + else + assoc_attrs + |> Enum.into(%{ + actor: actor, + account: account, + provider: provider, + provider_identifier: provider_identifier + }) + |> create_identity() + end end) {expires_at, attrs} = @@ -365,7 +369,9 @@ defmodule Domain.Fixtures.Auth do {context, attrs} = pop_assoc_fixture(attrs, :context, fn assoc_attrs -> - build_context(assoc_attrs) + assoc_attrs + |> Enum.into(%{type: if(actor.type == :service_account, do: :client, else: :browser)}) + |> build_context() end) {token, _attrs} = diff --git a/elixir/apps/domain/test/support/fixtures/gateways.ex b/elixir/apps/domain/test/support/fixtures/gateways.ex index 54882f34a..d1f1a839c 100644 --- a/elixir/apps/domain/test/support/fixtures/gateways.ex +++ b/elixir/apps/domain/test/support/fixtures/gateways.ex @@ -5,8 +5,7 @@ defmodule Domain.Fixtures.Gateways do def group_attrs(attrs \\ %{}) do Enum.into(attrs, %{ name: "group-#{unique_integer()}", - routing: "managed", - tokens: [%{}] + routing: "managed" }) end @@ -64,22 +63,17 @@ defmodule Domain.Fixtures.Gateways do |> Fixtures.Auth.create_subject() end) - Gateways.Token.Changeset.create(account, subject) - |> Ecto.Changeset.put_change(:group_id, group.id) - |> Repo.insert!() + {:ok, encoded_token} = Gateways.create_token(group, attrs, subject) + context = Fixtures.Auth.build_context(type: :gateway_group) + {:ok, {_account_id, id, nonce, secret}} = Domain.Tokens.peek_token(encoded_token, context) + %{Repo.get(Domain.Tokens.Token, id) | secret_nonce: nonce, secret_fragment: secret} end def gateway_attrs(attrs \\ %{}) do Enum.into(attrs, %{ external_id: Ecto.UUID.generate(), name: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}", - public_key: unique_public_key(), - last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", - last_seen_remote_ip: %Postgrex.INET{address: {189, 172, 73, 153}}, - last_seen_remote_ip_location_region: "US", - last_seen_remote_ip_location_city: "San Francisco", - last_seen_remote_ip_location_lat: 37.7758, - last_seen_remote_ip_location_lon: -122.4128 + public_key: unique_public_key() }) end @@ -98,12 +92,14 @@ defmodule Domain.Fixtures.Gateways do |> create_group() end) - {token, attrs} = - Map.pop_lazy(attrs, :token, fn -> - hd(group.tokens) + {context, attrs} = + pop_assoc_fixture(attrs, :context, fn assoc_attrs -> + assoc_attrs + |> Enum.into(%{type: :gateway_group}) + |> Fixtures.Auth.build_context() end) - {:ok, gateway} = Gateways.upsert_gateway(token, attrs) + {:ok, gateway} = Gateways.upsert_gateway(group, attrs, context) %{gateway | online?: false} end diff --git a/elixir/apps/domain/test/support/fixtures/relays.ex b/elixir/apps/domain/test/support/fixtures/relays.ex index 0c38569db..73ac1b122 100644 --- a/elixir/apps/domain/test/support/fixtures/relays.ex +++ b/elixir/apps/domain/test/support/fixtures/relays.ex @@ -4,8 +4,7 @@ defmodule Domain.Fixtures.Relays do def group_attrs(attrs \\ %{}) do Enum.into(attrs, %{ - name: "group-#{unique_integer()}", - tokens: [%{}] + name: "group-#{unique_integer()}" }) end @@ -69,9 +68,10 @@ defmodule Domain.Fixtures.Relays do |> Fixtures.Auth.create_subject() end) - Relays.Token.Changeset.create(account, subject) - |> Ecto.Changeset.put_change(:group_id, group.id) - |> Repo.insert!() + {:ok, encoded_token} = Relays.create_token(group, attrs, subject) + context = Fixtures.Auth.build_context(type: :relay_group) + {:ok, {_account_id, id, nonce, secret}} = Domain.Tokens.peek_token(encoded_token, context) + %{Repo.get(Domain.Tokens.Token, id) | secret_nonce: nonce, secret_fragment: secret} end def relay_attrs(attrs \\ %{}) do @@ -79,13 +79,7 @@ defmodule Domain.Fixtures.Relays do Enum.into(attrs, %{ ipv4: ipv4, - ipv6: unique_ipv6(), - last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", - last_seen_remote_ip: ipv4, - last_seen_remote_ip_location_region: "Mexico", - last_seen_remote_ip_location_city: "Merida", - last_seen_remote_ip_location_lat: 37.7749, - last_seen_remote_ip_location_lon: -120.4194 + ipv6: unique_ipv6() }) end @@ -104,12 +98,14 @@ defmodule Domain.Fixtures.Relays do |> create_group() end) - {token, attrs} = - Map.pop_lazy(attrs, :token, fn -> - hd(group.tokens) + {context, attrs} = + pop_assoc_fixture(attrs, :context, fn assoc_attrs -> + assoc_attrs + |> Enum.into(%{type: :relay_group}) + |> Fixtures.Auth.build_context() end) - {:ok, relay} = Relays.upsert_relay(token, attrs) + {:ok, relay} = Relays.upsert_relay(group, attrs, context) %{relay | online?: false} end diff --git a/elixir/apps/domain/test/support/fixtures/tokens.ex b/elixir/apps/domain/test/support/fixtures/tokens.ex index d8ad6c317..e74ac2df6 100644 --- a/elixir/apps/domain/test/support/fixtures/tokens.ex +++ b/elixir/apps/domain/test/support/fixtures/tokens.ex @@ -92,9 +92,13 @@ defmodule Domain.Fixtures.Tokens do {identity, attrs} = pop_assoc_fixture(attrs, :identity, fn assoc_attrs -> - assoc_attrs - |> Enum.into(%{account: account, actor: actor}) - |> Fixtures.Auth.create_identity() + if actor.type == :service_account do + %{id: nil} + else + assoc_attrs + |> Enum.into(%{account: account, actor: actor}) + |> Fixtures.Auth.create_identity() + end end) attrs = Map.put(attrs, :account_id, account.id) diff --git a/elixir/apps/web/lib/web/controllers/auth_controller.ex b/elixir/apps/web/lib/web/controllers/auth_controller.ex index 484a631d3..e8d5ed0e0 100644 --- a/elixir/apps/web/lib/web/controllers/auth_controller.ex +++ b/elixir/apps/web/lib/web/controllers/auth_controller.ex @@ -83,6 +83,9 @@ defmodule Web.AuthController do end defp maybe_send_magic_link_email(conn, provider_id, provider_identifier, redirect_params) do + context_type = Web.Auth.fetch_auth_context_type!(redirect_params) + context = Web.Auth.get_auth_context(conn, context_type) + with {:ok, provider} <- Domain.Auth.fetch_active_provider_by_id(provider_id), {:ok, identity} <- Domain.Auth.fetch_active_identity_by_provider_and_identifier( @@ -90,24 +93,25 @@ defmodule Web.AuthController do provider_identifier, preload: :account ), - {:ok, identity} <- Domain.Auth.Adapters.Email.request_sign_in_token(identity) do - # We split the secret into two components, the first 5 bytes is the code we send to the user - # the rest is the secret we store in the cookie. This is to prevent authorization code injection + {:ok, identity} <- Domain.Auth.Adapters.Email.request_sign_in_token(identity, context) do + # Nonce is the short part that is sent to the user in the email + nonce = identity.provider_virtual_state.nonce + + # Fragment is stored in the browser to prevent authorization code injection # attacks where you can trick user into logging in into a attacker account. - <> = - identity.provider_virtual_state.sign_in_token + fragment = identity.provider_virtual_state.fragment {:ok, _} = Web.Mailer.AuthEmail.sign_in_link_email( identity, - email_secret, + nonce, conn.assigns.user_agent, conn.remote_ip, redirect_params ) |> Web.Mailer.deliver() - put_auth_state(conn, provider.id, {nonce, redirect_params}) + put_auth_state(conn, provider.id, {fragment, redirect_params}) else _ -> conn end @@ -129,12 +133,12 @@ defmodule Web.AuthController do "account_id_or_slug" => account_id_or_slug, "provider_id" => provider_id, "identity_id" => identity_id, - "secret" => email_secret + "secret" => nonce } = params ) do - with {:ok, {nonce, redirect_params}, conn} <- fetch_auth_state(conn, provider_id) do + with {:ok, {fragment, redirect_params}, conn} <- fetch_auth_state(conn, provider_id) do conn = delete_auth_state(conn, provider_id) - secret = String.downcase(email_secret) <> nonce + secret = String.downcase(nonce) <> fragment context_type = Web.Auth.fetch_auth_context_type!(redirect_params) context = Web.Auth.get_auth_context(conn, context_type) nonce = Web.Auth.fetch_token_nonce!(redirect_params) diff --git a/elixir/apps/web/lib/web/live/actors/service_accounts/new_identity.ex b/elixir/apps/web/lib/web/live/actors/service_accounts/new_identity.ex index 503b039ab..86767b166 100644 --- a/elixir/apps/web/lib/web/live/actors/service_accounts/new_identity.ex +++ b/elixir/apps/web/lib/web/live/actors/service_accounts/new_identity.ex @@ -100,8 +100,8 @@ defmodule Web.Actors.ServiceAccounts.NewIdentity do with {:ok, encoded_token} <- Auth.create_service_account_token( socket.assigns.actor, - socket.assigns.subject, - attrs + attrs, + socket.assigns.subject ) do {:noreply, assign(socket, encoded_token: encoded_token)} else diff --git a/elixir/apps/web/lib/web/live/relay_groups/new_token.ex b/elixir/apps/web/lib/web/live/relay_groups/new_token.ex index ef540deaf..8e340a294 100644 --- a/elixir/apps/web/lib/web/live/relay_groups/new_token.ex +++ b/elixir/apps/web/lib/web/live/relay_groups/new_token.ex @@ -7,13 +7,9 @@ defmodule Web.RelayGroups.NewToken do {:ok, group} <- Relays.fetch_group_by_id(id, socket.assigns.subject) do {group, env} = if connected?(socket) do - {:ok, group} = - Relays.update_group(%{group | tokens: []}, %{tokens: [%{}]}, socket.assigns.subject) - + {:ok, encoded_token} = Relays.create_token(group, %{}, socket.assigns.subject) :ok = Relays.subscribe_for_relays_presence_in_group(group) - - token = Relays.encode_token!(hd(group.tokens)) - {group, env(token)} + {group, env(encoded_token)} else {group, nil} end @@ -226,7 +222,7 @@ defmodule Web.RelayGroups.NewToken do "#{vsn.major}.#{vsn.minor}" end - defp env(token) do + defp env(encoded_token) do api_url_override = if api_url = Domain.Config.get_env(:web, :api_url_override) do {"FIREZONE_API_URL", api_url} @@ -234,7 +230,7 @@ defmodule Web.RelayGroups.NewToken do [ {"FIREZONE_ID", Ecto.UUID.generate()}, - {"FIREZONE_TOKEN", token}, + {"FIREZONE_TOKEN", encoded_token}, {"PUBLIC_IP4_ADDR", "YOU_MUST_SET_THIS_VALUE"}, {"PUBLIC_IP6_ADDR", "YOU_MUST_SET_THIS_VALUE"}, api_url_override, diff --git a/elixir/apps/web/lib/web/live/relay_groups/show.ex b/elixir/apps/web/lib/web/live/relay_groups/show.ex index c722dfbfc..e0357f5ea 100644 --- a/elixir/apps/web/lib/web/live/relay_groups/show.ex +++ b/elixir/apps/web/lib/web/live/relay_groups/show.ex @@ -1,13 +1,13 @@ defmodule Web.RelayGroups.Show do use Web, :live_view - alias Domain.Relays + alias Domain.{Relays, Tokens} def mount(%{"id" => id}, _session, socket) do with true <- Domain.Config.self_hosted_relays_enabled?(), {:ok, group} <- Relays.fetch_group_by_id(id, socket.assigns.subject, preload: [ - relays: [token: [created_by_identity: [:actor]]], + relays: [], created_by_identity: [:actor] ] ) do @@ -63,7 +63,15 @@ defmodule Web.RelayGroups.Show do Deploy - <:content> + <:action :if={is_nil(@group.deleted_at)}> + <.delete_button + phx-click="revoke_all_tokens" + data-confirm="Are you sure you want to revoke all tokens? This will immediately sign the actor out of all clients." + > + Revoke All Tokens + + + <:content flash={@flash}>
<.table id="relays" rows={@group.relays}> <:col :let={relay} label="INSTANCE"> @@ -79,9 +87,6 @@ defmodule Web.RelayGroups.Show do - <:col :let={relay} label="TOKEN CREATED AT"> - <.created_by account={@account} schema={relay.token} /> - <:col :let={relay} label="STATUS"> <.connection_status schema={relay} /> @@ -111,7 +116,7 @@ defmodule Web.RelayGroups.Show do {:ok, group} = Relays.fetch_group_by_id(socket.assigns.group.id, socket.assigns.subject, preload: [ - relays: [token: [created_by_identity: [:actor]]], + relays: [], created_by_identity: [:actor] ] ) @@ -119,8 +124,18 @@ defmodule Web.RelayGroups.Show do {:noreply, assign(socket, group: group)} end + def handle_event("revoke_all_tokens", _params, socket) do + group = socket.assigns.group + {:ok, deleted_count} = Tokens.delete_tokens_for(group, socket.assigns.subject) + + socket = + socket + |> put_flash(:info, "#{deleted_count} token(s) were revoked.") + + {:noreply, socket} + end + def handle_event("delete", _params, socket) do - # TODO: make sure tokens are all deleted too! {:ok, _group} = Relays.delete_group(socket.assigns.group, socket.assigns.subject) {:noreply, push_navigate(socket, to: ~p"/#{socket.assigns.account}/relay_groups")} end diff --git a/elixir/apps/web/lib/web/live/sites/gateways/index.ex b/elixir/apps/web/lib/web/live/sites/gateways/index.ex index 7a2e880d8..468ea5c9b 100644 --- a/elixir/apps/web/lib/web/live/sites/gateways/index.ex +++ b/elixir/apps/web/lib/web/live/sites/gateways/index.ex @@ -9,9 +9,7 @@ defmodule Web.Sites.Gateways.Index do Gateways.fetch_group_by_id(id, socket.assigns.subject), # TODO: add LIMIT 100 ORDER BY last_seen_at DESC once we support filters {:ok, gateways} <- - Gateways.list_gateways_for_group(group, subject, - preload: [token: [created_by_identity: [:actor]]] - ) do + Gateways.list_gateways_for_group(group, subject) do gateways = Enum.sort_by(gateways, & &1.online?, :desc) :ok = Gateways.subscribe_for_gateways_presence_in_group(group) socket = assign(socket, group: group, gateways: gateways, page_title: "Site Gateways") @@ -54,9 +52,6 @@ defmodule Web.Sites.Gateways.Index do <%= gateway.last_seen_remote_ip %> - <:col :let={gateway} label="TOKEN CREATED AT"> - <.created_by account={@account} schema={gateway.token} /> - <:col :let={gateway} label="STATUS"> <.connection_status schema={gateway} /> diff --git a/elixir/apps/web/lib/web/live/sites/new_token.ex b/elixir/apps/web/lib/web/live/sites/new_token.ex index 6bb8e3800..1a838520a 100644 --- a/elixir/apps/web/lib/web/live/sites/new_token.ex +++ b/elixir/apps/web/lib/web/live/sites/new_token.ex @@ -6,13 +6,9 @@ defmodule Web.Sites.NewToken do with {:ok, group} <- Gateways.fetch_group_by_id(id, socket.assigns.subject) do {group, env} = if connected?(socket) do - {:ok, group} = - Gateways.update_group(%{group | tokens: []}, %{tokens: [%{}]}, socket.assigns.subject) - + {:ok, encoded_token} = Gateways.create_token(group, %{}, socket.assigns.subject) :ok = Gateways.subscribe_for_gateways_presence_in_group(group) - - token = Gateways.encode_token!(hd(group.tokens)) - {group, env(token)} + {group, env(encoded_token)} else {group, nil} end @@ -138,7 +134,7 @@ defmodule Web.Sites.NewToken do "#{vsn.major}.#{vsn.minor}" end - defp env(token) do + defp env(encoded_token) do api_url_override = if api_url = Domain.Config.get_env(:web, :api_url_override) do {"FIREZONE_API_URL", api_url} @@ -146,7 +142,7 @@ defmodule Web.Sites.NewToken do [ {"FIREZONE_ID", Ecto.UUID.generate()}, - {"FIREZONE_TOKEN", token}, + {"FIREZONE_TOKEN", encoded_token}, api_url_override, {"RUST_LOG", Enum.join( diff --git a/elixir/apps/web/lib/web/live/sites/show.ex b/elixir/apps/web/lib/web/live/sites/show.ex index 2846352b9..2c702ed8e 100644 --- a/elixir/apps/web/lib/web/live/sites/show.ex +++ b/elixir/apps/web/lib/web/live/sites/show.ex @@ -1,7 +1,7 @@ defmodule Web.Sites.Show do use Web, :live_view import Web.Sites.Components - alias Domain.{Gateways, Resources} + alias Domain.{Gateways, Resources, Tokens} def mount(%{"id" => id}, _session, socket) do with {:ok, group} <- @@ -12,9 +12,7 @@ defmodule Web.Sites.Show do ] ), {:ok, gateways} <- - Gateways.list_connected_gateways_for_group(group, socket.assigns.subject, - preload: [token: [created_by_identity: [:actor]]] - ), + Gateways.list_connected_gateways_for_group(group, socket.assigns.subject), resources = group.connections |> Enum.reject(&is_nil(&1.resource)) @@ -87,12 +85,20 @@ defmodule Web.Sites.Show do Deploy Gateway + <:action :if={is_nil(@group.deleted_at)}> + <.delete_button + phx-click="revoke_all_tokens" + data-confirm="Are you sure you want to revoke all tokens? This will immediately sign the actor out of all clients." + > + Revoke All Tokens + + <:help :if={is_nil(@group.deleted_at)}> Deploy gateways to terminate connections to your site's resources. All gateways deployed within a site must be able to reach all its resources. - <:content> + <:content flash={@flash}>
<.table id="gateways" rows={@gateways}> <:col :let={gateway} label="INSTANCE"> @@ -105,9 +111,6 @@ defmodule Web.Sites.Show do <%= gateway.last_seen_remote_ip %> - <:col :let={gateway} label="TOKEN CREATED AT"> - <.created_by account={@account} schema={gateway.token} /> - <:col :let={gateway} label="STATUS"> <.connection_status schema={gateway} /> @@ -218,15 +221,25 @@ defmodule Web.Sites.Show do """ end - def handle_info(%Phoenix.Socket.Broadcast{topic: "gateway_groups:" <> _account_id}, socket) do + def handle_info(%Phoenix.Socket.Broadcast{topic: "gateway_groups:" <> _group_id}, socket) do + {:ok, gateways} = + Gateways.list_connected_gateways_for_group(socket.assigns.group, socket.assigns.subject) + + {:noreply, assign(socket, gateways: gateways)} + end + + def handle_event("revoke_all_tokens", _params, socket) do + group = socket.assigns.group + {:ok, deleted_count} = Tokens.delete_tokens_for(group, socket.assigns.subject) + socket = - push_navigate(socket, to: ~p"/#{socket.assigns.account}/sites/#{socket.assigns.group}") + socket + |> put_flash(:info, "#{deleted_count} token(s) were revoked.") {:noreply, socket} end def handle_event("delete", _params, socket) do - # TODO: make sure tokens are all deleted too! {:ok, _group} = Gateways.delete_group(socket.assigns.group, socket.assigns.subject) {:noreply, push_navigate(socket, to: ~p"/#{socket.assigns.account}/sites")} end diff --git a/elixir/apps/web/lib/web/mailer/auth_email.ex b/elixir/apps/web/lib/web/mailer/auth_email.ex index 70651dfc6..6675e99bf 100644 --- a/elixir/apps/web/lib/web/mailer/auth_email.ex +++ b/elixir/apps/web/lib/web/mailer/auth_email.ex @@ -27,7 +27,7 @@ defmodule Web.Mailer.AuthEmail do def sign_in_link_email( %Domain.Auth.Identity{} = identity, - email_secret, + secret, user_agent, remote_ip, params \\ %{} @@ -35,7 +35,7 @@ defmodule Web.Mailer.AuthEmail do params = Map.merge(params, %{ identity_id: identity.id, - secret: email_secret + secret: secret }) sign_in_url = @@ -43,17 +43,19 @@ defmodule Web.Mailer.AuthEmail do ~p"/#{identity.account}/sign_in/providers/#{identity.provider_id}/verify_sign_in_token?#{params}" ) + sign_in_token_created_at = + Cldr.DateTime.to_string!(identity.provider_state["token_created_at"], Web.CLDR, + format: :short + ) <> " UTC" + default_email() |> subject("Firezone sign in token") |> to(identity.provider_identifier) |> render_body(__MODULE__, :sign_in_link, account: identity.account, client_platform: params["client_platform"], - sign_in_token_created_at: - Cldr.DateTime.to_string!(identity.provider_state["sign_in_token_created_at"], Web.CLDR, - format: :short - ) <> " UTC", - secret: email_secret, + sign_in_token_created_at: sign_in_token_created_at, + secret: secret, sign_in_url: sign_in_url, user_agent: user_agent, remote_ip: "#{:inet.ntoa(remote_ip)}" diff --git a/elixir/apps/web/test/web/live/actors/show_test.exs b/elixir/apps/web/test/web/live/actors/show_test.exs index c63bf9aa8..54304ca32 100644 --- a/elixir/apps/web/test/web/live/actors/show_test.exs +++ b/elixir/apps/web/test/web/live/actors/show_test.exs @@ -125,7 +125,7 @@ defmodule Web.Live.Actors.ShowTest do "#{flow.client.name} (#{client.last_seen_remote_ip})" assert row["gateway (ip)"] == - "#{flow.gateway.group.name}-#{flow.gateway.name} (189.172.73.153)" + "#{flow.gateway.group.name}-#{flow.gateway.name} (#{flow.gateway.last_seen_remote_ip})" end test "renders flows even for deleted policies", %{ @@ -165,7 +165,7 @@ defmodule Web.Live.Actors.ShowTest do "#{flow.client.name} (#{client.last_seen_remote_ip})" assert row["gateway (ip)"] == - "#{flow.gateway.group.name}-#{flow.gateway.name} (189.172.73.153)" + "#{flow.gateway.group.name}-#{flow.gateway.name} (#{flow.gateway.last_seen_remote_ip})" end test "renders flows even for deleted policy assocs", %{ @@ -206,7 +206,7 @@ defmodule Web.Live.Actors.ShowTest do "#{flow.client.name} (#{client.last_seen_remote_ip})" assert row["gateway (ip)"] == - "#{flow.gateway.group.name}-#{flow.gateway.name} (189.172.73.153)" + "#{flow.gateway.group.name}-#{flow.gateway.name} (#{flow.gateway.last_seen_remote_ip})" end describe "users" do diff --git a/elixir/apps/web/test/web/live/clients/show_test.exs b/elixir/apps/web/test/web/live/clients/show_test.exs index 4b4982f5a..f85cfe243 100644 --- a/elixir/apps/web/test/web/live/clients/show_test.exs +++ b/elixir/apps/web/test/web/live/clients/show_test.exs @@ -149,7 +149,7 @@ defmodule Web.Live.Clients.ShowTest do assert row["policy"] =~ flow.policy.resource.name assert row["gateway (ip)"] == - "#{flow.gateway.group.name}-#{flow.gateway.name} (189.172.73.153)" + "#{flow.gateway.group.name}-#{flow.gateway.name} (#{flow.gateway.last_seen_remote_ip})" end test "renders flows even for deleted policies", %{ @@ -185,7 +185,7 @@ defmodule Web.Live.Clients.ShowTest do assert row["policy"] =~ flow.policy.resource.name assert row["gateway (ip)"] == - "#{flow.gateway.group.name}-#{flow.gateway.name} (189.172.73.153)" + "#{flow.gateway.group.name}-#{flow.gateway.name} (#{flow.gateway.last_seen_remote_ip})" end test "renders flows even for deleted policy assocs", %{ @@ -222,7 +222,7 @@ defmodule Web.Live.Clients.ShowTest do assert row["policy"] =~ flow.policy.resource.name assert row["gateway (ip)"] == - "#{flow.gateway.group.name}-#{flow.gateway.name} (189.172.73.153)" + "#{flow.gateway.group.name}-#{flow.gateway.name} (#{flow.gateway.last_seen_remote_ip})" end test "allows editing clients", %{ diff --git a/elixir/apps/web/test/web/live/policies/show_test.exs b/elixir/apps/web/test/web/live/policies/show_test.exs index e4d3782da..969752f09 100644 --- a/elixir/apps/web/test/web/live/policies/show_test.exs +++ b/elixir/apps/web/test/web/live/policies/show_test.exs @@ -161,7 +161,7 @@ defmodule Web.Live.Policies.ShowTest do assert row["client, actor (ip)"] =~ to_string(flow.client_remote_ip) assert row["gateway (ip)"] =~ - "#{flow.gateway.group.name}-#{flow.gateway.name} (189.172.73.153)" + "#{flow.gateway.group.name}-#{flow.gateway.name} (#{flow.gateway.last_seen_remote_ip})" end test "allows deleting policy", %{ diff --git a/elixir/apps/web/test/web/live/relay_groups/new_token_test.exs b/elixir/apps/web/test/web/live/relay_groups/new_token_test.exs index fa398d33a..86d890b7b 100644 --- a/elixir/apps/web/test/web/live/relay_groups/new_token_test.exs +++ b/elixir/apps/web/test/web/live/relay_groups/new_token_test.exs @@ -38,7 +38,8 @@ defmodule Web.Live.RelayGroups.NewTokenTest do :ok = Domain.Relays.subscribe_for_relays_presence_in_group(group) relay = Fixtures.Relays.create_relay(account: account, group: group) - assert {:ok, _token} = Domain.Relays.authorize_relay(token) + context = Fixtures.Auth.build_context(type: :relay_group) + assert {:ok, _group} = Domain.Relays.authenticate(token, context) Domain.Relays.connect_relay(relay, "foo") assert_receive %Phoenix.Socket.Broadcast{topic: "relay_groups:" <> _group_id} diff --git a/elixir/apps/web/test/web/live/relay_groups/show_test.exs b/elixir/apps/web/test/web/live/relay_groups/show_test.exs index c689e6f47..a1f512d89 100644 --- a/elixir/apps/web/test/web/live/relay_groups/show_test.exs +++ b/elixir/apps/web/test/web/live/relay_groups/show_test.exs @@ -113,7 +113,6 @@ defmodule Web.Live.RelayGroups.ShowTest do test "renders relays table", %{ account: account, - actor: actor, identity: identity, group: group, relay: relay, @@ -129,7 +128,6 @@ defmodule Web.Live.RelayGroups.ShowTest do |> render() |> table_to_map() |> with_table_row("instance", "#{relay.ipv4} #{relay.ipv6}", fn row -> - assert row["token created at"] =~ actor.name assert row["status"] =~ "Offline" end) end @@ -177,6 +175,26 @@ defmodule Web.Live.RelayGroups.ShowTest do assert Repo.get(Domain.Relays.Group, group.id).deleted_at end + test "allows revoking all tokens", %{ + account: account, + group: group, + identity: identity, + conn: conn + } do + token = Fixtures.Relays.create_token(account: account, group: group) + + {:ok, lv, _html} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/relay_groups/#{group}") + + assert lv + |> element("button", "Revoke All") + |> render_click() =~ "1 token(s) were revoked." + + assert Repo.get_by(Domain.Tokens.Token, id: token.id).deleted_at + end + test "renders not found error when self_hosted_relays feature flag is false", %{ account: account, identity: identity, diff --git a/elixir/apps/web/test/web/live/resources/show_test.exs b/elixir/apps/web/test/web/live/resources/show_test.exs index e280a9a26..549e2b353 100644 --- a/elixir/apps/web/test/web/live/resources/show_test.exs +++ b/elixir/apps/web/test/web/live/resources/show_test.exs @@ -238,7 +238,7 @@ defmodule Web.Live.Resources.ShowTest do assert row["policy"] =~ flow.policy.resource.name assert row["gateway (ip)"] == - "#{flow.gateway.group.name}-#{flow.gateway.name} (189.172.73.153)" + "#{flow.gateway.group.name}-#{flow.gateway.name} (#{flow.gateway.last_seen_remote_ip})" assert row["client, actor (ip)"] =~ flow.client.name assert row["client, actor (ip)"] =~ "owned by #{flow.client.actor.name}" diff --git a/elixir/apps/web/test/web/live/sign_in/email_test.exs b/elixir/apps/web/test/web/live/sign_in/email_test.exs index 8173069bb..0b8748730 100644 --- a/elixir/apps/web/test/web/live/sign_in/email_test.exs +++ b/elixir/apps/web/test/web/live/sign_in/email_test.exs @@ -7,10 +7,11 @@ defmodule Web.SignIn.EmailTest do account = Fixtures.Accounts.create_account() provider = Fixtures.Auth.create_email_provider(account: account) actor = Fixtures.Actors.create_actor(account: account, type: :account_admin_user) + context = Fixtures.Auth.build_context() {:ok, identity} = Fixtures.Auth.create_identity(account: account, actor: actor, provider: provider) - |> Domain.Auth.Adapters.Email.request_sign_in_token() + |> Domain.Auth.Adapters.Email.request_sign_in_token(context) %{ account: account, diff --git a/elixir/apps/web/test/web/live/sites/new_token_test.exs b/elixir/apps/web/test/web/live/sites/new_token_test.exs index 73511417f..da72db61a 100644 --- a/elixir/apps/web/test/web/live/sites/new_token_test.exs +++ b/elixir/apps/web/test/web/live/sites/new_token_test.exs @@ -36,7 +36,8 @@ defmodule Web.Live.Sites.NewTokenTest do :ok = Domain.Gateways.subscribe_for_gateways_presence_in_group(group) gateway = Fixtures.Gateways.create_gateway(account: account, group: group) - assert {:ok, _token} = Domain.Gateways.authorize_gateway(token) + context = Fixtures.Auth.build_context(type: :gateway_group) + assert {:ok, _group} = Domain.Gateways.authenticate(token, context) Domain.Gateways.connect_gateway(gateway) assert_receive %Phoenix.Socket.Broadcast{topic: "gateway_groups:" <> _group_id} diff --git a/elixir/apps/web/test/web/live/sites/show_test.exs b/elixir/apps/web/test/web/live/sites/show_test.exs index e76f9b78a..6af7cd0d4 100644 --- a/elixir/apps/web/test/web/live/sites/show_test.exs +++ b/elixir/apps/web/test/web/live/sites/show_test.exs @@ -112,7 +112,6 @@ defmodule Web.Live.Sites.ShowTest do test "renders online gateways table", %{ account: account, - actor: actor, identity: identity, group: group, gateway: gateway, @@ -136,7 +135,6 @@ defmodule Web.Live.Sites.ShowTest do rows |> with_table_row("instance", gateway.name, fn row -> - assert row["token created at"] =~ actor.name assert row["status"] =~ "Online" end) end @@ -163,10 +161,29 @@ defmodule Web.Live.Sites.ShowTest do assert gateway.last_seen_remote_ip assert row["remote ip"] =~ to_string(gateway.last_seen_remote_ip) assert row["status"] =~ "Online" - assert row["token created at"] end) end + test "allows revoking all tokens", %{ + account: account, + group: group, + identity: identity, + conn: conn + } do + token = Fixtures.Gateways.create_token(account: account, group: group) + + {:ok, lv, _html} = + conn + |> authorize_conn(identity) + |> live(~p"/#{account}/sites/#{group}") + + assert lv + |> element("button", "Revoke All") + |> render_click() =~ "1 token(s) were revoked." + + assert Repo.get_by(Domain.Tokens.Token, id: token.id).deleted_at + end + test "renders resources table", %{ account: account, identity: identity, diff --git a/elixir/config/config.exs b/elixir/config/config.exs index 5290e3594..111d1ba5d 100644 --- a/elixir/config/config.exs +++ b/elixir/config/config.exs @@ -37,13 +37,7 @@ config :domain, Domain.Clients, upstream_dns: ["1.1.1.1"] config :domain, Domain.Gateways, gateway_ipv4_masquerade: true, - gateway_ipv6_masquerade: true, - key_base: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S3", - salt: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej3" - -config :domain, Domain.Relays, - key_base: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2", - salt: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" + gateway_ipv6_masquerade: true config :domain, Domain.Telemetry, enabled: true, diff --git a/elixir/config/runtime.exs b/elixir/config/runtime.exs index 59df93ee3..64f233307 100644 --- a/elixir/config/runtime.exs +++ b/elixir/config/runtime.exs @@ -36,13 +36,7 @@ if config_env() == :prod do config :domain, Domain.Gateways, gateway_ipv4_masquerade: compile_config!(:gateway_ipv4_masquerade), - gateway_ipv6_masquerade: compile_config!(:gateway_ipv6_masquerade), - key_base: compile_config!(:gateways_auth_token_key_base), - salt: compile_config!(:gateways_auth_token_salt) - - config :domain, Domain.Relays, - key_base: compile_config!(:relays_auth_token_key_base), - salt: compile_config!(:relays_auth_token_salt) + gateway_ipv6_masquerade: compile_config!(:gateway_ipv6_masquerade) config :domain, Domain.Telemetry, enabled: compile_config!(:telemetry_enabled), diff --git a/terraform/environments/production/main.tf b/terraform/environments/production/main.tf index 7a40866a9..5a874995f 100644 --- a/terraform/environments/production/main.tf +++ b/terraform/environments/production/main.tf @@ -191,26 +191,6 @@ resource "random_password" "tokens_salt" { special = false } -resource "random_password" "relays_auth_token_key_base" { - length = 64 - special = false -} - -resource "random_password" "relays_auth_token_salt" { - length = 32 - special = false -} - -resource "random_password" "gateways_auth_token_key_base" { - length = 64 - special = false -} - -resource "random_password" "gateways_auth_token_salt" { - length = 32 - special = false -} - resource "random_password" "secret_key_base" { length = 64 special = false @@ -428,22 +408,6 @@ locals { name = "TOKENS_SALT" value = base64encode(random_password.tokens_salt.result) }, - { - name = "RELAYS_AUTH_TOKEN_KEY_BASE" - value = base64encode(random_password.relays_auth_token_key_base.result) - }, - { - name = "RELAYS_AUTH_TOKEN_SALT" - value = base64encode(random_password.relays_auth_token_salt.result) - }, - { - name = "GATEWAYS_AUTH_TOKEN_KEY_BASE" - value = base64encode(random_password.gateways_auth_token_key_base.result) - }, - { - name = "GATEWAYS_AUTH_TOKEN_SALT" - value = base64encode(random_password.gateways_auth_token_salt.result) - }, { name = "SECRET_KEY_BASE" value = base64encode(random_password.secret_key_base.result) diff --git a/terraform/environments/staging/main.tf b/terraform/environments/staging/main.tf index 715e4277b..2705697bb 100644 --- a/terraform/environments/staging/main.tf +++ b/terraform/environments/staging/main.tf @@ -159,26 +159,6 @@ resource "random_password" "tokens_salt" { special = false } -resource "random_password" "relays_auth_token_key_base" { - length = 64 - special = false -} - -resource "random_password" "relays_auth_token_salt" { - length = 32 - special = false -} - -resource "random_password" "gateways_auth_token_key_base" { - length = 64 - special = false -} - -resource "random_password" "gateways_auth_token_salt" { - length = 32 - special = false -} - resource "random_password" "secret_key_base" { length = 64 special = false @@ -379,22 +359,6 @@ locals { name = "TOKENS_SALT" value = base64encode(random_password.tokens_salt.result) }, - { - name = "RELAYS_AUTH_TOKEN_KEY_BASE" - value = base64encode(random_password.relays_auth_token_key_base.result) - }, - { - name = "RELAYS_AUTH_TOKEN_SALT" - value = base64encode(random_password.relays_auth_token_salt.result) - }, - { - name = "GATEWAYS_AUTH_TOKEN_KEY_BASE" - value = base64encode(random_password.gateways_auth_token_key_base.result) - }, - { - name = "GATEWAYS_AUTH_TOKEN_SALT" - value = base64encode(random_password.gateways_auth_token_salt.result) - }, { name = "SECRET_KEY_BASE" value = base64encode(random_password.secret_key_base.result)