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:
Andrew Dryga
2024-01-16 15:39:00 -06:00
committed by GitHub
parent 5551eece5d
commit 832fc3f2e3
81 changed files with 1826 additions and 1432 deletions

View File

@@ -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"

View File

@@ -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)
...
```

View File

@@ -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)

View File

@@ -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
})

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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}

View File

@@ -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}

View File

@@ -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

View File

@@ -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,

View File

@@ -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 ->

View File

@@ -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()
]

View File

@@ -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

View File

@@ -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,

View File

@@ -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 ->

View File

@@ -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{}

View File

@@ -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 = [

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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}

View File

@@ -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

View File

@@ -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

View File

@@ -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:")

View File

@@ -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

View File

@@ -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"]

View File

@@ -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", %{

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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} =

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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>

View File

@@ -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(

View File

@@ -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

View File

@@ -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)}"

View File

@@ -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

View File

@@ -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", %{

View File

@@ -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", %{

View File

@@ -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}

View File

@@ -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,

View File

@@ -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}"

View File

@@ -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,

View File

@@ -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}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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),

View File

@@ -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)

View File

@@ -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)