mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 02:18:47 +00:00
refactor(portal): reduce cache memory usage (#10058)
Napkin math shows that we can save substantial memory (~3x or more) on the API nodes as connected clients/gateways grow if we just store the fields we need in order to keep the client and gateway state maintained in the channel pids. To facilitate this, we create new `Cacheable` structs that represent their `Domain` cousins, which use byte arrays for `id`s and strip out unused fields. Additionally, all business logic involved with maintaining these caches is now contained within two modules: `Domain.Cache.Client` and `Domain.Cache.Gateway`, and type specs have been added to aid in static analysis and code documentation. Comprehensive testing is now added not only for the cache modules, but for their associated channel modules as well to ensure we handle different kinds of edge cases gracefully. The `Events` nomenclature was renamed to `Changes` to better name what we are doing: Change-Data-Capture. Lastly, the following related changes are included in this PR since they were "in the way" so to speak of getting this done: - We save the last received LSN in each channel and drop the `change` with a warning if we receive it twice in a row, or we receive it out of order - The client/gateway version compatibility calculations have been moved to `Domain.Resources` and `Domain.Gateways` and have been simplified to make them easier to understand and maintain going forward. Related: #10174 Fixes: #9392 Fixes: #9965 Fixes: #9501 Fixes: #10227 --------- Signed-off-by: Jamil <jamilbk@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
4
elixir/.gitignore
vendored
4
elixir/.gitignore
vendored
@@ -32,3 +32,7 @@ apps/*/screenshots
|
||||
|
||||
# Uploads
|
||||
apps/web/priv/static/uploads
|
||||
|
||||
# ElixirLS
|
||||
# ElixirLS uses this directory to store its state.
|
||||
.elixir_ls/
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -54,5 +54,6 @@ defmodule API.Client.Socket do
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
||||
def id(socket), do: Tokens.socket_id(socket.assigns.subject.token_id)
|
||||
end
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
defmodule API.Client.Views.GatewayGroup do
|
||||
alias Domain.Gateways
|
||||
alias Domain.Cache.Cacheable
|
||||
|
||||
def render_many(gateway_groups) do
|
||||
Enum.map(gateway_groups, &render/1)
|
||||
end
|
||||
|
||||
def render(%Gateways.Group{} = gateway_group) do
|
||||
def render(%Cacheable.GatewayGroup{} = gateway_group) do
|
||||
%{
|
||||
id: gateway_group.id,
|
||||
id: Ecto.UUID.load!(gateway_group.id),
|
||||
name: gateway_group.name
|
||||
}
|
||||
end
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
defmodule API.Client.Views.Resource do
|
||||
alias API.Client.Views
|
||||
alias Domain.Resources
|
||||
alias Domain.Cache.Cacheable
|
||||
|
||||
def render_many(resources) do
|
||||
Enum.map(resources, &render/1)
|
||||
end
|
||||
|
||||
def render(%Resources.Resource{type: :internet} = resource) do
|
||||
def render(%Cacheable.Resource{} = resource) do
|
||||
resource
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:id, Ecto.UUID.load!(resource.id))
|
||||
|> render_resource()
|
||||
end
|
||||
|
||||
defp render_resource(%{type: :internet} = resource) do
|
||||
%{
|
||||
id: resource.id,
|
||||
type: :internet,
|
||||
@@ -15,7 +22,7 @@ defmodule API.Client.Views.Resource do
|
||||
}
|
||||
end
|
||||
|
||||
def render(%Resources.Resource{type: :ip} = resource) do
|
||||
defp render_resource(%{type: :ip} = resource) do
|
||||
{:ok, inet} = Domain.Types.IP.cast(resource.address)
|
||||
netmask = Domain.Types.CIDR.max_netmask(inet)
|
||||
address = to_string(%{inet | netmask: netmask})
|
||||
@@ -31,7 +38,7 @@ defmodule API.Client.Views.Resource do
|
||||
}
|
||||
end
|
||||
|
||||
def render(%Resources.Resource{} = resource) do
|
||||
defp render_resource(%{} = resource) do
|
||||
%{
|
||||
id: resource.id,
|
||||
type: resource.type,
|
||||
@@ -44,7 +51,7 @@ defmodule API.Client.Views.Resource do
|
||||
|> maybe_put_ip_stack(resource)
|
||||
end
|
||||
|
||||
def render_filter(%Resources.Resource.Filter{ports: ports} = filter) when length(ports) > 0 do
|
||||
defp render_filter(%{ports: ports} = filter) when length(ports) > 0 do
|
||||
Enum.map(filter.ports, fn port ->
|
||||
case String.split(port, "-") do
|
||||
[port_start, port_end] ->
|
||||
@@ -69,7 +76,7 @@ defmodule API.Client.Views.Resource do
|
||||
end)
|
||||
end
|
||||
|
||||
def render_filter(%Resources.Resource.Filter{} = filter) do
|
||||
defp render_filter(%{} = filter) do
|
||||
[
|
||||
%{
|
||||
protocol: filter.protocol
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
defmodule API.Gateway.Channel do
|
||||
use API, :channel
|
||||
alias API.Gateway.Views
|
||||
alias Domain.{Accounts, Flows, Gateways, PubSub, Relays, Resources}
|
||||
|
||||
alias Domain.{
|
||||
Accounts,
|
||||
Cache,
|
||||
Changes.Change,
|
||||
Flows,
|
||||
Gateways,
|
||||
PubSub,
|
||||
Relays,
|
||||
Resources
|
||||
}
|
||||
|
||||
alias Domain.Relays.Presence.Debouncer
|
||||
require Logger
|
||||
require OpenTelemetry.Tracer
|
||||
|
||||
# The interval at which the flow cache is pruned.
|
||||
@prune_flow_cache_every :timer.minutes(1)
|
||||
@prune_cache_every :timer.minutes(1)
|
||||
|
||||
# All relayed connections are dropped when this expires, so use
|
||||
# a long expiration time to avoid frequent disconnections.
|
||||
@@ -26,8 +36,8 @@ defmodule API.Gateway.Channel do
|
||||
@impl true
|
||||
def handle_info(:after_join, socket) do
|
||||
# Initialize the cache
|
||||
socket = hydrate_flows(socket)
|
||||
Process.send_after(self(), :prune_flow_cache, @prune_flow_cache_every)
|
||||
socket = assign(socket, cache: Cache.Gateway.hydrate(socket.assigns.gateway))
|
||||
Process.send_after(self(), :prune_cache, @prune_cache_every)
|
||||
|
||||
# Track gateway's presence
|
||||
:ok = Gateways.Presence.connect(socket.assigns.gateway)
|
||||
@@ -50,30 +60,9 @@ defmodule API.Gateway.Channel do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_info(:prune_flow_cache, socket) do
|
||||
Process.send_after(self(), :prune_flow_cache, @prune_flow_cache_every)
|
||||
|
||||
now = DateTime.utc_now()
|
||||
|
||||
# 1. Remove individual flows older than 14 days, then remove access entry if no flows left
|
||||
flows =
|
||||
socket.assigns.flows
|
||||
|> Enum.map(fn {tuple, flow_id_map} ->
|
||||
flow_id_map =
|
||||
Enum.reject(flow_id_map, fn {_flow_id, expires_at} ->
|
||||
DateTime.compare(expires_at, now) == :lt
|
||||
end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
{tuple, flow_id_map}
|
||||
end)
|
||||
|> Enum.into(%{})
|
||||
|> Enum.reject(fn {_tuple, flow_id_map} -> map_size(flow_id_map) == 0 end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
# The gateway has its own flow expiration, so no need to send `reject_access`
|
||||
|
||||
{:noreply, assign(socket, flows: flows)}
|
||||
def handle_info(:prune_cache, socket) do
|
||||
Process.send_after(self(), :prune_cache, @prune_cache_every)
|
||||
{:noreply, assign(socket, cache: Cache.Gateway.prune(socket.assigns.cache))}
|
||||
end
|
||||
|
||||
# Called to actually push relays_presence with a disconnected relay to the gateway
|
||||
@@ -85,115 +74,22 @@ defmodule API.Gateway.Channel do
|
||||
##### Reacting to domain events ####
|
||||
####################################
|
||||
|
||||
# ACCOUNTS
|
||||
def handle_info(%Change{lsn: lsn} = change, socket) do
|
||||
last_lsn = Map.get(socket.assigns, :last_lsn, 0)
|
||||
|
||||
# Resend init when config changes so that new slug may be applied
|
||||
def handle_info(
|
||||
{:updated, %Accounts.Account{slug: old_slug}, %Accounts.Account{slug: slug} = account},
|
||||
socket
|
||||
if lsn <= last_lsn do
|
||||
Logger.warning("Out of order or duplicate change received; ignoring",
|
||||
change: change,
|
||||
last_lsn: last_lsn
|
||||
)
|
||||
when old_slug != slug do
|
||||
{:ok, relays} = select_relays(socket)
|
||||
init(socket, account, relays)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
# FLOWS
|
||||
|
||||
def handle_info(
|
||||
{:deleted, %Flows.Flow{gateway_id: gateway_id} = flow},
|
||||
%{
|
||||
assigns: %{gateway: %{id: id}}
|
||||
} = socket
|
||||
)
|
||||
when gateway_id == id do
|
||||
tuple = {flow.client_id, flow.resource_id}
|
||||
|
||||
if flows_map = Map.get(socket.assigns.flows, tuple) do
|
||||
flow_map = Map.delete(flows_map, flow.id)
|
||||
remaining = map_size(flow_map)
|
||||
|
||||
if remaining == 0 do
|
||||
case Flows.reauthorize_flow(flow) do
|
||||
{:ok, new_flow} ->
|
||||
flow_map = %{
|
||||
new_flow.id => new_flow.expires_at
|
||||
}
|
||||
|
||||
Logger.info("Updated flow authorization",
|
||||
old_flow_id: flow.id,
|
||||
account_id: flow.account_id,
|
||||
client_id: flow.client_id,
|
||||
resource_id: flow.resource_id,
|
||||
new_flow_id: new_flow.id
|
||||
)
|
||||
|
||||
push(
|
||||
socket,
|
||||
"access_authorization_expiry_updated",
|
||||
Views.Flow.render(new_flow, new_flow.expires_at)
|
||||
)
|
||||
|
||||
{:noreply, assign(socket, flows: Map.put(socket.assigns.flows, tuple, flow_map))}
|
||||
|
||||
{:error, :forbidden} ->
|
||||
Logger.info("Last flow deleted; revoking access",
|
||||
flow_id: flow.id,
|
||||
account_id: flow.account_id,
|
||||
client_id: flow.client_id,
|
||||
resource_id: flow.resource_id
|
||||
)
|
||||
|
||||
# Send reject_access if access is no longer granted
|
||||
|
||||
# TODO: Verify that if the client's websocket connection flaps at the moment this
|
||||
# message is received by the gateway, and the client still has access to this resource,
|
||||
# the client will clear its state so it can request a new flow.
|
||||
push(socket, "reject_access", %{
|
||||
client_id: flow.client_id,
|
||||
resource_id: flow.resource_id
|
||||
})
|
||||
|
||||
{:noreply, assign(socket, flows: Map.delete(socket.assigns.flows, tuple))}
|
||||
end
|
||||
else
|
||||
Logger.info("Flow deleted but still has remaining flows for client/resource",
|
||||
flow_id: flow.id,
|
||||
account_id: flow.account_id,
|
||||
client_id: flow.client_id,
|
||||
resource_id: flow.resource_id,
|
||||
remaining: remaining
|
||||
)
|
||||
|
||||
{:noreply, assign(socket, flows: Map.put(socket.assigns.flows, tuple, flow_map))}
|
||||
end
|
||||
else
|
||||
{:noreply, socket}
|
||||
else
|
||||
socket = assign(socket, last_lsn: lsn)
|
||||
handle_change(change, socket)
|
||||
end
|
||||
end
|
||||
|
||||
# RESOURCES
|
||||
|
||||
# The gateway only handles filter changes. Other breaking changes are handled by deleting
|
||||
# relevant flows for the resource.
|
||||
def handle_info(
|
||||
{:updated, %Resources.Resource{filters: old_filters},
|
||||
%Resources.Resource{filters: filters, id: id} = resource},
|
||||
socket
|
||||
)
|
||||
when old_filters != filters do
|
||||
has_flows? =
|
||||
socket.assigns.flows
|
||||
|> Enum.any?(fn {{_client_id, resource_id}, _flow_map} -> resource_id == id end)
|
||||
|
||||
if has_flows? do
|
||||
push(socket, "resource_updated", Views.Resource.render(resource))
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
####################################
|
||||
#### Reacting to relay presence ####
|
||||
####################################
|
||||
@@ -216,8 +112,7 @@ defmodule API.Gateway.Channel do
|
||||
|
||||
:ok =
|
||||
Enum.each(relays, fn relay ->
|
||||
# TODO: WAL
|
||||
# Why are we unsubscribing and subscribing again?
|
||||
# TODO: Why are we unsubscribing and subscribing again?
|
||||
:ok = Domain.Relays.unsubscribe_from_relay_presence(relay)
|
||||
:ok = Domain.Relays.subscribe_to_relay_presence(relay)
|
||||
end)
|
||||
@@ -259,8 +154,7 @@ defmodule API.Gateway.Channel do
|
||||
|
||||
:ok =
|
||||
Enum.each(relays, fn relay ->
|
||||
# TODO: WAL
|
||||
# Why are we unsubscribing and subscribing again?
|
||||
# TODO: Why are we unsubscribing and subscribing again?
|
||||
:ok = Relays.unsubscribe_from_relay_presence(relay)
|
||||
:ok = Relays.subscribe_to_relay_presence(relay)
|
||||
end)
|
||||
@@ -300,9 +194,8 @@ defmodule API.Gateway.Channel do
|
||||
|
||||
def handle_info(
|
||||
{{:ice_candidates, gateway_id}, client_id, candidates},
|
||||
%{assigns: %{gateway: %{id: id}}} = socket
|
||||
)
|
||||
when gateway_id == id do
|
||||
%{assigns: %{gateway: %{id: gateway_id}}} = socket
|
||||
) do
|
||||
push(socket, "ice_candidates", %{
|
||||
client_id: client_id,
|
||||
candidates: candidates
|
||||
@@ -313,9 +206,8 @@ defmodule API.Gateway.Channel do
|
||||
|
||||
def handle_info(
|
||||
{{:invalidate_ice_candidates, gateway_id}, client_id, candidates},
|
||||
%{assigns: %{gateway: %{id: id}}} = socket
|
||||
)
|
||||
when gateway_id == id do
|
||||
%{assigns: %{gateway: %{id: gateway_id}}} = socket
|
||||
) do
|
||||
push(socket, "invalidate_ice_candidates", %{
|
||||
client_id: client_id,
|
||||
candidates: candidates
|
||||
@@ -326,12 +218,11 @@ defmodule API.Gateway.Channel do
|
||||
|
||||
def handle_info(
|
||||
{{:authorize_flow, gateway_id}, {channel_pid, socket_ref}, payload},
|
||||
%{assigns: %{gateway: %{id: id}}} = socket
|
||||
)
|
||||
when gateway_id == id do
|
||||
%{assigns: %{gateway: %{id: gateway_id}}} = socket
|
||||
) do
|
||||
%{
|
||||
client: client,
|
||||
resource: resource,
|
||||
resource: %Cache.Cacheable.Resource{} = resource,
|
||||
flow_id: flow_id,
|
||||
authorization_expires_at: authorization_expires_at,
|
||||
ice_credentials: ice_credentials,
|
||||
@@ -356,38 +247,36 @@ defmodule API.Gateway.Channel do
|
||||
expires_at: DateTime.to_unix(authorization_expires_at, :second)
|
||||
})
|
||||
|
||||
# Start tracking flow
|
||||
tuple = {client.id, resource.id}
|
||||
cache =
|
||||
socket.assigns.cache
|
||||
|> Cache.Gateway.put(
|
||||
client.id,
|
||||
resource.id,
|
||||
flow_id,
|
||||
authorization_expires_at
|
||||
)
|
||||
|
||||
flow_map =
|
||||
Map.get(socket.assigns.flows, tuple, %{})
|
||||
|> Map.put(flow_id, authorization_expires_at)
|
||||
|
||||
flows = Map.put(socket.assigns.flows, tuple, flow_map)
|
||||
socket = assign(socket, flows: flows)
|
||||
|
||||
{:noreply, socket}
|
||||
{:noreply, assign(socket, cache: cache)}
|
||||
end
|
||||
|
||||
# DEPRECATED IN 1.4
|
||||
def handle_info(
|
||||
{{:allow_access, gateway_id}, {channel_pid, socket_ref}, attrs},
|
||||
%{assigns: %{gateway: %{id: id}}} = socket
|
||||
)
|
||||
when gateway_id == id do
|
||||
%{assigns: %{gateway: %{id: gateway_id}}} = socket
|
||||
) do
|
||||
%{
|
||||
client: client,
|
||||
resource: resource,
|
||||
resource: %Cache.Cacheable.Resource{} = resource,
|
||||
flow_id: flow_id,
|
||||
authorization_expires_at: authorization_expires_at,
|
||||
client_payload: payload
|
||||
} = attrs
|
||||
|
||||
case API.Client.Channel.map_or_drop_compatible_resource(
|
||||
resource,
|
||||
socket.assigns.gateway.last_seen_version
|
||||
) do
|
||||
{:cont, resource} ->
|
||||
case Resources.adapt_resource_for_version(resource, socket.assigns.gateway.last_seen_version) do
|
||||
nil ->
|
||||
{:noreply, socket}
|
||||
|
||||
resource ->
|
||||
ref =
|
||||
encode_ref(
|
||||
socket,
|
||||
@@ -404,43 +293,38 @@ defmodule API.Gateway.Channel do
|
||||
client_ipv6: client.ipv6
|
||||
})
|
||||
|
||||
# Start tracking the flow
|
||||
tuple = {client.id, resource.id}
|
||||
cache =
|
||||
socket.assigns.cache
|
||||
|> Cache.Gateway.put(
|
||||
client.id,
|
||||
resource.id,
|
||||
flow_id,
|
||||
authorization_expires_at
|
||||
)
|
||||
|
||||
flow_map =
|
||||
Map.get(socket.assigns.flows, tuple, %{})
|
||||
|> Map.put(flow_id, authorization_expires_at)
|
||||
|
||||
flows = Map.put(socket.assigns.flows, tuple, flow_map)
|
||||
socket = assign(socket, flows: flows)
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
:drop ->
|
||||
{:noreply, socket}
|
||||
{:noreply, assign(socket, cache: cache)}
|
||||
end
|
||||
end
|
||||
|
||||
# DEPRECATED IN 1.4
|
||||
def handle_info(
|
||||
{{:request_connection, gateway_id}, {channel_pid, socket_ref}, attrs},
|
||||
%{assigns: %{gateway: %{id: id}}} = socket
|
||||
)
|
||||
when gateway_id == id do
|
||||
%{assigns: %{gateway: %{id: gateway_id}}} = socket
|
||||
) do
|
||||
%{
|
||||
client: client,
|
||||
resource: resource,
|
||||
resource: %Cache.Cacheable.Resource{} = resource,
|
||||
flow_id: flow_id,
|
||||
authorization_expires_at: authorization_expires_at,
|
||||
client_payload: payload,
|
||||
client_preshared_key: preshared_key
|
||||
} = attrs
|
||||
|
||||
case API.Client.Channel.map_or_drop_compatible_resource(
|
||||
resource,
|
||||
socket.assigns.gateway.last_seen_version
|
||||
) do
|
||||
{:cont, resource} ->
|
||||
case Resources.adapt_resource_for_version(resource, socket.assigns.gateway.last_seen_version) do
|
||||
nil ->
|
||||
{:noreply, socket}
|
||||
|
||||
resource ->
|
||||
ref =
|
||||
encode_ref(
|
||||
socket,
|
||||
@@ -454,20 +338,16 @@ defmodule API.Gateway.Channel do
|
||||
expires_at: DateTime.to_unix(authorization_expires_at, :second)
|
||||
})
|
||||
|
||||
# Start tracking the flow
|
||||
tuple = {client.id, resource.id}
|
||||
cache =
|
||||
socket.assigns.cache
|
||||
|> Cache.Gateway.put(
|
||||
client.id,
|
||||
resource.id,
|
||||
flow_id,
|
||||
authorization_expires_at
|
||||
)
|
||||
|
||||
flow_map =
|
||||
Map.get(socket.assigns.flows, tuple, %{})
|
||||
|> Map.put(flow_id, authorization_expires_at)
|
||||
|
||||
flows = Map.put(socket.assigns.flows, tuple, flow_map)
|
||||
socket = assign(socket, flows: flows)
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
:drop ->
|
||||
{:noreply, socket}
|
||||
{:noreply, assign(socket, cache: cache)}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -520,10 +400,10 @@ defmodule API.Gateway.Channel do
|
||||
socket
|
||||
) do
|
||||
case decode_ref(socket, signed_ref) do
|
||||
{:ok, {channel_pid, socket_ref, resource_id}} ->
|
||||
{:ok, {channel_pid, socket_ref, rid_bytes}} ->
|
||||
send(
|
||||
channel_pid,
|
||||
{:connect, socket_ref, resource_id, socket.assigns.gateway.public_key, payload}
|
||||
{:connect, socket_ref, rid_bytes, socket.assigns.gateway.public_key, payload}
|
||||
)
|
||||
|
||||
{:reply, :ok, socket}
|
||||
@@ -622,7 +502,7 @@ defmodule API.Gateway.Channel do
|
||||
DateTime.utc_now() |> DateTime.add(@relay_credentials_expire_in_hours, :hour)
|
||||
|
||||
push(socket, "init", %{
|
||||
authorizations: Views.Flow.render_many(socket.assigns.flows),
|
||||
authorizations: Views.Flow.render_many(socket.assigns.cache),
|
||||
account_slug: account.slug,
|
||||
interface: Views.Interface.render(socket.assigns.gateway),
|
||||
relays:
|
||||
@@ -647,28 +527,149 @@ defmodule API.Gateway.Channel do
|
||||
end
|
||||
end
|
||||
|
||||
defp hydrate_flows(socket) do
|
||||
OpenTelemetry.Tracer.with_span "gateway.hydrate_flows",
|
||||
attributes: %{
|
||||
gateway_id: socket.assigns.gateway.id,
|
||||
account_id: socket.assigns.gateway.account_id
|
||||
} do
|
||||
flows =
|
||||
Flows.all_gateway_flows_for_cache!(socket.assigns.gateway)
|
||||
# Reduces [ {client_id, resource_id}, {flow_id, inserted_at} ]
|
||||
#
|
||||
# to %{ {client_id, resource_id} => %{flow_id => expires_at} }
|
||||
#
|
||||
# This data structure is used to efficiently:
|
||||
# 1. Check if there are any active flows remaining for this client/resource?
|
||||
# 2. Remove a deleted flow
|
||||
|> Enum.reduce(%{}, fn {{client_id, resource_id}, {flow_id, expires_at}}, acc ->
|
||||
flow_id_map = Map.get(acc, {client_id, resource_id}, %{})
|
||||
##########################################
|
||||
#### Handling changes from the domain ####
|
||||
##########################################
|
||||
|
||||
Map.put(acc, {client_id, resource_id}, Map.put(flow_id_map, flow_id, expires_at))
|
||||
end)
|
||||
# ACCOUNTS
|
||||
|
||||
assign(socket, flows: flows)
|
||||
# Resend init when config changes so that new slug may be applied
|
||||
defp handle_change(
|
||||
%Change{
|
||||
op: :update,
|
||||
old_struct: %Accounts.Account{slug: old_slug},
|
||||
struct: %Accounts.Account{slug: slug} = account
|
||||
},
|
||||
socket
|
||||
)
|
||||
when old_slug != slug do
|
||||
{:ok, relays} = select_relays(socket)
|
||||
init(socket, account, relays)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
# FLOWS
|
||||
|
||||
defp handle_change(
|
||||
%Change{
|
||||
op: :delete,
|
||||
old_struct:
|
||||
%Flows.Flow{gateway_id: gateway_id, client_id: client_id, resource_id: resource_id} =
|
||||
flow
|
||||
},
|
||||
%{
|
||||
assigns: %{gateway: %{id: gateway_id}}
|
||||
} = socket
|
||||
) do
|
||||
socket.assigns.cache
|
||||
|> Cache.Gateway.reauthorize_deleted_flow(flow)
|
||||
|> case do
|
||||
{:ok, expires_at_unix, cache} ->
|
||||
Logger.info("Updating authorization expiration for deleted flow",
|
||||
deleted_flow: inspect(flow),
|
||||
new_expires_at: DateTime.from_unix!(expires_at_unix, :second)
|
||||
)
|
||||
|
||||
push(
|
||||
socket,
|
||||
"access_authorization_expiry_updated",
|
||||
Views.Flow.render(flow, expires_at_unix)
|
||||
)
|
||||
|
||||
{:noreply, assign(socket, cache: cache)}
|
||||
|
||||
{:error, :unauthorized, cache} ->
|
||||
Logger.info("No authorizations remaining for deleted flow, revoking access",
|
||||
deleted_flow: inspect(flow)
|
||||
)
|
||||
|
||||
# Note: There is an edge case here:
|
||||
# - Client authorizes flow for resource
|
||||
# - Client's websocket temporarily gets cut
|
||||
# - Admin deletes the policy
|
||||
# - We send reject_access to the gateway
|
||||
# - Admin recreates the same policy (same access)
|
||||
# - Client connection resumes
|
||||
# - Client sees exactly the same resource list
|
||||
# - Client now has lost the ability to recreate the flow because from its perspective, it is still connected
|
||||
# to this gateway.
|
||||
# - Packets to gateway are essentially blackholed until the client signs out and back in
|
||||
|
||||
# This will be fixed when the client responds to the ICMP prohibited by filter message:
|
||||
# https://github.com/firezone/firezone/issues/10074
|
||||
push(
|
||||
socket,
|
||||
"reject_access",
|
||||
%{client_id: client_id, resource_id: resource_id}
|
||||
)
|
||||
|
||||
{:noreply, assign(socket, cache: cache)}
|
||||
|
||||
{:error, :not_found} ->
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
# GATEWAYS
|
||||
|
||||
defp handle_change(
|
||||
%Change{
|
||||
op: :delete,
|
||||
old_struct: %Gateways.Gateway{id: gateway_id}
|
||||
},
|
||||
%{
|
||||
assigns: %{gateway: %{id: gateway_id}}
|
||||
} = socket
|
||||
) do
|
||||
{:stop, :shutdown, socket}
|
||||
end
|
||||
|
||||
# RESOURCES
|
||||
|
||||
# The gateway only handles filter changes for resources. Other addressability changes like address,
|
||||
# type, or ip_stack require sending reject_access to remove the resource state from the gateway.
|
||||
|
||||
defp handle_change(
|
||||
%Change{
|
||||
op: :update,
|
||||
old_struct: %Resources.Resource{
|
||||
address: old_address,
|
||||
ip_stack: old_ip_stack,
|
||||
type: old_type
|
||||
},
|
||||
struct: %Resources.Resource{address: address, ip_stack: ip_stack, type: type, id: id}
|
||||
},
|
||||
socket
|
||||
)
|
||||
when old_address != address or
|
||||
old_ip_stack != ip_stack or
|
||||
old_type != type do
|
||||
for {client_id, resource_id} <- Cache.Gateway.all_pairs_for_resource(socket.assigns.cache, id) do
|
||||
# Send reject_access to the gateway to reset state. Clients will need to reauthorize the resource.
|
||||
push(socket, "reject_access", %{client_id: client_id, resource_id: resource_id})
|
||||
end
|
||||
|
||||
# The cache will be updated by the flow deletion handler.
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp handle_change(
|
||||
%Change{
|
||||
op: :update,
|
||||
old_struct: %Resources.Resource{filters: old_filters},
|
||||
struct: %Resources.Resource{filters: filters} = resource
|
||||
},
|
||||
socket
|
||||
)
|
||||
when old_filters != filters do
|
||||
# Send regardless of cache state - if the Gateway has no flows for this resource,
|
||||
# it will simply ignore the message.
|
||||
resource = Cache.Cacheable.to_cache(resource)
|
||||
push(socket, "resource_updated", Views.Resource.render(resource))
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp handle_change(%Change{}, socket), do: {:noreply, socket}
|
||||
end
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
defmodule API.Gateway.Views.Flow do
|
||||
def render(flow, expires_at) do
|
||||
def render(flow, expires_at_unix) do
|
||||
%{
|
||||
client_id: flow.client_id,
|
||||
resource_id: flow.resource_id,
|
||||
expires_at: DateTime.to_unix(expires_at, :second)
|
||||
expires_at: expires_at_unix
|
||||
}
|
||||
end
|
||||
|
||||
def render_many(flows) do
|
||||
flows
|
||||
|> Enum.map(fn {{client_id, resource_id}, flow_map} ->
|
||||
expires_at = Enum.min(Map.values(flow_map))
|
||||
def render_many(cache) do
|
||||
cache
|
||||
|> Enum.map(fn {{cid_bytes, rid_bytes}, flow_map} ->
|
||||
# Use longest expiration to minimize unnecessary access churn
|
||||
expires_at_unix = Enum.max(Map.values(flow_map))
|
||||
|
||||
%{
|
||||
client_id: client_id,
|
||||
resource_id: resource_id,
|
||||
expires_at: DateTime.to_unix(expires_at, :second)
|
||||
client_id: Ecto.UUID.load!(cid_bytes),
|
||||
resource_id: Ecto.UUID.load!(rid_bytes),
|
||||
expires_at: expires_at_unix
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
defmodule API.Gateway.Views.Resource do
|
||||
alias Domain.Resources
|
||||
alias Domain.Cache.Cacheable
|
||||
|
||||
def render(%Resources.Resource{type: :internet} = resource) do
|
||||
def render(%Cacheable.Resource{} = resource) do
|
||||
resource =
|
||||
resource
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:id, Ecto.UUID.load!(resource.id))
|
||||
|
||||
render_resource(resource)
|
||||
end
|
||||
|
||||
defp render_resource(%{type: :internet} = resource) do
|
||||
%{
|
||||
id: resource.id,
|
||||
type: :internet
|
||||
}
|
||||
end
|
||||
|
||||
def render(%Resources.Resource{type: :dns} = resource) do
|
||||
defp render_resource(%{type: :dns} = resource) do
|
||||
%{
|
||||
id: resource.id,
|
||||
type: :dns,
|
||||
@@ -18,7 +27,7 @@ defmodule API.Gateway.Views.Resource do
|
||||
}
|
||||
end
|
||||
|
||||
def render(%Resources.Resource{type: :cidr} = resource) do
|
||||
defp render_resource(%{type: :cidr} = resource) do
|
||||
%{
|
||||
id: resource.id,
|
||||
type: :cidr,
|
||||
@@ -28,7 +37,7 @@ defmodule API.Gateway.Views.Resource do
|
||||
}
|
||||
end
|
||||
|
||||
def render(%Resources.Resource{type: :ip} = resource) do
|
||||
defp render_resource(%{type: :ip} = resource) do
|
||||
{:ok, inet} = Domain.Types.IP.cast(resource.address)
|
||||
netmask = Domain.Types.CIDR.max_netmask(inet)
|
||||
address = to_string(%{inet | netmask: netmask})
|
||||
@@ -42,7 +51,7 @@ defmodule API.Gateway.Views.Resource do
|
||||
}
|
||||
end
|
||||
|
||||
def render_filter(%Resources.Resource.Filter{ports: ports} = filter) when length(ports) > 0 do
|
||||
defp render_filter(%{ports: ports} = filter) when length(ports) > 0 do
|
||||
Enum.map(filter.ports, fn port ->
|
||||
case String.split(port, "-") do
|
||||
[port_start, port_end] ->
|
||||
@@ -67,7 +76,7 @@ defmodule API.Gateway.Views.Resource do
|
||||
end)
|
||||
end
|
||||
|
||||
def render_filter(%Resources.Resource.Filter{} = filter) do
|
||||
defp render_filter(%{} = filter) do
|
||||
[
|
||||
%{
|
||||
protocol: filter.protocol
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,8 @@
|
||||
defmodule API.Gateway.ChannelTest do
|
||||
use API.ChannelCase, async: true
|
||||
alias Domain.{Accounts, Events, Gateways, PubSub}
|
||||
alias Domain.{Accounts, Changes, Gateways, PubSub}
|
||||
import Domain.Cache.Cacheable, only: [to_cache: 1]
|
||||
import ExUnit.CaptureLog
|
||||
|
||||
setup do
|
||||
account = Fixtures.Accounts.create_account()
|
||||
@@ -91,10 +93,184 @@ defmodule API.Gateway.ChannelTest do
|
||||
end
|
||||
|
||||
describe "handle_info/2" do
|
||||
test "logs warning and ignores out of order %Change{}", %{socket: socket} do
|
||||
send(socket.channel_pid, %Changes.Change{lsn: 100})
|
||||
|
||||
assert %{assigns: %{last_lsn: 100}} = :sys.get_state(socket.channel_pid)
|
||||
|
||||
message =
|
||||
capture_log(fn ->
|
||||
send(socket.channel_pid, %Changes.Change{lsn: 50})
|
||||
|
||||
# Wait for the channel to process and emit the log
|
||||
Process.sleep(1)
|
||||
end)
|
||||
|
||||
assert message =~ "[warning] Out of order or duplicate change received; ignoring"
|
||||
|
||||
assert %{assigns: %{last_lsn: 100}} = :sys.get_state(socket.channel_pid)
|
||||
end
|
||||
|
||||
test ":prune_cache removes key completely when all flows are expired", %{
|
||||
account: account,
|
||||
client: client,
|
||||
resource: resource,
|
||||
socket: socket,
|
||||
gateway: gateway
|
||||
} do
|
||||
expired_flow =
|
||||
Fixtures.Flows.create_flow(
|
||||
account: account,
|
||||
client: client,
|
||||
resource: resource,
|
||||
gateway: gateway
|
||||
)
|
||||
|
||||
expired_expiration = DateTime.utc_now() |> DateTime.add(-30, :second)
|
||||
channel_pid = self()
|
||||
socket_ref = make_ref()
|
||||
preshared_key = "PSK"
|
||||
|
||||
ice_credentials = %{
|
||||
client: %{username: "A", password: "B"},
|
||||
gateway: %{username: "C", password: "D"}
|
||||
}
|
||||
|
||||
send(
|
||||
socket.channel_pid,
|
||||
{{:authorize_flow, gateway.id}, {channel_pid, socket_ref},
|
||||
%{
|
||||
client: client,
|
||||
resource: to_cache(resource),
|
||||
flow_id: expired_flow.id,
|
||||
authorization_expires_at: expired_expiration,
|
||||
ice_credentials: ice_credentials,
|
||||
preshared_key: preshared_key
|
||||
}}
|
||||
)
|
||||
|
||||
assert_push "authorize_flow", _payload
|
||||
|
||||
cid_bytes = Ecto.UUID.dump!(client.id)
|
||||
rid_bytes = Ecto.UUID.dump!(resource.id)
|
||||
|
||||
assert %{
|
||||
assigns: %{
|
||||
cache: %{
|
||||
{^cid_bytes, ^rid_bytes} => _flows
|
||||
}
|
||||
}
|
||||
} = :sys.get_state(socket.channel_pid)
|
||||
|
||||
send(socket.channel_pid, :prune_cache)
|
||||
|
||||
assert %{
|
||||
assigns: %{
|
||||
cache: %{}
|
||||
}
|
||||
} = :sys.get_state(socket.channel_pid)
|
||||
end
|
||||
|
||||
test ":prune_cache prunes only expired flows from the cache", %{
|
||||
account: account,
|
||||
client: client,
|
||||
resource: resource,
|
||||
socket: socket,
|
||||
gateway: gateway
|
||||
} do
|
||||
expired_flow =
|
||||
Fixtures.Flows.create_flow(
|
||||
account: account,
|
||||
client: client,
|
||||
resource: resource,
|
||||
gateway: gateway
|
||||
)
|
||||
|
||||
unexpired_flow =
|
||||
Fixtures.Flows.create_flow(
|
||||
account: account,
|
||||
client: client,
|
||||
resource: resource,
|
||||
gateway: gateway
|
||||
)
|
||||
|
||||
expired_expiration = DateTime.utc_now() |> DateTime.add(-30, :second)
|
||||
unexpired_expiration = DateTime.utc_now() |> DateTime.add(30, :second)
|
||||
|
||||
channel_pid = self()
|
||||
socket_ref = make_ref()
|
||||
preshared_key = "PSK"
|
||||
|
||||
ice_credentials = %{
|
||||
client: %{username: "A", password: "B"},
|
||||
gateway: %{username: "C", password: "D"}
|
||||
}
|
||||
|
||||
send(
|
||||
socket.channel_pid,
|
||||
{{:authorize_flow, gateway.id}, {channel_pid, socket_ref},
|
||||
%{
|
||||
client: client,
|
||||
resource: to_cache(resource),
|
||||
flow_id: expired_flow.id,
|
||||
authorization_expires_at: expired_expiration,
|
||||
ice_credentials: ice_credentials,
|
||||
preshared_key: preshared_key
|
||||
}}
|
||||
)
|
||||
|
||||
assert_push "authorize_flow", _payload
|
||||
|
||||
send(
|
||||
socket.channel_pid,
|
||||
{{:authorize_flow, gateway.id}, {channel_pid, socket_ref},
|
||||
%{
|
||||
client: client,
|
||||
resource: to_cache(resource),
|
||||
flow_id: unexpired_flow.id,
|
||||
authorization_expires_at: unexpired_expiration,
|
||||
ice_credentials: ice_credentials,
|
||||
preshared_key: preshared_key
|
||||
}}
|
||||
)
|
||||
|
||||
cid_bytes = Ecto.UUID.dump!(client.id)
|
||||
rid_bytes = Ecto.UUID.dump!(resource.id)
|
||||
|
||||
assert %{
|
||||
assigns: %{
|
||||
cache: %{
|
||||
{^cid_bytes, ^rid_bytes} => flows
|
||||
}
|
||||
}
|
||||
} = :sys.get_state(socket.channel_pid)
|
||||
|
||||
assert flows == %{
|
||||
Ecto.UUID.dump!(expired_flow.id) => DateTime.to_unix(expired_expiration, :second),
|
||||
Ecto.UUID.dump!(unexpired_flow.id) =>
|
||||
DateTime.to_unix(unexpired_expiration, :second)
|
||||
}
|
||||
|
||||
send(socket.channel_pid, :prune_cache)
|
||||
|
||||
assert %{
|
||||
assigns: %{
|
||||
cache: %{
|
||||
{^cid_bytes, ^rid_bytes} => flows
|
||||
}
|
||||
}
|
||||
} = :sys.get_state(socket.channel_pid)
|
||||
|
||||
assert flows == %{
|
||||
Ecto.UUID.dump!(unexpired_flow.id) =>
|
||||
DateTime.to_unix(unexpired_expiration, :second)
|
||||
}
|
||||
end
|
||||
|
||||
test "resends init when account slug changes", %{
|
||||
account: account
|
||||
} do
|
||||
:ok = Domain.PubSub.Account.subscribe(account.id)
|
||||
:ok = PubSub.Account.subscribe(account.id)
|
||||
|
||||
old_data = %{
|
||||
"id" => account.id,
|
||||
@@ -106,9 +282,13 @@ defmodule API.Gateway.ChannelTest do
|
||||
"slug" => "new-slug"
|
||||
}
|
||||
|
||||
Events.Hooks.Accounts.on_update(old_data, data)
|
||||
Changes.Hooks.Accounts.on_update(100, old_data, data)
|
||||
|
||||
assert_receive {:updated, %Accounts.Account{}, %Accounts.Account{}}
|
||||
assert_receive %Changes.Change{
|
||||
lsn: 100,
|
||||
old_struct: %Accounts.Account{},
|
||||
struct: %Accounts.Account{slug: "new-slug"}
|
||||
}
|
||||
|
||||
# Consume first init from join
|
||||
assert_push "init", _payload
|
||||
@@ -122,11 +302,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
account: account,
|
||||
token: token
|
||||
} do
|
||||
# Prevents test from failing due to expected socket disconnect
|
||||
Process.flag(:trap_exit, true)
|
||||
|
||||
:ok = Domain.PubSub.Account.subscribe(account.id)
|
||||
:ok = Domain.PubSub.subscribe("sessions:#{token.id}")
|
||||
:ok = PubSub.subscribe("sessions:#{token.id}")
|
||||
|
||||
data = %{
|
||||
"id" => token.id,
|
||||
@@ -134,9 +310,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
"type" => "gateway_group"
|
||||
}
|
||||
|
||||
Events.Hooks.Tokens.on_delete(data)
|
||||
|
||||
assert_receive {:deleted, deleted_token}
|
||||
Changes.Hooks.Tokens.on_delete(100, data)
|
||||
|
||||
assert_receive %Phoenix.Socket.Broadcast{
|
||||
topic: topic,
|
||||
@@ -144,7 +318,29 @@ defmodule API.Gateway.ChannelTest do
|
||||
}
|
||||
|
||||
assert topic == "sessions:#{token.id}"
|
||||
assert deleted_token.id == token.id
|
||||
end
|
||||
|
||||
test "disconnect socket when gateway is deleted", %{
|
||||
account: account,
|
||||
gateway: gateway
|
||||
} do
|
||||
Process.flag(:trap_exit, true)
|
||||
|
||||
:ok = PubSub.Account.subscribe(account.id)
|
||||
|
||||
data = %{
|
||||
"id" => gateway.id,
|
||||
"account_id" => account.id
|
||||
}
|
||||
|
||||
Changes.Hooks.Gateways.on_delete(100, data)
|
||||
|
||||
assert_receive %Changes.Change{
|
||||
lsn: 100,
|
||||
old_struct: %Gateways.Gateway{}
|
||||
}
|
||||
|
||||
assert_receive {:EXIT, _pid, _reason}
|
||||
end
|
||||
|
||||
test "pushes allow_access message", %{
|
||||
@@ -175,7 +371,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
{{:allow_access, gateway.id}, {channel_pid, socket_ref},
|
||||
%{
|
||||
client: client,
|
||||
resource: resource,
|
||||
resource: to_cache(resource),
|
||||
flow_id: flow.id,
|
||||
authorization_expires_at: expires_at,
|
||||
client_payload: client_payload
|
||||
@@ -239,7 +435,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
{{:allow_access, gateway.id}, {channel_pid, socket_ref},
|
||||
%{
|
||||
client: client,
|
||||
resource: resource,
|
||||
resource: to_cache(resource),
|
||||
flow_id: flow.id,
|
||||
authorization_expires_at: expires_at,
|
||||
client_payload: client_payload
|
||||
@@ -266,24 +462,26 @@ defmodule API.Gateway.ChannelTest do
|
||||
client: client,
|
||||
resource: resource,
|
||||
gateway: gateway,
|
||||
relay: relay,
|
||||
socket: socket,
|
||||
subject: subject
|
||||
} do
|
||||
channel_pid = self()
|
||||
socket_ref = make_ref()
|
||||
expires_at = DateTime.utc_now() |> DateTime.add(30, :second)
|
||||
client_payload = "RTC_SD_or_DNS_Q"
|
||||
|
||||
stamp_secret = Ecto.UUID.generate()
|
||||
:ok = Domain.Relays.connect_relay(relay, stamp_secret)
|
||||
in_one_hour = DateTime.utc_now() |> DateTime.add(1, :hour)
|
||||
in_one_day = DateTime.utc_now() |> DateTime.add(1, :day)
|
||||
|
||||
:ok = PubSub.Account.subscribe(account.id)
|
||||
|
||||
flow1 =
|
||||
Fixtures.Flows.create_flow(
|
||||
account: account,
|
||||
subject: subject,
|
||||
client: client,
|
||||
resource: resource
|
||||
gateway: gateway,
|
||||
resource: resource,
|
||||
expires_at: in_one_hour
|
||||
)
|
||||
|
||||
flow2 =
|
||||
@@ -291,51 +489,70 @@ defmodule API.Gateway.ChannelTest do
|
||||
account: account,
|
||||
subject: subject,
|
||||
client: client,
|
||||
resource: resource
|
||||
gateway: gateway,
|
||||
resource: resource,
|
||||
expires_at: in_one_day
|
||||
)
|
||||
|
||||
send(
|
||||
socket.channel_pid,
|
||||
{{:allow_access, gateway.id}, {channel_pid, socket_ref},
|
||||
%{
|
||||
client: client,
|
||||
resource: to_cache(resource),
|
||||
flow_id: flow1.id,
|
||||
authorization_expires_at: flow1.expires_at,
|
||||
client_payload: client_payload
|
||||
}}
|
||||
)
|
||||
|
||||
assert_push "allow_access", %{}
|
||||
|
||||
send(
|
||||
socket.channel_pid,
|
||||
{{:allow_access, gateway.id}, {channel_pid, socket_ref},
|
||||
%{
|
||||
client: client,
|
||||
resource: to_cache(resource),
|
||||
flow_id: flow2.id,
|
||||
authorization_expires_at: flow2.expires_at,
|
||||
client_payload: client_payload
|
||||
}}
|
||||
)
|
||||
|
||||
assert_push "allow_access", %{}
|
||||
|
||||
data = %{
|
||||
"id" => flow1.id,
|
||||
"client_id" => client.id,
|
||||
"resource_id" => resource.id,
|
||||
"account_id" => account.id,
|
||||
"token_id" => flow1.token_id,
|
||||
"gateway_id" => gateway.id,
|
||||
"policy_id" => flow1.policy_id,
|
||||
"actor_group_membership_id" => flow1.actor_group_membership_id,
|
||||
"expires_at" => flow1.expires_at
|
||||
}
|
||||
|
||||
send(
|
||||
socket.channel_pid,
|
||||
{{:allow_access, gateway.id}, {channel_pid, socket_ref},
|
||||
%{
|
||||
client: client,
|
||||
resource: resource,
|
||||
flow_id: flow1.id,
|
||||
authorization_expires_at: expires_at,
|
||||
client_payload: client_payload
|
||||
}}
|
||||
)
|
||||
Changes.Hooks.Flows.on_delete(100, data)
|
||||
|
||||
assert_push "allow_access", %{}
|
||||
flow_id = flow1.id
|
||||
|
||||
send(
|
||||
socket.channel_pid,
|
||||
{{:allow_access, gateway.id}, {channel_pid, socket_ref},
|
||||
%{
|
||||
client: client,
|
||||
resource: resource,
|
||||
flow_id: flow2.id,
|
||||
authorization_expires_at: expires_at,
|
||||
client_payload: client_payload
|
||||
}}
|
||||
)
|
||||
|
||||
assert_push "allow_access", %{}
|
||||
|
||||
Events.Hooks.Flows.on_delete(data)
|
||||
assert_receive %Changes.Change{
|
||||
lsn: 100,
|
||||
old_struct: %Domain.Flows.Flow{id: ^flow_id}
|
||||
}
|
||||
|
||||
refute_push "allow_access", _payload
|
||||
refute_push "reject_access", %{}
|
||||
|
||||
assert_push "access_authorization_expiry_updated", payload
|
||||
|
||||
assert payload == %{
|
||||
client_id: client.id,
|
||||
resource_id: resource.id,
|
||||
expires_at: DateTime.to_unix(flow2.expires_at, :second)
|
||||
}
|
||||
end
|
||||
|
||||
test "handles flow deletion event when access is removed", %{
|
||||
@@ -381,7 +598,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
{{:allow_access, gateway.id}, {channel_pid, socket_ref},
|
||||
%{
|
||||
client: client,
|
||||
resource: resource,
|
||||
resource: to_cache(resource),
|
||||
flow_id: flow.id,
|
||||
authorization_expires_at: expires_at,
|
||||
client_payload: client_payload
|
||||
@@ -390,7 +607,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
|
||||
assert_push "allow_access", %{}
|
||||
|
||||
Events.Hooks.Flows.on_delete(data)
|
||||
Changes.Hooks.Flows.on_delete(100, data)
|
||||
|
||||
assert_push "reject_access", %{
|
||||
client_id: client_id,
|
||||
@@ -460,7 +677,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
{{:allow_access, gateway.id}, {channel_pid, socket_ref},
|
||||
%{
|
||||
client: client,
|
||||
resource: resource,
|
||||
resource: to_cache(resource),
|
||||
flow_id: flow.id,
|
||||
authorization_expires_at: expires_at,
|
||||
client_payload: client_payload
|
||||
@@ -474,7 +691,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
{{:allow_access, gateway.id}, {channel_pid, socket_ref},
|
||||
%{
|
||||
client: other_client,
|
||||
resource: resource,
|
||||
resource: to_cache(resource),
|
||||
flow_id: other_flow1.id,
|
||||
authorization_expires_at: expires_at,
|
||||
client_payload: client_payload
|
||||
@@ -488,7 +705,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
{{:allow_access, gateway.id}, {channel_pid, socket_ref},
|
||||
%{
|
||||
client: client,
|
||||
resource: other_resource,
|
||||
resource: to_cache(other_resource),
|
||||
flow_id: other_flow2.id,
|
||||
authorization_expires_at: expires_at,
|
||||
client_payload: client_payload
|
||||
@@ -497,13 +714,19 @@ defmodule API.Gateway.ChannelTest do
|
||||
|
||||
assert_push "allow_access", %{}
|
||||
|
||||
assert %{assigns: %{flows: flows}} =
|
||||
assert %{assigns: %{cache: cache}} =
|
||||
:sys.get_state(socket.channel_pid)
|
||||
|
||||
assert flows == %{
|
||||
{client.id, resource.id} => %{flow.id => expires_at},
|
||||
{other_client.id, resource.id} => %{other_flow1.id => expires_at},
|
||||
{client.id, other_resource.id} => %{other_flow2.id => expires_at}
|
||||
assert cache == %{
|
||||
{Ecto.UUID.dump!(client.id), Ecto.UUID.dump!(resource.id)} => %{
|
||||
Ecto.UUID.dump!(flow.id) => DateTime.to_unix(expires_at, :second)
|
||||
},
|
||||
{Ecto.UUID.dump!(other_client.id), Ecto.UUID.dump!(resource.id)} => %{
|
||||
Ecto.UUID.dump!(other_flow1.id) => DateTime.to_unix(expires_at, :second)
|
||||
},
|
||||
{Ecto.UUID.dump!(client.id), Ecto.UUID.dump!(other_resource.id)} => %{
|
||||
Ecto.UUID.dump!(other_flow2.id) => DateTime.to_unix(expires_at, :second)
|
||||
}
|
||||
}
|
||||
|
||||
data = %{
|
||||
@@ -518,7 +741,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
"expires_at" => other_flow1.expires_at
|
||||
}
|
||||
|
||||
Events.Hooks.Flows.on_delete(data)
|
||||
Changes.Hooks.Flows.on_delete(100, data)
|
||||
|
||||
assert_push "reject_access", %{
|
||||
client_id: client_id,
|
||||
@@ -540,7 +763,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
"expires_at" => other_flow2.expires_at
|
||||
}
|
||||
|
||||
Events.Hooks.Flows.on_delete(data)
|
||||
Changes.Hooks.Flows.on_delete(200, data)
|
||||
|
||||
assert_push "reject_access", %{
|
||||
client_id: client_id,
|
||||
@@ -580,7 +803,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
{{:allow_access, gateway.id}, {channel_pid, socket_ref},
|
||||
%{
|
||||
client: client,
|
||||
resource: resource,
|
||||
resource: to_cache(resource),
|
||||
flow_id: flow.id,
|
||||
authorization_expires_at: expires_at,
|
||||
client_payload: client_payload
|
||||
@@ -597,27 +820,101 @@ defmodule API.Gateway.ChannelTest do
|
||||
|
||||
data = Map.put(old_data, "name", "New Resource Name")
|
||||
|
||||
Events.Hooks.Resources.on_update(old_data, data)
|
||||
Changes.Hooks.Resources.on_update(100, old_data, data)
|
||||
|
||||
client_id = client.id
|
||||
resource_id = resource.id
|
||||
flow_id = flow.id
|
||||
cid_bytes = Ecto.UUID.dump!(client.id)
|
||||
rid_bytes = Ecto.UUID.dump!(resource.id)
|
||||
fid_bytes = Ecto.UUID.dump!(flow.id)
|
||||
expires_at_unix = DateTime.to_unix(expires_at, :second)
|
||||
|
||||
assert %{
|
||||
assigns: %{
|
||||
flows: %{{^client_id, ^resource_id} => %{^flow_id => ^expires_at}}
|
||||
cache: %{{^cid_bytes, ^rid_bytes} => %{^fid_bytes => ^expires_at_unix}}
|
||||
}
|
||||
} = :sys.get_state(socket.channel_pid)
|
||||
|
||||
refute_push "resource_updated", _payload
|
||||
end
|
||||
|
||||
test "sends reject_access when resource addressability changes", %{
|
||||
client: client,
|
||||
gateway: gateway,
|
||||
account: account,
|
||||
resource: resource,
|
||||
socket: socket
|
||||
} do
|
||||
channel_pid = self()
|
||||
socket_ref = make_ref()
|
||||
expires_at = DateTime.utc_now() |> DateTime.add(30, :second)
|
||||
client_payload = "RTC_SD_or_DNS_Q"
|
||||
|
||||
flow =
|
||||
Fixtures.Flows.create_flow(
|
||||
account: account,
|
||||
client: client,
|
||||
resource: resource,
|
||||
gateway: gateway
|
||||
)
|
||||
|
||||
:ok = PubSub.Account.subscribe(account.id)
|
||||
|
||||
send(
|
||||
socket.channel_pid,
|
||||
{{:allow_access, gateway.id}, {channel_pid, socket_ref},
|
||||
%{
|
||||
client: client,
|
||||
resource: to_cache(resource),
|
||||
flow_id: flow.id,
|
||||
authorization_expires_at: expires_at,
|
||||
client_payload: client_payload
|
||||
}}
|
||||
)
|
||||
|
||||
assert_push "allow_access", %{}
|
||||
|
||||
old_data = %{
|
||||
"id" => resource.id,
|
||||
"account_id" => resource.account_id,
|
||||
"address" => resource.address,
|
||||
"name" => resource.name,
|
||||
"type" => "dns",
|
||||
"filters" => [],
|
||||
"ip_stack" => "dual"
|
||||
}
|
||||
|
||||
data = %{
|
||||
"id" => resource.id,
|
||||
"account_id" => resource.account_id,
|
||||
"address" => "new-address",
|
||||
"name" => resource.name,
|
||||
"type" => "dns",
|
||||
"filters" => [],
|
||||
"ip_stack" => "dual"
|
||||
}
|
||||
|
||||
:ok = Changes.Hooks.Resources.on_update(100, old_data, data)
|
||||
|
||||
resource_id = resource.id
|
||||
|
||||
assert_receive %Changes.Change{
|
||||
lsn: 100,
|
||||
old_struct: %Domain.Resources.Resource{id: ^resource_id},
|
||||
struct: %Domain.Resources.Resource{id: ^resource_id, address: "new-address"}
|
||||
}
|
||||
|
||||
assert_push "reject_access", payload
|
||||
|
||||
assert payload == %{
|
||||
client_id: client.id,
|
||||
resource_id: resource.id
|
||||
}
|
||||
end
|
||||
|
||||
test "sends resource_updated when filters change", %{
|
||||
client: client,
|
||||
gateway: gateway,
|
||||
account: account,
|
||||
resource: resource,
|
||||
relay: relay,
|
||||
socket: socket
|
||||
} do
|
||||
flow =
|
||||
@@ -632,15 +929,12 @@ defmodule API.Gateway.ChannelTest do
|
||||
expires_at = DateTime.utc_now() |> DateTime.add(30, :second)
|
||||
client_payload = "RTC_SD_or_DNS_Q"
|
||||
|
||||
stamp_secret = Ecto.UUID.generate()
|
||||
:ok = Domain.Relays.connect_relay(relay, stamp_secret)
|
||||
|
||||
send(
|
||||
socket.channel_pid,
|
||||
{{:allow_access, gateway.id}, {channel_pid, socket_ref},
|
||||
%{
|
||||
client: client,
|
||||
resource: resource,
|
||||
resource: to_cache(resource),
|
||||
flow_id: flow.id,
|
||||
authorization_expires_at: expires_at,
|
||||
client_payload: client_payload
|
||||
@@ -667,7 +961,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
|
||||
data = Map.put(old_data, "filters", filters)
|
||||
|
||||
Events.Hooks.Resources.on_update(old_data, data)
|
||||
Changes.Hooks.Resources.on_update(100, old_data, data)
|
||||
|
||||
assert_push "resource_updated", payload
|
||||
|
||||
@@ -685,6 +979,48 @@ defmodule API.Gateway.ChannelTest do
|
||||
}
|
||||
end
|
||||
|
||||
test "sends resource_updated when filters change even without resource in cache", %{
|
||||
resource: resource,
|
||||
socket: _socket
|
||||
} do
|
||||
# Don't create any flows - simulate a gateway that reconnected
|
||||
# and doesn't have this resource in its cache yet
|
||||
|
||||
old_data = %{
|
||||
"id" => resource.id,
|
||||
"account_id" => resource.account_id,
|
||||
"address" => resource.address,
|
||||
"name" => resource.name,
|
||||
"type" => "dns",
|
||||
"filters" => [],
|
||||
"ip_stack" => "dual"
|
||||
}
|
||||
|
||||
filters = [
|
||||
%{"protocol" => "tcp", "ports" => ["443"]},
|
||||
%{"protocol" => "udp", "ports" => ["53"]}
|
||||
]
|
||||
|
||||
data = Map.put(old_data, "filters", filters)
|
||||
|
||||
# Trigger the resource update
|
||||
Changes.Hooks.Resources.on_update(100, old_data, data)
|
||||
|
||||
# Should still receive the update even though resource isn't in cache
|
||||
assert_push "resource_updated", payload
|
||||
|
||||
assert payload == %{
|
||||
address: resource.address,
|
||||
id: resource.id,
|
||||
name: resource.name,
|
||||
type: :dns,
|
||||
filters: [
|
||||
%{protocol: :tcp, port_range_start: 443, port_range_end: 443},
|
||||
%{protocol: :udp, port_range_start: 53, port_range_end: 53}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
test "subscribes for relays presence", %{gateway: gateway, gateway_group: gateway_group} do
|
||||
relay_group = Fixtures.Relays.create_global_group()
|
||||
stamp_secret = Ecto.UUID.generate()
|
||||
@@ -876,7 +1212,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
{{:request_connection, gateway.id}, {channel_pid, socket_ref},
|
||||
%{
|
||||
client: client,
|
||||
resource: resource,
|
||||
resource: to_cache(resource),
|
||||
flow_id: flow.id,
|
||||
authorization_expires_at: expires_at,
|
||||
client_payload: client_payload,
|
||||
@@ -949,7 +1285,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
{{:request_connection, gateway.id}, {channel_pid, socket_ref},
|
||||
%{
|
||||
client: client,
|
||||
resource: resource,
|
||||
resource: to_cache(resource),
|
||||
flow_id: flow.id,
|
||||
authorization_expires_at: expires_at,
|
||||
client_payload: client_payload,
|
||||
@@ -971,7 +1307,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
"expires_at" => flow.expires_at
|
||||
}
|
||||
|
||||
Events.Hooks.Flows.on_delete(data)
|
||||
Changes.Hooks.Flows.on_delete(100, data)
|
||||
|
||||
assert_push "reject_access", %{
|
||||
client_id: client_id,
|
||||
@@ -1011,7 +1347,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
{{:authorize_flow, gateway.id}, {channel_pid, socket_ref},
|
||||
%{
|
||||
client: client,
|
||||
resource: resource,
|
||||
resource: to_cache(resource),
|
||||
flow_id: flow.id,
|
||||
authorization_expires_at: expires_at,
|
||||
ice_credentials: ice_credentials,
|
||||
@@ -1083,7 +1419,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
{{:authorize_flow, gateway.id}, {channel_pid, socket_ref},
|
||||
%{
|
||||
client: client,
|
||||
resource: resource,
|
||||
resource: to_cache(resource),
|
||||
flow_id: flow.id,
|
||||
authorization_expires_at: expires_at,
|
||||
ice_credentials: ice_credentials,
|
||||
@@ -1105,7 +1441,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
"expires_at" => flow.expires_at
|
||||
}
|
||||
|
||||
Events.Hooks.Flows.on_delete(data)
|
||||
Changes.Hooks.Flows.on_delete(100, data)
|
||||
|
||||
assert_push "reject_access", %{
|
||||
client_id: client_id,
|
||||
@@ -1146,7 +1482,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
gateway_public_key = gateway.public_key
|
||||
gateway_ipv4 = gateway.ipv4
|
||||
gateway_ipv6 = gateway.ipv6
|
||||
resource_id = resource.id
|
||||
rid_bytes = Ecto.UUID.dump!(resource.id)
|
||||
|
||||
ice_credentials = %{
|
||||
client: %{username: "A", password: "B"},
|
||||
@@ -1158,7 +1494,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
{{:authorize_flow, gateway.id}, {channel_pid, socket_ref},
|
||||
%{
|
||||
client: client,
|
||||
resource: resource,
|
||||
resource: to_cache(resource),
|
||||
flow_id: flow.id,
|
||||
authorization_expires_at: expires_at,
|
||||
ice_credentials: ice_credentials,
|
||||
@@ -1174,7 +1510,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
assert_receive {
|
||||
:connect,
|
||||
^socket_ref,
|
||||
^resource_id,
|
||||
^rid_bytes,
|
||||
^gateway_group_id,
|
||||
^gateway_id,
|
||||
^gateway_public_key,
|
||||
@@ -1226,7 +1562,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
{{:request_connection, gateway.id}, {channel_pid, socket_ref},
|
||||
%{
|
||||
client: client,
|
||||
resource: resource,
|
||||
resource: to_cache(resource),
|
||||
flow_id: flow.id,
|
||||
authorization_expires_at: expires_at,
|
||||
client_payload: payload,
|
||||
@@ -1261,8 +1597,8 @@ defmodule API.Gateway.ChannelTest do
|
||||
})
|
||||
|
||||
assert_reply push_ref, :ok
|
||||
assert_receive {:connect, ^socket_ref, resource_id, ^gateway_public_key, ^payload}
|
||||
assert resource_id == resource.id
|
||||
assert_receive {:connect, ^socket_ref, rid_bytes, ^gateway_public_key, ^payload}
|
||||
assert Ecto.UUID.load!(rid_bytes) == resource.id
|
||||
end
|
||||
|
||||
test "connection_ready pushes an error when ref is invalid", %{
|
||||
@@ -1283,7 +1619,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
} do
|
||||
candidates = ["foo", "bar"]
|
||||
|
||||
:ok = Domain.PubSub.Account.subscribe(account.id)
|
||||
:ok = PubSub.Account.subscribe(account.id)
|
||||
|
||||
attrs = %{
|
||||
"candidates" => candidates,
|
||||
@@ -1309,7 +1645,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
|
||||
:ok = Domain.Clients.Presence.connect(client)
|
||||
PubSub.subscribe(Domain.Tokens.socket_id(subject.token_id))
|
||||
:ok = Domain.PubSub.Account.subscribe(gateway.account_id)
|
||||
:ok = PubSub.Account.subscribe(gateway.account_id)
|
||||
|
||||
push(socket, "broadcast_ice_candidates", attrs)
|
||||
|
||||
@@ -1326,7 +1662,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
} do
|
||||
candidates = ["foo", "bar"]
|
||||
|
||||
:ok = Domain.PubSub.Account.subscribe(account.id)
|
||||
:ok = PubSub.Account.subscribe(account.id)
|
||||
|
||||
attrs = %{
|
||||
"candidates" => candidates,
|
||||
@@ -1350,7 +1686,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
"client_ids" => [client.id]
|
||||
}
|
||||
|
||||
:ok = Domain.PubSub.Account.subscribe(gateway.account_id)
|
||||
:ok = PubSub.Account.subscribe(gateway.account_id)
|
||||
:ok = Domain.Clients.Presence.connect(client)
|
||||
PubSub.subscribe(Domain.Tokens.socket_id(subject.token_id))
|
||||
|
||||
|
||||
@@ -81,10 +81,9 @@ defmodule Domain.Actors do
|
||||
|> Repo.preload(preload)
|
||||
end
|
||||
|
||||
def all_memberships_for_actor!(%Actor{} = actor) do
|
||||
def all_memberships_for_actor_id!(actor_id) do
|
||||
Membership.Query.all()
|
||||
|> Membership.Query.by_account_id(actor.account_id)
|
||||
|> Membership.Query.by_actor_id(actor.id)
|
||||
|> Membership.Query.by_actor_id(actor_id)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@@ -255,7 +254,7 @@ defmodule Domain.Actors do
|
||||
{:ok, [group]} ->
|
||||
{:ok, _policies} = Policies.delete_policies_for(group, subject)
|
||||
|
||||
# TODO: WAL
|
||||
# TODO: Hard delete
|
||||
# Consider using a trigger or transaction to handle the side effects of soft-deletions to ensure consistency
|
||||
{_count, _memberships} =
|
||||
Membership.Query.all()
|
||||
@@ -282,7 +281,7 @@ defmodule Domain.Actors do
|
||||
|> Group.Query.by_provider_id(provider.id)
|
||||
|> Group.Query.by_account_id(provider.account_id)
|
||||
|
||||
# TODO: WAL
|
||||
# TODO: Hard delete
|
||||
# Consider using a trigger or transaction to handle the side effects of soft-deletions to ensure consistency
|
||||
{_count, _memberships} =
|
||||
Membership.Query.by_group_provider_id(provider.id)
|
||||
@@ -319,7 +318,7 @@ defmodule Domain.Actors do
|
||||
{:ok, _policies} = Domain.Policies.delete_policies_for(group)
|
||||
end)
|
||||
|
||||
# TODO: WAL
|
||||
# TODO: Hard delete
|
||||
# Consider using a trigger or transaction to handle the side effects of soft-deletions to ensure consistency
|
||||
{_count, _memberships} =
|
||||
Membership.Query.by_group_id({:in, Enum.map(groups, & &1.id)})
|
||||
@@ -598,7 +597,7 @@ defmodule Domain.Actors do
|
||||
:ok = Auth.delete_identities_for(actor, subject)
|
||||
:ok = Clients.delete_clients_for(actor, subject)
|
||||
|
||||
# TODO: WAL
|
||||
# TODO: Hard delete
|
||||
# Consider using a trigger or transaction to handle the side effects of soft-deletions to ensure consistency
|
||||
{_count, _memberships} =
|
||||
Membership.Query.by_actor_id(actor.id)
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
defmodule Domain.Actors.Membership do
|
||||
use Domain, :schema
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
id: Ecto.UUID.t(),
|
||||
group_id: Ecto.UUID.t(),
|
||||
actor_id: Ecto.UUID.t(),
|
||||
account_id: Ecto.UUID.t()
|
||||
}
|
||||
|
||||
schema "actor_group_memberships" do
|
||||
belongs_to :group, Domain.Actors.Group
|
||||
belongs_to :actor, Domain.Actors.Actor
|
||||
|
||||
@@ -81,7 +81,7 @@ defmodule Domain.Application do
|
||||
|
||||
defp replication do
|
||||
connection_modules = [
|
||||
Domain.Events.ReplicationConnection,
|
||||
Domain.Changes.ReplicationConnection,
|
||||
Domain.ChangeLogs.ReplicationConnection
|
||||
]
|
||||
|
||||
|
||||
45
elixir/apps/domain/lib/domain/cache/cacheable.ex
vendored
Normal file
45
elixir/apps/domain/lib/domain/cache/cacheable.ex
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
defprotocol Domain.Cache.Cacheable do
|
||||
@type uuid_binary :: <<_::128>>
|
||||
|
||||
@doc "Converts a Domain struct to its cache representation."
|
||||
def to_cache(struct)
|
||||
end
|
||||
|
||||
defimpl Domain.Cache.Cacheable, for: Domain.Gateways.Group do
|
||||
def to_cache(%Domain.Gateways.Group{} = gateway_group) do
|
||||
%Domain.Cache.Cacheable.GatewayGroup{
|
||||
id: Ecto.UUID.dump!(gateway_group.id),
|
||||
name: gateway_group.name
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Domain.Cache.Cacheable, for: Domain.Resources.Resource do
|
||||
def to_cache(%Domain.Resources.Resource{} = resource) do
|
||||
%Domain.Cache.Cacheable.Resource{
|
||||
id: Ecto.UUID.dump!(resource.id),
|
||||
type: resource.type,
|
||||
name: resource.name,
|
||||
address: resource.address,
|
||||
address_description: resource.address_description,
|
||||
ip_stack: resource.ip_stack,
|
||||
filters: Enum.map(resource.filters, &Map.from_struct/1),
|
||||
gateway_groups:
|
||||
if(is_list(resource.gateway_groups),
|
||||
do: Enum.map(resource.gateway_groups, &Domain.Cache.Cacheable.to_cache/1),
|
||||
else: []
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Domain.Cache.Cacheable, for: Domain.Policies.Policy do
|
||||
def to_cache(%Domain.Policies.Policy{} = policy) do
|
||||
%Domain.Cache.Cacheable.Policy{
|
||||
id: Ecto.UUID.dump!(policy.id),
|
||||
resource_id: Ecto.UUID.dump!(policy.resource_id),
|
||||
actor_group_id: Ecto.UUID.dump!(policy.actor_group_id),
|
||||
conditions: Enum.map(policy.conditions, &Map.from_struct/1)
|
||||
}
|
||||
end
|
||||
end
|
||||
11
elixir/apps/domain/lib/domain/cache/cacheable/gateway_group.ex
vendored
Normal file
11
elixir/apps/domain/lib/domain/cache/cacheable/gateway_group.ex
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
defmodule Domain.Cache.Cacheable.GatewayGroup do
|
||||
defstruct [
|
||||
:id,
|
||||
:name
|
||||
]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
id: Domain.Cache.Cacheable.uuid_binary(),
|
||||
name: String.t()
|
||||
}
|
||||
end
|
||||
34
elixir/apps/domain/lib/domain/cache/cacheable/policy.ex
vendored
Normal file
34
elixir/apps/domain/lib/domain/cache/cacheable/policy.ex
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
defmodule Domain.Cache.Cacheable.Policy do
|
||||
defstruct [
|
||||
:id,
|
||||
:resource_id,
|
||||
:actor_group_id,
|
||||
:conditions
|
||||
]
|
||||
|
||||
@type condition :: %{
|
||||
property:
|
||||
:remote_ip_location_region
|
||||
| :remote_ip
|
||||
| :provider_id
|
||||
| :current_utc_datetime
|
||||
| :client_verified,
|
||||
operator:
|
||||
:contains
|
||||
| :does_not_contain
|
||||
| :is_in
|
||||
| :is_not_in
|
||||
| :is_in_day_of_week_time_ranges
|
||||
| :is_in_cidr
|
||||
| :is_not_in_cidr
|
||||
| :is,
|
||||
values: [String.t()]
|
||||
}
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
id: Domain.Cache.Cacheable.uuid_binary(),
|
||||
resource_id: Domain.Cache.Cacheable.uuid_binary(),
|
||||
actor_group_id: Domain.Cache.Cacheable.uuid_binary(),
|
||||
conditions: [condition()]
|
||||
}
|
||||
end
|
||||
28
elixir/apps/domain/lib/domain/cache/cacheable/resource.ex
vendored
Normal file
28
elixir/apps/domain/lib/domain/cache/cacheable/resource.ex
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
defmodule Domain.Cache.Cacheable.Resource do
|
||||
defstruct [
|
||||
:id,
|
||||
:name,
|
||||
:type,
|
||||
:address,
|
||||
:address_description,
|
||||
:ip_stack,
|
||||
:filters,
|
||||
:gateway_groups
|
||||
]
|
||||
|
||||
@type filter :: %{
|
||||
protocol: :tcp | :udp | :icmp,
|
||||
ports: [String.t()]
|
||||
}
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
id: Domain.Cache.Cacheable.uuid_binary(),
|
||||
name: String.t(),
|
||||
type: :cidr | :ip | :dns | :internet,
|
||||
address: String.t(),
|
||||
address_description: String.t(),
|
||||
ip_stack: atom(),
|
||||
filters: [filter()],
|
||||
gateway_groups: [Domain.Cache.Cacheable.GatewayGroup.t()]
|
||||
}
|
||||
end
|
||||
561
elixir/apps/domain/lib/domain/cache/client.ex
vendored
Normal file
561
elixir/apps/domain/lib/domain/cache/client.ex
vendored
Normal file
@@ -0,0 +1,561 @@
|
||||
defmodule Domain.Cache.Client do
|
||||
@moduledoc """
|
||||
This cache is used in the client channel to maintain a materialized view of the client access state.
|
||||
The cache is updated via WAL messages streamed from the Domain.Changes.ReplicationConnection module.
|
||||
|
||||
We use basic data structures and binary representations instead of full Ecto schema structs
|
||||
to minimize memory usage. The rough structure of the cache data structure and some napkin math
|
||||
on their memory usage (assuming "worst-case" usage scenarios) is described below.
|
||||
|
||||
Data structure:
|
||||
|
||||
%{
|
||||
policies: %{id:uuidv4:16 => {
|
||||
resource_id:uuidv4:16,
|
||||
actor_group_id:uuidv4:16,
|
||||
conditions:[%{
|
||||
property:atom:0,
|
||||
operator:atom:0,
|
||||
values:
|
||||
[string:varies]:(16 * len)}:(40 - small map)
|
||||
]:(16 * len)
|
||||
}:16
|
||||
}:(num_keys * 1.8 * 8 - large map)
|
||||
|
||||
resources: %{id:uuidv4:16 => {
|
||||
name: string:(~ 1.25 bytes per char),
|
||||
address:string:(~ 1.25 bytes per char),
|
||||
address_description:string:(~ 1.25 bytes per char),
|
||||
ip_stack: atom:0,
|
||||
type: atom:0,
|
||||
filters: [%{protocol: atom:0, ports: [string:(~ 1.25 bytes per char)]}:(40 - small map)]:(16 * len),
|
||||
gateway_groups: [%{
|
||||
name:string:(~1.25 bytes per char),
|
||||
resource_id:uuidv4:16,
|
||||
gateway_group_id:uuidv4:16
|
||||
}]
|
||||
}},
|
||||
|
||||
memberships: %{group_id:uuidv4:16 => membership_id:uuidv4:16},
|
||||
|
||||
connectable_resources: [Cache.Cacheable.Resource.t()]
|
||||
}
|
||||
|
||||
|
||||
For 1,000 policies, 500 resources, 100 memberships, 100 flows (per connected client):
|
||||
|
||||
513,400 bytes, 280,700 bytes, 24,640 bytes, 24,640 bytes
|
||||
|
||||
= 843,380 bytes
|
||||
= ~ 1 MB (per client)
|
||||
|
||||
"""
|
||||
|
||||
alias Domain.{Actors, Auth, Clients, Cache, Gateways, Resources, Policies}
|
||||
require Logger
|
||||
require OpenTelemetry.Tracer
|
||||
import Ecto.UUID, only: [dump!: 1, load!: 1]
|
||||
|
||||
defstruct [
|
||||
# A map of all the policies that match an actor group we're in.
|
||||
:policies,
|
||||
|
||||
# A map of all the resources associated to the policies above.
|
||||
:resources,
|
||||
|
||||
# A map of actor group IDs to membership IDs we're in.
|
||||
:memberships,
|
||||
|
||||
# The list resources the client can currently connect to. This is defined as:
|
||||
# 1. The resource is authorized based on policies and conditions
|
||||
# 2. The resource is compatible with the client (i.e. the client can connect to it)
|
||||
# 3. The resource has at least one gateway group associated with it
|
||||
:connectable_resources
|
||||
]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
policies: %{Cache.Cacheable.uuid_binary() => Domain.Cache.Cacheable.Policy.t()},
|
||||
resources: %{Cache.Cacheable.uuid_binary() => Domain.Cache.Cacheable.Resource.t()},
|
||||
memberships: %{Cache.Cacheable.uuid_binary() => Cache.Cacheable.uuid_binary()},
|
||||
connectable_resources: [Cache.Cacheable.Resource.t()]
|
||||
}
|
||||
|
||||
@doc """
|
||||
Authorizes a new flow for the given client and resource or returns a list of violated properties if
|
||||
the resource is not authorized for the client.
|
||||
"""
|
||||
|
||||
@spec authorize_resource(t(), Clients.Client.t(), Ecto.UUID.t(), Auth.Subject.t()) ::
|
||||
{:ok, Cache.Cacheable.Resource.t(), Ecto.UUID.t(), Ecto.UUID.t(), non_neg_integer()}
|
||||
| {:error, :not_found}
|
||||
| {:error, {:forbidden, violated_properties: [atom()]}}
|
||||
|
||||
def authorize_resource(cache, client, resource_id, subject) do
|
||||
rid_bytes = dump!(resource_id)
|
||||
|
||||
resource = Enum.find(cache.connectable_resources, :not_found, fn r -> r.id == rid_bytes end)
|
||||
|
||||
policy =
|
||||
cache.policies
|
||||
|> Enum.filter(fn {_id, policy} -> policy.resource_id == rid_bytes end)
|
||||
|> Enum.map(fn {_id, policy} -> policy end)
|
||||
|> Policies.longest_conforming_policy_for_client(client, subject.expires_at)
|
||||
|
||||
with %Cache.Cacheable.Resource{} <- resource,
|
||||
{:ok, policy, expires_at} <- policy,
|
||||
{:ok, mid_bytes} <- Map.fetch(cache.memberships, policy.actor_group_id) do
|
||||
membership_id = load!(mid_bytes)
|
||||
policy_id = load!(policy.id)
|
||||
{:ok, resource, membership_id, policy_id, expires_at}
|
||||
else
|
||||
:not_found ->
|
||||
Logger.warning("resource not found in connectable resources",
|
||||
connectable_resources: inspect(cache.connectable_resources),
|
||||
subject: inspect(subject),
|
||||
client: inspect(client),
|
||||
resource_id: resource_id
|
||||
)
|
||||
|
||||
{:error, :not_found}
|
||||
|
||||
:error ->
|
||||
Logger.warning("membership not found in cache",
|
||||
memberships: inspect(cache.memberships),
|
||||
subject: inspect(subject),
|
||||
client: inspect(client),
|
||||
resource_id: resource_id
|
||||
)
|
||||
|
||||
{:error, :not_found}
|
||||
|
||||
{:error, {:forbidden, violated_properties: violated_properties}} ->
|
||||
{:error, {:forbidden, violated_properties: violated_properties}}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Recomputes the list of connectable resources, returning the newly connectable resources
|
||||
and the IDs of resources that are no longer connectable so that the client may update its
|
||||
state. This should be called periodically to handle differences due to time-based policy conditions.
|
||||
|
||||
If opts[:toggle] is set to true, we ensure that all added resources also have
|
||||
"""
|
||||
|
||||
@spec recompute_connectable_resources(t() | nil, Clients.Client.t(), Keyword.t()) ::
|
||||
{:ok, [Domain.Cache.Cacheable.Resource.t()], [Ecto.UUID.t()], t()}
|
||||
|
||||
def recompute_connectable_resources(nil, client) do
|
||||
hydrate(client)
|
||||
|> recompute_connectable_resources(client)
|
||||
end
|
||||
|
||||
def recompute_connectable_resources(cache, client, opts \\ []) do
|
||||
{toggle, _opts} = Keyword.pop(opts, :toggle)
|
||||
|
||||
connectable_resources =
|
||||
cache.policies
|
||||
|> conforming_resource_ids(client)
|
||||
|> adapted_resources(cache.resources, client)
|
||||
|
||||
added = connectable_resources -- cache.connectable_resources
|
||||
|
||||
added_ids = Enum.map(added, & &1.id)
|
||||
|
||||
# connlib can handle all resource attribute changes except for changing sites, so we can omit the deleted IDs
|
||||
# of added resources since they'll be updated gracefully.
|
||||
removed =
|
||||
(cache.connectable_resources -- connectable_resources)
|
||||
|> Enum.filter(fn r ->
|
||||
if toggle do
|
||||
r
|
||||
else
|
||||
r.id not in added_ids
|
||||
end
|
||||
end)
|
||||
|
||||
cache = %{cache | connectable_resources: connectable_resources}
|
||||
|
||||
{:ok, added, Enum.map(removed, & &1.id) |> Enum.map(&load!/1), cache}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fetches a membership id by an actor_group_id.
|
||||
"""
|
||||
|
||||
@spec fetch_membership_id(t(), Cache.Cacheable.uuid_binary()) ::
|
||||
{:ok, Ecto.UUID.t()} | {:error, :not_found}
|
||||
|
||||
def fetch_membership_id(cache, gid_bytes) do
|
||||
cache.memberships
|
||||
|> Map.fetch(gid_bytes)
|
||||
|> case do
|
||||
{:ok, mid_bytes} -> {:ok, load!(mid_bytes)}
|
||||
:error -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a new membership to the cache, potentially fetching the missing policies and resources
|
||||
that we don't already have in our cache.
|
||||
|
||||
Since this affects connectable resources, we recompute the connectable resources, which could
|
||||
yield deleted IDs, so we send those back.
|
||||
"""
|
||||
|
||||
@spec add_membership(t(), Clients.Client.t()) ::
|
||||
{:ok, [Domain.Cache.Cacheable.Resource.t()], [Ecto.UUID.t()], t()}
|
||||
|
||||
def add_membership(cache, client) do
|
||||
# TODO: Optimization
|
||||
# For simplicity, we rehydrate the cache here. This could be made more efficient by calculating which
|
||||
# policies and resources we are missing, and selectively fetching, filtering, and updating the cache.
|
||||
# This is not expected to cause an issue in production since in most cases, bulk new memberships would imply
|
||||
# bulk new groups, which shouldn't have much if any policies associated to them.
|
||||
previously_connectable = cache.connectable_resources
|
||||
|
||||
# Use the previous connectable IDs so that the recomputation yields the difference
|
||||
cache = %{hydrate(client) | connectable_resources: previously_connectable}
|
||||
|
||||
recompute_connectable_resources(cache, client)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes all policies, resources, and memberships associated with the given group_id from the cache.
|
||||
"""
|
||||
|
||||
@spec delete_membership(t(), Actors.Membership.t(), Clients.Client.t()) ::
|
||||
{:ok, [Cache.Cacheable.Resource.t()], [Ecto.UUID.t()], t()}
|
||||
|
||||
def delete_membership(cache, membership, client) do
|
||||
gid_bytes = dump!(membership.group_id)
|
||||
|
||||
updated_policies =
|
||||
cache.policies
|
||||
|> Enum.reject(fn {_id, p} -> p.actor_group_id == gid_bytes end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
# Only remove resources that have no remaining policies
|
||||
remaining_resource_ids =
|
||||
updated_policies
|
||||
|> Enum.map(fn {_id, p} -> p.resource_id end)
|
||||
|> Enum.uniq()
|
||||
|> MapSet.new()
|
||||
|
||||
updated_resources =
|
||||
cache.resources
|
||||
|> Enum.filter(fn {rid_bytes, _resource} ->
|
||||
MapSet.member?(remaining_resource_ids, rid_bytes)
|
||||
end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
updated_memberships =
|
||||
cache.memberships
|
||||
|> Map.delete(gid_bytes)
|
||||
|
||||
cache = %{
|
||||
cache
|
||||
| policies: updated_policies,
|
||||
resources: updated_resources,
|
||||
memberships: updated_memberships
|
||||
}
|
||||
|
||||
recompute_connectable_resources(cache, client)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates any relevant resources in the cache with the new group name.
|
||||
"""
|
||||
|
||||
@spec update_resources_with_group_name(t(), Gateways.Group.t(), Clients.Client.t()) ::
|
||||
{:ok, [Domain.Cache.Cacheable.Resource.t()], [Ecto.UUID.t()], t()}
|
||||
|
||||
def update_resources_with_group_name(cache, group, client) do
|
||||
group = Domain.Cache.Cacheable.to_cache(group)
|
||||
|
||||
# Get updated resources
|
||||
resources =
|
||||
cache.resources
|
||||
|> Enum.map(fn {id, resource} ->
|
||||
# Replace old group with new
|
||||
gateway_groups =
|
||||
Enum.map(resource.gateway_groups, fn gg ->
|
||||
if gg.id == group.id do
|
||||
group
|
||||
else
|
||||
gg
|
||||
end
|
||||
end)
|
||||
|
||||
{id, %{resource | gateway_groups: gateway_groups}}
|
||||
end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
cache = %{cache | resources: resources}
|
||||
|
||||
# For these updates we need to make sure the resource is toggled deleted then created.
|
||||
# See https://github.com/firezone/firezone/issues/9881
|
||||
recompute_connectable_resources(cache, client, toggle: true)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a new policy to the cache. If the policy includes a resource we do not already have in the cache,
|
||||
we fetch the resource from the database and add it to the cache.
|
||||
|
||||
If the resource is compatible with and authorized for the current client, we return the resource,
|
||||
otherwise we just return the updated cache.
|
||||
"""
|
||||
|
||||
@spec add_policy(t(), Policies.Policy.t(), Clients.Client.t(), Auth.Subject.t()) ::
|
||||
{:ok, [Domain.Cache.Cacheable.Resource.t()], [Ecto.UUID.t()], t()}
|
||||
|
||||
def add_policy(cache, %{resource_id: resource_id} = policy, client, subject) do
|
||||
policy = Domain.Cache.Cacheable.to_cache(policy)
|
||||
|
||||
if Map.has_key?(cache.memberships, policy.actor_group_id) do
|
||||
# Add policy to the cache
|
||||
cache = %{cache | policies: Map.put(cache.policies, policy.id, policy)}
|
||||
|
||||
# Maybe add resource to the cache if we don't already have it
|
||||
cache =
|
||||
if Map.has_key?(cache.resources, policy.resource_id) do
|
||||
cache
|
||||
else
|
||||
# Need to fetch the resource from the DB
|
||||
opts = [preload: :gateway_groups]
|
||||
{:ok, resource} = Resources.fetch_resource_by_id(resource_id, subject, opts)
|
||||
|
||||
resource = Domain.Cache.Cacheable.to_cache(resource)
|
||||
|
||||
%{cache | resources: Map.put(cache.resources, resource.id, resource)}
|
||||
end
|
||||
|
||||
recompute_connectable_resources(cache, client)
|
||||
else
|
||||
{:ok, [], [], cache}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates policy in cache with given policy if it exists. Breaking policy changes are handled separately
|
||||
with a delete and then add operation.
|
||||
"""
|
||||
|
||||
@spec update_policy(t(), Policies.Policy.t()) :: {:ok, [], [], t()}
|
||||
|
||||
def update_policy(cache, policy) do
|
||||
policy = Domain.Cache.Cacheable.to_cache(policy)
|
||||
policies = Map.replace(cache.policies, policy.id, policy)
|
||||
{:ok, [], [], %{cache | policies: policies}}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes a policy from the cache. If we can't find another policy granting access to the resource,
|
||||
we return the deleted resource ID.
|
||||
"""
|
||||
@spec delete_policy(t(), Policies.Policy.t(), Clients.Client.t()) ::
|
||||
{:ok, [Domain.Cache.Cacheable.Resource.t()], [Ecto.UUID.t()], t()}
|
||||
def delete_policy(cache, policy, client) do
|
||||
policy = Domain.Cache.Cacheable.to_cache(policy)
|
||||
|
||||
if Map.has_key?(cache.policies, policy.id) do
|
||||
# Update the cache
|
||||
cache = %{cache | policies: Map.delete(cache.policies, policy.id)}
|
||||
|
||||
# Remove the resource if no policies are left for it
|
||||
no_more_policies? =
|
||||
cache.policies
|
||||
|> Enum.all?(fn {_id, p} -> p.resource_id != policy.resource_id end)
|
||||
|
||||
resources =
|
||||
if no_more_policies? do
|
||||
Map.delete(cache.resources, policy.resource_id)
|
||||
else
|
||||
cache.resources
|
||||
end
|
||||
|
||||
cache = %{cache | resources: resources}
|
||||
|
||||
recompute_connectable_resources(cache, client)
|
||||
else
|
||||
{:ok, [], [], cache}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a gateway group (by virtue of the added resource connection) to the appropriate resource in the cache.
|
||||
|
||||
Since resource connection is a join record, we need to fetch the group from the DB to get its name.
|
||||
|
||||
Since adding a gateway group requires re-evaluating policies, the resource could now be connectable or not connectable
|
||||
so we return either the deleted resource ID or the updated resource if there's a change. Otherwise we simply
|
||||
return the updated cache.
|
||||
"""
|
||||
@spec add_resource_connection(
|
||||
t(),
|
||||
Resources.Connection.t(),
|
||||
Auth.Subject.t(),
|
||||
Clients.Client.t()
|
||||
) ::
|
||||
{:ok, [Domain.Cache.Cacheable.Resource.t()], [Ecto.UUID.t()], t()}
|
||||
def add_resource_connection(cache, connection, subject, client) do
|
||||
rid_bytes = dump!(connection.resource_id)
|
||||
|
||||
if Map.has_key?(cache.resources, rid_bytes) do
|
||||
# We need the gateway group to add it
|
||||
{:ok, gateway_group} = Gateways.fetch_group_by_id(connection.gateway_group_id, subject)
|
||||
gateway_group = Domain.Cache.Cacheable.to_cache(gateway_group)
|
||||
|
||||
# Update the cache
|
||||
resources =
|
||||
cache.resources
|
||||
|> Map.update!(rid_bytes, fn resource ->
|
||||
if gateway_group in resource.gateway_groups do
|
||||
# Duplicates here mean something is amiss, so be noisy about it.
|
||||
Logger.error("Duplicate gateway group in resource cache",
|
||||
resource: resource,
|
||||
gateway_group: gateway_group
|
||||
)
|
||||
|
||||
resource
|
||||
else
|
||||
%{resource | gateway_groups: [gateway_group | resource.gateway_groups]}
|
||||
end
|
||||
end)
|
||||
|
||||
cache = %{cache | resources: resources}
|
||||
|
||||
# For these updates we need to make sure the resource is toggled deleted then created.
|
||||
# See https://github.com/firezone/firezone/issues/9881
|
||||
recompute_connectable_resources(cache, client, toggle: true)
|
||||
else
|
||||
{:ok, [], [], cache}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a gateway group (by virtue of the deleted resource connection) from the appropriate resource in the cache.
|
||||
If the resource has no more gateway groups, we return the resource ID so the client can remove it. Otherwise, we
|
||||
return the updated resource.
|
||||
"""
|
||||
|
||||
@spec delete_resource_connection(t(), Resources.Connection.t(), Clients.Client.t()) ::
|
||||
{:ok, [Domain.Cache.Cacheable.Resource.t()], [Ecto.UUID.t()], t()}
|
||||
|
||||
def delete_resource_connection(cache, connection, client) do
|
||||
rid_bytes = dump!(connection.resource_id)
|
||||
|
||||
if Map.has_key?(cache.resources, rid_bytes) do
|
||||
# Update the cache
|
||||
resources =
|
||||
cache.resources
|
||||
|> Map.update!(rid_bytes, fn resource ->
|
||||
gateway_groups =
|
||||
Enum.reject(resource.gateway_groups, fn gg ->
|
||||
gg.id == dump!(connection.gateway_group_id)
|
||||
end)
|
||||
|
||||
%{resource | gateway_groups: gateway_groups}
|
||||
end)
|
||||
|
||||
cache = %{cache | resources: resources}
|
||||
|
||||
# For these updates we need to make sure the resource is toggled deleted then created.
|
||||
# See https://github.com/firezone/firezone/issues/9881
|
||||
recompute_connectable_resources(cache, client, toggle: true)
|
||||
else
|
||||
{:ok, [], [], cache}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a resource in the cache with the given resource if it exists.
|
||||
|
||||
If the resource's address has changed and we are no longer compatible with it, we
|
||||
need to remove it from the client's list of resources.
|
||||
|
||||
Otherwise, if the resource's address has changed and we are _now_ compatible with it, we need
|
||||
to add it to the client's list of resources.
|
||||
|
||||
If the resource has not meaningfully changed (i.e. the cached versions are the same),
|
||||
we return only the updated cache.
|
||||
"""
|
||||
|
||||
@spec update_resource(t(), Resources.Resource.t(), Clients.Client.t()) ::
|
||||
{:ok, [Domain.Cache.Cacheable.Resource.t()], [Ecto.UUID.t()], t()}
|
||||
|
||||
def update_resource(cache, resource, client) do
|
||||
resource = Domain.Cache.Cacheable.to_cache(resource)
|
||||
|
||||
if Map.has_key?(cache.resources, resource.id) do
|
||||
# Copy preloaded gateway groups
|
||||
resource = %{
|
||||
resource
|
||||
| gateway_groups: Map.get(cache.resources, resource.id).gateway_groups
|
||||
}
|
||||
|
||||
# Update the cache
|
||||
resources = %{cache.resources | resource.id => resource}
|
||||
cache = %{cache | resources: resources}
|
||||
|
||||
recompute_connectable_resources(cache, client)
|
||||
else
|
||||
{:ok, [], [], cache}
|
||||
end
|
||||
end
|
||||
|
||||
defp hydrate(client) do
|
||||
attributes = %{
|
||||
actor_id: client.actor_id
|
||||
}
|
||||
|
||||
OpenTelemetry.Tracer.with_span "Cache.Cacheable.hydrate", attributes: attributes do
|
||||
{_policies, cache} =
|
||||
Policies.all_policies_for_actor_id!(client.actor_id)
|
||||
|> Enum.map_reduce(%{policies: %{}, resources: %{}}, fn policy, cache ->
|
||||
resource = Cache.Cacheable.to_cache(policy.resource)
|
||||
resources = Map.put(cache.resources, resource.id, resource)
|
||||
|
||||
policy = Cache.Cacheable.to_cache(policy)
|
||||
policies = Map.put(cache.policies, policy.id, policy)
|
||||
|
||||
{policy, %{cache | policies: policies, resources: resources}}
|
||||
end)
|
||||
|
||||
memberships =
|
||||
Actors.all_memberships_for_actor_id!(client.actor_id)
|
||||
|> Enum.map(fn membership ->
|
||||
{dump!(membership.group_id), dump!(membership.id)}
|
||||
end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
cache
|
||||
|> Map.put(:memberships, memberships)
|
||||
|> Map.put(:connectable_resources, [])
|
||||
end
|
||||
end
|
||||
|
||||
defp adapted_resources(conforming_resource_ids, resources, client) do
|
||||
conforming_resource_ids
|
||||
|> Enum.map(fn id -> Map.get(resources, id) end)
|
||||
|> Enum.map(fn r -> adapt(r, client) end)
|
||||
|> Enum.reject(fn r -> is_nil(r) end)
|
||||
|> Enum.filter(fn r -> r.gateway_groups != [] end)
|
||||
end
|
||||
|
||||
defp conforming_resource_ids(policies, client) when is_map(policies) do
|
||||
policies
|
||||
|> Map.values()
|
||||
|> conforming_resource_ids(client)
|
||||
end
|
||||
|
||||
defp conforming_resource_ids(policies, client) do
|
||||
policies
|
||||
|> Policies.filter_by_conforming_policies_for_client(client)
|
||||
|> Enum.map(& &1.resource_id)
|
||||
|> Enum.uniq()
|
||||
end
|
||||
|
||||
defp adapt(resource, client) do
|
||||
Resources.adapt_resource_for_version(resource, client.last_seen_version)
|
||||
end
|
||||
end
|
||||
206
elixir/apps/domain/lib/domain/cache/gateway.ex
vendored
Normal file
206
elixir/apps/domain/lib/domain/cache/gateway.ex
vendored
Normal file
@@ -0,0 +1,206 @@
|
||||
defmodule Domain.Cache.Gateway do
|
||||
@moduledoc """
|
||||
This cache is used in the gateway channel processes to maintain a materialized view of the gateway flow state.
|
||||
The cache is updated via WAL messages streamed from the Domain.Changes.ReplicationConnection module.
|
||||
|
||||
We use basic data structures and binary representations instead of full Ecto schema structs
|
||||
to minimize memory usage. The rough structure of the two cached data structures and some napkin math
|
||||
on their memory usage (assuming "worst-case" usage scenarios) is described below.
|
||||
|
||||
Data structure:
|
||||
|
||||
%{{client_id:uuidv4:16, resource_id:uuidv4:16}:16 => %{flow_id:uuidv4:16 => expires_at:integer:8}:40}:(num_keys * 1.8 * 8 - large map)
|
||||
|
||||
For 10,000 client/resource entries, consisting of 10 flows each:
|
||||
|
||||
10,000 keys, 100,000 values
|
||||
480,000 bytes (outer map keys), 6,400,000 bytes (inner map), 144,000 bytes (outer map overhead)
|
||||
|
||||
= 7,024,000
|
||||
= ~ 7 MB
|
||||
"""
|
||||
|
||||
alias Domain.{Cache, Flows, Gateways}
|
||||
import Ecto.UUID, only: [dump!: 1, load!: 1]
|
||||
|
||||
require OpenTelemetry.Tracer
|
||||
|
||||
# Type definitions
|
||||
@type client_resource_key ::
|
||||
{client_id :: Cache.Cacheable.uuid_binary(),
|
||||
resource_id :: Cache.Cacheable.uuid_binary()}
|
||||
@type flow_map :: %{
|
||||
(flow_id :: Cache.Cacheable.uuid_binary()) => expires_at_unix :: non_neg_integer
|
||||
}
|
||||
@type t :: %{client_resource_key() => flow_map()}
|
||||
|
||||
@doc """
|
||||
Fetches relevant flows from the DB and transforms them into the cache format.
|
||||
"""
|
||||
@spec hydrate(Gateways.Gateway.t()) :: t()
|
||||
def hydrate(gateway) do
|
||||
OpenTelemetry.Tracer.with_span "Domain.Cache.hydrate_flows",
|
||||
attributes: %{
|
||||
gateway_id: gateway.id,
|
||||
account_id: gateway.account_id
|
||||
} do
|
||||
Flows.all_gateway_flows_for_cache!(gateway)
|
||||
|> Enum.reduce(%{}, fn {{client_id, resource_id}, {flow_id, expires_at}}, acc ->
|
||||
cid_bytes = dump!(client_id)
|
||||
rid_bytes = dump!(resource_id)
|
||||
fid_bytes = dump!(flow_id)
|
||||
expires_at_unix = DateTime.to_unix(expires_at, :second)
|
||||
|
||||
flow_id_map = Map.get(acc, {cid_bytes, rid_bytes}, %{})
|
||||
|
||||
Map.put(acc, {cid_bytes, rid_bytes}, Map.put(flow_id_map, fid_bytes, expires_at_unix))
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes expired flows from the cache.
|
||||
"""
|
||||
@spec prune(t()) :: t()
|
||||
def prune(cache) do
|
||||
now_unix = DateTime.utc_now() |> DateTime.to_unix(:second)
|
||||
|
||||
# 1. Remove individual flows older than 14 days, then remove access entry if no flows left
|
||||
cache
|
||||
|> Enum.map(fn {tuple, flow_id_map} ->
|
||||
flow_id_map =
|
||||
Enum.reject(flow_id_map, fn {_fid_bytes, expires_at_unix} ->
|
||||
expires_at_unix < now_unix
|
||||
end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
{tuple, flow_id_map}
|
||||
end)
|
||||
|> Enum.into(%{})
|
||||
|> Enum.reject(fn {_tuple, flow_id_map} -> map_size(flow_id_map) == 0 end)
|
||||
|> Enum.into(%{})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fetches the max expiration for a client-resource from the cache, or nil if not found.
|
||||
"""
|
||||
@spec get(t(), Ecto.UUID.t(), Ecto.UUID.t()) :: non_neg_integer() | nil
|
||||
def get(cache, client_id, resource_id) do
|
||||
tuple = {dump!(client_id), dump!(resource_id)}
|
||||
|
||||
case Map.get(cache, tuple) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
flow_id_map ->
|
||||
# Use longest expiration to minimize unnecessary access churn
|
||||
flow_id_map
|
||||
|> Map.values()
|
||||
|> Enum.max()
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Add a flow to the cache. Returns the updated cache.
|
||||
"""
|
||||
@spec put(t(), Ecto.UUID.t(), Cache.Cacheable.uuid_binary(), Ecto.UUID.t(), DateTime.t()) :: t()
|
||||
def put(%{} = cache, client_id, rid_bytes, flow_id, %DateTime{} = expires_at) do
|
||||
tuple = {dump!(client_id), rid_bytes}
|
||||
|
||||
flow_id_map =
|
||||
Map.get(cache, tuple, %{})
|
||||
|> Map.put(dump!(flow_id), DateTime.to_unix(expires_at, :second))
|
||||
|
||||
Map.put(cache, tuple, flow_id_map)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Delete a flow from the cache. If another flow exists for the same client/resource,
|
||||
we return the max expiration for that resource.
|
||||
If not, we optimistically try to reauthorize access by creating a new flow. This prevents
|
||||
removal of access on the Gateway but not the client, which would cause connectivity issues.
|
||||
If we can't create a new authorization, we send unauthorized so that access is removed.
|
||||
"""
|
||||
@spec reauthorize_deleted_flow(t(), Flows.Flow.t()) ::
|
||||
{:ok, non_neg_integer(), t()} | {:error, :unauthorized, t()} | {:error, :not_found}
|
||||
def reauthorize_deleted_flow(cache, %Flows.Flow{} = flow) do
|
||||
key = flow_key(flow)
|
||||
flow_id = dump!(flow.id)
|
||||
|
||||
case get_and_remove_flow(cache, key, flow_id) do
|
||||
{:not_found, _cache} ->
|
||||
{:error, :not_found}
|
||||
|
||||
{:last_flow_removed, cache} ->
|
||||
handle_last_flow_removal(cache, key, flow)
|
||||
|
||||
{:flow_removed, remaining_flows, cache} ->
|
||||
max_expiration = remaining_flows |> Map.values() |> Enum.max()
|
||||
{:ok, max_expiration, cache}
|
||||
end
|
||||
end
|
||||
|
||||
defp flow_key(%Flows.Flow{client_id: client_id, resource_id: resource_id}) do
|
||||
{dump!(client_id), dump!(resource_id)}
|
||||
end
|
||||
|
||||
defp get_and_remove_flow(cache, key, flow_id) do
|
||||
case Map.fetch(cache, key) do
|
||||
:error ->
|
||||
{:not_found, cache}
|
||||
|
||||
{:ok, flow_map} ->
|
||||
case Map.pop(flow_map, flow_id) do
|
||||
{nil, _} ->
|
||||
{:not_found, cache}
|
||||
|
||||
{_expiration, remaining_flows} when remaining_flows == %{} ->
|
||||
{:last_flow_removed, Map.delete(cache, key)}
|
||||
|
||||
{_expiration, remaining_flows} ->
|
||||
{:flow_removed, remaining_flows, Map.put(cache, key, remaining_flows)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_last_flow_removal(cache, key, flow) do
|
||||
case Flows.reauthorize_flow(flow) do
|
||||
{:ok, new_flow} ->
|
||||
new_flow_id = dump!(new_flow.id)
|
||||
expires_at_unix = DateTime.to_unix(new_flow.expires_at, :second)
|
||||
new_flow_map = %{new_flow_id => expires_at_unix}
|
||||
|
||||
{:ok, expires_at_unix, Map.put(cache, key, new_flow_map)}
|
||||
|
||||
:error ->
|
||||
{:error, :unauthorized, cache}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Check if the cache has a resource entry for the given resource_id.
|
||||
Returns true if the resource is present, false otherwise.
|
||||
"""
|
||||
@spec has_resource?(t(), Ecto.UUID.t()) :: boolean()
|
||||
def has_resource?(%{} = cache, resource_id) do
|
||||
rid_bytes = dump!(resource_id)
|
||||
|
||||
cache
|
||||
|> Map.keys()
|
||||
|> Enum.any?(fn {_, rid} ->
|
||||
rid == rid_bytes
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Return a list of all pairs matching the resource ID.
|
||||
"""
|
||||
@spec all_pairs_for_resource(t(), Ecto.UUID.t()) :: [{Ecto.UUID.t(), Ecto.UUID.t()}]
|
||||
def all_pairs_for_resource(%{} = cache, resource_id) do
|
||||
rid_bytes = dump!(resource_id)
|
||||
|
||||
cache
|
||||
|> Enum.filter(fn {{_, rid}, _} -> rid == rid_bytes end)
|
||||
|> Enum.map(fn {{cid, _}, _} -> {load!(cid), resource_id} end)
|
||||
end
|
||||
end
|
||||
15
elixir/apps/domain/lib/domain/changes/change.ex
Normal file
15
elixir/apps/domain/lib/domain/changes/change.ex
Normal file
@@ -0,0 +1,15 @@
|
||||
defmodule Domain.Changes.Change do
|
||||
defstruct [
|
||||
:lsn,
|
||||
:op,
|
||||
:old_struct,
|
||||
:struct
|
||||
]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
lsn: integer(),
|
||||
op: :insert | :update | :delete,
|
||||
old_struct: struct() | nil,
|
||||
struct: struct() | nil
|
||||
}
|
||||
end
|
||||
10
elixir/apps/domain/lib/domain/changes/hooks.ex
Normal file
10
elixir/apps/domain/lib/domain/changes/hooks.ex
Normal file
@@ -0,0 +1,10 @@
|
||||
defmodule Domain.Changes.Hooks do
|
||||
@moduledoc """
|
||||
A simple behavior to define hooks needed for processing WAL events.
|
||||
"""
|
||||
|
||||
@callback on_insert(lsn :: integer(), data :: map()) :: :ok | {:error, term()}
|
||||
@callback on_update(lsn :: integer(), old_data :: map(), data :: map()) ::
|
||||
:ok | {:error, term()}
|
||||
@callback on_delete(lsn :: integer(), old_data :: map()) :: :ok | {:error, term()}
|
||||
end
|
||||
54
elixir/apps/domain/lib/domain/changes/hooks/accounts.ex
Normal file
54
elixir/apps/domain/lib/domain/changes/hooks/accounts.ex
Normal file
@@ -0,0 +1,54 @@
|
||||
defmodule Domain.Changes.Hooks.Accounts do
|
||||
@behaviour Domain.Changes.Hooks
|
||||
alias Domain.{Accounts, Changes.Change, Flows, PubSub}
|
||||
import Domain.SchemaHelpers
|
||||
require Logger
|
||||
|
||||
@impl true
|
||||
def on_insert(_lsn, _data), do: :ok
|
||||
|
||||
# Account slug changed - disconnect gateways for updated init
|
||||
|
||||
@impl true
|
||||
|
||||
# Account disabled - process as a delete
|
||||
def on_update(
|
||||
lsn,
|
||||
%{"disabled_at" => nil} = old_data,
|
||||
%{"disabled_at" => disabled_at}
|
||||
)
|
||||
when not is_nil(disabled_at) do
|
||||
on_delete(lsn, old_data)
|
||||
end
|
||||
|
||||
# Account soft-deleted - process as a delete
|
||||
def on_update(
|
||||
lsn,
|
||||
%{"deleted_at" => nil} = old_data,
|
||||
%{"deleted_at" => deleted_at}
|
||||
)
|
||||
when not is_nil(deleted_at) do
|
||||
on_delete(lsn, old_data)
|
||||
end
|
||||
|
||||
def on_update(lsn, old_data, data) do
|
||||
old_account = struct_from_params(Accounts.Account, old_data)
|
||||
account = struct_from_params(Accounts.Account, data)
|
||||
change = %Change{lsn: lsn, op: :update, old_struct: old_account, struct: account}
|
||||
|
||||
PubSub.Account.broadcast(account.id, change)
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
||||
def on_delete(lsn, old_data) do
|
||||
account = struct_from_params(Accounts.Account, old_data)
|
||||
change = %Change{lsn: lsn, op: :delete, old_struct: account}
|
||||
|
||||
# TODO: Hard delete
|
||||
# This can be removed upon implementation of hard delete
|
||||
Flows.delete_flows_for(account)
|
||||
|
||||
PubSub.Account.broadcast(account.id, change)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
defmodule Domain.Changes.Hooks.ActorGroupMemberships do
|
||||
@behaviour Domain.Changes.Hooks
|
||||
alias Domain.{Actors, Changes.Change, Flows, PubSub}
|
||||
import Domain.SchemaHelpers
|
||||
|
||||
@impl true
|
||||
def on_insert(lsn, data) do
|
||||
membership = struct_from_params(Actors.Membership, data)
|
||||
change = %Change{lsn: lsn, op: :insert, struct: membership}
|
||||
|
||||
PubSub.Account.broadcast(membership.account_id, change)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def on_update(_lsn, _old_data, _data), do: :ok
|
||||
|
||||
@impl true
|
||||
def on_delete(lsn, old_data) do
|
||||
membership = struct_from_params(Actors.Membership, old_data)
|
||||
change = %Change{lsn: lsn, op: :delete, old_struct: membership}
|
||||
|
||||
# TODO: Hard delete
|
||||
# This can be removed upon implementation of hard delete
|
||||
Flows.delete_flows_for(membership)
|
||||
|
||||
PubSub.Account.broadcast(membership.account_id, change)
|
||||
end
|
||||
end
|
||||
44
elixir/apps/domain/lib/domain/changes/hooks/clients.ex
Normal file
44
elixir/apps/domain/lib/domain/changes/hooks/clients.ex
Normal file
@@ -0,0 +1,44 @@
|
||||
defmodule Domain.Changes.Hooks.Clients do
|
||||
@behaviour Domain.Changes.Hooks
|
||||
alias Domain.{Changes.Change, Clients, Flows, PubSub}
|
||||
import Domain.SchemaHelpers
|
||||
|
||||
@impl true
|
||||
def on_insert(_lsn, _data), do: :ok
|
||||
|
||||
@impl true
|
||||
|
||||
# Soft-delete
|
||||
def on_update(lsn, %{"deleted_at" => nil} = old_data, %{"deleted_at" => deleted_at})
|
||||
when not is_nil(deleted_at) do
|
||||
on_delete(lsn, old_data)
|
||||
end
|
||||
|
||||
# Regular update
|
||||
def on_update(lsn, old_data, data) do
|
||||
old_client = struct_from_params(Clients.Client, old_data)
|
||||
client = struct_from_params(Clients.Client, data)
|
||||
change = %Change{lsn: lsn, op: :update, old_struct: old_client, struct: client}
|
||||
|
||||
# Unverifying a client
|
||||
# This is a special case - we need to delete associated flows when unverifying a client since
|
||||
# it could affect connectivity if any policies are based on the verified status.
|
||||
if not is_nil(old_client.verified_at) and is_nil(client.verified_at) do
|
||||
Flows.delete_flows_for(client)
|
||||
end
|
||||
|
||||
PubSub.Account.broadcast(client.account_id, change)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def on_delete(lsn, old_data) do
|
||||
client = struct_from_params(Clients.Client, old_data)
|
||||
change = %Change{lsn: lsn, op: :delete, old_struct: client}
|
||||
|
||||
# TODO: Hard delete
|
||||
# This can be removed upon implementation of hard delete
|
||||
Flows.delete_flows_for(client)
|
||||
|
||||
PubSub.Account.broadcast(client.account_id, change)
|
||||
end
|
||||
end
|
||||
26
elixir/apps/domain/lib/domain/changes/hooks/flows.ex
Normal file
26
elixir/apps/domain/lib/domain/changes/hooks/flows.ex
Normal file
@@ -0,0 +1,26 @@
|
||||
defmodule Domain.Changes.Hooks.Flows do
|
||||
@behaviour Domain.Changes.Hooks
|
||||
alias Domain.{Changes.Change, Flows, PubSub}
|
||||
import Domain.SchemaHelpers
|
||||
|
||||
@impl true
|
||||
|
||||
# We don't react directly to flow creation events because connection setup
|
||||
# is latency sensitive and we've already broadcasted the relevant message from
|
||||
# client pid to gateway pid directly.
|
||||
def on_insert(_lsn, _data), do: :ok
|
||||
|
||||
@impl true
|
||||
|
||||
# Flows are never updated
|
||||
def on_update(_lsn, _old_data, _data), do: :ok
|
||||
|
||||
@impl true
|
||||
|
||||
# This will trigger reject_access for any subscribed gateways
|
||||
def on_delete(lsn, old_data) do
|
||||
flow = struct_from_params(Flows.Flow, old_data)
|
||||
change = %Change{lsn: lsn, op: :delete, old_struct: flow}
|
||||
PubSub.Account.broadcast(flow.account_id, change)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
defmodule Domain.Changes.Hooks.GatewayGroups do
|
||||
@behaviour Domain.Changes.Hooks
|
||||
alias Domain.{Changes.Change, Gateways, PubSub}
|
||||
import Domain.SchemaHelpers
|
||||
|
||||
@impl true
|
||||
def on_insert(_lsn, _data), do: :ok
|
||||
|
||||
# Soft-delete
|
||||
@impl true
|
||||
def on_update(lsn, %{"deleted_at" => nil} = old_data, %{"deleted_at" => deleted_at})
|
||||
when not is_nil(deleted_at) do
|
||||
on_delete(lsn, old_data)
|
||||
end
|
||||
|
||||
# Regular update
|
||||
def on_update(lsn, old_data, data) do
|
||||
old_gateway_group = struct_from_params(Gateways.Group, old_data)
|
||||
gateway_group = struct_from_params(Gateways.Group, data)
|
||||
change = %Change{lsn: lsn, op: :update, old_struct: old_gateway_group, struct: gateway_group}
|
||||
|
||||
PubSub.Account.broadcast(gateway_group.account_id, change)
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
||||
# Deleting a gateway group will delete the associated resource connection, where
|
||||
# we handle removing it from the client's resource list.
|
||||
def on_delete(_lsn, _old_data), do: :ok
|
||||
end
|
||||
30
elixir/apps/domain/lib/domain/changes/hooks/gateways.ex
Normal file
30
elixir/apps/domain/lib/domain/changes/hooks/gateways.ex
Normal file
@@ -0,0 +1,30 @@
|
||||
defmodule Domain.Changes.Hooks.Gateways do
|
||||
@behaviour Domain.Changes.Hooks
|
||||
alias Domain.{Changes.Change, Flows, Gateways, PubSub}
|
||||
import Domain.SchemaHelpers
|
||||
|
||||
@impl true
|
||||
def on_insert(_lsn, _data), do: :ok
|
||||
|
||||
# Soft-delete
|
||||
@impl true
|
||||
def on_update(lsn, %{"deleted_at" => nil} = old_data, %{"deleted_at" => deleted_at})
|
||||
when not is_nil(deleted_at) do
|
||||
on_delete(lsn, old_data)
|
||||
end
|
||||
|
||||
# Regular update
|
||||
def on_update(_lsn, _old_data, _data), do: :ok
|
||||
|
||||
@impl true
|
||||
def on_delete(lsn, old_data) do
|
||||
gateway = struct_from_params(Gateways.Gateway, old_data)
|
||||
change = %Change{lsn: lsn, op: :delete, old_struct: gateway}
|
||||
|
||||
# TODO: Hard delete
|
||||
# This can be removed upon implementation of hard delete
|
||||
Flows.delete_flows_for(gateway)
|
||||
|
||||
PubSub.Account.broadcast(gateway.account_id, change)
|
||||
end
|
||||
end
|
||||
64
elixir/apps/domain/lib/domain/changes/hooks/policies.ex
Normal file
64
elixir/apps/domain/lib/domain/changes/hooks/policies.ex
Normal file
@@ -0,0 +1,64 @@
|
||||
defmodule Domain.Changes.Hooks.Policies do
|
||||
@behaviour Domain.Changes.Hooks
|
||||
alias Domain.{Changes.Change, Flows, Policies, PubSub}
|
||||
import Domain.SchemaHelpers
|
||||
|
||||
@impl true
|
||||
def on_insert(lsn, data) do
|
||||
policy = struct_from_params(Policies.Policy, data)
|
||||
change = %Change{lsn: lsn, op: :insert, struct: policy}
|
||||
|
||||
PubSub.Account.broadcast(policy.account_id, change)
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
||||
# Disable - process as delete
|
||||
def on_update(lsn, %{"disabled_at" => nil}, %{"disabled_at" => disabled_at} = data)
|
||||
when not is_nil(disabled_at) do
|
||||
on_delete(lsn, data)
|
||||
end
|
||||
|
||||
# Enable - process as insert
|
||||
def on_update(lsn, %{"disabled_at" => disabled_at}, %{"disabled_at" => nil} = data)
|
||||
when not is_nil(disabled_at) do
|
||||
on_insert(lsn, data)
|
||||
end
|
||||
|
||||
# Soft-delete - process as delete
|
||||
def on_update(lsn, %{"deleted_at" => nil} = old_data, %{"deleted_at" => deleted_at})
|
||||
when not is_nil(deleted_at) do
|
||||
on_delete(lsn, old_data)
|
||||
end
|
||||
|
||||
# Regular update
|
||||
def on_update(lsn, old_data, data) do
|
||||
old_policy = struct_from_params(Policies.Policy, old_data)
|
||||
policy = struct_from_params(Policies.Policy, data)
|
||||
change = %Change{lsn: lsn, op: :update, old_struct: old_policy, struct: policy}
|
||||
|
||||
# Breaking updates
|
||||
# This is a special case - we need to delete related flows because connectivity has changed
|
||||
# The Gateway PID will receive flow deletion messages and process them to potentially reject
|
||||
# access. The client PID (if connected) will toggle the resource deleted/created.
|
||||
if old_policy.conditions != policy.conditions or
|
||||
old_policy.actor_group_id != policy.actor_group_id or
|
||||
old_policy.resource_id != policy.resource_id do
|
||||
Flows.delete_flows_for(old_policy)
|
||||
end
|
||||
|
||||
PubSub.Account.broadcast(policy.account_id, change)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def on_delete(lsn, old_data) do
|
||||
policy = struct_from_params(Policies.Policy, old_data)
|
||||
change = %Change{lsn: lsn, op: :delete, old_struct: policy}
|
||||
|
||||
# TODO: Hard delete
|
||||
# This can be removed upon implementation of hard delete
|
||||
Flows.delete_flows_for(policy)
|
||||
|
||||
PubSub.Account.broadcast(policy.account_id, change)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
defmodule Domain.Changes.Hooks.ResourceConnections do
|
||||
@behaviour Domain.Changes.Hooks
|
||||
alias Domain.{Changes.Change, Flows, Resources, PubSub}
|
||||
import Domain.SchemaHelpers
|
||||
|
||||
@impl true
|
||||
def on_insert(lsn, data) do
|
||||
connection = struct_from_params(Resources.Connection, data)
|
||||
change = %Change{lsn: lsn, op: :insert, struct: connection}
|
||||
|
||||
PubSub.Account.broadcast(connection.account_id, change)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def on_update(_lsn, _old_data, _data), do: :ok
|
||||
|
||||
@impl true
|
||||
def on_delete(lsn, old_data) do
|
||||
connection = struct_from_params(Resources.Connection, old_data)
|
||||
change = %Change{lsn: lsn, op: :delete, old_struct: connection}
|
||||
|
||||
Flows.delete_flows_for(connection)
|
||||
|
||||
PubSub.Account.broadcast(connection.account_id, change)
|
||||
end
|
||||
end
|
||||
57
elixir/apps/domain/lib/domain/changes/hooks/resources.ex
Normal file
57
elixir/apps/domain/lib/domain/changes/hooks/resources.ex
Normal file
@@ -0,0 +1,57 @@
|
||||
defmodule Domain.Changes.Hooks.Resources do
|
||||
@behaviour Domain.Changes.Hooks
|
||||
alias Domain.{Changes.Change, Flows, PubSub, Resources}
|
||||
import Domain.SchemaHelpers
|
||||
|
||||
@impl true
|
||||
def on_insert(lsn, data) do
|
||||
resource = struct_from_params(Resources.Resource, data)
|
||||
change = %Change{lsn: lsn, op: :insert, struct: resource}
|
||||
|
||||
PubSub.Account.broadcast(resource.account_id, change)
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
||||
# Soft-delete - process as delete
|
||||
def on_update(lsn, %{"deleted_at" => nil} = old_data, %{"deleted_at" => deleted_at})
|
||||
when not is_nil(deleted_at) do
|
||||
on_delete(lsn, old_data)
|
||||
end
|
||||
|
||||
# Regular update
|
||||
def on_update(lsn, old_data, data) do
|
||||
old_resource = struct_from_params(Resources.Resource, old_data)
|
||||
resource = struct_from_params(Resources.Resource, data)
|
||||
change = %Change{lsn: lsn, op: :update, old_struct: old_resource, struct: resource}
|
||||
|
||||
# Breaking updates
|
||||
|
||||
# This is a special case - we need to delete related flows because connectivity has changed
|
||||
# Gateway _does_ handle resource filter changes so we don't need to delete flows
|
||||
# for those changes - they're processed by the Gateway channel pid.
|
||||
|
||||
# The Gateway channel will process these flow deletions and re-authorize the flow.
|
||||
# However, the gateway will also react to the resource update and send reject_access
|
||||
# so that the Gateway's state is updated correctly, and the client can create a new flow.
|
||||
if old_resource.ip_stack != resource.ip_stack or
|
||||
old_resource.type != resource.type or
|
||||
old_resource.address != resource.address do
|
||||
Flows.delete_flows_for(resource)
|
||||
end
|
||||
|
||||
PubSub.Account.broadcast(resource.account_id, change)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def on_delete(lsn, old_data) do
|
||||
resource = struct_from_params(Resources.Resource, old_data)
|
||||
change = %Change{lsn: lsn, op: :delete, old_struct: resource}
|
||||
|
||||
# TODO: Hard delete
|
||||
# This can be removed upon implementation of hard delete
|
||||
Flows.delete_flows_for(resource)
|
||||
|
||||
PubSub.Account.broadcast(resource.account_id, change)
|
||||
end
|
||||
end
|
||||
51
elixir/apps/domain/lib/domain/changes/hooks/tokens.ex
Normal file
51
elixir/apps/domain/lib/domain/changes/hooks/tokens.ex
Normal file
@@ -0,0 +1,51 @@
|
||||
defmodule Domain.Changes.Hooks.Tokens do
|
||||
@behaviour Domain.Changes.Hooks
|
||||
alias Domain.{Flows, PubSub, Tokens}
|
||||
import Domain.SchemaHelpers
|
||||
|
||||
@impl true
|
||||
def on_insert(_lsn, _data), do: :ok
|
||||
|
||||
@impl true
|
||||
|
||||
# updates for email and relay_group tokens have no side effects
|
||||
def on_update(_lsn, %{"type" => "email"}, _data), do: :ok
|
||||
|
||||
def on_update(_lsn, _old_data, %{"type" => "email"}), do: :ok
|
||||
|
||||
def on_update(_lsn, %{"type" => "relay_group"}, _data), do: :ok
|
||||
|
||||
def on_update(_lsn, _old_data, %{"type" => "relay_group"}), do: :ok
|
||||
|
||||
# Soft-delete - process as delete
|
||||
def on_update(lsn, %{"deleted_at" => nil} = old_data, %{"deleted_at" => deleted_at})
|
||||
when not is_nil(deleted_at) do
|
||||
on_delete(lsn, old_data)
|
||||
end
|
||||
|
||||
# Regular update
|
||||
def on_update(_lsn, _old_data, _new_data), do: :ok
|
||||
|
||||
@impl true
|
||||
def on_delete(_lsn, old_data) do
|
||||
token = struct_from_params(Tokens.Token, old_data)
|
||||
|
||||
# TODO: Hard delete
|
||||
# This can be removed upon implementation of hard delete
|
||||
Flows.delete_flows_for(token)
|
||||
|
||||
# We don't need to broadcast deleted tokens since the disconnect_socket/1
|
||||
# function will handle any disconnects for us directly.
|
||||
|
||||
# Disconnect all sockets using this token
|
||||
disconnect_socket(token)
|
||||
end
|
||||
|
||||
# This is a special message that disconnects all sockets using this token,
|
||||
# such as for LiveViews.
|
||||
defp disconnect_socket(token) do
|
||||
topic = Domain.Tokens.socket_id(token.id)
|
||||
payload = %Phoenix.Socket.Broadcast{topic: topic, event: "disconnect"}
|
||||
PubSub.broadcast(topic, payload)
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,6 @@
|
||||
defmodule Domain.Events.ReplicationConnection do
|
||||
defmodule Domain.Changes.ReplicationConnection do
|
||||
use Domain.Replication.Connection
|
||||
alias Domain.Events.Hooks
|
||||
alias Domain.Changes.Hooks
|
||||
|
||||
@tables_to_hooks %{
|
||||
"accounts" => Hooks.Accounts,
|
||||
@@ -15,12 +15,12 @@ defmodule Domain.Events.ReplicationConnection do
|
||||
"tokens" => Hooks.Tokens
|
||||
}
|
||||
|
||||
def on_write(state, _lsn, op, table, old_data, data) do
|
||||
def on_write(state, lsn, op, table, old_data, data) do
|
||||
if hook = Map.get(@tables_to_hooks, table) do
|
||||
case op do
|
||||
:insert -> :ok = hook.on_insert(data)
|
||||
:update -> :ok = hook.on_update(old_data, data)
|
||||
:delete -> :ok = hook.on_delete(old_data)
|
||||
:insert -> :ok = hook.on_insert(lsn, data)
|
||||
:update -> :ok = hook.on_update(lsn, old_data, data)
|
||||
:delete -> :ok = hook.on_delete(lsn, old_data)
|
||||
end
|
||||
else
|
||||
log_warning(op, table)
|
||||
@@ -31,7 +31,7 @@ defmodule Domain.Events.ReplicationConnection do
|
||||
|
||||
defp log_warning(op, table) do
|
||||
Logger.warning(
|
||||
"No hook defined for #{op} on table #{table}. Please implement Domain.Events.Hooks for this table."
|
||||
"No hook defined for #{op} on table #{table}. Please implement Domain.Changes.Hooks for this table."
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,39 @@
|
||||
defmodule Domain.Clients.Client do
|
||||
use Domain, :schema
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
id: Ecto.UUID.t(),
|
||||
external_id: String.t(),
|
||||
name: String.t(),
|
||||
public_key: String.t(),
|
||||
psk_base: binary(),
|
||||
ipv4: Domain.Types.IP.t(),
|
||||
ipv6: Domain.Types.IP.t(),
|
||||
last_seen_user_agent: String.t(),
|
||||
last_seen_remote_ip: Domain.Types.IP.t(),
|
||||
last_seen_remote_ip_location_region: String.t(),
|
||||
last_seen_remote_ip_location_city: String.t(),
|
||||
last_seen_remote_ip_location_lat: float(),
|
||||
last_seen_remote_ip_location_lon: float(),
|
||||
last_seen_version: String.t(),
|
||||
last_seen_at: DateTime.t(),
|
||||
online?: boolean(),
|
||||
account_id: Ecto.UUID.t(),
|
||||
actor_id: Ecto.UUID.t(),
|
||||
identity_id: Ecto.UUID.t(),
|
||||
last_used_token_id: Ecto.UUID.t(),
|
||||
device_serial: String.t() | nil,
|
||||
device_uuid: String.t() | nil,
|
||||
identifier_for_vendor: String.t() | nil,
|
||||
firebase_installation_id: String.t() | nil,
|
||||
verified_at: DateTime.t() | nil,
|
||||
verified_by: :system | :actor | :identity | nil,
|
||||
verified_by_subject: map() | nil,
|
||||
deleted_at: DateTime.t() | nil,
|
||||
inserted_at: DateTime.t(),
|
||||
updated_at: DateTime.t()
|
||||
}
|
||||
|
||||
schema "clients" do
|
||||
field :external_id, :string
|
||||
|
||||
|
||||
@@ -323,12 +323,12 @@ defmodule Domain.Config.Definitions do
|
||||
@doc """
|
||||
Name of the replication slot used by Firezone.
|
||||
"""
|
||||
defconfig(:database_events_replication_slot_name, :string, default: "events_slot")
|
||||
defconfig(:database_changes_replication_slot_name, :string, default: "changes_slot")
|
||||
|
||||
@doc """
|
||||
Name of the publication used by Firezone.
|
||||
"""
|
||||
defconfig(:database_events_publication_name, :string, default: "events")
|
||||
defconfig(:database_changes_publication_name, :string, default: "changes")
|
||||
|
||||
@doc """
|
||||
Name of the replication slot used by Firezone.
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
defmodule Domain.Events.Hooks do
|
||||
@moduledoc """
|
||||
A simple behavior to define hooks needed for processing WAL events.
|
||||
"""
|
||||
|
||||
@callback on_insert(data :: map()) :: :ok | {:error, term()}
|
||||
@callback on_update(old_data :: map(), data :: map()) :: :ok | {:error, term()}
|
||||
@callback on_delete(old_data :: map()) :: :ok | {:error, term()}
|
||||
end
|
||||
@@ -1,48 +0,0 @@
|
||||
defmodule Domain.Events.Hooks.Accounts do
|
||||
@behaviour Domain.Events.Hooks
|
||||
alias Domain.{Accounts, PubSub, SchemaHelpers}
|
||||
require Logger
|
||||
|
||||
@impl true
|
||||
def on_insert(_data), do: :ok
|
||||
|
||||
# Account slug changed - disconnect gateways for updated init
|
||||
|
||||
@impl true
|
||||
|
||||
# Account disabled - process as a delete
|
||||
def on_update(
|
||||
%{"disabled_at" => nil} = old_data,
|
||||
%{"disabled_at" => disabled_at}
|
||||
)
|
||||
when not is_nil(disabled_at) do
|
||||
on_delete(old_data)
|
||||
end
|
||||
|
||||
# Account soft-deleted - process as a delete
|
||||
def on_update(
|
||||
%{"deleted_at" => nil} = old_data,
|
||||
%{"deleted_at" => deleted_at}
|
||||
)
|
||||
when not is_nil(deleted_at) do
|
||||
on_delete(old_data)
|
||||
end
|
||||
|
||||
def on_update(old_data, data) do
|
||||
old_account = SchemaHelpers.struct_from_params(Accounts.Account, old_data)
|
||||
account = SchemaHelpers.struct_from_params(Accounts.Account, data)
|
||||
PubSub.Account.broadcast(account.id, {:updated, old_account, account})
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
||||
def on_delete(old_data) do
|
||||
account = SchemaHelpers.struct_from_params(Accounts.Account, old_data)
|
||||
|
||||
# TODO: Hard delete
|
||||
# This can be removed upon implementation of hard delete
|
||||
Domain.Flows.delete_flows_for(account)
|
||||
|
||||
PubSub.Account.broadcast(account.id, {:deleted, account})
|
||||
end
|
||||
end
|
||||
@@ -1,24 +0,0 @@
|
||||
defmodule Domain.Events.Hooks.ActorGroupMemberships do
|
||||
@behaviour Domain.Events.Hooks
|
||||
alias Domain.{Actors, SchemaHelpers, PubSub}
|
||||
|
||||
@impl true
|
||||
def on_insert(data) do
|
||||
membership = SchemaHelpers.struct_from_params(Actors.Membership, data)
|
||||
PubSub.Account.broadcast(membership.account_id, {:created, membership})
|
||||
end
|
||||
|
||||
@impl true
|
||||
def on_update(_old_data, _data), do: :ok
|
||||
|
||||
@impl true
|
||||
def on_delete(old_data) do
|
||||
membership = SchemaHelpers.struct_from_params(Actors.Membership, old_data)
|
||||
|
||||
# TODO: Hard delete
|
||||
# This can be removed upon implementation of hard delete
|
||||
Domain.Flows.delete_flows_for(membership)
|
||||
|
||||
PubSub.Account.broadcast(membership.account_id, {:deleted, membership})
|
||||
end
|
||||
end
|
||||
@@ -1,41 +0,0 @@
|
||||
defmodule Domain.Events.Hooks.Clients do
|
||||
@behaviour Domain.Events.Hooks
|
||||
alias Domain.{Clients, SchemaHelpers, PubSub}
|
||||
|
||||
@impl true
|
||||
def on_insert(_data), do: :ok
|
||||
|
||||
@impl true
|
||||
|
||||
# Soft-delete
|
||||
def on_update(%{"deleted_at" => nil} = old_data, %{"deleted_at" => deleted_at})
|
||||
when not is_nil(deleted_at) do
|
||||
on_delete(old_data)
|
||||
end
|
||||
|
||||
# Regular update
|
||||
def on_update(old_data, data) do
|
||||
old_client = SchemaHelpers.struct_from_params(Clients.Client, old_data)
|
||||
client = SchemaHelpers.struct_from_params(Clients.Client, data)
|
||||
|
||||
# Unverifying a client
|
||||
# This is a special case - we need to delete associated flows when unverifying a client since
|
||||
# it could affect connectivity if any policies are based on the verified status.
|
||||
if not is_nil(old_client.verified_at) and is_nil(client.verified_at) do
|
||||
Domain.Flows.delete_flows_for(client)
|
||||
end
|
||||
|
||||
PubSub.Account.broadcast(client.account_id, {:updated, old_client, client})
|
||||
end
|
||||
|
||||
@impl true
|
||||
def on_delete(old_data) do
|
||||
client = SchemaHelpers.struct_from_params(Clients.Client, old_data)
|
||||
|
||||
# TODO: Hard delete
|
||||
# This can be removed upon implementation of hard delete
|
||||
Domain.Flows.delete_flows_for(client)
|
||||
|
||||
PubSub.Account.broadcast(client.account_id, {:deleted, client})
|
||||
end
|
||||
end
|
||||
@@ -1,24 +0,0 @@
|
||||
defmodule Domain.Events.Hooks.Flows do
|
||||
@behaviour Domain.Events.Hooks
|
||||
alias Domain.{Flows, PubSub, SchemaHelpers}
|
||||
|
||||
@impl true
|
||||
|
||||
# We don't react directly to flow creation events because connection setup
|
||||
# is latency sensitive and we've already broadcasted the relevant message from
|
||||
# client pid to gateway pid directly.
|
||||
def on_insert(_data), do: :ok
|
||||
|
||||
@impl true
|
||||
|
||||
# Flows are never updated
|
||||
def on_update(_old_data, _data), do: :ok
|
||||
|
||||
@impl true
|
||||
|
||||
# This will trigger reject_access for any subscribed gateways
|
||||
def on_delete(old_data) do
|
||||
flow = SchemaHelpers.struct_from_params(Flows.Flow, old_data)
|
||||
PubSub.Account.broadcast(flow.account_id, {:deleted, flow})
|
||||
end
|
||||
end
|
||||
@@ -1,31 +0,0 @@
|
||||
defmodule Domain.Events.Hooks.GatewayGroups do
|
||||
@behaviour Domain.Events.Hooks
|
||||
alias Domain.{Gateways, PubSub, SchemaHelpers}
|
||||
|
||||
@impl true
|
||||
def on_insert(_data), do: :ok
|
||||
|
||||
# Soft-delete
|
||||
@impl true
|
||||
def on_update(%{"deleted_at" => nil} = old_data, %{"deleted_at" => deleted_at})
|
||||
when not is_nil(deleted_at) do
|
||||
on_delete(old_data)
|
||||
end
|
||||
|
||||
# Regular update
|
||||
def on_update(old_data, data) do
|
||||
old_gateway_group = SchemaHelpers.struct_from_params(Gateways.Group, old_data)
|
||||
gateway_group = SchemaHelpers.struct_from_params(Gateways.Group, data)
|
||||
|
||||
PubSub.Account.broadcast(
|
||||
gateway_group.account_id,
|
||||
{:updated, old_gateway_group, gateway_group}
|
||||
)
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
||||
# Deleting a gateway group will delete the associated resource connection, where
|
||||
# we handle removing it from the client's resource list.
|
||||
def on_delete(_old_data), do: :ok
|
||||
end
|
||||
@@ -1,28 +0,0 @@
|
||||
defmodule Domain.Events.Hooks.Gateways do
|
||||
@behaviour Domain.Events.Hooks
|
||||
alias Domain.{Gateways, PubSub, SchemaHelpers}
|
||||
|
||||
@impl true
|
||||
def on_insert(_data), do: :ok
|
||||
|
||||
# Soft-delete
|
||||
@impl true
|
||||
def on_update(%{"deleted_at" => nil} = old_data, %{"deleted_at" => deleted_at})
|
||||
when not is_nil(deleted_at) do
|
||||
on_delete(old_data)
|
||||
end
|
||||
|
||||
# Regular update
|
||||
def on_update(_old_data, _data), do: :ok
|
||||
|
||||
@impl true
|
||||
def on_delete(old_data) do
|
||||
gateway = SchemaHelpers.struct_from_params(Gateways.Gateway, old_data)
|
||||
|
||||
# TODO: Hard delete
|
||||
# This can be removed upon implementation of hard delete
|
||||
Domain.Flows.delete_flows_for(gateway)
|
||||
|
||||
PubSub.Account.broadcast(gateway.account_id, {:deleted, gateway})
|
||||
end
|
||||
end
|
||||
@@ -1,60 +0,0 @@
|
||||
defmodule Domain.Events.Hooks.Policies do
|
||||
@behaviour Domain.Events.Hooks
|
||||
alias Domain.{Policies, PubSub, SchemaHelpers}
|
||||
require Logger
|
||||
|
||||
@impl true
|
||||
def on_insert(data) do
|
||||
policy = SchemaHelpers.struct_from_params(Policies.Policy, data)
|
||||
PubSub.Account.broadcast(policy.account_id, {:created, policy})
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
||||
# Disable - process as delete
|
||||
def on_update(%{"disabled_at" => nil}, %{"disabled_at" => disabled_at} = data)
|
||||
when not is_nil(disabled_at) do
|
||||
on_delete(data)
|
||||
end
|
||||
|
||||
# Enable - process as insert
|
||||
def on_update(%{"disabled_at" => disabled_at}, %{"disabled_at" => nil} = data)
|
||||
when not is_nil(disabled_at) do
|
||||
on_insert(data)
|
||||
end
|
||||
|
||||
# Soft-delete - process as delete
|
||||
def on_update(%{"deleted_at" => nil} = old_data, %{"deleted_at" => deleted_at})
|
||||
when not is_nil(deleted_at) do
|
||||
on_delete(old_data)
|
||||
end
|
||||
|
||||
# Regular update
|
||||
def on_update(old_data, data) do
|
||||
old_policy = SchemaHelpers.struct_from_params(Policies.Policy, old_data)
|
||||
policy = SchemaHelpers.struct_from_params(Policies.Policy, data)
|
||||
|
||||
# Breaking updates
|
||||
# This is a special case - we need to delete related flows because connectivity has changed
|
||||
# The Gateway PID will receive flow deletion messages and process them to potentially reject
|
||||
# access. The client PID (if connected) will toggle the resource deleted/created.
|
||||
if old_policy.conditions != policy.conditions or
|
||||
old_policy.actor_group_id != policy.actor_group_id or
|
||||
old_policy.resource_id != policy.resource_id do
|
||||
Domain.Flows.delete_flows_for(policy)
|
||||
end
|
||||
|
||||
PubSub.Account.broadcast(policy.account_id, {:updated, old_policy, policy})
|
||||
end
|
||||
|
||||
@impl true
|
||||
def on_delete(old_data) do
|
||||
policy = SchemaHelpers.struct_from_params(Policies.Policy, old_data)
|
||||
|
||||
# TODO: Hard delete
|
||||
# This can be removed upon implementation of hard delete
|
||||
Domain.Flows.delete_flows_for(policy)
|
||||
|
||||
PubSub.Account.broadcast(policy.account_id, {:deleted, policy})
|
||||
end
|
||||
end
|
||||
@@ -1,19 +0,0 @@
|
||||
defmodule Domain.Events.Hooks.ResourceConnections do
|
||||
@behaviour Domain.Events.Hooks
|
||||
alias Domain.{SchemaHelpers, Resources, PubSub}
|
||||
|
||||
@impl true
|
||||
def on_insert(data) do
|
||||
connection = SchemaHelpers.struct_from_params(Resources.Connection, data)
|
||||
PubSub.Account.broadcast(connection.account_id, {:created, connection})
|
||||
end
|
||||
|
||||
@impl true
|
||||
def on_update(_old_data, _data), do: :ok
|
||||
|
||||
@impl true
|
||||
def on_delete(old_data) do
|
||||
connection = SchemaHelpers.struct_from_params(Resources.Connection, old_data)
|
||||
PubSub.Account.broadcast(connection.account_id, {:deleted, connection})
|
||||
end
|
||||
end
|
||||
@@ -1,52 +0,0 @@
|
||||
defmodule Domain.Events.Hooks.Resources do
|
||||
@behaviour Domain.Events.Hooks
|
||||
alias Domain.{SchemaHelpers, PubSub, Resources}
|
||||
|
||||
@impl true
|
||||
def on_insert(data) do
|
||||
resource = SchemaHelpers.struct_from_params(Resources.Resource, data)
|
||||
PubSub.Account.broadcast(resource.account_id, {:created, resource})
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
||||
# Soft-delete - process as delete
|
||||
def on_update(%{"deleted_at" => nil} = old_data, %{"deleted_at" => deleted_at})
|
||||
when not is_nil(deleted_at) do
|
||||
on_delete(old_data)
|
||||
end
|
||||
|
||||
# Regular update
|
||||
def on_update(old_data, data) do
|
||||
old_resource = SchemaHelpers.struct_from_params(Resources.Resource, old_data)
|
||||
resource = SchemaHelpers.struct_from_params(Resources.Resource, data)
|
||||
|
||||
# Breaking updates
|
||||
|
||||
# This is a special case - we need to delete related flows because connectivity has changed
|
||||
# Gateway _does_ handle resource filter changes so we don't need to delete flows
|
||||
# for those changes - they're processed by the Gateway channel pid.
|
||||
|
||||
# The Gateway channel will process these flow deletions and end up sending reject_access for any
|
||||
# affected flows. If the client is connected at the time of the update, it will handle this
|
||||
# by toggling the resource deleted then created.
|
||||
if old_resource.ip_stack != resource.ip_stack or
|
||||
old_resource.type != resource.type or
|
||||
old_resource.address != resource.address do
|
||||
Domain.Flows.delete_flows_for(resource)
|
||||
end
|
||||
|
||||
PubSub.Account.broadcast(resource.account_id, {:updated, old_resource, resource})
|
||||
end
|
||||
|
||||
@impl true
|
||||
def on_delete(old_data) do
|
||||
resource = SchemaHelpers.struct_from_params(Resources.Resource, old_data)
|
||||
|
||||
# TODO: Hard delete
|
||||
# This can be removed upon implementation of hard delete
|
||||
Domain.Flows.delete_flows_for(resource)
|
||||
|
||||
PubSub.Account.broadcast(resource.account_id, {:deleted, resource})
|
||||
end
|
||||
end
|
||||
@@ -1,45 +0,0 @@
|
||||
defmodule Domain.Events.Hooks.Tokens do
|
||||
@behaviour Domain.Events.Hooks
|
||||
alias Domain.{PubSub, Tokens, SchemaHelpers}
|
||||
|
||||
@impl true
|
||||
def on_insert(_data), do: :ok
|
||||
|
||||
@impl true
|
||||
|
||||
# updates for email tokens have no side effects
|
||||
def on_update(%{"type" => "email"}, _data), do: :ok
|
||||
|
||||
def on_update(_old_data, %{"type" => "email"}), do: :ok
|
||||
|
||||
# Soft-delete - process as delete
|
||||
def on_update(%{"deleted_at" => nil} = old_data, %{"deleted_at" => deleted_at})
|
||||
when not is_nil(deleted_at) do
|
||||
on_delete(old_data)
|
||||
end
|
||||
|
||||
# Regular update
|
||||
def on_update(_old_data, _new_data), do: :ok
|
||||
|
||||
@impl true
|
||||
def on_delete(old_data) do
|
||||
token = SchemaHelpers.struct_from_params(Tokens.Token, old_data)
|
||||
|
||||
# Disconnect all sockets using this token
|
||||
disconnect_socket(token)
|
||||
|
||||
# TODO: Hard delete
|
||||
# This can be removed upon implementation of hard delete
|
||||
Domain.Flows.delete_flows_for(token)
|
||||
|
||||
PubSub.Account.broadcast(token.account_id, {:deleted, token})
|
||||
end
|
||||
|
||||
# This is a special message that disconnects all sockets using this token,
|
||||
# such as for LiveViews.
|
||||
defp disconnect_socket(token) do
|
||||
topic = Domain.Tokens.socket_id(token.id)
|
||||
payload = %Phoenix.Socket.Broadcast{topic: topic, event: "disconnect"}
|
||||
Domain.PubSub.broadcast(topic, payload)
|
||||
end
|
||||
end
|
||||
@@ -20,7 +20,7 @@ defmodule Domain.Flows do
|
||||
account_id: account_id
|
||||
},
|
||||
resource_id,
|
||||
%Policies.Policy{} = policy,
|
||||
policy_id,
|
||||
membership_id,
|
||||
%Auth.Subject{
|
||||
account: %{id: account_id},
|
||||
@@ -42,7 +42,7 @@ defmodule Domain.Flows do
|
||||
flow =
|
||||
Flow.Changeset.create(%{
|
||||
token_id: token_id,
|
||||
policy_id: policy.id,
|
||||
policy_id: policy_id,
|
||||
client_id: client_id,
|
||||
gateway_id: gateway_id,
|
||||
resource_id: resource_id,
|
||||
@@ -71,20 +71,25 @@ defmodule Domain.Flows do
|
||||
# to the Resource beyond its intended expiration time.
|
||||
#
|
||||
# So, we use the minimum of either the policy condition or the origin flow's expiration time.
|
||||
# This will be much smoother once https://github.com/firezone/firezone/issues/10074 is implemented,
|
||||
# since we won't need to be so careful about reject_access messages to the gateway.
|
||||
def reauthorize_flow(%Flow{} = flow) do
|
||||
with {:ok, client} <- Clients.fetch_client_by_id(flow.client_id, preload: :identity),
|
||||
# TODO: Hard delete
|
||||
# We need to ensure token and gateway haven't been deleted after the initial flow was created
|
||||
# This can be removed after hard-delete since we'll get a DB error if these associations no longer exist
|
||||
{:ok, _token} <- Tokens.fetch_token_by_id(flow.token_id),
|
||||
{:ok, _gateway} <- Gateways.fetch_gateway_by_id(flow.gateway_id),
|
||||
{:ok, gateway} <- Gateways.fetch_gateway_by_id(flow.gateway_id),
|
||||
# We only want to reauthorize the resource for this gateway if the resource is still connected to its
|
||||
# gateway_group.
|
||||
policies when policies != [] <-
|
||||
Policies.all_policies_for_resource_id_and_actor_id!(
|
||||
Policies.all_policies_in_gateway_group_for_resource_id_and_actor_id!(
|
||||
flow.account_id,
|
||||
gateway.group_id,
|
||||
flow.resource_id,
|
||||
client.actor_id
|
||||
),
|
||||
{:ok, expires_at, policy} <-
|
||||
{:ok, policy, expires_at} <-
|
||||
Policies.longest_conforming_policy_for_client(policies, client, flow.expires_at),
|
||||
{:ok, membership} <-
|
||||
Actors.fetch_membership_by_actor_id_and_group_id(
|
||||
@@ -106,14 +111,20 @@ defmodule Domain.Flows do
|
||||
expires_at: expires_at
|
||||
})
|
||||
|> Repo.insert() do
|
||||
Logger.info("Reauthorized flow",
|
||||
old_flow: inspect(flow),
|
||||
new_flow: inspect(new_flow)
|
||||
)
|
||||
|
||||
{:ok, new_flow}
|
||||
else
|
||||
reason ->
|
||||
Logger.info("Failed to reauthorize flow",
|
||||
old_flow: inspect(flow),
|
||||
reason: inspect(reason)
|
||||
)
|
||||
|
||||
{:error, :forbidden}
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
@@ -226,6 +237,14 @@ defmodule Domain.Flows do
|
||||
|> Repo.delete_all()
|
||||
end
|
||||
|
||||
def delete_flows_for(%Domain.Resources.Connection{} = connection) do
|
||||
Flow.Query.all()
|
||||
|> Flow.Query.by_account_id(connection.account_id)
|
||||
|> Flow.Query.by_resource_id(connection.resource_id)
|
||||
|> Flow.Query.by_gateway_group_id(connection.gateway_group_id)
|
||||
|> Repo.delete_all()
|
||||
end
|
||||
|
||||
def delete_flows_for(%Domain.Tokens.Token{account_id: nil}) do
|
||||
# Tokens without an account_id are not associated with any flows. I.e. global relay tokens
|
||||
{0, []}
|
||||
@@ -238,14 +257,12 @@ defmodule Domain.Flows do
|
||||
|> Repo.delete_all()
|
||||
end
|
||||
|
||||
def delete_stale_flows_on_connect(%Clients.Client{} = client, resources)
|
||||
when is_list(resources) do
|
||||
authorized_resource_ids = Enum.map(resources, & &1.id)
|
||||
|
||||
def delete_stale_flows_on_connect(%Clients.Client{} = client, resource_ids)
|
||||
when is_list(resource_ids) do
|
||||
Flow.Query.all()
|
||||
|> Flow.Query.by_account_id(client.account_id)
|
||||
|> Flow.Query.by_client_id(client.id)
|
||||
|> Flow.Query.by_not_in_resource_ids(authorized_resource_ids)
|
||||
|> Flow.Query.by_not_in_resource_ids(resource_ids)
|
||||
|> Repo.delete_all()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
defmodule Domain.Flows.Flow do
|
||||
use Domain, :schema
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
id: Ecto.UUID.t(),
|
||||
policy_id: Ecto.UUID.t(),
|
||||
client_id: Ecto.UUID.t(),
|
||||
gateway_id: Ecto.UUID.t(),
|
||||
resource_id: Ecto.UUID.t(),
|
||||
token_id: Ecto.UUID.t(),
|
||||
actor_group_membership_id: Ecto.UUID.t(),
|
||||
account_id: Ecto.UUID.t(),
|
||||
client_remote_ip: Domain.Types.IP.t(),
|
||||
client_user_agent: String.t(),
|
||||
gateway_remote_ip: Domain.Types.IP.t(),
|
||||
expires_at: DateTime.t(),
|
||||
inserted_at: DateTime.t()
|
||||
}
|
||||
|
||||
schema "flows" do
|
||||
belongs_to :policy, Domain.Policies.Policy
|
||||
belongs_to :client, Domain.Clients.Client
|
||||
|
||||
@@ -49,6 +49,12 @@ defmodule Domain.Flows.Flow.Query do
|
||||
where(queryable, [flows: flows], flows.actor_group_membership_id == ^membership_id)
|
||||
end
|
||||
|
||||
def by_gateway_group_id(queryable, gateway_group_id) do
|
||||
queryable
|
||||
|> with_joined_gateway_group()
|
||||
|> where([gateway_group: gateway_group], gateway_group.id == ^gateway_group_id)
|
||||
end
|
||||
|
||||
def by_identity_id(queryable, identity_id) do
|
||||
queryable
|
||||
|> with_joined_client()
|
||||
@@ -103,6 +109,22 @@ defmodule Domain.Flows.Flow.Query do
|
||||
end)
|
||||
end
|
||||
|
||||
def with_joined_gateway_group(queryable) do
|
||||
queryable
|
||||
|> with_joined_gateway()
|
||||
|> with_named_binding(:gateway_group, fn queryable, binding ->
|
||||
join(queryable, :inner, [gateway: gateway], gateway_group in assoc(gateway, :group),
|
||||
as: ^binding
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
def with_joined_gateway(queryable) do
|
||||
with_named_binding(queryable, :gateway, fn queryable, binding ->
|
||||
join(queryable, :inner, [flows: flows], gateway in assoc(flows, ^binding), as: ^binding)
|
||||
end)
|
||||
end
|
||||
|
||||
# Pagination
|
||||
|
||||
@impl Domain.Repo.Query
|
||||
|
||||
@@ -2,9 +2,11 @@ defmodule Domain.Gateways do
|
||||
use Supervisor
|
||||
alias Domain.Accounts.Account
|
||||
alias Domain.{Repo, Auth, Geo}
|
||||
alias Domain.{Accounts, Resources, Tokens, Billing}
|
||||
alias Domain.{Accounts, Cache, Clients, Resources, Tokens, Billing}
|
||||
alias Domain.Gateways.{Authorizer, Gateway, Group, Presence}
|
||||
|
||||
require Logger
|
||||
|
||||
def start_link(opts) do
|
||||
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
@@ -264,47 +266,30 @@ defmodule Domain.Gateways do
|
||||
|> Map.keys()
|
||||
end
|
||||
|
||||
def all_connected_gateways_for_resource(
|
||||
%Resources.Resource{} = resource,
|
||||
%Auth.Subject{} = subject,
|
||||
opts \\ []
|
||||
def all_compatible_gateways_for_client_and_resource(
|
||||
%Clients.Client{} = client,
|
||||
%Cache.Cacheable.Resource{} = resource,
|
||||
%Auth.Subject{} = subject
|
||||
) do
|
||||
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.connect_gateways_permission()) do
|
||||
{preload, _opts} = Keyword.pop(opts, :preload, [])
|
||||
resource_id = Ecto.UUID.load!(resource.id)
|
||||
|
||||
connected_gateway_ids =
|
||||
Presence.Account.list(resource.account_id)
|
||||
Presence.Account.list(subject.account.id)
|
||||
|> Map.keys()
|
||||
|
||||
gateways =
|
||||
Gateway.Query.not_deleted()
|
||||
|> Gateway.Query.by_ids(connected_gateway_ids)
|
||||
|> Gateway.Query.by_account_id(resource.account_id)
|
||||
|> Gateway.Query.by_resource_id(resource.id)
|
||||
|> Gateway.Query.by_account_id(subject.account.id)
|
||||
|> Gateway.Query.by_resource_id(resource_id)
|
||||
|> Repo.all()
|
||||
|> Repo.preload(preload)
|
||||
|> filter_compatible_gateways(resource, client.last_seen_version)
|
||||
|
||||
{:ok, gateways}
|
||||
end
|
||||
end
|
||||
|
||||
def gateway_can_connect_to_resource?(%Gateway{} = gateway, %Resources.Resource{} = resource) do
|
||||
connected_gateway_ids =
|
||||
Presence.Account.list(resource.account_id)
|
||||
|> Map.keys()
|
||||
|
||||
cond do
|
||||
gateway.id not in connected_gateway_ids ->
|
||||
false
|
||||
|
||||
not Resources.connected?(resource, gateway) ->
|
||||
false
|
||||
|
||||
true ->
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def change_gateway(%Gateway{} = gateway, attrs \\ %{}) do
|
||||
Gateway.Changeset.update(gateway, attrs)
|
||||
end
|
||||
@@ -419,4 +404,35 @@ defmodule Domain.Gateways do
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
# Filters gateways by the resource type, gateway version, and client version.
|
||||
# We support gateways running in one less minor and one greater minor version than the client.
|
||||
# So 1.2.x clients are compatible with 1.1.x and 1.3.x gateways, but not with 1.0.x or 1.4.x.
|
||||
# The internet resource requires gateway 1.3.0 or greater.
|
||||
defp filter_compatible_gateways(gateways, resource, client_version) do
|
||||
case Version.parse(client_version) do
|
||||
{:ok, version} ->
|
||||
gateways
|
||||
|> Enum.filter(fn gateway ->
|
||||
case Version.parse(gateway.last_seen_version) do
|
||||
{:ok, gateway_version} ->
|
||||
Version.match?(gateway_version, ">= #{version.major}.#{version.minor - 1}.0") and
|
||||
Version.match?(gateway_version, "< #{version.major}.#{version.minor + 2}.0") and
|
||||
not is_nil(
|
||||
Resources.adapt_resource_for_version(resource, gateway.last_seen_version)
|
||||
)
|
||||
|
||||
_ ->
|
||||
Logger.warning("Unable to parse gateway version: #{gateway.last_seen_version}")
|
||||
|
||||
false
|
||||
end
|
||||
end)
|
||||
|
||||
:error ->
|
||||
Logger.warning("Unable to parse client version: #{client_version}")
|
||||
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
defmodule Domain.Gateways.Gateway do
|
||||
use Domain, :schema
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
id: Ecto.UUID.t(),
|
||||
external_id: String.t(),
|
||||
name: String.t(),
|
||||
public_key: String.t(),
|
||||
psk_base: binary(),
|
||||
ipv4: Domain.Types.IP.t(),
|
||||
ipv6: Domain.Types.IP.t(),
|
||||
last_seen_user_agent: String.t(),
|
||||
last_seen_remote_ip: Domain.Types.IP.t(),
|
||||
last_seen_remote_ip_location_region: String.t(),
|
||||
last_seen_remote_ip_location_city: String.t(),
|
||||
last_seen_remote_ip_location_lat: float(),
|
||||
last_seen_remote_ip_location_lon: float(),
|
||||
last_seen_version: String.t(),
|
||||
last_seen_at: DateTime.t(),
|
||||
online?: boolean(),
|
||||
account_id: Ecto.UUID.t(),
|
||||
group_id: Ecto.UUID.t(),
|
||||
deleted_at: DateTime.t(),
|
||||
inserted_at: DateTime.t(),
|
||||
updated_at: DateTime.t()
|
||||
}
|
||||
|
||||
schema "gateways" do
|
||||
field :external_id, :string
|
||||
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
defmodule Domain.Gateways.Group do
|
||||
use Domain, :schema
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
id: Ecto.UUID.t(),
|
||||
name: String.t(),
|
||||
managed_by: :account | :system,
|
||||
account_id: Ecto.UUID.t(),
|
||||
created_by: :actor | :identity | :system,
|
||||
created_by_subject: map(),
|
||||
deleted_at: DateTime.t() | nil,
|
||||
inserted_at: DateTime.t(),
|
||||
updated_at: DateTime.t()
|
||||
}
|
||||
|
||||
schema "gateway_groups" do
|
||||
field :name, :string
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
defmodule Domain.Policies do
|
||||
alias Domain.Repo
|
||||
alias Domain.{Auth, Actors, Clients, Resources}
|
||||
alias Domain.{Auth, Actors, Cache.Cacheable, Clients, Resources}
|
||||
alias Domain.Policies.{Authorizer, Policy, Condition}
|
||||
|
||||
def fetch_policy_by_id(id, %Auth.Subject{} = subject, opts \\ []) do
|
||||
@@ -58,10 +58,9 @@ defmodule Domain.Policies do
|
||||
end
|
||||
end
|
||||
|
||||
def all_policies_for_actor!(%Actors.Actor{} = actor) do
|
||||
def all_policies_for_actor_id!(actor_id) do
|
||||
Policy.Query.not_disabled()
|
||||
|> Policy.Query.by_account_id(actor.account_id)
|
||||
|> Policy.Query.by_actor_id(actor.id)
|
||||
|> Policy.Query.by_actor_id(actor_id)
|
||||
|> Policy.Query.with_preloaded_resource_gateway_groups()
|
||||
|> Repo.all()
|
||||
end
|
||||
@@ -74,10 +73,16 @@ defmodule Domain.Policies do
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def all_policies_for_resource_id_and_actor_id!(account_id, resource_id, actor_id) do
|
||||
def all_policies_in_gateway_group_for_resource_id_and_actor_id!(
|
||||
account_id,
|
||||
gateway_group_id,
|
||||
resource_id,
|
||||
actor_id
|
||||
) do
|
||||
Policy.Query.not_disabled()
|
||||
|> Policy.Query.by_account_id(account_id)
|
||||
|> Policy.Query.by_resource_id(resource_id)
|
||||
|> Policy.Query.by_gateway_group_id(gateway_group_id)
|
||||
|> Policy.Query.by_actor_id(actor_id)
|
||||
|> Repo.all()
|
||||
end
|
||||
@@ -218,17 +223,36 @@ defmodule Domain.Policies do
|
||||
succeeded
|
||||
|> Enum.max_by(fn {expires_at, _policy} -> expires_at || @infinity end)
|
||||
|
||||
{:ok, min_expires_at(expires_at, token_expires_at), policy}
|
||||
{:ok, policy, min_expires_at(expires_at, token_expires_at)}
|
||||
end
|
||||
end
|
||||
|
||||
# All client tokens have *some* expiration
|
||||
def min_expires_at(nil, nil),
|
||||
def ensure_client_conforms_policy_conditions(
|
||||
%Clients.Client{} = client,
|
||||
%__MODULE__.Policy{} = policy
|
||||
) do
|
||||
ensure_client_conforms_policy_conditions(client, Cacheable.to_cache(policy))
|
||||
end
|
||||
|
||||
def ensure_client_conforms_policy_conditions(
|
||||
%Clients.Client{} = client,
|
||||
%Cacheable.Policy{} = policy
|
||||
) do
|
||||
case Condition.Evaluator.ensure_conforms(policy.conditions, client) do
|
||||
{:ok, expires_at} ->
|
||||
{:ok, expires_at}
|
||||
|
||||
{:error, violated_properties} ->
|
||||
{:error, {:forbidden, violated_properties: violated_properties}}
|
||||
end
|
||||
end
|
||||
|
||||
defp min_expires_at(nil, nil),
|
||||
do: raise("Both policy_expires_at and token_expires_at cannot be nil")
|
||||
|
||||
def min_expires_at(nil, token_expires_at), do: token_expires_at
|
||||
defp min_expires_at(nil, token_expires_at), do: token_expires_at
|
||||
|
||||
def min_expires_at(%DateTime{} = policy_expires_at, %DateTime{} = token_expires_at) do
|
||||
defp min_expires_at(%DateTime{} = policy_expires_at, %DateTime{} = token_expires_at) do
|
||||
if DateTime.compare(policy_expires_at, token_expires_at) == :lt do
|
||||
policy_expires_at
|
||||
else
|
||||
@@ -243,14 +267,4 @@ defmodule Domain.Policies do
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
end
|
||||
|
||||
defp ensure_client_conforms_policy_conditions(%Clients.Client{} = client, %Policy{} = policy) do
|
||||
case Condition.Evaluator.ensure_conforms(policy.conditions, client) do
|
||||
{:ok, expires_at} ->
|
||||
{:ok, expires_at}
|
||||
|
||||
{:error, violated_properties} ->
|
||||
{:error, {:forbidden, violated_properties: violated_properties}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
defmodule Domain.Policies.Condition do
|
||||
use Domain, :schema
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
property:
|
||||
:remote_ip_location_region
|
||||
| :remote_ip
|
||||
| :provider_id
|
||||
| :current_utc_datetime
|
||||
| :client_verified,
|
||||
operator:
|
||||
:contains
|
||||
| :does_not_contain
|
||||
| :is_in
|
||||
| :is_not_in
|
||||
| :is_in_day_of_week_time_ranges
|
||||
| :is_in_cidr
|
||||
| :is_not_in_cidr
|
||||
| :is,
|
||||
values: [String.t()]
|
||||
}
|
||||
|
||||
@primary_key false
|
||||
embedded_schema do
|
||||
field :property, Ecto.Enum, values: ~w[
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
defmodule Domain.Policies.Condition.Evaluator do
|
||||
alias Domain.Clients
|
||||
alias Domain.Policies.Condition
|
||||
|
||||
@days_of_week ~w[M T W R F S U]
|
||||
|
||||
@@ -35,7 +34,7 @@ defmodule Domain.Policies.Condition.Evaluator do
|
||||
do: Enum.min([expires_at, min_expires_at], DateTime)
|
||||
|
||||
def fetch_conformation_expiration(
|
||||
%Condition{property: :remote_ip_location_region, operator: :is_in, values: values},
|
||||
%{property: :remote_ip_location_region, operator: :is_in, values: values},
|
||||
%Clients.Client{} = client
|
||||
) do
|
||||
if client.last_seen_remote_ip_location_region in values do
|
||||
@@ -46,7 +45,7 @@ defmodule Domain.Policies.Condition.Evaluator do
|
||||
end
|
||||
|
||||
def fetch_conformation_expiration(
|
||||
%Condition{property: :remote_ip_location_region, operator: :is_not_in, values: values},
|
||||
%{property: :remote_ip_location_region, operator: :is_not_in, values: values},
|
||||
%Clients.Client{} = client
|
||||
) do
|
||||
if client.last_seen_remote_ip_location_region in values do
|
||||
@@ -57,7 +56,7 @@ defmodule Domain.Policies.Condition.Evaluator do
|
||||
end
|
||||
|
||||
def fetch_conformation_expiration(
|
||||
%Condition{property: :remote_ip, operator: :is_in_cidr, values: values},
|
||||
%{property: :remote_ip, operator: :is_in_cidr, values: values},
|
||||
%Clients.Client{} = client
|
||||
) do
|
||||
Enum.reduce_while(values, :error, fn cidr, :error ->
|
||||
@@ -73,7 +72,7 @@ defmodule Domain.Policies.Condition.Evaluator do
|
||||
end
|
||||
|
||||
def fetch_conformation_expiration(
|
||||
%Condition{property: :remote_ip, operator: :is_not_in_cidr, values: values},
|
||||
%{property: :remote_ip, operator: :is_not_in_cidr, values: values},
|
||||
%Clients.Client{} = client
|
||||
) do
|
||||
Enum.reduce_while(values, {:ok, nil}, fn cidr, {:ok, nil} ->
|
||||
@@ -89,7 +88,7 @@ defmodule Domain.Policies.Condition.Evaluator do
|
||||
end
|
||||
|
||||
def fetch_conformation_expiration(
|
||||
%Condition{property: :provider_id, operator: :is_in, values: values},
|
||||
%{property: :provider_id, operator: :is_in, values: values},
|
||||
%Clients.Client{} = client
|
||||
) do
|
||||
if client.identity.provider_id in values do
|
||||
@@ -100,7 +99,7 @@ defmodule Domain.Policies.Condition.Evaluator do
|
||||
end
|
||||
|
||||
def fetch_conformation_expiration(
|
||||
%Condition{property: :provider_id, operator: :is_not_in, values: values},
|
||||
%{property: :provider_id, operator: :is_not_in, values: values},
|
||||
%Clients.Client{} = client
|
||||
) do
|
||||
if client.identity.provider_id in values do
|
||||
@@ -111,7 +110,7 @@ defmodule Domain.Policies.Condition.Evaluator do
|
||||
end
|
||||
|
||||
def fetch_conformation_expiration(
|
||||
%Condition{
|
||||
%{
|
||||
property: :client_verified,
|
||||
operator: :is,
|
||||
values: ["true"]
|
||||
@@ -126,7 +125,7 @@ defmodule Domain.Policies.Condition.Evaluator do
|
||||
end
|
||||
|
||||
def fetch_conformation_expiration(
|
||||
%Condition{
|
||||
%{
|
||||
property: :client_verified,
|
||||
operator: :is,
|
||||
values: _other
|
||||
@@ -137,7 +136,7 @@ defmodule Domain.Policies.Condition.Evaluator do
|
||||
end
|
||||
|
||||
def fetch_conformation_expiration(
|
||||
%Condition{
|
||||
%{
|
||||
property: :current_utc_datetime,
|
||||
operator: :is_in_day_of_week_time_ranges,
|
||||
values: values
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
defmodule Domain.Policies.Policy do
|
||||
use Domain, :schema
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
id: Ecto.UUID.t(),
|
||||
persistent_id: Ecto.UUID.t(),
|
||||
description: String.t() | nil,
|
||||
conditions: [Domain.Policies.Condition.t()],
|
||||
actor_group_id: Ecto.UUID.t(),
|
||||
resource_id: Ecto.UUID.t(),
|
||||
account_id: Ecto.UUID.t(),
|
||||
created_by: :actor | :identity,
|
||||
created_by_subject: map(),
|
||||
replaced_by_policy_id: Ecto.UUID.t() | nil,
|
||||
disabled_at: DateTime.t() | nil,
|
||||
deleted_at: DateTime.t() | nil,
|
||||
inserted_at: DateTime.t(),
|
||||
updated_at: DateTime.t()
|
||||
}
|
||||
|
||||
schema "policies" do
|
||||
field :persistent_id, Ecto.UUID
|
||||
|
||||
|
||||
@@ -65,6 +65,16 @@ defmodule Domain.Policies.Policy.Query do
|
||||
|> where([memberships: memberships], memberships.actor_id == ^actor_id)
|
||||
end
|
||||
|
||||
def by_gateway_group_id(queryable, gateway_group_id) do
|
||||
queryable
|
||||
|> with_joined_resource()
|
||||
|> with_joined_resource_connections()
|
||||
|> where(
|
||||
[resource_connections: resource_connections],
|
||||
resource_connections.gateway_group_id == ^gateway_group_id
|
||||
)
|
||||
end
|
||||
|
||||
def count_by_resource_id(queryable) do
|
||||
queryable
|
||||
|> group_by([policies: policies], policies.resource_id)
|
||||
@@ -100,6 +110,15 @@ defmodule Domain.Policies.Policy.Query do
|
||||
end)
|
||||
end
|
||||
|
||||
def with_joined_resource_connections(queryable) do
|
||||
with_named_binding(queryable, :resource_connections, fn queryable, binding ->
|
||||
join(queryable, :inner, [resource: resource], rc in Domain.Resources.Connection,
|
||||
on: rc.resource_id == resource.id,
|
||||
as: ^binding
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
def with_joined_memberships(queryable) do
|
||||
queryable
|
||||
|> with_joined_actor_group()
|
||||
|
||||
@@ -51,12 +51,6 @@ defmodule Domain.PubSub do
|
||||
|> Domain.PubSub.subscribe()
|
||||
end
|
||||
|
||||
def broadcast(nil, payload) do
|
||||
Logger.warning("Broadcasting to nil account_id is not allowed",
|
||||
payload: inspect(payload)
|
||||
)
|
||||
end
|
||||
|
||||
def broadcast(account_id, payload) do
|
||||
account_id
|
||||
|> topic()
|
||||
|
||||
@@ -104,7 +104,7 @@ defmodule Domain.Relays do
|
||||
|
||||
Group.Changeset.delete(group)
|
||||
end,
|
||||
# TODO: WAL
|
||||
# TODO: Remove self-hosted relays
|
||||
after_commit: &disconnect_relays_in_group/1
|
||||
)
|
||||
end
|
||||
@@ -309,7 +309,7 @@ defmodule Domain.Relays do
|
||||
|> Authorizer.for_subject(subject)
|
||||
|> Repo.fetch_and_update(Relay.Query,
|
||||
with: &Relay.Changeset.delete/1,
|
||||
# TODO: WAL
|
||||
# TODO: Remove self-hosted relays
|
||||
after_commit: &disconnect_relay/1
|
||||
)
|
||||
end
|
||||
@@ -344,8 +344,7 @@ defmodule Domain.Relays do
|
||||
|> Enum.map(&Enum.random(elem(&1, 1)))
|
||||
end
|
||||
|
||||
# TODO: WAL
|
||||
# Refactor to use new conventions
|
||||
# TODO: Refactor to use new conventions
|
||||
def connect_relay(%Relay{} = relay, secret) do
|
||||
with {:ok, _} <-
|
||||
Presence.track(self(), group_presence_topic(relay.group_id), relay.id, %{}),
|
||||
@@ -364,8 +363,7 @@ defmodule Domain.Relays do
|
||||
|
||||
### Presence
|
||||
|
||||
# TODO: WAL
|
||||
# Move these to Presence module
|
||||
# TODO: Move these to Presence module
|
||||
|
||||
defp presence_topic(relay_or_id),
|
||||
do: "presences:#{relay_topic(relay_or_id)}"
|
||||
@@ -387,8 +385,7 @@ defmodule Domain.Relays do
|
||||
|
||||
### PubSub
|
||||
|
||||
# TODO: WAL
|
||||
# Move these to PubSub module
|
||||
# TODO: Move these to PubSub module
|
||||
|
||||
defp relay_topic(%Relay{} = relay), do: relay_topic(relay.id)
|
||||
defp relay_topic(relay_id), do: "relays:#{relay_id}"
|
||||
|
||||
@@ -249,8 +249,6 @@ defmodule Domain.Replication.Decoder do
|
||||
}
|
||||
end
|
||||
|
||||
# TODO: WAL
|
||||
# Verify this is correct with real data from Postgres
|
||||
defp decode_message_impl(<<"O", lsn::binary-8, name::binary>>) do
|
||||
%Origin{
|
||||
origin_commit_lsn: decode_lsn(lsn),
|
||||
@@ -264,8 +262,6 @@ defmodule Domain.Replication.Decoder do
|
||||
| [name | [<<replica_identity::binary-1, _number_of_columns::integer-16, columns::binary>>]]
|
||||
] = String.split(rest, <<0>>, parts: 3)
|
||||
|
||||
# TODO: WAL
|
||||
# Handle case where pg_catalog is blank, we should still return the schema as pg_catalog
|
||||
friendly_replica_identity =
|
||||
case replica_identity do
|
||||
"d" -> :default
|
||||
|
||||
@@ -90,6 +90,13 @@ defmodule Domain.Resources do
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_all_resources_by_ids(ids) do
|
||||
Resource.Query.not_deleted()
|
||||
|> Resource.Query.by_id({:in, ids})
|
||||
|> Repo.all()
|
||||
|> Repo.preload(:gateway_groups)
|
||||
end
|
||||
|
||||
def fetch_resource_by_id_or_persistent_id!(id) do
|
||||
if Repo.valid_uuid?(id) do
|
||||
Resource.Query.not_deleted()
|
||||
@@ -305,37 +312,61 @@ defmodule Domain.Resources do
|
||||
end
|
||||
|
||||
def connected?(
|
||||
%Resource{account_id: account_id} = resource,
|
||||
%Gateways.Gateway{account_id: account_id} = gateway
|
||||
resource_id,
|
||||
%Gateways.Gateway{} = gateway
|
||||
) do
|
||||
Connection.Query.by_resource_id(resource.id)
|
||||
Connection.Query.by_resource_id(resource_id)
|
||||
|> Connection.Query.by_gateway_group_id(gateway.group_id)
|
||||
|> Repo.exists?()
|
||||
end
|
||||
|
||||
@doc false
|
||||
# This is the code that will be removed in future version of Firezone (in 1.3-1.4)
|
||||
# and is reused to prevent breaking changes
|
||||
def map_resource_address(address, acc \\ "")
|
||||
@doc """
|
||||
This does two things:
|
||||
1. Filters out resources that are not compatible with the given client or gateway version.
|
||||
2. Converts DNS resource addresses back to the pre-1.2.0 format if the client or gateway version is < 1.2.0.
|
||||
"""
|
||||
def adapt_resource_for_version(resource, client_or_gateway_version) do
|
||||
cond do
|
||||
# internet resources require client and gateway >= 1.3.0
|
||||
resource.type == :internet and Version.match?(client_or_gateway_version, "< 1.3.0") ->
|
||||
nil
|
||||
|
||||
def map_resource_address(["*", "*" | rest], ""),
|
||||
# non-internet resource, pass as-is
|
||||
Version.match?(client_or_gateway_version, ">= 1.2.0") ->
|
||||
resource
|
||||
|
||||
# we need convert dns resource addresses back to pre-1.2.0 format
|
||||
true ->
|
||||
resource.address
|
||||
|> String.codepoints()
|
||||
|> map_resource_address()
|
||||
|> case do
|
||||
{:cont, address} -> %{resource | address: address}
|
||||
:drop -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp map_resource_address(address, acc \\ "")
|
||||
|
||||
defp map_resource_address(["*", "*" | rest], ""),
|
||||
do: map_resource_address(rest, "*")
|
||||
|
||||
def map_resource_address(["*", "*" | _rest], _acc),
|
||||
defp map_resource_address(["*", "*" | _rest], _acc),
|
||||
do: :drop
|
||||
|
||||
def map_resource_address(["*" | rest], ""),
|
||||
defp map_resource_address(["*" | rest], ""),
|
||||
do: map_resource_address(rest, "?")
|
||||
|
||||
def map_resource_address(["*" | _rest], _acc),
|
||||
defp map_resource_address(["*" | _rest], _acc),
|
||||
do: :drop
|
||||
|
||||
def map_resource_address(["?" | _rest], _acc),
|
||||
defp map_resource_address(["?" | _rest], _acc),
|
||||
do: :drop
|
||||
|
||||
def map_resource_address([char | rest], acc),
|
||||
defp map_resource_address([char | rest], acc),
|
||||
do: map_resource_address(rest, acc <> char)
|
||||
|
||||
def map_resource_address([], acc),
|
||||
defp map_resource_address([], acc),
|
||||
do: {:cont, acc}
|
||||
end
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
defmodule Domain.Resources.Connection do
|
||||
use Domain, :schema
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
resource_id: Ecto.UUID.t(),
|
||||
gateway_group_id: Ecto.UUID.t(),
|
||||
created_by: :actor | :identity | :system,
|
||||
created_by_subject: map(),
|
||||
account_id: Ecto.UUID.t()
|
||||
}
|
||||
|
||||
@primary_key false
|
||||
schema "resource_connections" do
|
||||
belongs_to :resource, Domain.Resources.Resource, primary_key: true
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
defmodule Domain.Resources.Resource do
|
||||
use Domain, :schema
|
||||
|
||||
@type filter :: %{
|
||||
protocol: :tcp | :udp | :icmp,
|
||||
ports: [Domain.Types.Int4Range.t()]
|
||||
}
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
id: Ecto.UUID.t(),
|
||||
persistent_id: Ecto.UUID.t() | nil,
|
||||
address: String.t(),
|
||||
address_description: String.t() | nil,
|
||||
name: String.t(),
|
||||
type: :cidr | :ip | :dns | :internet,
|
||||
ip_stack: :ipv4_only | :ipv6_only | :dual,
|
||||
filters: [filter()],
|
||||
account_id: Ecto.UUID.t(),
|
||||
created_by: String.t(),
|
||||
created_by_subject: map(),
|
||||
replaced_by_resource_id: Ecto.UUID.t() | nil,
|
||||
deleted_at: DateTime.t() | nil,
|
||||
inserted_at: DateTime.t(),
|
||||
updated_at: DateTime.t()
|
||||
}
|
||||
|
||||
schema "resources" do
|
||||
field :persistent_id, Ecto.UUID
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ defmodule Domain.Types.Int4Range do
|
||||
@format_error "bad format"
|
||||
@cast_error "lower value cannot be higher than upper value"
|
||||
|
||||
@type t :: [String.t()]
|
||||
|
||||
def type, do: :int4range
|
||||
|
||||
def cast(str) when is_binary(str) do
|
||||
|
||||
@@ -5,6 +5,11 @@ defmodule Domain.Types.IP do
|
||||
"""
|
||||
@behaviour Ecto.Type
|
||||
|
||||
@type t :: %Postgrex.INET{
|
||||
address: tuple(),
|
||||
netmask: nil | integer()
|
||||
}
|
||||
|
||||
def type, do: :inet
|
||||
|
||||
def embed_as(_), do: :self
|
||||
|
||||
@@ -1210,7 +1210,7 @@ defmodule Domain.Repo.Seeds do
|
||||
user_iphone,
|
||||
gateway1,
|
||||
cidr_resource.id,
|
||||
policy,
|
||||
policy.id,
|
||||
membership.id,
|
||||
unprivileged_subject,
|
||||
unprivileged_subject.expires_at
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
defmodule Domain.Events.Hooks.AccountsTest do
|
||||
defmodule Domain.Changes.Hooks.AccountsTest do
|
||||
use Domain.DataCase, async: true
|
||||
alias Domain.Accounts
|
||||
import Domain.Events.Hooks.Accounts
|
||||
alias Domain.{Accounts, Changes.Change, PubSub}
|
||||
import Domain.Changes.Hooks.Accounts
|
||||
|
||||
describe "insert/1" do
|
||||
test "returns :ok" do
|
||||
assert :ok == on_insert(%{})
|
||||
test "returns :ok for empty data" do
|
||||
assert :ok == on_insert(0, %{})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,7 +13,7 @@ defmodule Domain.Events.Hooks.AccountsTest do
|
||||
test "sends delete when account is disabled" do
|
||||
account_id = "00000000-0000-0000-0000-000000000001"
|
||||
|
||||
:ok = Domain.PubSub.Account.subscribe(account_id)
|
||||
:ok = PubSub.Account.subscribe(account_id)
|
||||
|
||||
old_data = %{
|
||||
"id" => account_id,
|
||||
@@ -25,15 +25,15 @@ defmodule Domain.Events.Hooks.AccountsTest do
|
||||
"disabled_at" => "2023-10-01T00:00:00Z"
|
||||
}
|
||||
|
||||
assert :ok == on_update(old_data, data)
|
||||
assert_receive {:deleted, %Accounts.Account{} = account}
|
||||
assert :ok == on_update(0, old_data, data)
|
||||
assert_receive %Change{op: :delete, old_struct: %Accounts.Account{} = account, lsn: 0}
|
||||
|
||||
assert account.id == account_id
|
||||
end
|
||||
|
||||
test "sends delete when soft-deleted" do
|
||||
account_id = "00000000-0000-0000-0000-000000000002"
|
||||
:ok = Domain.PubSub.Account.subscribe(account_id)
|
||||
:ok = PubSub.Account.subscribe(account_id)
|
||||
|
||||
old_data = %{
|
||||
"id" => account_id,
|
||||
@@ -45,8 +45,8 @@ defmodule Domain.Events.Hooks.AccountsTest do
|
||||
"deleted_at" => "2023-10-01T00:00:00Z"
|
||||
}
|
||||
|
||||
assert :ok == on_update(old_data, data)
|
||||
assert_receive {:deleted, %Accounts.Account{} = account}
|
||||
assert :ok == on_update(0, old_data, data)
|
||||
assert_receive %Change{op: :delete, old_struct: %Accounts.Account{} = account, lsn: 0}
|
||||
|
||||
assert account.id == account_id
|
||||
end
|
||||
@@ -55,15 +55,15 @@ defmodule Domain.Events.Hooks.AccountsTest do
|
||||
describe "delete/1" do
|
||||
test "delete broadcasts deleted account" do
|
||||
account_id = "00000000-0000-0000-0000-000000000003"
|
||||
:ok = Domain.PubSub.Account.subscribe(account_id)
|
||||
:ok = PubSub.Account.subscribe(account_id)
|
||||
|
||||
old_data = %{
|
||||
"id" => account_id,
|
||||
"deleted_at" => "2023-10-01T00:00:00Z"
|
||||
}
|
||||
|
||||
assert :ok == on_delete(old_data)
|
||||
assert_receive {:deleted, %Accounts.Account{} = account}
|
||||
assert :ok == on_delete(0, old_data)
|
||||
assert_receive %Change{op: :delete, old_struct: %Accounts.Account{} = account, lsn: 0}
|
||||
assert account.id == account_id
|
||||
assert account.deleted_at == ~U[2023-10-01 00:00:00.000000Z]
|
||||
end
|
||||
@@ -77,7 +77,7 @@ defmodule Domain.Events.Hooks.AccountsTest do
|
||||
"deleted_at" => "2023-10-01T00:00:00Z"
|
||||
}
|
||||
|
||||
assert :ok == on_delete(old_data)
|
||||
assert :ok == on_delete(0, old_data)
|
||||
assert Repo.get_by(Domain.Flows.Flow, id: flow.id) == nil
|
||||
end
|
||||
end
|
||||
@@ -1,8 +1,7 @@
|
||||
defmodule Domain.Events.Hooks.ActorGroupMembershipsTest do
|
||||
defmodule Domain.Changes.Hooks.ActorGroupMembershipsTest do
|
||||
use API.ChannelCase, async: true
|
||||
import Domain.Events.Hooks.ActorGroupMemberships
|
||||
alias Domain.Actors
|
||||
alias Domain.PubSub
|
||||
import Domain.Changes.Hooks.ActorGroupMemberships
|
||||
alias Domain.{Actors, Changes.Change, Flows, PubSub}
|
||||
|
||||
describe "insert/1" do
|
||||
test "broadcasts membership" do
|
||||
@@ -18,8 +17,8 @@ defmodule Domain.Events.Hooks.ActorGroupMembershipsTest do
|
||||
"group_id" => group_id
|
||||
}
|
||||
|
||||
assert :ok == on_insert(data)
|
||||
assert_receive {:created, %Actors.Membership{} = membership}
|
||||
assert :ok == on_insert(0, data)
|
||||
assert_receive %Change{op: :insert, struct: %Actors.Membership{} = membership, lsn: 0}
|
||||
assert membership.account_id == account_id
|
||||
assert membership.actor_id == actor_id
|
||||
assert membership.group_id == group_id
|
||||
@@ -28,7 +27,7 @@ defmodule Domain.Events.Hooks.ActorGroupMembershipsTest do
|
||||
|
||||
describe "update/2" do
|
||||
test "returns :ok" do
|
||||
assert :ok == on_update(%{}, %{})
|
||||
assert :ok == on_update(0, %{}, %{})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -60,9 +59,9 @@ defmodule Domain.Events.Hooks.ActorGroupMembershipsTest do
|
||||
"group_id" => "00000000-0000-0000-0000-000000000003"
|
||||
}
|
||||
|
||||
assert :ok == on_delete(old_data)
|
||||
assert :ok == on_delete(0, old_data)
|
||||
|
||||
assert_receive {:deleted, %Actors.Membership{} = membership}
|
||||
assert_receive %Change{op: :delete, old_struct: %Actors.Membership{} = membership, lsn: 0}
|
||||
assert membership.id == "00000000-0000-0000-0000-000000000000"
|
||||
assert membership.account_id == "00000000-0000-0000-0000-000000000001"
|
||||
assert membership.actor_id == "00000000-0000-0000-0000-000000000002"
|
||||
@@ -80,10 +79,10 @@ defmodule Domain.Events.Hooks.ActorGroupMembershipsTest do
|
||||
"group_id" => membership.group_id
|
||||
}
|
||||
|
||||
assert ^flow = Repo.get_by(Domain.Flows.Flow, actor_group_membership_id: membership.id)
|
||||
assert :ok == on_delete(old_data)
|
||||
assert nil == Repo.get_by(Domain.Flows.Flow, actor_group_membership_id: membership.id)
|
||||
assert ^unrelated_flow = Repo.get_by(Domain.Flows.Flow, id: unrelated_flow.id)
|
||||
assert ^flow = Repo.get_by(Flows.Flow, actor_group_membership_id: membership.id)
|
||||
assert :ok == on_delete(0, old_data)
|
||||
assert nil == Repo.get_by(Flows.Flow, actor_group_membership_id: membership.id)
|
||||
assert ^unrelated_flow = Repo.get_by(Flows.Flow, id: unrelated_flow.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,11 +1,11 @@
|
||||
defmodule Domain.Events.Hooks.ClientsTest do
|
||||
defmodule Domain.Changes.Hooks.ClientsTest do
|
||||
use Domain.DataCase, async: true
|
||||
import Domain.Events.Hooks.Clients
|
||||
alias Domain.{Clients, PubSub}
|
||||
import Domain.Changes.Hooks.Clients
|
||||
alias Domain.{Changes.Change, Clients, Flows, PubSub}
|
||||
|
||||
describe "insert/1" do
|
||||
test "returns :ok" do
|
||||
assert :ok == on_insert(%{})
|
||||
assert :ok == on_insert(0, %{})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,8 +22,8 @@ defmodule Domain.Events.Hooks.ClientsTest do
|
||||
"account_id" => client.account_id
|
||||
}
|
||||
|
||||
assert :ok == on_update(old_data, data)
|
||||
assert_receive {:deleted, %Clients.Client{} = deleted_client}
|
||||
assert :ok == on_update(0, old_data, data)
|
||||
assert_receive %Change{op: :delete, old_struct: %Clients.Client{} = deleted_client, lsn: 0}
|
||||
assert deleted_client.id == client.id
|
||||
end
|
||||
|
||||
@@ -35,9 +35,15 @@ defmodule Domain.Events.Hooks.ClientsTest do
|
||||
old_data = %{"id" => client.id, "name" => "Old Name", "account_id" => client.account_id}
|
||||
data = %{"id" => client.id, "name" => "New Name", "account_id" => client.account_id}
|
||||
|
||||
assert :ok == on_update(old_data, data)
|
||||
assert :ok == on_update(0, old_data, data)
|
||||
|
||||
assert_receive %Change{
|
||||
op: :update,
|
||||
old_struct: %Clients.Client{} = old_client,
|
||||
struct: %Clients.Client{} = new_client,
|
||||
lsn: 0
|
||||
}
|
||||
|
||||
assert_receive {:updated, %Clients.Client{} = old_client, %Clients.Client{} = new_client}
|
||||
assert old_client.name == "Old Name"
|
||||
assert new_client.name == "New Name"
|
||||
assert new_client.id == client.id
|
||||
@@ -57,11 +63,18 @@ defmodule Domain.Events.Hooks.ClientsTest do
|
||||
data = %{"id" => client.id, "verified_at" => nil, "account_id" => client.account_id}
|
||||
|
||||
assert flow = Fixtures.Flows.create_flow(client: client, account: account)
|
||||
assert :ok == on_update(old_data, data)
|
||||
assert_receive {:updated, %Clients.Client{}, %Clients.Client{} = new_client}
|
||||
assert :ok == on_update(0, old_data, data)
|
||||
|
||||
assert_receive %Change{
|
||||
op: :update,
|
||||
old_struct: %Clients.Client{},
|
||||
struct: %Clients.Client{} = new_client,
|
||||
lsn: 0
|
||||
}
|
||||
|
||||
assert is_nil(new_client.verified_at)
|
||||
assert new_client.id == client.id
|
||||
refute Repo.get_by(Domain.Flows.Flow, id: flow.id)
|
||||
refute Repo.get_by(Flows.Flow, id: flow.id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -73,8 +86,8 @@ defmodule Domain.Events.Hooks.ClientsTest do
|
||||
|
||||
old_data = %{"id" => client.id, "account_id" => client.account_id}
|
||||
|
||||
assert :ok == on_delete(old_data)
|
||||
assert_receive {:deleted, %Clients.Client{} = deleted_client}
|
||||
assert :ok == on_delete(0, old_data)
|
||||
assert_receive %Change{op: :delete, old_struct: %Clients.Client{} = deleted_client, lsn: 0}
|
||||
assert deleted_client.id == client.id
|
||||
end
|
||||
|
||||
@@ -85,8 +98,8 @@ defmodule Domain.Events.Hooks.ClientsTest do
|
||||
old_data = %{"id" => client.id, "account_id" => client.account_id}
|
||||
|
||||
assert flow = Fixtures.Flows.create_flow(client: client, account: account)
|
||||
assert :ok == on_delete(old_data)
|
||||
refute Repo.get_by(Domain.Flows.Flow, id: flow.id)
|
||||
assert :ok == on_delete(0, old_data)
|
||||
refute Repo.get_by(Flows.Flow, id: flow.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,23 +1,23 @@
|
||||
defmodule Domain.Events.Hooks.FlowsTest do
|
||||
defmodule Domain.Changes.Hooks.FlowsTest do
|
||||
use Domain.DataCase, async: true
|
||||
import Domain.Events.Hooks.Flows
|
||||
alias Domain.Flows
|
||||
import Domain.Changes.Hooks.Flows
|
||||
alias Domain.{Changes.Change, Flows, PubSub}
|
||||
|
||||
describe "insert/1" do
|
||||
test "returns :ok" do
|
||||
assert :ok == on_insert(%{})
|
||||
assert :ok == on_insert(0, %{})
|
||||
end
|
||||
end
|
||||
|
||||
describe "update/2" do
|
||||
test "returns :ok" do
|
||||
assert :ok == on_update(%{}, %{})
|
||||
assert :ok == on_update(0, %{}, %{})
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete/1" do
|
||||
test "delete broadcasts deleted flow" do
|
||||
:ok = Domain.PubSub.Account.subscribe("00000000-0000-0000-0000-000000000000")
|
||||
:ok = PubSub.Account.subscribe("00000000-0000-0000-0000-000000000000")
|
||||
|
||||
old_data = %{
|
||||
"id" => "00000000-0000-0000-0000-000000000001",
|
||||
@@ -31,9 +31,9 @@ defmodule Domain.Events.Hooks.FlowsTest do
|
||||
"inserted_at" => "2023-01-01T00:00:00Z"
|
||||
}
|
||||
|
||||
assert :ok == on_delete(old_data)
|
||||
assert :ok == on_delete(0, old_data)
|
||||
|
||||
assert_receive {:deleted, %Flows.Flow{} = flow}
|
||||
assert_receive %Change{op: :delete, old_struct: %Flows.Flow{} = flow, lsn: 0}
|
||||
|
||||
assert flow.id == "00000000-0000-0000-0000-000000000001"
|
||||
assert flow.account_id == "00000000-0000-0000-0000-000000000000"
|
||||
@@ -1,11 +1,11 @@
|
||||
defmodule Domain.Events.Hooks.GatewayGroupsTest do
|
||||
defmodule Domain.Changes.Hooks.GatewayGroupsTest do
|
||||
use ExUnit.Case, async: true
|
||||
import Domain.Events.Hooks.GatewayGroups
|
||||
alias Domain.Gateways
|
||||
import Domain.Changes.Hooks.GatewayGroups
|
||||
alias Domain.{Changes.Change, Gateways, PubSub}
|
||||
|
||||
describe "insert/1" do
|
||||
test "returns :ok" do
|
||||
assert :ok == on_insert(%{})
|
||||
assert :ok == on_insert(0, %{})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,13 +14,13 @@ defmodule Domain.Events.Hooks.GatewayGroupsTest do
|
||||
# Deleting a gateway group will delete the associated gateways which
|
||||
# handles all side effects we need to handle, including removing any
|
||||
# resources from the client's resource list.
|
||||
assert :ok = on_delete(%{})
|
||||
assert :ok = on_delete(0, %{})
|
||||
end
|
||||
|
||||
test "broadcasts updated gateway group" do
|
||||
account_id = "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
:ok = Domain.PubSub.Account.subscribe(account_id)
|
||||
:ok = PubSub.Account.subscribe(account_id)
|
||||
|
||||
old_data = %{
|
||||
"id" => "00000000-0000-0000-0000-000000000001",
|
||||
@@ -31,9 +31,15 @@ defmodule Domain.Events.Hooks.GatewayGroupsTest do
|
||||
|
||||
data = Map.put(old_data, "name", "Updated Gateway Group")
|
||||
|
||||
assert :ok == on_update(old_data, data)
|
||||
assert :ok == on_update(0, old_data, data)
|
||||
|
||||
assert_receive %Change{
|
||||
op: :update,
|
||||
old_struct: %Gateways.Group{} = old_group,
|
||||
struct: %Gateways.Group{} = new_group,
|
||||
lsn: 0
|
||||
}
|
||||
|
||||
assert_receive {:updated, %Gateways.Group{} = old_group, %Gateways.Group{} = new_group}
|
||||
assert old_group.id == old_data["id"]
|
||||
assert new_group.name == data["name"]
|
||||
end
|
||||
@@ -44,7 +50,7 @@ defmodule Domain.Events.Hooks.GatewayGroupsTest do
|
||||
# Deleting a gateway group will delete the associated gateways which
|
||||
# handles all side effects we need to handle, including removing any
|
||||
# resources from the client's resource list.
|
||||
assert :ok = on_delete(%{})
|
||||
assert :ok = on_delete(0, %{})
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,10 +1,11 @@
|
||||
defmodule Domain.Events.Hooks.GatewaysTest do
|
||||
defmodule Domain.Changes.Hooks.GatewaysTest do
|
||||
use Domain.DataCase, async: true
|
||||
import Domain.Events.Hooks.Gateways
|
||||
import Domain.Changes.Hooks.Gateways
|
||||
alias Domain.{Changes.Change, Gateways, PubSub}
|
||||
|
||||
describe "insert/1" do
|
||||
test "returns :ok" do
|
||||
assert :ok == on_insert(%{})
|
||||
assert :ok == on_insert(0, %{})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,14 +14,19 @@ defmodule Domain.Events.Hooks.GatewaysTest do
|
||||
account = Fixtures.Accounts.create_account()
|
||||
gateway = Fixtures.Gateways.create_gateway(account: account)
|
||||
|
||||
:ok = Domain.PubSub.Account.subscribe(account.id)
|
||||
:ok = PubSub.Account.subscribe(account.id)
|
||||
|
||||
old_data = %{"id" => gateway.id, "deleted_at" => nil, "account_id" => account.id}
|
||||
data = Map.put(old_data, "deleted_at", "2023-01-01T00:00:00Z")
|
||||
|
||||
assert :ok = on_update(old_data, data)
|
||||
assert :ok = on_update(0, old_data, data)
|
||||
|
||||
assert_receive %Change{
|
||||
op: :delete,
|
||||
old_struct: %Gateways.Gateway{} = deleted_gateway,
|
||||
lsn: 0
|
||||
}
|
||||
|
||||
assert_receive {:deleted, %Domain.Gateways.Gateway{} = deleted_gateway}
|
||||
assert deleted_gateway.id == gateway.id
|
||||
end
|
||||
|
||||
@@ -32,12 +38,12 @@ defmodule Domain.Events.Hooks.GatewaysTest do
|
||||
data = Map.put(old_data, "deleted_at", "2023-01-01T00:00:00Z")
|
||||
|
||||
assert flow = Fixtures.Flows.create_flow(gateway: gateway, account: account)
|
||||
assert :ok = on_update(old_data, data)
|
||||
assert :ok = on_update(0, old_data, data)
|
||||
refute Repo.get_by(Domain.Flows.Flow, id: flow.id)
|
||||
end
|
||||
|
||||
test "update returns :ok" do
|
||||
assert :ok = on_update(%{}, %{})
|
||||
assert :ok = on_update(0, %{}, %{})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -46,7 +52,7 @@ defmodule Domain.Events.Hooks.GatewaysTest do
|
||||
account = Fixtures.Accounts.create_account()
|
||||
gateway = Fixtures.Gateways.create_gateway(account: account)
|
||||
|
||||
:ok = Domain.PubSub.Account.subscribe(account.id)
|
||||
:ok = PubSub.Account.subscribe(account.id)
|
||||
|
||||
old_data = %{
|
||||
"id" => gateway.id,
|
||||
@@ -55,9 +61,14 @@ defmodule Domain.Events.Hooks.GatewaysTest do
|
||||
"deleted_at" => nil
|
||||
}
|
||||
|
||||
assert :ok = on_delete(old_data)
|
||||
assert :ok = on_delete(0, old_data)
|
||||
|
||||
assert_receive %Change{
|
||||
op: :delete,
|
||||
old_struct: %Gateways.Gateway{} = deleted_gateway,
|
||||
lsn: 0
|
||||
}
|
||||
|
||||
assert_receive {:deleted, %Domain.Gateways.Gateway{} = deleted_gateway}
|
||||
assert deleted_gateway.id == gateway.id
|
||||
end
|
||||
|
||||
@@ -68,7 +79,7 @@ defmodule Domain.Events.Hooks.GatewaysTest do
|
||||
old_data = %{"id" => gateway.id, "account_id" => account.id, "deleted_at" => nil}
|
||||
|
||||
assert flow = Fixtures.Flows.create_flow(gateway: gateway, account: account)
|
||||
assert :ok = on_delete(old_data)
|
||||
assert :ok = on_delete(0, old_data)
|
||||
refute Repo.get_by(Domain.Flows.Flow, id: flow.id)
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,7 @@
|
||||
defmodule Domain.Events.Hooks.PoliciesTest do
|
||||
defmodule Domain.Changes.Hooks.PoliciesTest do
|
||||
use Domain.DataCase, async: true
|
||||
import Domain.Events.Hooks.Policies
|
||||
alias Domain.{Policies, PubSub}
|
||||
import Domain.Changes.Hooks.Policies
|
||||
alias Domain.{Changes.Change, Policies, PubSub}
|
||||
|
||||
describe "insert/1" do
|
||||
test "broadcasts created policy" do
|
||||
@@ -18,8 +18,8 @@ defmodule Domain.Events.Hooks.PoliciesTest do
|
||||
"deleted_at" => nil
|
||||
}
|
||||
|
||||
assert :ok == on_insert(data)
|
||||
assert_receive {:created, %Policies.Policy{} = policy}
|
||||
assert :ok == on_insert(0, data)
|
||||
assert_receive %Change{op: :insert, struct: %Policies.Policy{} = policy, lsn: 0}
|
||||
|
||||
assert policy.id == data["id"]
|
||||
assert policy.account_id == data["account_id"]
|
||||
@@ -49,8 +49,13 @@ defmodule Domain.Events.Hooks.PoliciesTest do
|
||||
# Create a flow that should be deleted
|
||||
flow = Fixtures.Flows.create_flow(policy: policy, resource: resource, account: account)
|
||||
|
||||
assert :ok == on_update(old_data, data)
|
||||
assert_receive {:deleted, %Policies.Policy{} = broadcasted_policy}
|
||||
assert :ok == on_update(0, old_data, data)
|
||||
|
||||
assert_receive %Change{
|
||||
op: :delete,
|
||||
old_struct: %Policies.Policy{} = broadcasted_policy,
|
||||
lsn: 0
|
||||
}
|
||||
|
||||
assert broadcasted_policy.id == data["id"]
|
||||
assert broadcasted_policy.account_id == data["account_id"]
|
||||
@@ -75,8 +80,8 @@ defmodule Domain.Events.Hooks.PoliciesTest do
|
||||
|
||||
data = Map.put(old_data, "disabled_at", nil)
|
||||
|
||||
assert :ok == on_update(old_data, data)
|
||||
assert_receive {:created, %Policies.Policy{} = policy}
|
||||
assert :ok == on_update(0, old_data, data)
|
||||
assert_receive %Change{op: :insert, struct: %Policies.Policy{} = policy, lsn: 0}
|
||||
|
||||
assert policy.id == data["id"]
|
||||
assert policy.account_id == data["account_id"]
|
||||
@@ -100,8 +105,8 @@ defmodule Domain.Events.Hooks.PoliciesTest do
|
||||
|
||||
data = Map.put(old_data, "deleted_at", "2023-10-01T00:00:00Z")
|
||||
|
||||
assert :ok == on_update(old_data, data)
|
||||
assert_receive {:deleted, %Policies.Policy{} = policy}
|
||||
assert :ok == on_update(0, old_data, data)
|
||||
assert_receive %Change{op: :delete, old_struct: %Policies.Policy{} = policy, lsn: 0}
|
||||
|
||||
assert policy.id == old_data["id"]
|
||||
assert policy.account_id == old_data["account_id"]
|
||||
@@ -136,7 +141,7 @@ defmodule Domain.Events.Hooks.PoliciesTest do
|
||||
account: account
|
||||
)
|
||||
|
||||
assert :ok = on_update(old_data, data)
|
||||
assert :ok = on_update(0, old_data, data)
|
||||
refute Repo.get_by(Domain.Flows.Flow, id: flow.id)
|
||||
end
|
||||
|
||||
@@ -157,8 +162,15 @@ defmodule Domain.Events.Hooks.PoliciesTest do
|
||||
|
||||
data = Map.put(old_data, "description", "Updated description")
|
||||
|
||||
assert :ok == on_update(old_data, data)
|
||||
assert_receive {:updated, %Policies.Policy{} = old_policy, %Policies.Policy{} = new_policy}
|
||||
assert :ok == on_update(0, old_data, data)
|
||||
|
||||
assert_receive %Change{
|
||||
op: :update,
|
||||
old_struct: %Policies.Policy{} = old_policy,
|
||||
struct: %Policies.Policy{} = new_policy,
|
||||
lsn: 0
|
||||
}
|
||||
|
||||
assert old_policy.id == old_data["id"]
|
||||
assert new_policy.description == data["description"]
|
||||
assert new_policy.account_id == old_data["account_id"]
|
||||
@@ -186,7 +198,7 @@ defmodule Domain.Events.Hooks.PoliciesTest do
|
||||
account: account
|
||||
)
|
||||
|
||||
assert :ok = on_update(old_data, data)
|
||||
assert :ok = on_update(0, old_data, data)
|
||||
refute Repo.get_by(Domain.Flows.Flow, id: flow.id)
|
||||
end
|
||||
|
||||
@@ -206,7 +218,7 @@ defmodule Domain.Events.Hooks.PoliciesTest do
|
||||
|
||||
assert flow = Fixtures.Flows.create_flow(policy: policy, account: account)
|
||||
|
||||
assert :ok = on_update(old_data, data)
|
||||
assert :ok = on_update(0, old_data, data)
|
||||
refute Repo.get_by(Domain.Flows.Flow, id: flow.id)
|
||||
end
|
||||
|
||||
@@ -232,7 +244,7 @@ defmodule Domain.Events.Hooks.PoliciesTest do
|
||||
|
||||
assert flow = Fixtures.Flows.create_flow(policy: policy, account: account)
|
||||
|
||||
assert :ok = on_update(old_data, data)
|
||||
assert :ok = on_update(0, old_data, data)
|
||||
refute Repo.get_by(Domain.Flows.Flow, id: flow.id)
|
||||
end
|
||||
end
|
||||
@@ -250,8 +262,13 @@ defmodule Domain.Events.Hooks.PoliciesTest do
|
||||
"resource_id" => policy.resource_id
|
||||
}
|
||||
|
||||
assert :ok == on_delete(old_data)
|
||||
assert_receive {:deleted, %Policies.Policy{} = policy}
|
||||
assert :ok == on_delete(0, old_data)
|
||||
|
||||
assert_receive %Change{
|
||||
op: :delete,
|
||||
old_struct: %Policies.Policy{} = policy,
|
||||
lsn: 0
|
||||
}
|
||||
|
||||
assert policy.id == old_data["id"]
|
||||
assert policy.account_id == old_data["account_id"]
|
||||
@@ -273,7 +290,7 @@ defmodule Domain.Events.Hooks.PoliciesTest do
|
||||
|
||||
assert flow = Fixtures.Flows.create_flow(policy: policy, account: account)
|
||||
|
||||
assert :ok = on_delete(old_data)
|
||||
assert :ok = on_delete(0, old_data)
|
||||
refute Repo.get_by(Domain.Flows.Flow, id: flow.id)
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,7 @@
|
||||
defmodule Domain.Events.Hooks.ResourceConnectionsTest do
|
||||
defmodule Domain.Changes.Hooks.ResourceConnectionsTest do
|
||||
use Domain.DataCase, async: true
|
||||
import Domain.Events.Hooks.ResourceConnections
|
||||
import Domain.Changes.Hooks.ResourceConnections
|
||||
alias Domain.{Changes.Change, Resources, PubSub}
|
||||
|
||||
describe "insert/1" do
|
||||
test "broadcasts created resource connection" do
|
||||
@@ -8,18 +9,22 @@ defmodule Domain.Events.Hooks.ResourceConnectionsTest do
|
||||
resource = Fixtures.Resources.create_resource(account: account)
|
||||
gateway_group = Fixtures.Gateways.create_group(account: account)
|
||||
|
||||
:ok = Domain.PubSub.Account.subscribe(account.id)
|
||||
:ok = PubSub.Account.subscribe(account.id)
|
||||
|
||||
data = %{
|
||||
"account_id" => account.id,
|
||||
"resource_id" => resource.id,
|
||||
"gateway_group_id" => gateway_group.id,
|
||||
"deleted_at" => nil
|
||||
"gateway_group_id" => gateway_group.id
|
||||
}
|
||||
|
||||
assert :ok == on_insert(data)
|
||||
assert :ok == on_insert(0, data)
|
||||
|
||||
assert_receive %Change{
|
||||
op: :insert,
|
||||
struct: %Resources.Connection{} = connection,
|
||||
lsn: 0
|
||||
}
|
||||
|
||||
assert_receive {:created, %Domain.Resources.Connection{} = connection}
|
||||
assert connection.account_id == data["account_id"]
|
||||
assert connection.resource_id == data["resource_id"]
|
||||
assert connection.gateway_group_id == data["gateway_group_id"]
|
||||
@@ -28,28 +33,35 @@ defmodule Domain.Events.Hooks.ResourceConnectionsTest do
|
||||
|
||||
describe "update/2" do
|
||||
test "returns :ok" do
|
||||
assert :ok = on_update(%{}, %{})
|
||||
assert :ok = on_update(0, %{}, %{})
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete/1" do
|
||||
test "deletes flows for resource and gateway group" do
|
||||
end
|
||||
|
||||
test "broadcasts deleted connection" do
|
||||
account = Fixtures.Accounts.create_account()
|
||||
resource = Fixtures.Resources.create_resource(account: account)
|
||||
gateway_group = Fixtures.Gateways.create_group(account: account)
|
||||
|
||||
:ok = Domain.PubSub.Account.subscribe(account.id)
|
||||
:ok = PubSub.Account.subscribe(account.id)
|
||||
|
||||
old_data = %{
|
||||
"account_id" => account.id,
|
||||
"resource_id" => resource.id,
|
||||
"gateway_group_id" => gateway_group.id,
|
||||
"deleted_at" => nil
|
||||
"gateway_group_id" => gateway_group.id
|
||||
}
|
||||
|
||||
assert :ok == on_delete(old_data)
|
||||
assert :ok == on_delete(0, old_data)
|
||||
|
||||
assert_receive %Change{
|
||||
op: :delete,
|
||||
old_struct: %Resources.Connection{} = deleted_connection,
|
||||
lsn: 0
|
||||
}
|
||||
|
||||
assert_receive {:deleted, %Domain.Resources.Connection{} = deleted_connection}
|
||||
assert deleted_connection.account_id == old_data["account_id"]
|
||||
assert deleted_connection.resource_id == old_data["resource_id"]
|
||||
assert deleted_connection.gateway_group_id == old_data["gateway_group_id"]
|
||||
@@ -1,7 +1,7 @@
|
||||
defmodule Domain.Events.Hooks.ResourcesTest do
|
||||
defmodule Domain.Changes.Hooks.ResourcesTest do
|
||||
use Domain.DataCase, async: true
|
||||
import Domain.Events.Hooks.Resources
|
||||
alias Domain.PubSub
|
||||
import Domain.Changes.Hooks.Resources
|
||||
alias Domain.{Changes.Change, Flows, Resources, PubSub}
|
||||
|
||||
describe "insert/1" do
|
||||
test "broadcasts created resource" do
|
||||
@@ -22,8 +22,14 @@ defmodule Domain.Events.Hooks.ResourcesTest do
|
||||
"deleted_at" => nil
|
||||
}
|
||||
|
||||
assert :ok == on_insert(data)
|
||||
assert_receive {:created, %Domain.Resources.Resource{} = created_resource}
|
||||
assert :ok == on_insert(0, data)
|
||||
|
||||
assert_receive %Change{
|
||||
op: :insert,
|
||||
struct: %Resources.Resource{} = created_resource,
|
||||
lsn: 0
|
||||
}
|
||||
|
||||
assert created_resource.id == resource.id
|
||||
assert created_resource.account_id == resource.account_id
|
||||
assert created_resource.type == resource.type
|
||||
@@ -55,9 +61,13 @@ defmodule Domain.Events.Hooks.ResourcesTest do
|
||||
|
||||
data = Map.put(old_data, "deleted_at", "2023-10-01T00:00:00Z")
|
||||
|
||||
assert :ok == on_update(old_data, data)
|
||||
assert :ok == on_update(0, old_data, data)
|
||||
|
||||
assert_receive {:deleted, %Domain.Resources.Resource{} = deleted_resource}
|
||||
assert_receive %Change{
|
||||
op: :delete,
|
||||
old_struct: %Resources.Resource{} = deleted_resource,
|
||||
lsn: 0
|
||||
}
|
||||
|
||||
assert deleted_resource.id == resource.id
|
||||
assert deleted_resource.account_id == resource.account_id
|
||||
@@ -87,8 +97,8 @@ defmodule Domain.Events.Hooks.ResourcesTest do
|
||||
data = Map.put(old_data, "deleted_at", "2023-10-01T00:00:00Z")
|
||||
|
||||
assert flow = Fixtures.Flows.create_flow(resource: resource, account: account)
|
||||
assert :ok = on_update(old_data, data)
|
||||
refute Repo.get_by(Domain.Flows.Flow, id: flow.id)
|
||||
assert :ok = on_update(0, old_data, data)
|
||||
refute Repo.get_by(Flows.Flow, id: flow.id)
|
||||
end
|
||||
|
||||
test "regular update broadcasts updated resource" do
|
||||
@@ -111,10 +121,14 @@ defmodule Domain.Events.Hooks.ResourcesTest do
|
||||
|
||||
data = Map.put(old_data, "address", "new-address.example.com")
|
||||
|
||||
assert :ok == on_update(old_data, data)
|
||||
assert :ok == on_update(0, old_data, data)
|
||||
|
||||
assert_receive {:updated, %Domain.Resources.Resource{},
|
||||
%Domain.Resources.Resource{} = updated_resource}
|
||||
assert_receive %Change{
|
||||
op: :update,
|
||||
old_struct: %Resources.Resource{},
|
||||
struct: %Resources.Resource{} = updated_resource,
|
||||
lsn: 0
|
||||
}
|
||||
|
||||
assert updated_resource.id == resource.id
|
||||
assert updated_resource.account_id == resource.account_id
|
||||
@@ -144,8 +158,8 @@ defmodule Domain.Events.Hooks.ResourcesTest do
|
||||
data = Map.put(old_data, "type", "cidr")
|
||||
|
||||
assert flow = Fixtures.Flows.create_flow(resource: resource, account: account)
|
||||
assert :ok = on_update(old_data, data)
|
||||
refute Repo.get_by(Domain.Flows.Flow, id: flow.id)
|
||||
assert :ok = on_update(0, old_data, data)
|
||||
refute Repo.get_by(Flows.Flow, id: flow.id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -168,9 +182,13 @@ defmodule Domain.Events.Hooks.ResourcesTest do
|
||||
"deleted_at" => nil
|
||||
}
|
||||
|
||||
assert :ok == on_delete(old_data)
|
||||
assert :ok == on_delete(0, old_data)
|
||||
|
||||
assert_receive {:deleted, %Domain.Resources.Resource{} = deleted_resource}
|
||||
assert_receive %Change{
|
||||
op: :delete,
|
||||
old_struct: %Resources.Resource{} = deleted_resource,
|
||||
lsn: 0
|
||||
}
|
||||
|
||||
assert deleted_resource.id == resource.id
|
||||
assert deleted_resource.account_id == resource.account_id
|
||||
@@ -198,8 +216,8 @@ defmodule Domain.Events.Hooks.ResourcesTest do
|
||||
}
|
||||
|
||||
assert flow = Fixtures.Flows.create_flow(resource: resource, account: account)
|
||||
assert :ok = on_delete(old_data)
|
||||
refute Repo.get_by(Domain.Flows.Flow, id: flow.id)
|
||||
assert :ok = on_delete(0, old_data)
|
||||
refute Repo.get_by(Flows.Flow, id: flow.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,22 +1,24 @@
|
||||
defmodule Domain.Events.Hooks.TokensTest do
|
||||
defmodule Domain.Changes.Hooks.TokensTest do
|
||||
use Domain.DataCase, async: true
|
||||
import Domain.Events.Hooks.Tokens
|
||||
import Domain.Changes.Hooks.Tokens
|
||||
alias Domain.{Flows, PubSub}
|
||||
|
||||
describe "insert/1" do
|
||||
test "returns :ok" do
|
||||
assert :ok == on_insert(%{})
|
||||
assert :ok == on_insert(0, %{})
|
||||
end
|
||||
end
|
||||
|
||||
describe "update/2" do
|
||||
test "returns :ok for email token updates" do
|
||||
assert :ok = on_update(%{"type" => "email"}, %{"type" => "email"})
|
||||
assert :ok = on_update(0, %{"type" => "email"}, %{"type" => "email"})
|
||||
end
|
||||
|
||||
test "soft-delete broadcasts deleted token" do
|
||||
test "soft-delete broadcasts disconnect" do
|
||||
account = Fixtures.Accounts.create_account()
|
||||
token = Fixtures.Tokens.create_token(account: account)
|
||||
:ok = Domain.PubSub.Account.subscribe(account.id)
|
||||
|
||||
:ok = PubSub.subscribe("sessions:#{token.id}")
|
||||
|
||||
old_data = %{
|
||||
"id" => token.id,
|
||||
@@ -25,13 +27,14 @@ defmodule Domain.Events.Hooks.TokensTest do
|
||||
"deleted_at" => nil
|
||||
}
|
||||
|
||||
data = Map.put(old_data, "deleted_at", "2023-10-01T00:00:00Z")
|
||||
assert :ok == on_delete(0, old_data)
|
||||
|
||||
assert :ok == on_update(old_data, data)
|
||||
assert_receive {:deleted, %Domain.Tokens.Token{} = deleted_token}
|
||||
assert deleted_token.id == old_data["id"]
|
||||
assert deleted_token.account_id == old_data["account_id"]
|
||||
assert deleted_token.type == old_data["type"]
|
||||
assert_receive %Phoenix.Socket.Broadcast{
|
||||
topic: topic,
|
||||
event: "disconnect"
|
||||
}
|
||||
|
||||
assert topic == "sessions:#{token.id}"
|
||||
end
|
||||
|
||||
test "soft-delete deletes flows" do
|
||||
@@ -49,20 +52,21 @@ defmodule Domain.Events.Hooks.TokensTest do
|
||||
|
||||
assert flow = Fixtures.Flows.create_flow(account: account, token: token)
|
||||
assert flow.token_id == token.id
|
||||
assert :ok = on_update(old_data, data)
|
||||
refute Repo.get_by(Domain.Flows.Flow, id: flow.id)
|
||||
assert :ok = on_update(0, old_data, data)
|
||||
refute Repo.get_by(Flows.Flow, id: flow.id)
|
||||
end
|
||||
|
||||
test "regular update returns :ok" do
|
||||
assert :ok = on_update(%{}, %{})
|
||||
assert :ok = on_update(0, %{}, %{})
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete/1" do
|
||||
test "broadcasts deleted token" do
|
||||
test "broadcasts disconnect message" do
|
||||
account = Fixtures.Accounts.create_account()
|
||||
token = Fixtures.Tokens.create_token(account: account)
|
||||
:ok = Domain.PubSub.Account.subscribe(account.id)
|
||||
|
||||
:ok = PubSub.subscribe("sessions:#{token.id}")
|
||||
|
||||
old_data = %{
|
||||
"id" => token.id,
|
||||
@@ -71,12 +75,14 @@ defmodule Domain.Events.Hooks.TokensTest do
|
||||
"deleted_at" => nil
|
||||
}
|
||||
|
||||
assert :ok == on_delete(old_data)
|
||||
assert :ok == on_delete(0, old_data)
|
||||
|
||||
assert_receive {:deleted, %Domain.Tokens.Token{} = deleted_token}
|
||||
assert deleted_token.id == old_data["id"]
|
||||
assert deleted_token.account_id == old_data["account_id"]
|
||||
assert deleted_token.type == old_data["type"]
|
||||
assert_receive %Phoenix.Socket.Broadcast{
|
||||
topic: topic,
|
||||
event: "disconnect"
|
||||
}
|
||||
|
||||
assert topic == "sessions:#{token.id}"
|
||||
end
|
||||
|
||||
test "deletes flows" do
|
||||
@@ -91,8 +97,8 @@ defmodule Domain.Events.Hooks.TokensTest do
|
||||
}
|
||||
|
||||
assert flow = Fixtures.Flows.create_flow(account: account, token: token)
|
||||
assert :ok = on_delete(old_data)
|
||||
refute Repo.get_by(Domain.Flows.Flow, id: flow.id)
|
||||
assert :ok = on_delete(0, old_data)
|
||||
refute Repo.get_by(Flows.Flow, id: flow.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,12 +1,12 @@
|
||||
defmodule Domain.Events.ReplicationConnectionTest do
|
||||
defmodule Domain.Changes.ReplicationConnectionTest do
|
||||
use Domain.DataCase, async: true
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
alias Domain.Events.ReplicationConnection
|
||||
alias Domain.Changes.ReplicationConnection
|
||||
|
||||
setup do
|
||||
tables =
|
||||
Application.fetch_env!(:domain, Domain.Events.ReplicationConnection)
|
||||
Application.fetch_env!(:domain, Domain.Changes.ReplicationConnection)
|
||||
|> Keyword.fetch!(:table_subscriptions)
|
||||
|
||||
%{tables: tables}
|
||||
@@ -29,7 +29,7 @@ defmodule Domain.Events.ReplicationConnectionTest do
|
||||
end)
|
||||
|
||||
assert log_output =~ "No hook defined for insert on table unknown_table"
|
||||
assert log_output =~ "Please implement Domain.Events.Hooks for this table"
|
||||
assert log_output =~ "Please implement Domain.Changes.Hooks for this table"
|
||||
end
|
||||
|
||||
test "handles known tables without errors", %{tables: tables} do
|
||||
@@ -94,7 +94,7 @@ defmodule Domain.Events.ReplicationConnectionTest do
|
||||
end)
|
||||
|
||||
assert log_output =~ "No hook defined for update on table unknown_table"
|
||||
assert log_output =~ "Please implement Domain.Events.Hooks for this table"
|
||||
assert log_output =~ "Please implement Domain.Changes.Hooks for this table"
|
||||
end
|
||||
|
||||
test "handles known tables", %{tables: tables} do
|
||||
@@ -136,12 +136,15 @@ defmodule Domain.Events.ReplicationConnectionTest do
|
||||
end)
|
||||
|
||||
assert log_output =~ "No hook defined for delete on table unknown_table"
|
||||
assert log_output =~ "Please implement Domain.Events.Hooks for this table"
|
||||
assert log_output =~ "Please implement Domain.Changes.Hooks for this table"
|
||||
end
|
||||
|
||||
test "handles known tables", %{tables: tables} do
|
||||
# Fill in some dummy foreign keys
|
||||
old_data = %{
|
||||
"account_id" => Ecto.UUID.generate(),
|
||||
"resource_id" => Ecto.UUID.generate(),
|
||||
"gateway_group_id" => Ecto.UUID.generate(),
|
||||
"id" => Ecto.UUID.generate(),
|
||||
"name" => "deleted item"
|
||||
}
|
||||
@@ -227,7 +230,7 @@ defmodule Domain.Events.ReplicationConnectionTest do
|
||||
end)
|
||||
|
||||
assert log_output =~ "No hook defined for insert on table test_table_insert"
|
||||
assert log_output =~ "Please implement Domain.Events.Hooks for this table"
|
||||
assert log_output =~ "Please implement Domain.Changes.Hooks for this table"
|
||||
|
||||
# Test update
|
||||
log_output =
|
||||
@@ -236,7 +239,7 @@ defmodule Domain.Events.ReplicationConnectionTest do
|
||||
end)
|
||||
|
||||
assert log_output =~ "No hook defined for update on table test_table_update"
|
||||
assert log_output =~ "Please implement Domain.Events.Hooks for this table"
|
||||
assert log_output =~ "Please implement Domain.Changes.Hooks for this table"
|
||||
|
||||
# Test delete
|
||||
log_output =
|
||||
@@ -245,7 +248,7 @@ defmodule Domain.Events.ReplicationConnectionTest do
|
||||
end)
|
||||
|
||||
assert log_output =~ "No hook defined for delete on table test_table_delete"
|
||||
assert log_output =~ "Please implement Domain.Events.Hooks for this table"
|
||||
assert log_output =~ "Please implement Domain.Changes.Hooks for this table"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -271,16 +274,16 @@ defmodule Domain.Events.ReplicationConnectionTest do
|
||||
test "all configured tables have hook modules" do
|
||||
# This test ensures our tables_to_hooks map is properly configured
|
||||
tables_to_hooks = %{
|
||||
"accounts" => Domain.Events.Hooks.Accounts,
|
||||
"actor_group_memberships" => Domain.Events.Hooks.ActorGroupMemberships,
|
||||
"clients" => Domain.Events.Hooks.Clients,
|
||||
"flows" => Domain.Events.Hooks.Flows,
|
||||
"gateway_groups" => Domain.Events.Hooks.GatewayGroups,
|
||||
"gateways" => Domain.Events.Hooks.Gateways,
|
||||
"policies" => Domain.Events.Hooks.Policies,
|
||||
"resource_connections" => Domain.Events.Hooks.ResourceConnections,
|
||||
"resources" => Domain.Events.Hooks.Resources,
|
||||
"tokens" => Domain.Events.Hooks.Tokens
|
||||
"accounts" => Domain.Changes.Hooks.Accounts,
|
||||
"actor_group_memberships" => Domain.Changes.Hooks.ActorGroupMemberships,
|
||||
"clients" => Domain.Changes.Hooks.Clients,
|
||||
"flows" => Domain.Changes.Hooks.Flows,
|
||||
"gateway_groups" => Domain.Changes.Hooks.GatewayGroups,
|
||||
"gateways" => Domain.Changes.Hooks.Gateways,
|
||||
"policies" => Domain.Changes.Hooks.Policies,
|
||||
"resource_connections" => Domain.Changes.Hooks.ResourceConnections,
|
||||
"resources" => Domain.Changes.Hooks.Resources,
|
||||
"tokens" => Domain.Changes.Hooks.Tokens
|
||||
}
|
||||
|
||||
# Verify the mapping includes all expected tables
|
||||
@@ -1,4 +0,0 @@
|
||||
defmodule Domain.EventsTest do
|
||||
# TODO: WAL
|
||||
# Add integration tests to ensure side effects trigger broadcasts in general
|
||||
end
|
||||
@@ -55,198 +55,6 @@ defmodule Domain.FlowsTest do
|
||||
end
|
||||
|
||||
describe "create_flow/7" do
|
||||
# test "returns error when resource does not exist", %{
|
||||
# client: client,
|
||||
# gateway: gateway,
|
||||
# subject: subject
|
||||
# } do
|
||||
# resource_id = Ecto.UUID.generate()
|
||||
# assert authorize_flow(client, gateway, resource_id, subject) == {:error, :not_found}
|
||||
# end
|
||||
#
|
||||
# test "returns error when UUID is invalid", %{
|
||||
# client: client,
|
||||
# gateway: gateway,
|
||||
# subject: subject
|
||||
# } do
|
||||
# assert authorize_flow(client, gateway, "foo", subject) == {:error, :not_found}
|
||||
# end
|
||||
#
|
||||
# test "returns authorized resource", %{
|
||||
# client: client,
|
||||
# gateway: gateway,
|
||||
# resource: resource,
|
||||
# policy: policy,
|
||||
# subject: subject
|
||||
# } do
|
||||
# assert {:ok, fetched_resource, _flow, _expires_at} =
|
||||
# authorize_flow(client, gateway, resource.id, subject)
|
||||
#
|
||||
# assert fetched_resource.id == resource.id
|
||||
# assert hd(fetched_resource.authorized_by_policies).id == policy.id
|
||||
# end
|
||||
#
|
||||
# test "returns error when some conditions are not satisfied", %{
|
||||
# account: account,
|
||||
# actor_group: actor_group,
|
||||
# client: client,
|
||||
# gateway_group: gateway_group,
|
||||
# gateway: gateway,
|
||||
# subject: subject
|
||||
# } do
|
||||
# resource =
|
||||
# Fixtures.Resources.create_resource(
|
||||
# account: account,
|
||||
# connections: [%{gateway_group_id: gateway_group.id}]
|
||||
# )
|
||||
#
|
||||
# Fixtures.Policies.create_policy(
|
||||
# account: account,
|
||||
# actor_group: actor_group,
|
||||
# resource: resource,
|
||||
# conditions: [
|
||||
# %{
|
||||
# property: :remote_ip_location_region,
|
||||
# operator: :is_in,
|
||||
# values: ["AU"]
|
||||
# },
|
||||
# %{
|
||||
# property: :remote_ip_location_region,
|
||||
# operator: :is_not_in,
|
||||
# values: [client.last_seen_remote_ip_location_region]
|
||||
# },
|
||||
# %{
|
||||
# property: :remote_ip,
|
||||
# operator: :is_in_cidr,
|
||||
# values: ["0.0.0.0/0", "0::/0"]
|
||||
# }
|
||||
# ]
|
||||
# )
|
||||
#
|
||||
# assert authorize_flow(client, gateway, resource.id, subject) ==
|
||||
# {:error, {:forbidden, violated_properties: [:remote_ip_location_region]}}
|
||||
# end
|
||||
#
|
||||
# test "returns error when all conditions are not satisfied", %{
|
||||
# account: account,
|
||||
# actor_group: actor_group,
|
||||
# client: client,
|
||||
# gateway_group: gateway_group,
|
||||
# gateway: gateway,
|
||||
# subject: subject
|
||||
# } do
|
||||
# resource =
|
||||
# Fixtures.Resources.create_resource(
|
||||
# account: account,
|
||||
# connections: [%{gateway_group_id: gateway_group.id}]
|
||||
# )
|
||||
#
|
||||
# Fixtures.Policies.create_policy(
|
||||
# account: account,
|
||||
# actor_group: actor_group,
|
||||
# resource: resource,
|
||||
# conditions: [
|
||||
# %{
|
||||
# property: :remote_ip_location_region,
|
||||
# operator: :is_in,
|
||||
# values: ["AU"]
|
||||
# },
|
||||
# %{
|
||||
# property: :remote_ip_location_region,
|
||||
# operator: :is_not_in,
|
||||
# values: [client.last_seen_remote_ip_location_region]
|
||||
# }
|
||||
# ]
|
||||
# )
|
||||
#
|
||||
# assert authorize_flow(client, gateway, resource.id, subject) ==
|
||||
# {:error, {:forbidden, violated_properties: [:remote_ip_location_region]}}
|
||||
# end
|
||||
#
|
||||
# test "creates a flow when the only policy conditions are satisfied", %{
|
||||
# account: account,
|
||||
# actor: actor,
|
||||
# resource: resource,
|
||||
# client: client,
|
||||
# policy: policy,
|
||||
# gateway: gateway,
|
||||
# subject: subject
|
||||
# } do
|
||||
# actor_group2 = Fixtures.Actors.create_group(account: account)
|
||||
# Fixtures.Actors.create_membership(account: account, actor: actor, group: actor_group2)
|
||||
#
|
||||
# time = Time.utc_now()
|
||||
# midnight = Time.from_iso8601!("23:59:59.999999")
|
||||
#
|
||||
# date = Date.utc_today()
|
||||
# day_of_week = Enum.at(~w[M T W R F S U], Date.day_of_week(date) - 1)
|
||||
#
|
||||
# Fixtures.Policies.create_policy(
|
||||
# account: account,
|
||||
# actor_group: actor_group2,
|
||||
# resource: resource,
|
||||
# conditions: [
|
||||
# %{
|
||||
# property: :remote_ip_location_region,
|
||||
# operator: :is_not_in,
|
||||
# values: [client.last_seen_remote_ip_location_region]
|
||||
# },
|
||||
# %{
|
||||
# property: :current_utc_datetime,
|
||||
# operator: :is_in_day_of_week_time_ranges,
|
||||
# values: [
|
||||
# "#{day_of_week}/#{time}-#{midnight}/UTC"
|
||||
# ]
|
||||
# }
|
||||
# ]
|
||||
# )
|
||||
#
|
||||
# assert {:ok, _fetched_resource, flow, expires_at} =
|
||||
# authorize_flow(client, gateway, resource.id, subject)
|
||||
#
|
||||
# assert flow.policy_id == policy.id
|
||||
# assert DateTime.diff(expires_at, DateTime.new!(date, midnight)) < 5
|
||||
# end
|
||||
#
|
||||
# test "creates a flow when all conditions for at least one of the policies are satisfied", %{
|
||||
# account: account,
|
||||
# actor_group: actor_group,
|
||||
# client: client,
|
||||
# gateway_group: gateway_group,
|
||||
# gateway: gateway,
|
||||
# subject: subject
|
||||
# } do
|
||||
# resource =
|
||||
# Fixtures.Resources.create_resource(
|
||||
# account: account,
|
||||
# connections: [%{gateway_group_id: gateway_group.id}]
|
||||
# )
|
||||
#
|
||||
# Fixtures.Policies.create_policy(
|
||||
# account: account,
|
||||
# actor_group: actor_group,
|
||||
# resource: resource,
|
||||
# conditions: [
|
||||
# %{
|
||||
# property: :remote_ip_location_region,
|
||||
# operator: :is_in,
|
||||
# values: [client.last_seen_remote_ip_location_region]
|
||||
# },
|
||||
# %{
|
||||
# property: :remote_ip,
|
||||
# operator: :is_in_cidr,
|
||||
# values: ["0.0.0.0/0", "0::/0"]
|
||||
# }
|
||||
# ]
|
||||
# )
|
||||
#
|
||||
# assert {:ok, _fetched_resource, flow, expires_at} =
|
||||
# authorize_flow(client, gateway, resource.id, subject)
|
||||
#
|
||||
# assert flow.resource_id == resource.id
|
||||
# assert expires_at == subject.expires_at
|
||||
# end
|
||||
|
||||
test "creates a new flow for users", %{
|
||||
account: account,
|
||||
gateway: gateway,
|
||||
@@ -266,7 +74,7 @@ defmodule Domain.FlowsTest do
|
||||
client,
|
||||
gateway,
|
||||
resource.id,
|
||||
policy,
|
||||
policy.id,
|
||||
membership.id,
|
||||
subject,
|
||||
subject.expires_at
|
||||
@@ -306,7 +114,7 @@ defmodule Domain.FlowsTest do
|
||||
client,
|
||||
gateway,
|
||||
resource.id,
|
||||
policy,
|
||||
policy.id,
|
||||
membership.id,
|
||||
subject,
|
||||
subject.expires_at
|
||||
@@ -323,36 +131,6 @@ defmodule Domain.FlowsTest do
|
||||
assert flow.actor_group_membership_id == membership.id
|
||||
assert flow.expires_at == subject.expires_at
|
||||
end
|
||||
|
||||
# TODO: Rename Flows
|
||||
# Authorization a policy authorization ("flow") makes no sense
|
||||
# test "returns error when subject has no permission to create flows", %{
|
||||
# client: client,
|
||||
# gateway: gateway,
|
||||
# resource: resource,
|
||||
# policy: policy,
|
||||
# subject: subject
|
||||
# } do
|
||||
# subject = Fixtures.Auth.remove_permissions(subject)
|
||||
#
|
||||
# assert create_flow(client, gateway, resource.id, policy, subject) ==
|
||||
# {:error,
|
||||
# {:unauthorized,
|
||||
# reason: :missing_permissions,
|
||||
# missing_permissions: [
|
||||
# Flows.Authorizer.create_flows_permission()
|
||||
# ]}}
|
||||
#
|
||||
# subject = Fixtures.Auth.add_permission(subject, Flows.Authorizer.create_flows_permission())
|
||||
#
|
||||
# assert create_flow(client, gateway, resource.id, policy, subject) ==
|
||||
# {:error,
|
||||
# {:unauthorized,
|
||||
# reason: :missing_permissions,
|
||||
# missing_permissions: [
|
||||
# Domain.Resources.Authorizer.view_available_resources_permission()
|
||||
# ]}}
|
||||
# end
|
||||
end
|
||||
|
||||
describe "reauthorize_flow/1" do
|
||||
@@ -448,7 +226,7 @@ defmodule Domain.FlowsTest do
|
||||
]
|
||||
)
|
||||
|
||||
assert {:error, :forbidden} = reauthorize_flow(flow)
|
||||
assert :error = reauthorize_flow(flow)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -833,9 +611,9 @@ defmodule Domain.FlowsTest do
|
||||
)
|
||||
|
||||
# Only authorize resource1 and resource2, resource3 should be deleted
|
||||
authorized_resources = [resource1, resource2]
|
||||
authorized_resource_ids = [resource1.id, resource2.id]
|
||||
|
||||
assert {1, nil} = delete_stale_flows_on_connect(client, authorized_resources)
|
||||
assert {1, nil} = delete_stale_flows_on_connect(client, authorized_resource_ids)
|
||||
|
||||
# Verify flow3 was deleted but flow1 and flow2 remain
|
||||
assert {:ok, ^flow1} = fetch_flow_by_id(flow1.id, subject)
|
||||
@@ -876,9 +654,9 @@ defmodule Domain.FlowsTest do
|
||||
)
|
||||
|
||||
# Authorize all resources
|
||||
authorized_resources = [resource1, resource2]
|
||||
authorized_resource_ids = [resource1.id, resource2.id]
|
||||
|
||||
assert {0, nil} = delete_stale_flows_on_connect(client, authorized_resources)
|
||||
assert {0, nil} = delete_stale_flows_on_connect(client, authorized_resource_ids)
|
||||
|
||||
# Verify both flows still exist
|
||||
assert {:ok, ^flow1} = fetch_flow_by_id(flow1.id, subject)
|
||||
@@ -918,9 +696,9 @@ defmodule Domain.FlowsTest do
|
||||
)
|
||||
|
||||
# Empty authorized list - all flows should be deleted
|
||||
authorized_resources = []
|
||||
authorized_resource_ids = []
|
||||
|
||||
assert {2, nil} = delete_stale_flows_on_connect(client, authorized_resources)
|
||||
assert {2, nil} = delete_stale_flows_on_connect(client, authorized_resource_ids)
|
||||
|
||||
# Verify both flows were deleted
|
||||
assert {:error, :not_found} = fetch_flow_by_id(flow1.id, subject)
|
||||
@@ -964,9 +742,9 @@ defmodule Domain.FlowsTest do
|
||||
)
|
||||
|
||||
# Only authorize resource2 for the first client (resource1 should be deleted)
|
||||
authorized_resources = [resource2]
|
||||
authorized_resource_ids = [resource2.id]
|
||||
|
||||
assert {1, nil} = delete_stale_flows_on_connect(client, authorized_resources)
|
||||
assert {1, nil} = delete_stale_flows_on_connect(client, authorized_resource_ids)
|
||||
|
||||
# Verify only the first client's flow was deleted
|
||||
assert {:error, :not_found} = fetch_flow_by_id(flow1.id, subject)
|
||||
@@ -998,9 +776,9 @@ defmodule Domain.FlowsTest do
|
||||
Fixtures.Flows.create_flow()
|
||||
|
||||
# Empty authorized list for current account
|
||||
authorized_resources = []
|
||||
authorized_resource_ids = []
|
||||
|
||||
assert {1, nil} = delete_stale_flows_on_connect(client, authorized_resources)
|
||||
assert {1, nil} = delete_stale_flows_on_connect(client, authorized_resource_ids)
|
||||
|
||||
# Verify only the current account's flow was deleted
|
||||
assert {:error, :not_found} = fetch_flow_by_id(flow1.id, subject)
|
||||
@@ -1012,9 +790,9 @@ defmodule Domain.FlowsTest do
|
||||
client: client
|
||||
} do
|
||||
# Try to delete stale flows for a client with no flows
|
||||
authorized_resources = []
|
||||
authorized_resource_ids = []
|
||||
|
||||
assert {0, nil} = delete_stale_flows_on_connect(client, authorized_resources)
|
||||
assert {0, nil} = delete_stale_flows_on_connect(client, authorized_resource_ids)
|
||||
end
|
||||
|
||||
test "handles case when client has no flows but resources are provided", %{
|
||||
@@ -1023,9 +801,10 @@ defmodule Domain.FlowsTest do
|
||||
} do
|
||||
# Create a client with no flows
|
||||
client_with_no_flows = Fixtures.Clients.create_client(account: account)
|
||||
authorized_resources = [resource]
|
||||
authorized_resource_ids = [resource.id]
|
||||
|
||||
assert {0, nil} = delete_stale_flows_on_connect(client_with_no_flows, authorized_resources)
|
||||
assert {0, nil} =
|
||||
delete_stale_flows_on_connect(client_with_no_flows, authorized_resource_ids)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,6 +2,7 @@ defmodule Domain.GatewaysTest do
|
||||
use Domain.DataCase, async: true
|
||||
import Domain.Gateways
|
||||
alias Domain.{Gateways, Tokens, Resources}
|
||||
import Domain.Cache.Cacheable, only: [to_cache: 1]
|
||||
|
||||
setup do
|
||||
account = Fixtures.Accounts.create_account()
|
||||
@@ -689,22 +690,31 @@ defmodule Domain.GatewaysTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "all_connected_gateways_for_resource/3" do
|
||||
describe "all_compatible_gateways_for_client_and_resource/3" do
|
||||
setup %{account: account} do
|
||||
client = Fixtures.Clients.create_client(account: account)
|
||||
|
||||
%{client: client}
|
||||
end
|
||||
|
||||
test "returns empty list when there are no online gateways", %{
|
||||
account: account,
|
||||
client: client,
|
||||
subject: subject
|
||||
} do
|
||||
resource = Fixtures.Resources.create_resource(account: account)
|
||||
resource = Fixtures.Resources.create_resource(account: account) |> to_cache()
|
||||
|
||||
Fixtures.Gateways.create_gateway(account: account)
|
||||
|
||||
Fixtures.Gateways.create_gateway(account: account)
|
||||
|> Fixtures.Gateways.delete_gateway()
|
||||
|
||||
assert all_connected_gateways_for_resource(resource, subject) == {:ok, []}
|
||||
assert all_compatible_gateways_for_client_and_resource(client, resource, subject) ==
|
||||
{:ok, []}
|
||||
end
|
||||
|
||||
test "returns list of connected gateways for a given resource", %{
|
||||
test "returns list of connected gateways for a given client and resource", %{
|
||||
client: client,
|
||||
account: account,
|
||||
subject: subject
|
||||
} do
|
||||
@@ -715,54 +725,81 @@ defmodule Domain.GatewaysTest do
|
||||
account: account,
|
||||
connections: [%{gateway_group_id: gateway.group_id}]
|
||||
)
|
||||
|> to_cache()
|
||||
|
||||
assert Gateways.Presence.connect(gateway) == :ok
|
||||
|
||||
assert {:ok, [connected_gateway]} = all_connected_gateways_for_resource(resource, subject)
|
||||
assert {:ok, [connected_gateway]} =
|
||||
all_compatible_gateways_for_client_and_resource(client, resource, subject)
|
||||
|
||||
assert connected_gateway.id == gateway.id
|
||||
end
|
||||
|
||||
test "does not return connected gateways that are not connected to given resource", %{
|
||||
test "returns empty list when client is more than 1 version ahead", %{
|
||||
client: client,
|
||||
account: account,
|
||||
subject: subject
|
||||
} do
|
||||
resource = Fixtures.Resources.create_resource(account: account)
|
||||
client = %{client | last_seen_version: "1.5.0"}
|
||||
gateway = Fixtures.Gateways.create_gateway(account: account)
|
||||
|
||||
assert Gateways.Presence.connect(gateway) == :ok
|
||||
|
||||
assert all_connected_gateways_for_resource(resource, subject) == {:ok, []}
|
||||
end
|
||||
end
|
||||
|
||||
describe "gateway_can_connect_to_resource?/2" do
|
||||
test "returns true when gateway can connect to resource", %{account: account} do
|
||||
gateway = Fixtures.Gateways.create_gateway(account: account)
|
||||
:ok = Gateways.Presence.connect(gateway)
|
||||
|
||||
resource =
|
||||
Fixtures.Resources.create_resource(
|
||||
account: account,
|
||||
connections: [%{gateway_group_id: gateway.group_id}]
|
||||
)
|
||||
|> to_cache()
|
||||
|
||||
assert gateway_can_connect_to_resource?(gateway, resource)
|
||||
assert Gateways.Presence.connect(gateway) == :ok
|
||||
|
||||
assert all_compatible_gateways_for_client_and_resource(client, resource, subject) ==
|
||||
{:ok, []}
|
||||
end
|
||||
|
||||
test "returns false when gateway cannot connect to resource", %{account: account} do
|
||||
gateway = Fixtures.Gateways.create_gateway(account: account)
|
||||
:ok = Gateways.Presence.connect(gateway)
|
||||
test "returns empty list of resource is not compatible", %{
|
||||
client: client,
|
||||
account: account,
|
||||
subject: subject
|
||||
} do
|
||||
gateway =
|
||||
Fixtures.Gateways.create_gateway(
|
||||
account: account,
|
||||
context:
|
||||
Fixtures.Auth.build_context(
|
||||
type: :gateway_group,
|
||||
user_agent: "Linux/1.2.3 connlib/1.0.0"
|
||||
)
|
||||
)
|
||||
|
||||
resource = Fixtures.Resources.create_resource(account: account)
|
||||
client = %{client | last_seen_version: "1.3.0"}
|
||||
|
||||
refute gateway_can_connect_to_resource?(gateway, resource)
|
||||
group = Fixtures.Gateways.create_internet_group(account: account)
|
||||
|
||||
resource =
|
||||
Fixtures.Resources.create_internet_resource(
|
||||
account: account,
|
||||
connections: [%{gateway_group_id: group.id}]
|
||||
)
|
||||
|> to_cache()
|
||||
|
||||
assert Gateways.Presence.connect(gateway) == :ok
|
||||
|
||||
assert all_compatible_gateways_for_client_and_resource(client, resource, subject) ==
|
||||
{:ok, []}
|
||||
end
|
||||
|
||||
test "returns false when gateway is offline", %{account: account} do
|
||||
test "does not return connected gateways that are not connected to given resource", %{
|
||||
client: client,
|
||||
account: account,
|
||||
subject: subject
|
||||
} do
|
||||
resource = Fixtures.Resources.create_resource(account: account) |> to_cache()
|
||||
gateway = Fixtures.Gateways.create_gateway(account: account)
|
||||
resource = Fixtures.Resources.create_resource(account: account)
|
||||
|
||||
refute gateway_can_connect_to_resource?(gateway, resource)
|
||||
assert Gateways.Presence.connect(gateway) == :ok
|
||||
|
||||
assert all_compatible_gateways_for_client_and_resource(client, resource, subject) ==
|
||||
{:ok, []}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -234,6 +234,49 @@ defmodule Domain.PoliciesTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "all_policies_in_gateway_group_for_resource_id_and_actor_id!/4" do
|
||||
test "does not return policies for other gateway groups", %{
|
||||
account: account,
|
||||
resource: resource,
|
||||
actor_group: actor_group,
|
||||
actor: actor
|
||||
} do
|
||||
policy =
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
resource: resource,
|
||||
actor_group: actor_group
|
||||
)
|
||||
|
||||
connections = resource.connections
|
||||
original_gateway_group_id = List.first(connections).gateway_group_id
|
||||
|
||||
new_gateway_group = Fixtures.Gateways.create_group(account: account)
|
||||
|
||||
# Admin moves resource to another site
|
||||
connections = [%{gateway_group_id: new_gateway_group.id}]
|
||||
resource = Fixtures.Resources.update_resource(resource, connections: connections)
|
||||
|
||||
# Since this function is used to reauthorize flows, we need to ensure we don't find
|
||||
# policies for the old gateway group
|
||||
assert [] =
|
||||
all_policies_in_gateway_group_for_resource_id_and_actor_id!(
|
||||
account.id,
|
||||
original_gateway_group_id,
|
||||
resource.id,
|
||||
actor.id
|
||||
)
|
||||
|
||||
assert [^policy] =
|
||||
all_policies_in_gateway_group_for_resource_id_and_actor_id!(
|
||||
account.id,
|
||||
new_gateway_group.id,
|
||||
resource.id,
|
||||
actor.id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "create_policy/2" do
|
||||
test "returns changeset error on empty params", %{subject: subject} do
|
||||
assert {:error, changeset} = create_policy(%{}, subject)
|
||||
@@ -1190,7 +1233,7 @@ defmodule Domain.PoliciesTest do
|
||||
|
||||
in_three_days = DateTime.utc_now() |> DateTime.add(3, :day)
|
||||
|
||||
assert {:ok, expires_at, ^policy1} =
|
||||
assert {:ok, ^policy1, expires_at} =
|
||||
longest_conforming_policy_for_client(
|
||||
[policy1, policy2],
|
||||
client,
|
||||
@@ -1201,7 +1244,7 @@ defmodule Domain.PoliciesTest do
|
||||
|
||||
in_one_minute = DateTime.utc_now() |> DateTime.add(1, :minute)
|
||||
|
||||
assert {:ok, expires_at, ^policy1} =
|
||||
assert {:ok, ^policy1, expires_at} =
|
||||
longest_conforming_policy_for_client(
|
||||
[policy1, policy2],
|
||||
client,
|
||||
|
||||
@@ -1501,37 +1501,145 @@ defmodule Domain.ResourcesTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "connected?/2" do
|
||||
test "returns true when resource has a connection to a gateway", %{
|
||||
account: account,
|
||||
subject: subject
|
||||
} do
|
||||
group = Fixtures.Gateways.create_group(account: account, subject: subject)
|
||||
gateway = Fixtures.Gateways.create_gateway(account: account, group: group)
|
||||
describe "adapt_resource_for_version/2" do
|
||||
setup do
|
||||
account = Fixtures.Accounts.create_account()
|
||||
|
||||
resource =
|
||||
ip_resource =
|
||||
Fixtures.Resources.create_resource(type: :ip, account: account, address: "1.1.1.1")
|
||||
|
||||
cidr_resource =
|
||||
Fixtures.Resources.create_resource(type: :cidr, account: account, address: "1.1.1.1/32")
|
||||
|
||||
dns_resource =
|
||||
Fixtures.Resources.create_resource(
|
||||
account: account,
|
||||
connections: [%{gateway_group_id: group.id}]
|
||||
type: :dns,
|
||||
account: account
|
||||
)
|
||||
|
||||
assert connected?(resource, gateway)
|
||||
internet_group = Fixtures.Gateways.create_internet_group(account: account)
|
||||
|
||||
internet_resource =
|
||||
Fixtures.Resources.create_internet_resource(
|
||||
connections: [%{gateway_group_id: internet_group.id}],
|
||||
account: account
|
||||
)
|
||||
|
||||
%{
|
||||
account: account,
|
||||
ip_resource: ip_resource,
|
||||
cidr_resource: cidr_resource,
|
||||
dns_resource: dns_resource,
|
||||
internet_resource: internet_resource
|
||||
}
|
||||
end
|
||||
|
||||
test "raises resource and gateway don't belong to the same account" do
|
||||
gateway = Fixtures.Gateways.create_gateway()
|
||||
resource = Fixtures.Resources.create_resource()
|
||||
test "for ip resource returns the same resource for all versions", %{ip_resource: ip_resource} do
|
||||
versions = ~w(
|
||||
1.0.0
|
||||
1.1.0
|
||||
1.2.0
|
||||
1.3.0
|
||||
1.4.0
|
||||
1.5.0
|
||||
1.6.0
|
||||
)
|
||||
|
||||
assert_raise FunctionClauseError, fn ->
|
||||
connected?(resource, gateway)
|
||||
for version <- versions do
|
||||
assert adapt_resource_for_version(ip_resource, version) == ip_resource
|
||||
end
|
||||
end
|
||||
|
||||
test "returns false when resource has no connection to a gateway", %{account: account} do
|
||||
gateway = Fixtures.Gateways.create_gateway(account: account)
|
||||
resource = Fixtures.Resources.create_resource(account: account)
|
||||
test "for cidr resource returns the same resource for all versions", %{
|
||||
cidr_resource: cidr_resource
|
||||
} do
|
||||
versions = ~w(
|
||||
1.0.0
|
||||
1.1.0
|
||||
1.2.0
|
||||
1.3.0
|
||||
1.4.0
|
||||
1.5.0
|
||||
1.6.0
|
||||
)
|
||||
|
||||
refute connected?(resource, gateway)
|
||||
for version <- versions do
|
||||
assert adapt_resource_for_version(cidr_resource, version) == cidr_resource
|
||||
end
|
||||
end
|
||||
|
||||
test "for dns resource transforms the address for versions < 1.2.0", %{
|
||||
dns_resource: dns_resource
|
||||
} do
|
||||
addresses = [
|
||||
{"**.example.com", "*.example.com"},
|
||||
{"*.example.com", "?.example.com"},
|
||||
{"foo.bar.example.com", "foo.bar.example.com"}
|
||||
]
|
||||
|
||||
versions = ~w(
|
||||
1.0.0
|
||||
1.1.0
|
||||
)
|
||||
|
||||
for version <- versions do
|
||||
for address <- addresses do
|
||||
dns_resource = %{dns_resource | address: elem(address, 0)}
|
||||
adapted = adapt_resource_for_version(dns_resource, version)
|
||||
|
||||
assert adapted.address == elem(address, 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "for dns resource returns nil for incompatible addresses", %{
|
||||
dns_resource: dns_resource
|
||||
} do
|
||||
addresses = ~w(
|
||||
foo.?.example.com
|
||||
foo.bar*bar.example.com
|
||||
)
|
||||
|
||||
versions = ~w(
|
||||
1.0.0
|
||||
1.1.0
|
||||
)
|
||||
|
||||
for version <- versions do
|
||||
for address <- addresses do
|
||||
dns_resource = %{dns_resource | address: address}
|
||||
assert nil == adapt_resource_for_version(dns_resource, version)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "for internet resource returns nil for versions < 1.3.0", %{
|
||||
internet_resource: internet_resource
|
||||
} do
|
||||
versions = ~w(
|
||||
1.0.0
|
||||
1.1.0
|
||||
1.2.0
|
||||
)
|
||||
|
||||
for version <- versions do
|
||||
assert nil == adapt_resource_for_version(internet_resource, version)
|
||||
end
|
||||
end
|
||||
|
||||
test "for internet resource returns resource as-is for versions >= 1.3.0", %{
|
||||
internet_resource: internet_resource
|
||||
} do
|
||||
versions = ~w(
|
||||
1.3.0
|
||||
1.4.0
|
||||
1.5.0
|
||||
1.6.0
|
||||
)
|
||||
|
||||
for version <- versions do
|
||||
assert adapt_resource_for_version(internet_resource, version) == internet_resource
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -69,4 +69,17 @@ defmodule Domain.Fixtures.Policies do
|
||||
{:ok, policy} = Policies.delete_policy(policy, subject)
|
||||
policy
|
||||
end
|
||||
|
||||
def update_policy(policy, attrs) do
|
||||
policy = Repo.preload(policy, :account)
|
||||
|
||||
subject =
|
||||
Fixtures.Auth.create_subject(
|
||||
account: policy.account,
|
||||
actor: [type: :account_admin_user]
|
||||
)
|
||||
|
||||
{:ok, policy} = Policies.update_policy(policy, Enum.into(attrs, %{}), subject)
|
||||
policy
|
||||
end
|
||||
end
|
||||
|
||||
@@ -84,4 +84,18 @@ defmodule Domain.Fixtures.Resources do
|
||||
{:ok, resource} = Domain.Resources.delete_resource(resource, subject)
|
||||
resource
|
||||
end
|
||||
|
||||
def update_resource(resource, attrs) do
|
||||
attrs = Enum.into(attrs, %{})
|
||||
resource = Repo.preload(resource, :account)
|
||||
|
||||
subject =
|
||||
Fixtures.Auth.create_subject(
|
||||
account: resource.account,
|
||||
actor: [type: :account_admin_user]
|
||||
)
|
||||
|
||||
{:ok, resource} = Domain.Resources.update_resource(resource, attrs, subject)
|
||||
resource
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
defmodule Web.Policies.Index do
|
||||
use Web, :live_view
|
||||
alias Domain.{PubSub, Policies}
|
||||
alias Domain.{Changes.Change, PubSub, Policies}
|
||||
|
||||
def mount(_params, _session, socket) do
|
||||
if connected?(socket) do
|
||||
@@ -123,11 +123,11 @@ defmodule Web.Policies.Index do
|
||||
when event in ["paginate", "order_by", "filter", "reload"],
|
||||
do: handle_live_table_event(event, params, socket)
|
||||
|
||||
def handle_info({_action, %Policies.Policy{}, %Policies.Policy{}}, socket) do
|
||||
def handle_info(%Change{old_struct: %Policies.Policy{}}, socket) do
|
||||
{:noreply, assign(socket, stale: true)}
|
||||
end
|
||||
|
||||
def handle_info({_action, %Policies.Policy{}}, socket) do
|
||||
def handle_info(%Change{struct: %Policies.Policy{}}, socket) do
|
||||
{:noreply, assign(socket, stale: true)}
|
||||
end
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
defmodule Web.Resources.Index do
|
||||
use Web, :live_view
|
||||
alias Domain.PubSub
|
||||
alias Domain.Resources
|
||||
alias Domain.{Changes.Change, PubSub, Resources}
|
||||
|
||||
def mount(_params, _session, socket) do
|
||||
if connected?(socket) do
|
||||
@@ -170,11 +169,11 @@ defmodule Web.Resources.Index do
|
||||
when event in ["paginate", "order_by", "filter", "reload"],
|
||||
do: handle_live_table_event(event, params, socket)
|
||||
|
||||
def handle_info({_action, %Resources.Resource{}, %Resources.Resource{}}, socket) do
|
||||
def handle_info(%Change{old_struct: %Resources.Resource{}}, socket) do
|
||||
{:noreply, assign(socket, stale: true)}
|
||||
end
|
||||
|
||||
def handle_info({_action, %Resources.Resource{}}, socket) do
|
||||
def handle_info(%Change{struct: %Resources.Resource{}}, socket) do
|
||||
{:noreply, assign(socket, stale: true)}
|
||||
end
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ defmodule Web.Acceptance.AuthTest do
|
||||
for token <- tokens do
|
||||
assert %DateTime{} = token.deleted_at
|
||||
|
||||
Domain.Events.Hooks.Tokens.on_delete(%{
|
||||
Domain.Changes.Hooks.Tokens.on_delete(0, %{
|
||||
"remaining_attempts" => token.remaining_attempts,
|
||||
"actor_id" => token.actor_id,
|
||||
"name" => token.name,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
defmodule Web.Live.Policies.IndexTest do
|
||||
use Web.ConnCase, async: true
|
||||
alias Domain.Events
|
||||
alias Domain.Changes
|
||||
|
||||
setup do
|
||||
account = Fixtures.Accounts.create_account()
|
||||
@@ -95,7 +95,7 @@ defmodule Web.Live.Policies.IndexTest do
|
||||
|
||||
policy = Fixtures.Policies.create_policy(account: account, description: "foo bar")
|
||||
|
||||
Events.Hooks.Policies.on_insert(%{
|
||||
Changes.Hooks.Policies.on_insert(0, %{
|
||||
"id" => policy.id,
|
||||
"actor_group_id" => policy.actor_group_id,
|
||||
"resource_id" => policy.resource_id,
|
||||
@@ -128,17 +128,13 @@ defmodule Web.Live.Policies.IndexTest do
|
||||
|
||||
Domain.Policies.delete_policy(policy, subject)
|
||||
|
||||
Events.Hooks.Policies.on_delete(%{
|
||||
Changes.Hooks.Policies.on_delete(0, %{
|
||||
"id" => policy.id,
|
||||
"actor_group_id" => policy.actor_group_id,
|
||||
"resource_id" => policy.resource_id,
|
||||
"account_id" => account.id
|
||||
})
|
||||
|
||||
# TODO: WAL
|
||||
# Remove this after direct broadcast
|
||||
Process.sleep(100)
|
||||
|
||||
reload_btn =
|
||||
lv
|
||||
|> element("#policies-reload-btn")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
defmodule Web.Live.Resources.EditTest do
|
||||
use Web.ConnCase, async: true
|
||||
alias Domain.{Events, PubSub, Resources}
|
||||
alias Domain.{Changes, PubSub, Resources}
|
||||
|
||||
setup do
|
||||
account = Fixtures.Accounts.create_account()
|
||||
@@ -245,7 +245,7 @@ defmodule Web.Live.Resources.EditTest do
|
||||
}
|
||||
|
||||
data = Map.put(old_data, "filters", attrs.filters)
|
||||
:ok = Events.Hooks.Resources.on_update(old_data, data)
|
||||
:ok = Changes.Hooks.Resources.on_update(0, old_data, data)
|
||||
|
||||
{:ok, _lv, html} =
|
||||
lv
|
||||
@@ -253,7 +253,11 @@ defmodule Web.Live.Resources.EditTest do
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, ~p"/#{account}/resources")
|
||||
|
||||
assert_receive {:updated, %Resources.Resource{}, %Resources.Resource{} = updated_resource}
|
||||
assert_receive %Changes.Change{
|
||||
lsn: 0,
|
||||
old_struct: %Resources.Resource{},
|
||||
struct: %Resources.Resource{} = updated_resource
|
||||
}
|
||||
|
||||
assert updated_resource.id == resource.id
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
defmodule Web.Live.Resources.IndexTest do
|
||||
use Web.ConnCase, async: true
|
||||
alias Domain.Events
|
||||
alias Domain.Changes
|
||||
|
||||
setup do
|
||||
account = Fixtures.Accounts.create_account()
|
||||
@@ -271,7 +271,7 @@ defmodule Web.Live.Resources.IndexTest do
|
||||
resource = Fixtures.Resources.create_resource(account: account)
|
||||
|
||||
# Simulate WAL broadcast
|
||||
Events.Hooks.Resources.on_insert(%{
|
||||
Changes.Hooks.Resources.on_insert(0, %{
|
||||
"id" => resource.id,
|
||||
"account_id" => account.id
|
||||
})
|
||||
@@ -302,7 +302,8 @@ defmodule Web.Live.Resources.IndexTest do
|
||||
|
||||
Domain.Resources.delete_resource(resource, subject)
|
||||
|
||||
Events.Hooks.Resources.on_update(
|
||||
Changes.Hooks.Resources.on_update(
|
||||
0,
|
||||
%{
|
||||
"id" => resource.id,
|
||||
"account_id" => account.id,
|
||||
|
||||
@@ -78,9 +78,9 @@ config :domain, Domain.ChangeLogs.ReplicationConnection,
|
||||
# We want to flush at most 500 change logs at a time
|
||||
flush_buffer_size: 500
|
||||
|
||||
config :domain, Domain.Events.ReplicationConnection,
|
||||
replication_slot_name: "events_slot",
|
||||
publication_name: "events_publication",
|
||||
config :domain, Domain.Changes.ReplicationConnection,
|
||||
replication_slot_name: "changes_slot",
|
||||
publication_name: "changes_publication",
|
||||
enabled: true,
|
||||
connection_opts: [
|
||||
hostname: "localhost",
|
||||
@@ -93,8 +93,8 @@ config :domain, Domain.Events.ReplicationConnection,
|
||||
password: "postgres"
|
||||
],
|
||||
# When changing these, make sure to also:
|
||||
# 1. Make appropriate changes to `Domain.Events.ReplicationConnection`
|
||||
# 2. Add an appropriate `Domain.Events.Hooks` module
|
||||
# 1. Make appropriate changes to `Domain.Changes.ReplicationConnection`
|
||||
# 2. Add an appropriate `Domain.Changes.Hooks` module
|
||||
# 3. Add tests and test WAL locally
|
||||
table_subscriptions: ~w[
|
||||
accounts
|
||||
|
||||
@@ -44,10 +44,10 @@ if config_env() == :prod do
|
||||
database: env_var_to_config!(:database_name)
|
||||
]
|
||||
|
||||
config :domain, Domain.Events.ReplicationConnection,
|
||||
config :domain, Domain.Changes.ReplicationConnection,
|
||||
enabled: env_var_to_config!(:background_jobs_enabled),
|
||||
replication_slot_name: env_var_to_config!(:database_events_replication_slot_name),
|
||||
publication_name: env_var_to_config!(:database_events_publication_name),
|
||||
replication_slot_name: env_var_to_config!(:database_changes_replication_slot_name),
|
||||
publication_name: env_var_to_config!(:database_changes_publication_name),
|
||||
connection_opts: [
|
||||
hostname: env_var_to_config!(:database_host),
|
||||
port: env_var_to_config!(:database_port),
|
||||
|
||||
@@ -53,9 +53,9 @@ config :domain, Domain.ChangeLogs.ReplicationConnection,
|
||||
database: "firezone_test#{partition_suffix}"
|
||||
]
|
||||
|
||||
config :domain, Domain.Events.ReplicationConnection,
|
||||
replication_slot_name: "test_events_slot",
|
||||
publication_name: "test_events_publication",
|
||||
config :domain, Domain.Changes.ReplicationConnection,
|
||||
replication_slot_name: "test_changes_slot",
|
||||
publication_name: "test_changes_publication",
|
||||
enabled: false,
|
||||
connection_opts: [
|
||||
database: "firezone_test#{partition_suffix}"
|
||||
|
||||
Reference in New Issue
Block a user