mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
Implement rest of TODOs after token refactoring (#3160)
- [x] Introduce api_client actor type and code to create and authenticate using it's token - [x] Unify Tokens usage for Relays and Gateways - [x] Unify Tokens usage for magic links Closes #2367 Ref #2696
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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()
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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:")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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", %{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} =
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
<<email_secret::binary-size(5), nonce::binary>> =
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
</.add_button>
|
||||
</:action>
|
||||
<: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
|
||||
</.delete_button>
|
||||
</:action>
|
||||
<:content flash={@flash}>
|
||||
<div class="relative overflow-x-auto">
|
||||
<.table id="relays" rows={@group.relays}>
|
||||
<:col :let={relay} label="INSTANCE">
|
||||
@@ -79,9 +87,6 @@ defmodule Web.RelayGroups.Show do
|
||||
</code>
|
||||
</.link>
|
||||
</:col>
|
||||
<:col :let={relay} label="TOKEN CREATED AT">
|
||||
<.created_by account={@account} schema={relay.token} />
|
||||
</:col>
|
||||
<:col :let={relay} label="STATUS">
|
||||
<.connection_status schema={relay} />
|
||||
</:col>
|
||||
@@ -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
|
||||
|
||||
@@ -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 %>
|
||||
</code>
|
||||
</:col>
|
||||
<:col :let={gateway} label="TOKEN CREATED AT">
|
||||
<.created_by account={@account} schema={gateway.token} />
|
||||
</:col>
|
||||
<:col :let={gateway} label="STATUS">
|
||||
<.connection_status schema={gateway} />
|
||||
</:col>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
</.add_button>
|
||||
</:action>
|
||||
<: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
|
||||
</.delete_button>
|
||||
</:action>
|
||||
<: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.
|
||||
</:help>
|
||||
<:content>
|
||||
<:content flash={@flash}>
|
||||
<div class="relative overflow-x-auto">
|
||||
<.table id="gateways" rows={@gateways}>
|
||||
<:col :let={gateway} label="INSTANCE">
|
||||
@@ -105,9 +111,6 @@ defmodule Web.Sites.Show do
|
||||
<%= gateway.last_seen_remote_ip %>
|
||||
</code>
|
||||
</:col>
|
||||
<:col :let={gateway} label="TOKEN CREATED AT">
|
||||
<.created_by account={@account} schema={gateway.token} />
|
||||
</:col>
|
||||
<:col :let={gateway} label="STATUS">
|
||||
<.connection_status schema={gateway} />
|
||||
</:col>
|
||||
@@ -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
|
||||
|
||||
@@ -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)}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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", %{
|
||||
|
||||
@@ -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", %{
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user