Rename Devices to Clients in Elixir app (#2008)

Renaming it back to clients to reflect service accounts and headless
clients use cases in the terminology. Such a rename will be very painful
on live data so better if we do it early on.

---------

Co-authored-by: Jamil Bou Kheir <jamilbk@users.noreply.github.com>
This commit is contained in:
Andrew Dryga
2023-09-13 06:37:27 -06:00
committed by GitHub
parent 178b68d770
commit 85b4aba9bc
66 changed files with 806 additions and 778 deletions

View File

@@ -94,7 +94,7 @@ Now you can verify that it's working by connecting to a websocket:
</details>
<details>
<summary>Device</summary>
<summary>Client</summary>
```elixir
export CLIENT_TOKEN_FROM_SEEDS="SFMyNTY.g2gDaANkAAhpZGVudGl0eW0AAAAkN2RhN2QxY2QtMTExYy00NGE3LWI1YWMtNDAyN2I5ZDIzMGU1bQAAACDZI3ehOZSu3JOSMREkvzrtKjs8jkrW6fpbVw9opDYmi24GANjCD-qIAWIB4TOA.XhoLEDjIzuv1SXEVUV6lfIHW12n5-J5aBDUKCl8ovMk"
@@ -102,25 +102,25 @@ Now you can verify that it's working by connecting to a websocket:
# Panel will only accept token if it's coming with this User-Agent header and from IP 172.28.0.1
export CLIENT_USER_AGENT="iOS/12.5 (iPhone) connlib/0.7.412"
websocat --header="User-Agent: ${CLIENT_USER_AGENT}" "ws://127.0.0.1:8081/device/websocket?token=${CLIENT_TOKEN_FROM_SEEDS}&external_id=thisisrandomandpersistent&name_suffix=kkX1&public_key=kceI60D6PrwOIiGoVz6hD7VYCgD1H57IVQlPJTTieUE="
websocat --header="User-Agent: ${CLIENT_USER_AGENT}" "ws://127.0.0.1:8081/client/websocket?token=${CLIENT_TOKEN_FROM_SEEDS}&external_id=thisisrandomandpersistent&name_suffix=kkX1&public_key=kceI60D6PrwOIiGoVz6hD7VYCgD1H57IVQlPJTTieUE="
# Here is what you will see in docker logs firezone-api-1
# firezone-api-1 | {"domain":["elixir"],"erl_level":"info","logging.googleapis.com/sourceLocation":{"file":"lib/phoenix/logger.ex","line":306,"function":"Elixir.Phoenix.Logger.phoenix_socket_connected/4"},"message":"CONNECTED TO API.Device.Socket in 83ms\n Transport: :websocket\n Serializer: Phoenix.Socket.V1.JSONSerializer\n Parameters: %{\"external_id\" => \"thisisrandomandpersistent\", \"name_suffix\" => \"kkX1\", \"public_key\" => \"[FILTERED]\", \"token\" => \"[FILTERED]\"}","severity":"INFO","time":"2023-06-23T21:01:49.566Z"}
# firezone-api-1 | {"domain":["elixir"],"erl_level":"info","logging.googleapis.com/sourceLocation":{"file":"lib/phoenix/logger.ex","line":306,"function":"Elixir.Phoenix.Logger.phoenix_socket_connected/4"},"message":"CONNECTED TO API.Client.Socket in 83ms\n Transport: :websocket\n Serializer: Phoenix.Socket.V1.JSONSerializer\n Parameters: %{\"external_id\" => \"thisisrandomandpersistent\", \"name_suffix\" => \"kkX1\", \"public_key\" => \"[FILTERED]\", \"token\" => \"[FILTERED]\"}","severity":"INFO","time":"2023-06-23T21:01:49.566Z"}
# After this you need to join the `device` topic and pass a `stamp_secret` in the payload.
# After this you need to join the `client` topic and pass a `stamp_secret` in the payload.
# For details on this structure see https://hexdocs.pm/phoenix/Phoenix.Socket.Message.html
{"event":"phx_join","topic":"device","payload":{},"ref":"unique_string_ref","join_ref":"unique_join_ref"}
{"event":"phx_join","topic":"client","payload":{},"ref":"unique_string_ref","join_ref":"unique_join_ref"}
{"ref":"unique_string_ref","topic":"device","event":"phx_reply","payload":{"status":"ok","response":{}}}
{"ref":null,"topic":"device","event":"init","payload":{"interface":{"ipv6":"fd00:2021:1111::11:f4bd","upstream_dns":[],"ipv4":"100.71.71.245"},"resources":[{"id":"4429d3aa-53ea-4c03-9435-4dee2899672b","name":"172.20.0.1/16","type":"cidr","address":"172.20.0.0/16"},{"id":"85a1cffc-70d3-46dd-aa6b-776192af7b06","name":"gitlab.mycorp.com","type":"dns","address":"gitlab.mycorp.com","ipv6":"fd00:2021:1111::5:b370","ipv4":"100.85.109.146"}]}}
{"ref":"unique_string_ref","topic":"client","event":"phx_reply","payload":{"status":"ok","response":{}}}
{"ref":null,"topic":"client","event":"init","payload":{"interface":{"ipv6":"fd00:2021:1111::11:f4bd","upstream_dns":[],"ipv4":"100.71.71.245"},"resources":[{"id":"4429d3aa-53ea-4c03-9435-4dee2899672b","name":"172.20.0.1/16","type":"cidr","address":"172.20.0.0/16"},{"id":"85a1cffc-70d3-46dd-aa6b-776192af7b06","name":"gitlab.mycorp.com","type":"dns","address":"gitlab.mycorp.com","ipv6":"fd00:2021:1111::5:b370","ipv4":"100.85.109.146"}]}}
# List online relays for a Resource
{"event":"list_relays","topic":"device","payload":{"resource_id":"4429d3aa-53ea-4c03-9435-4dee2899672b"},"ref":"unique_list_relays_ref"}
{"event":"list_relays","topic":"client","payload":{"resource_id":"4429d3aa-53ea-4c03-9435-4dee2899672b"},"ref":"unique_list_relays_ref"}
{"ref":"unique_list_relays_ref","topic":"device","event":"phx_reply","payload":{"status":"ok","response":{"relays":[{"type":"stun","uri":"stun:172.28.0.101:3478"},{"type":"turn","username":"1719090081:UVxHhieTJWaD8_Sg","password":"Ml65XDZyYpuBiEIvk/q0Zy6EEJ1ZwGa4pWztXFP+tOo","uri":"turn:172.28.0.101:3478","expires_at":1719090081}],"resource_id":"4429d3aa-53ea-4c03-9435-4dee2899672b"}}}
{"ref":"unique_list_relays_ref","topic":"client","event":"phx_reply","payload":{"status":"ok","response":{"relays":[{"type":"stun","uri":"stun:172.28.0.101:3478"},{"type":"turn","username":"1719090081:UVxHhieTJWaD8_Sg","password":"Ml65XDZyYpuBiEIvk/q0Zy6EEJ1ZwGa4pWztXFP+tOo","uri":"turn:172.28.0.101:3478","expires_at":1719090081}],"resource_id":"4429d3aa-53ea-4c03-9435-4dee2899672b"}}}
# Initiate connection to a resource
{"event":"request_connection","topic":"device","payload":{"resource_id":"4429d3aa-53ea-4c03-9435-4dee2899672b","device_rtc_session_description":"RTC_SD","device_preshared_key":"+HapiGI5UdeRjKuKTwk4ZPPYpCnlXHvvqebcIevL+2A="},"ref":"unique_request_connection_ref"}
{"event":"request_connection","topic":"client","payload":{"resource_id":"4429d3aa-53ea-4c03-9435-4dee2899672b","client_rtc_session_description":"RTC_SD","client_preshared_key":"+HapiGI5UdeRjKuKTwk4ZPPYpCnlXHvvqebcIevL+2A="},"ref":"unique_request_connection_ref"}
```
@@ -212,7 +212,7 @@ identity = Domain.Repo.get_by(Domain.Auth.Identity, provider_id: provider.id, pr
subject = Domain.Auth.build_subject(identity, nil, user_agent, remote_ip)
```
Listing connected gateways, relays, devices for an account:
Listing connected gateways, relays, clients for an account:
```elixir
account_id = "c89bcc8c-9392-4dae-a40d-888aef6d28e0"
@@ -220,7 +220,7 @@ account_id = "c89bcc8c-9392-4dae-a40d-888aef6d28e0"
%{
gateways: Domain.Gateways.Presence.list("gateways:#{account_id}"),
relays: Domain.Relays.Presence.list("relays:#{account_id}"),
devices: Domain.Devices.Presence.list("devices:#{account_id}"),
clients: Domain.Clients.Presence.list("clients:#{account_id}"),
}
```

View File

@@ -1,11 +1,11 @@
defmodule API.Device.Channel do
defmodule API.Client.Channel do
use API, :channel
alias API.Device.Views
alias Domain.{Devices, Resources, Gateways, Relays}
alias API.Client.Views
alias Domain.{Clients, Resources, Gateways, Relays}
require Logger
@impl true
def join("device", _payload, socket) do
def join("client", _payload, socket) do
expires_in =
DateTime.diff(socket.assigns.subject.expires_at, DateTime.utc_now(), :millisecond)
@@ -20,15 +20,15 @@ defmodule API.Device.Channel do
@impl true
def handle_info(:after_join, socket) do
API.Endpoint.subscribe("device:#{socket.assigns.device.id}")
:ok = Devices.connect_device(socket.assigns.device)
API.Endpoint.subscribe("client:#{socket.assigns.client.id}")
:ok = Clients.connect_client(socket.assigns.client)
{:ok, resources} = Domain.Resources.list_resources(socket.assigns.subject)
:ok =
push(socket, "init", %{
resources: Views.Resource.render_many(resources),
interface: Views.Interface.render(socket.assigns.device)
interface: Views.Interface.render(socket.assigns.client)
})
{:noreply, socket}
@@ -40,7 +40,7 @@ defmodule API.Device.Channel do
end
# This message is sent by the gateway when it is ready
# to accept the connection from the device
# to accept the connection from the client
def handle_info(
{:connect, socket_ref, resource_id, gateway_public_key, rtc_session_description},
socket
@@ -108,7 +108,7 @@ defmodule API.Device.Channel do
end
end
# This message is sent by the device when it already has connection to a gateway,
# This message is sent by the client when it already has connection to a gateway,
# but wants to connect to a new resource
def handle_in(
"reuse_connection",
@@ -127,7 +127,7 @@ defmodule API.Device.Channel do
gateway,
{:allow_access,
%{
device_id: socket.assigns.device.id,
client_id: socket.assigns.client.id,
resource_id: resource.id,
authorization_expires_at: socket.assigns.subject.expires_at
}}
@@ -140,14 +140,14 @@ defmodule API.Device.Channel do
end
end
# This message is sent by the device when it wants to connect to a new gateway
# This message is sent by the client when it wants to connect to a new gateway
def handle_in(
"request_connection",
%{
"gateway_id" => gateway_id,
"resource_id" => resource_id,
"device_rtc_session_description" => device_rtc_session_description,
"device_preshared_key" => preshared_key
"client_rtc_session_description" => client_rtc_session_description,
"client_preshared_key" => preshared_key
},
socket
) do
@@ -160,11 +160,11 @@ defmodule API.Device.Channel do
gateway,
{:request_connection, {self(), socket_ref(socket)},
%{
device_id: socket.assigns.device.id,
client_id: socket.assigns.client.id,
resource_id: resource.id,
authorization_expires_at: socket.assigns.subject.expires_at,
device_rtc_session_description: device_rtc_session_description,
device_preshared_key: preshared_key
client_rtc_session_description: client_rtc_session_description,
client_preshared_key: preshared_key
}}
)

View File

@@ -1,11 +1,11 @@
defmodule API.Device.Socket do
defmodule API.Client.Socket do
use Phoenix.Socket
alias Domain.{Auth, Devices}
alias Domain.{Auth, Clients}
require Logger
## Channels
channel "device", API.Device.Channel
channel "client", API.Client.Channel
## Authentication
@@ -20,11 +20,11 @@ defmodule API.Device.Socket do
real_ip = API.Sockets.real_ip(x_headers, peer_data)
with {:ok, subject} <- Auth.sign_in(token, user_agent, real_ip),
{:ok, device} <- Devices.upsert_device(attrs, subject) do
{:ok, client} <- Clients.upsert_client(attrs, subject) do
socket =
socket
|> assign(:subject, subject)
|> assign(:device, device)
|> assign(:client, client)
{:ok, socket}
else
@@ -32,7 +32,7 @@ defmodule API.Device.Socket do
{:error, :invalid_token}
{:error, reason} ->
Logger.debug("Error connecting device websocket: #{inspect(reason)}")
Logger.debug("Error connecting client websocket: #{inspect(reason)}")
{:error, reason}
end
end
@@ -42,5 +42,5 @@ defmodule API.Device.Socket do
end
@impl true
def id(socket), do: "device:#{socket.assigns.device.id}"
def id(socket), do: "client:#{socket.assigns.client.id}"
end

View File

@@ -1,15 +1,15 @@
defmodule API.Device.Views.Interface do
alias Domain.Devices
defmodule API.Client.Views.Interface do
alias Domain.Clients
def render(%Devices.Device{} = device) do
def render(%Clients.Client{} = client) do
upstream_dns =
Devices.fetch_device_config!(device)
Clients.fetch_client_config!(client)
|> Keyword.fetch!(:upstream_dns)
%{
upstream_dns: upstream_dns,
ipv4: device.ipv4,
ipv6: device.ipv6
ipv4: client.ipv4,
ipv6: client.ipv6
}
end
end

View File

@@ -1,4 +1,4 @@
defmodule API.Device.Views.Relay do
defmodule API.Client.Views.Relay do
alias Domain.Relays
def render_many(relays, expires_at) do

View File

@@ -1,4 +1,4 @@
defmodule API.Device.Views.Resource do
defmodule API.Client.Views.Resource do
alias Domain.Resources
def render_many(resources) do

View File

@@ -21,7 +21,7 @@ defmodule API.Endpoint do
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
socket "/gateway", API.Gateway.Socket, API.Sockets.options()
socket "/device", API.Device.Socket, API.Sockets.options()
socket "/client", API.Client.Socket, API.Sockets.options()
socket "/relay", API.Relay.Socket, API.Sockets.options()
plug :healthz

View File

@@ -1,7 +1,7 @@
defmodule API.Gateway.Channel do
use API, :channel
alias API.Gateway.Views
alias Domain.{Devices, Resources, Relays, Gateways}
alias Domain.{Clients, Resources, Relays, Gateways}
require Logger
def broadcast(%Gateways.Gateway{} = gateway, payload) do
@@ -33,7 +33,7 @@ defmodule API.Gateway.Channel do
def handle_info({:allow_access, attrs}, socket) do
%{
device_id: device_id,
client_id: client_id,
resource_id: resource_id,
authorization_expires_at: authorization_expires_at
} = attrs
@@ -41,7 +41,7 @@ defmodule API.Gateway.Channel do
resource = Resources.fetch_resource_by_id!(resource_id)
push(socket, "allow_access", %{
device_id: device_id,
client_id: client_id,
resource: Views.Resource.render(resource),
expires_at: DateTime.to_unix(authorization_expires_at, :second)
})
@@ -51,19 +51,19 @@ defmodule API.Gateway.Channel do
def handle_info({:request_connection, {channel_pid, socket_ref}, attrs}, socket) do
%{
device_id: device_id,
client_id: client_id,
resource_id: resource_id,
authorization_expires_at: authorization_expires_at,
device_rtc_session_description: rtc_session_description,
device_preshared_key: preshared_key
client_rtc_session_description: rtc_session_description,
client_preshared_key: preshared_key
} = attrs
Logger.debug("Gateway received connection request message",
device_id: device_id,
client_id: client_id,
resource_id: resource_id
)
device = Devices.fetch_device_by_id!(device_id, preload: [:actor])
client = Clients.fetch_client_by_id!(client_id, preload: [:actor])
resource = Resources.fetch_resource_by_id!(resource_id)
{:ok, relays} = Relays.list_connected_relays_for_resource(resource)
@@ -71,15 +71,15 @@ defmodule API.Gateway.Channel do
push(socket, "request_connection", %{
ref: ref,
actor: Views.Actor.render(device.actor),
actor: Views.Actor.render(client.actor),
relays: Views.Relay.render_many(relays, authorization_expires_at),
resource: Views.Resource.render(resource),
device: Views.Device.render(device, rtc_session_description, preshared_key),
client: Views.Client.render(client, rtc_session_description, preshared_key),
expires_at: DateTime.to_unix(authorization_expires_at, :second)
})
Logger.debug("Awaiting gateway connection_ready message",
device_id: device_id,
client_id: client_id,
resource_id: resource_id,
ref: ref
)
@@ -108,7 +108,7 @@ defmodule API.Gateway.Channel do
rtc_session_description}
)
Logger.debug("Gateway replied to the Device with :connect message",
Logger.debug("Gateway replied to the Client with :connect message",
resource_id: resource_id,
channel_pid: inspect(channel_pid),
ref: ref
@@ -123,7 +123,7 @@ defmodule API.Gateway.Channel do
# "ended_at" => ended_at,
# "metrics" => [
# %{
# "device_id" => device_id,
# "client_id" => client_id,
# "resource_id" => resource_id,
# "rx_bytes" => 0,
# "tx_packets" => 0

View File

@@ -1,16 +1,16 @@
defmodule API.Gateway.Views.Device do
alias Domain.Devices
defmodule API.Gateway.Views.Client do
alias Domain.Clients
def render(%Devices.Device{} = device, device_rtc_session_description, preshared_key) do
def render(%Clients.Client{} = client, client_rtc_session_description, preshared_key) do
%{
id: device.id,
rtc_session_description: device_rtc_session_description,
id: client.id,
rtc_session_description: client_rtc_session_description,
peer: %{
persistent_keepalive: 25,
public_key: device.public_key,
public_key: client.public_key,
preshared_key: preshared_key,
ipv4: device.ipv4,
ipv6: device.ipv6
ipv4: client.ipv4,
ipv6: client.ipv6
}
}
end

View File

@@ -1,5 +1,5 @@
defmodule API.Gateway.Views.Relay do
def render_many(relays, expires_at) do
Enum.flat_map(relays, &API.Device.Views.Relay.render(&1, expires_at))
Enum.flat_map(relays, &API.Client.Views.Relay.render(&1, expires_at))
end
end

View File

@@ -1,13 +1,13 @@
defmodule API.Device.ChannelTest do
defmodule API.Client.ChannelTest do
use API.ChannelCase
setup do
account = Fixtures.Accounts.create_account()
Fixtures.Config.upsert_configuration(account: account, devices_upstream_dns: ["1.1.1.1"])
Fixtures.Config.upsert_configuration(account: account, clients_upstream_dns: ["1.1.1.1"])
actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
identity = Fixtures.Auth.create_identity(actor: actor, account: account)
subject = Fixtures.Auth.create_subject(identity: identity)
device = Fixtures.Devices.create_device(subject: subject)
client = Fixtures.Clients.create_client(subject: subject)
gateway = Fixtures.Gateways.create_gateway(account: account)
dns_resource =
@@ -29,19 +29,19 @@ defmodule API.Device.ChannelTest do
subject = %{subject | expires_at: expires_at}
{:ok, _reply, socket} =
API.Device.Socket
|> socket("device:#{device.id}", %{
device: device,
API.Client.Socket
|> socket("client:#{client.id}", %{
client: client,
subject: subject
})
|> subscribe_and_join(API.Device.Channel, "device")
|> subscribe_and_join(API.Client.Channel, "client")
%{
account: account,
actor: actor,
identity: identity,
subject: subject,
device: device,
client: client,
gateway: gateway,
dns_resource: dns_resource,
cidr_resource: cidr_resource,
@@ -50,14 +50,14 @@ defmodule API.Device.ChannelTest do
end
describe "join/3" do
test "tracks presence after join", %{account: account, device: device} do
presence = Domain.Devices.Presence.list("devices:#{account.id}")
test "tracks presence after join", %{account: account, client: client} do
presence = Domain.Clients.Presence.list("clients:#{account.id}")
assert %{metas: [%{online_at: online_at, phx_ref: _ref}]} = Map.fetch!(presence, device.id)
assert %{metas: [%{online_at: online_at, phx_ref: _ref}]} = Map.fetch!(presence, client.id)
assert is_number(online_at)
end
test "expires the channel when token is expired", %{device: device, subject: subject} do
test "expires the channel when token is expired", %{client: client, subject: subject} do
expires_at = DateTime.utc_now() |> DateTime.add(25, :millisecond)
subject = %{subject | expires_at: expires_at}
@@ -66,18 +66,18 @@ defmodule API.Device.ChannelTest do
Process.flag(:trap_exit, true)
{:ok, _reply, _socket} =
API.Device.Socket
|> socket("device:#{device.id}", %{
device: device,
API.Client.Socket
|> socket("client:#{client.id}", %{
client: client,
subject: subject
})
|> subscribe_and_join(API.Device.Channel, "device")
|> subscribe_and_join(API.Client.Channel, "client")
assert_push "token_expired", %{}, 250
end
test "sends list of resources after join", %{
device: device,
client: client,
dns_resource: dns_resource,
cidr_resource: cidr_resource
} do
@@ -100,8 +100,8 @@ defmodule API.Device.ChannelTest do
} in resources
assert interface == %{
ipv4: device.ipv4,
ipv6: device.ipv6,
ipv4: client.ipv4,
ipv6: client.ipv6,
upstream_dns: [
%Postgrex.INET{address: {1, 1, 1, 1}}
]
@@ -274,11 +274,11 @@ defmodule API.Device.ChannelTest do
test "broadcasts allow_access to the gateways and then returns connect message", %{
dns_resource: resource,
gateway: gateway,
device: device,
client: client,
socket: socket
} do
resource_id = resource.id
device_id = device.id
client_id = client.id
:ok = Domain.Gateways.connect_gateway(gateway)
Phoenix.PubSub.subscribe(Domain.PubSub, API.Gateway.Socket.id(gateway))
@@ -294,7 +294,7 @@ defmodule API.Device.ChannelTest do
assert %{
resource_id: ^resource_id,
device_id: ^device_id,
client_id: ^client_id,
authorization_expires_at: authorization_expires_at
} = payload
@@ -307,8 +307,8 @@ defmodule API.Device.ChannelTest do
attrs = %{
"resource_id" => Ecto.UUID.generate(),
"gateway_id" => gateway.id,
"device_rtc_session_description" => "RTC_SD",
"device_preshared_key" => "PSK"
"client_rtc_session_description" => "RTC_SD",
"client_preshared_key" => "PSK"
}
ref = push(socket, "request_connection", attrs)
@@ -319,8 +319,8 @@ defmodule API.Device.ChannelTest do
attrs = %{
"resource_id" => resource.id,
"gateway_id" => Ecto.UUID.generate(),
"device_rtc_session_description" => "RTC_SD",
"device_preshared_key" => "PSK"
"client_rtc_session_description" => "RTC_SD",
"client_preshared_key" => "PSK"
}
ref = push(socket, "request_connection", attrs)
@@ -338,8 +338,8 @@ defmodule API.Device.ChannelTest do
attrs = %{
"resource_id" => resource.id,
"gateway_id" => gateway.id,
"device_rtc_session_description" => "RTC_SD",
"device_preshared_key" => "PSK"
"client_rtc_session_description" => "RTC_SD",
"client_preshared_key" => "PSK"
}
ref = push(socket, "request_connection", attrs)
@@ -354,8 +354,8 @@ defmodule API.Device.ChannelTest do
attrs = %{
"resource_id" => resource.id,
"gateway_id" => gateway.id,
"device_rtc_session_description" => "RTC_SD",
"device_preshared_key" => "PSK"
"client_rtc_session_description" => "RTC_SD",
"client_preshared_key" => "PSK"
}
ref = push(socket, "request_connection", attrs)
@@ -365,12 +365,12 @@ defmodule API.Device.ChannelTest do
test "broadcasts request_connection to the gateways and then returns connect message", %{
dns_resource: resource,
gateway: gateway,
device: device,
client: client,
socket: socket
} do
public_key = gateway.public_key
resource_id = resource.id
device_id = device.id
client_id = client.id
:ok = Domain.Gateways.connect_gateway(gateway)
Phoenix.PubSub.subscribe(Domain.PubSub, API.Gateway.Socket.id(gateway))
@@ -378,8 +378,8 @@ defmodule API.Device.ChannelTest do
attrs = %{
"resource_id" => resource.id,
"gateway_id" => gateway.id,
"device_rtc_session_description" => "RTC_SD",
"device_preshared_key" => "PSK"
"client_rtc_session_description" => "RTC_SD",
"client_preshared_key" => "PSK"
}
ref = push(socket, "request_connection", attrs)
@@ -388,9 +388,9 @@ defmodule API.Device.ChannelTest do
assert %{
resource_id: ^resource_id,
device_id: ^device_id,
device_preshared_key: "PSK",
device_rtc_session_description: "RTC_SD",
client_id: ^client_id,
client_preshared_key: "PSK",
client_rtc_session_description: "RTC_SD",
authorization_expires_at: authorization_expires_at
} = payload

View File

@@ -1,7 +1,7 @@
defmodule API.Device.SocketTest do
defmodule API.Client.SocketTest do
use API.ChannelCase, async: true
import API.Device.Socket, only: [id: 1]
alias API.Device.Socket
import API.Client.Socket, only: [id: 1]
alias API.Client.Socket
alias Domain.Auth
@connect_info %{
@@ -20,41 +20,41 @@ defmodule API.Device.SocketTest do
assert connect(Socket, attrs, connect_info: @connect_info) == {:error, :invalid_token}
end
test "creates a new device" do
test "creates a new client" do
subject = Fixtures.Auth.create_subject()
{:ok, token} = Auth.create_session_token_from_subject(subject)
attrs = connect_attrs(token: token)
assert {:ok, socket} = connect(Socket, attrs, connect_info: connect_info(subject))
assert device = Map.fetch!(socket.assigns, :device)
assert client = Map.fetch!(socket.assigns, :client)
assert device.external_id == attrs["external_id"]
assert device.public_key == attrs["public_key"]
assert device.last_seen_user_agent == subject.context.user_agent
assert device.last_seen_remote_ip.address == subject.context.remote_ip
assert device.last_seen_version == "0.7.412"
assert client.external_id == attrs["external_id"]
assert client.public_key == attrs["public_key"]
assert client.last_seen_user_agent == subject.context.user_agent
assert client.last_seen_remote_ip.address == subject.context.remote_ip
assert client.last_seen_version == "0.7.412"
end
test "updates existing device" do
test "updates existing client" do
subject = Fixtures.Auth.create_subject()
existing_device = Fixtures.Devices.create_device(subject: subject)
existing_client = Fixtures.Clients.create_client(subject: subject)
{:ok, token} = Auth.create_session_token_from_subject(subject)
attrs = connect_attrs(token: token, external_id: existing_device.external_id)
attrs = connect_attrs(token: token, external_id: existing_client.external_id)
assert {:ok, socket} = connect(Socket, attrs, connect_info: connect_info(subject))
assert device = Repo.one(Domain.Devices.Device)
assert device.id == socket.assigns.device.id
assert client = Repo.one(Domain.Clients.Client)
assert client.id == socket.assigns.client.id
end
end
describe "id/1" do
test "creates a channel for a device" do
device = Fixtures.Devices.create_device()
socket = socket(API.Device.Socket, "", %{device: device})
test "creates a channel for a client" do
client = Fixtures.Clients.create_client()
socket = socket(API.Client.Socket, "", %{client: client})
assert id(socket) == "device:#{device.id}"
assert id(socket) == "client:#{client.id}"
end
end
@@ -67,7 +67,7 @@ defmodule API.Device.SocketTest do
end
defp connect_attrs(attrs) do
Fixtures.Devices.device_attrs()
Fixtures.Clients.client_attrs()
|> Map.take(~w[external_id public_key]a)
|> Map.merge(Enum.into(attrs, %{}))
|> Enum.into(%{}, fn {k, v} -> {to_string(k), v} end)

View File

@@ -6,7 +6,7 @@ defmodule API.Gateway.ChannelTest do
actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
identity = Fixtures.Auth.create_identity(actor: actor, account: account)
subject = Fixtures.Auth.create_subject(identity: identity)
device = Fixtures.Devices.create_device(subject: subject)
client = Fixtures.Clients.create_client(subject: subject)
gateway = Fixtures.Gateways.create_gateway(account: account)
resource =
@@ -27,7 +27,7 @@ defmodule API.Gateway.ChannelTest do
actor: actor,
identity: identity,
subject: subject,
device: device,
client: client,
gateway: gateway,
resource: resource,
relay: relay,
@@ -61,7 +61,7 @@ defmodule API.Gateway.ChannelTest do
describe "handle_info/2 :allow_access" do
test "pushes allow_access message", %{
device: device,
client: client,
resource: resource,
relay: relay,
socket: socket
@@ -75,7 +75,7 @@ defmodule API.Gateway.ChannelTest do
socket.channel_pid,
{:allow_access,
%{
device_id: device.id,
client_id: client.id,
resource_id: resource.id,
authorization_expires_at: expires_at
}}
@@ -97,14 +97,14 @@ defmodule API.Gateway.ChannelTest do
]
}
assert payload.device_id == device.id
assert payload.client_id == client.id
assert DateTime.from_unix!(payload.expires_at) == DateTime.truncate(expires_at, :second)
end
end
describe "handle_info/2 :request_connection" do
test "pushes request_connection message", %{
device: device,
client: client,
resource: resource,
relay: relay,
socket: socket
@@ -122,18 +122,18 @@ defmodule API.Gateway.ChannelTest do
socket.channel_pid,
{:request_connection, {channel_pid, socket_ref},
%{
device_id: device.id,
client_id: client.id,
resource_id: resource.id,
authorization_expires_at: expires_at,
device_rtc_session_description: rtc_session_description,
device_preshared_key: preshared_key
client_rtc_session_description: rtc_session_description,
client_preshared_key: preshared_key
}}
)
assert_push "request_connection", payload
assert is_binary(payload.ref)
assert payload.actor == %{id: device.actor_id}
assert payload.actor == %{id: client.actor_id}
ipv4_stun_uri = "stun:#{relay.ipv4}:#{relay.port}"
ipv4_turn_uri = "turn:#{relay.ipv4}:#{relay.port}"
@@ -186,14 +186,14 @@ defmodule API.Gateway.ChannelTest do
]
}
assert payload.device == %{
id: device.id,
assert payload.client == %{
id: client.id,
peer: %{
ipv4: device.ipv4,
ipv6: device.ipv6,
ipv4: client.ipv4,
ipv6: client.ipv6,
persistent_keepalive: 25,
preshared_key: preshared_key,
public_key: device.public_key
public_key: client.public_key
},
rtc_session_description: rtc_session_description
}
@@ -203,8 +203,8 @@ defmodule API.Gateway.ChannelTest do
end
describe "handle_in/3 connection_ready" do
test "forwards RFC session description to the device channel", %{
device: device,
test "forwards RFC session description to the client channel", %{
client: client,
resource: resource,
relay: relay,
gateway: gateway,
@@ -224,11 +224,11 @@ defmodule API.Gateway.ChannelTest do
socket.channel_pid,
{:request_connection, {channel_pid, socket_ref},
%{
device_id: device.id,
client_id: client.id,
resource_id: resource.id,
authorization_expires_at: expires_at,
device_rtc_session_description: rtc_session_description,
device_preshared_key: preshared_key
client_rtc_session_description: rtc_session_description,
client_preshared_key: preshared_key
}}
)

View File

@@ -3,7 +3,7 @@ defmodule API.ChannelCase do
use Domain.CaseTemplate
@presences [
Domain.Devices.Presence,
Domain.Clients.Presence,
Domain.Gateways.Presence,
Domain.Relays.Presence
]

View File

@@ -21,7 +21,7 @@ defmodule Domain.Accounts.Account do
has_many :resources, Domain.Resources.Resource, where: [deleted_at: nil]
has_many :resource_connections, Domain.Resources.Connection, where: [deleted_at: nil]
has_many :devices, Domain.Devices.Device, where: [deleted_at: nil]
has_many :clients, Domain.Clients.Client, where: [deleted_at: nil]
has_many :gateways, Domain.Gateways.Gateway, where: [deleted_at: nil]
has_many :gateway_groups, Domain.Gateways.Group, where: [deleted_at: nil]

View File

@@ -1,8 +1,8 @@
defmodule Domain.Actors do
alias Domain.Actors.Membership
alias Web.Devices
alias Web.Clients
alias Domain.{Repo, Validator}
alias Domain.{Accounts, Auth, Devices}
alias Domain.{Accounts, Auth, Clients}
alias Domain.Actors.{Authorizer, Actor, Group}
require Ecto.Query
@@ -306,7 +306,7 @@ defmodule Domain.Actors do
with: fn actor ->
if actor.type != :account_admin_user or other_enabled_admins_exist?(actor) do
:ok = Auth.delete_actor_identities(actor)
:ok = Devices.delete_actor_devices(actor)
:ok = Clients.delete_actor_clients(actor)
Actor.Changeset.delete_actor(actor)
else

View File

@@ -7,7 +7,7 @@ defmodule Domain.Actors.Actor do
field :name, :string
has_many :identities, Domain.Auth.Identity, where: [deleted_at: nil]
has_many :devices, Domain.Devices.Device, where: [deleted_at: nil]
has_many :clients, Domain.Clients.Client, where: [deleted_at: nil]
has_many :memberships, Domain.Actors.Membership, on_replace: :delete
has_many :groups, through: [:memberships, :group]

View File

@@ -29,7 +29,7 @@ defmodule Domain.Application do
Domain.Auth,
Domain.Relays,
Domain.Gateways,
Domain.Devices,
Domain.Clients,
# Observability
# Domain.Telemetry

View File

@@ -3,7 +3,7 @@ defmodule Domain.Auth.Context do
This structure represents an authentication context for a user or an API token.
Context is then used in the audit logging to persist additional metadata about
the device and IP address used to perform the action.
the client and IP address used to perform the action.
"""
@type t :: %__MODULE__{
remote_ip: :inet.ip_address(),

View File

@@ -18,7 +18,7 @@ defmodule Domain.Auth.Identity do
field :created_by, Ecto.Enum, values: ~w[system provider identity]a
belongs_to :created_by_identity, Domain.Auth.Identity
has_many :devices, Domain.Devices.Device, where: [deleted_at: nil]
has_many :clients, Domain.Clients.Client, where: [deleted_at: nil]
field :deleted_at, :utc_datetime_usec
timestamps(updated_at: false)

View File

@@ -14,7 +14,7 @@ defmodule Domain.Auth.Roles do
Domain.Actors.Authorizer,
Domain.Auth.Authorizer,
Domain.Config.Authorizer,
Domain.Devices.Authorizer,
Domain.Clients.Authorizer,
Domain.Gateways.Authorizer,
Domain.Policies.Authorizer,
Domain.Relays.Authorizer,

View File

@@ -3,7 +3,7 @@ defmodule Domain.Config.Configuration do
alias Domain.Config.Logo
schema "configurations" do
field :devices_upstream_dns, {:array, :string}, default: []
field :clients_upstream_dns, {:array, :string}, default: []
embeds_one :logo, Logo, on_replace: :delete

View File

@@ -2,14 +2,14 @@ defmodule Domain.Config.Configuration.Changeset do
use Domain, :changeset
import Domain.Config, only: [config_changeset: 2]
@fields ~w[devices_upstream_dns]a
@fields ~w[clients_upstream_dns]a
def changeset(configuration, attrs) do
changeset =
configuration
|> cast(attrs, @fields)
|> cast_embed(:logo)
|> trim_change(:devices_upstream_dns)
|> trim_change(:clients_upstream_dns)
Enum.reduce(@fields, changeset, fn field, changeset ->
config_changeset(changeset, field)

View File

@@ -86,9 +86,9 @@ defmodule Domain.Config.Definitions do
:cookie_signing_salt,
:cookie_encryption_salt
]},
{"Devices",
{"Clients",
[
:devices_upstream_dns
:clients_upstream_dns
]},
{"Authorization",
"""
@@ -403,19 +403,19 @@ defmodule Domain.Config.Definitions do
)
##############################################
## Devices
## Clients
##############################################
@doc """
Comma-separated list of upstream DNS servers to use for devices.
Comma-separated list of upstream DNS servers to use for clients.
It can be either an IP address or a FQDN if you intend to use a DNS-over-TLS server.
Leave this blank to omit the `DNS` section from generated configs,
which will make devices use default system-provided DNS even when VPN session is active.
which will make clients use default system-provided DNS even when VPN session is active.
"""
defconfig(
:devices_upstream_dns,
:clients_upstream_dns,
{:array, ",", {:one_of, [Types.IP, :string]}, validate_unique: true},
default: [],
changeset: fn

View File

@@ -1,8 +1,8 @@
defmodule Domain.Devices do
defmodule Domain.Clients do
use Supervisor
alias Domain.{Repo, Auth, Validator}
alias Domain.Actors
alias Domain.Devices.{Device, Authorizer, Presence}
alias Domain.Clients.{Client, Authorizer, Presence}
def start_link(opts) do
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
@@ -17,38 +17,38 @@ defmodule Domain.Devices do
end
def count_by_account_id(account_id) do
Device.Query.by_account_id(account_id)
Client.Query.by_account_id(account_id)
|> Repo.aggregate(:count)
end
def count_by_actor_id(actor_id) do
Device.Query.by_actor_id(actor_id)
Client.Query.by_actor_id(actor_id)
|> Repo.aggregate(:count)
end
def fetch_device_by_id(id, %Auth.Subject{} = subject, opts \\ []) do
def fetch_client_by_id(id, %Auth.Subject{} = subject, opts \\ []) do
required_permissions =
{:one_of,
[
Authorizer.manage_devices_permission(),
Authorizer.manage_own_devices_permission()
Authorizer.manage_clients_permission(),
Authorizer.manage_own_clients_permission()
]}
with :ok <- Auth.ensure_has_permissions(subject, required_permissions),
true <- Validator.valid_uuid?(id) do
{preload, _opts} = Keyword.pop(opts, :preload, [])
Device.Query.by_id(id)
Client.Query.by_id(id)
|> Authorizer.for_subject(subject)
|> Repo.fetch()
|> case do
{:ok, device} ->
device =
device
{:ok, client} ->
client =
client
|> Repo.preload(preload)
|> preload_online_status()
{:ok, device}
{:ok, client}
{:error, reason} ->
{:error, reason}
@@ -59,63 +59,63 @@ defmodule Domain.Devices do
end
end
def fetch_device_by_id!(id, opts \\ []) do
def fetch_client_by_id!(id, opts \\ []) do
{preload, _opts} = Keyword.pop(opts, :preload, [])
Device.Query.by_id(id)
Client.Query.by_id(id)
|> Repo.one!()
|> Repo.preload(preload)
|> preload_online_status()
end
def list_devices(%Auth.Subject{} = subject, opts \\ []) do
def list_clients(%Auth.Subject{} = subject, opts \\ []) do
{preload, _opts} = Keyword.pop(opts, :preload, [])
required_permissions =
{:one_of,
[
Authorizer.manage_devices_permission(),
Authorizer.manage_own_devices_permission()
Authorizer.manage_clients_permission(),
Authorizer.manage_own_clients_permission()
]}
with :ok <- Auth.ensure_has_permissions(subject, required_permissions) do
{:ok, devices} =
Device.Query.all()
{:ok, clients} =
Client.Query.all()
|> Authorizer.for_subject(subject)
|> Repo.list()
devices =
devices
clients =
clients
|> preload_online_statuses()
{:ok, Repo.preload(devices, preload)}
{:ok, Repo.preload(clients, preload)}
end
end
def list_devices_for_actor(%Actors.Actor{} = actor, %Auth.Subject{} = subject) do
list_devices_by_actor_id(actor.id, subject)
def list_clients_for_actor(%Actors.Actor{} = actor, %Auth.Subject{} = subject) do
list_clients_by_actor_id(actor.id, subject)
end
def list_devices_by_actor_id(actor_id, %Auth.Subject{} = subject) do
def list_clients_by_actor_id(actor_id, %Auth.Subject{} = subject) do
required_permissions =
{:one_of,
[
Authorizer.manage_devices_permission(),
Authorizer.manage_own_devices_permission()
Authorizer.manage_clients_permission(),
Authorizer.manage_own_clients_permission()
]}
with :ok <- Auth.ensure_has_permissions(subject, required_permissions),
true <- Validator.valid_uuid?(actor_id) do
{:ok, devices} =
Device.Query.by_actor_id(actor_id)
{:ok, clients} =
Client.Query.by_actor_id(actor_id)
|> Authorizer.for_subject(subject)
|> Repo.list()
devices =
devices
clients =
clients
|> preload_online_statuses()
{:ok, devices}
{:ok, clients}
else
false -> {:error, :not_found}
other -> other
@@ -123,67 +123,67 @@ defmodule Domain.Devices do
end
# TODO: this is ugly!
defp preload_online_status(device) do
connected_devices = Presence.list("devices:#{device.id}")
%{device | online?: Map.has_key?(connected_devices, device.id)}
defp preload_online_status(client) do
connected_clients = Presence.list("clients:#{client.id}")
%{client | online?: Map.has_key?(connected_clients, client.id)}
end
defp preload_online_statuses([]), do: []
defp preload_online_statuses([device | _] = devices) do
connected_devices = Presence.list("devices:#{device.account_id}")
defp preload_online_statuses([client | _] = clients) do
connected_clients = Presence.list("clients:#{client.account_id}")
Enum.map(devices, fn device ->
%{device | online?: Map.has_key?(connected_devices, device.id)}
Enum.map(clients, fn client ->
%{client | online?: Map.has_key?(connected_clients, client.id)}
end)
end
def change_device(%Device{} = device, attrs \\ %{}) do
Device.Changeset.update(device, attrs)
def change_client(%Client{} = client, attrs \\ %{}) do
Client.Changeset.update(client, attrs)
end
def upsert_device(attrs \\ %{}, %Auth.Subject{identity: %Auth.Identity{} = identity} = subject) do
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_own_devices_permission()) do
changeset = Device.Changeset.upsert(identity, subject.context, attrs)
def upsert_client(attrs \\ %{}, %Auth.Subject{identity: %Auth.Identity{} = identity} = subject) do
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_own_clients_permission()) do
changeset = Client.Changeset.upsert(identity, subject.context, attrs)
Ecto.Multi.new()
|> Ecto.Multi.insert(:device, changeset,
conflict_target: Device.Changeset.upsert_conflict_target(),
on_conflict: Device.Changeset.upsert_on_conflict(),
|> Ecto.Multi.insert(:client, changeset,
conflict_target: Client.Changeset.upsert_conflict_target(),
on_conflict: Client.Changeset.upsert_on_conflict(),
returning: true
)
|> resolve_address_multi(:ipv4)
|> resolve_address_multi(:ipv6)
|> Ecto.Multi.update(:device_with_address, fn
%{device: %Device{} = device, ipv4: ipv4, ipv6: ipv6} ->
Device.Changeset.finalize_upsert(device, ipv4, ipv6)
|> Ecto.Multi.update(:client_with_address, fn
%{client: %Client{} = client, ipv4: ipv4, ipv6: ipv6} ->
Client.Changeset.finalize_upsert(client, ipv4, ipv6)
end)
|> Repo.transaction()
|> case do
{:ok, %{device_with_address: device}} -> {:ok, device}
{:error, :device, changeset, _effects_so_far} -> {:error, changeset}
{:ok, %{client_with_address: client}} -> {:ok, client}
{:error, :client, changeset, _effects_so_far} -> {:error, changeset}
end
end
end
defp resolve_address_multi(multi, type) do
Ecto.Multi.run(multi, type, fn _repo, %{device: %Device{} = device} ->
if address = Map.get(device, type) do
Ecto.Multi.run(multi, type, fn _repo, %{client: %Client{} = client} ->
if address = Map.get(client, type) do
{:ok, address}
else
{:ok, Domain.Network.fetch_next_available_address!(device.account_id, type)}
{:ok, Domain.Network.fetch_next_available_address!(client.account_id, type)}
end
end)
end
def update_device(%Device{} = device, attrs, %Auth.Subject{} = subject) do
with :ok <- authorize_actor_device_management(device.actor_id, subject) do
Device.Query.by_id(device.id)
def update_client(%Client{} = client, attrs, %Auth.Subject{} = subject) do
with :ok <- authorize_actor_client_management(client.actor_id, subject) do
Client.Query.by_id(client.id)
|> Authorizer.for_subject(subject)
|> Repo.fetch_and_update(with: &Device.Changeset.update(&1, attrs))
|> Repo.fetch_and_update(with: &Client.Changeset.update(&1, attrs))
|> case do
{:ok, device} ->
{:ok, preload_online_status(device)}
{:ok, client} ->
{:ok, preload_online_status(client)}
{:error, reason} ->
{:error, reason}
@@ -191,49 +191,49 @@ defmodule Domain.Devices do
end
end
def delete_device(%Device{} = device, %Auth.Subject{} = subject) do
with :ok <- authorize_actor_device_management(device.actor_id, subject) do
Device.Query.by_id(device.id)
def delete_client(%Client{} = client, %Auth.Subject{} = subject) do
with :ok <- authorize_actor_client_management(client.actor_id, subject) do
Client.Query.by_id(client.id)
|> Authorizer.for_subject(subject)
|> Repo.fetch_and_update(with: &Device.Changeset.delete/1)
|> Repo.fetch_and_update(with: &Client.Changeset.delete/1)
end
end
def delete_actor_devices(%Actors.Actor{} = actor) do
def delete_actor_clients(%Actors.Actor{} = actor) do
{_count, nil} =
Device.Query.by_actor_id(actor.id)
Client.Query.by_actor_id(actor.id)
|> Repo.update_all(set: [deleted_at: DateTime.utc_now()])
:ok
end
def authorize_actor_device_management(%Actors.Actor{} = actor, %Auth.Subject{} = subject) do
authorize_actor_device_management(actor.id, subject)
def authorize_actor_client_management(%Actors.Actor{} = actor, %Auth.Subject{} = subject) do
authorize_actor_client_management(actor.id, subject)
end
def authorize_actor_device_management(actor_id, %Auth.Subject{actor: %{id: actor_id}} = subject) do
Auth.ensure_has_permissions(subject, Authorizer.manage_own_devices_permission())
def authorize_actor_client_management(actor_id, %Auth.Subject{actor: %{id: actor_id}} = subject) do
Auth.ensure_has_permissions(subject, Authorizer.manage_own_clients_permission())
end
def authorize_actor_device_management(_actor_id, %Auth.Subject{} = subject) do
Auth.ensure_has_permissions(subject, Authorizer.manage_devices_permission())
def authorize_actor_client_management(_actor_id, %Auth.Subject{} = subject) do
Auth.ensure_has_permissions(subject, Authorizer.manage_clients_permission())
end
def connect_device(%Device{} = device) do
def connect_client(%Client{} = client) do
{:ok, _} =
Presence.track(self(), "devices:#{device.account_id}", device.id, %{
Presence.track(self(), "clients:#{client.account_id}", client.id, %{
online_at: System.system_time(:second)
})
{:ok, _} = Presence.track(self(), "actor_devices:#{device.actor_id}", device.id, %{})
{:ok, _} = Presence.track(self(), "actor_clients:#{client.actor_id}", client.id, %{})
:ok
end
def fetch_device_config!(%Device{} = device) do
def fetch_client_config!(%Client{} = client) do
%{
devices_upstream_dns: upstream_dns
} = Domain.Config.fetch_resolved_configs!(device.account_id, [:devices_upstream_dns])
clients_upstream_dns: upstream_dns
} = Domain.Config.fetch_resolved_configs!(client.account_id, [:clients_upstream_dns])
[upstream_dns: upstream_dns]
end

View File

@@ -1,22 +1,22 @@
defmodule Domain.Devices.Authorizer do
defmodule Domain.Clients.Authorizer do
use Domain.Auth.Authorizer
alias Domain.Devices.Device
alias Domain.Clients.Client
def manage_own_devices_permission, do: build(Device, :manage_own)
def manage_devices_permission, do: build(Device, :manage)
def manage_own_clients_permission, do: build(Client, :manage_own)
def manage_clients_permission, do: build(Client, :manage)
@impl Domain.Auth.Authorizer
def list_permissions_for_role(:account_admin_user) do
[
manage_own_devices_permission(),
manage_devices_permission()
manage_own_clients_permission(),
manage_clients_permission()
]
end
def list_permissions_for_role(:account_user) do
[
manage_own_devices_permission()
manage_own_clients_permission()
]
end
@@ -27,13 +27,13 @@ defmodule Domain.Devices.Authorizer do
@impl Domain.Auth.Authorizer
def for_subject(queryable, %Subject{} = subject) do
cond do
has_permission?(subject, manage_devices_permission()) ->
Device.Query.by_account_id(queryable, subject.account.id)
has_permission?(subject, manage_clients_permission()) ->
Client.Query.by_account_id(queryable, subject.account.id)
has_permission?(subject, manage_own_devices_permission()) ->
has_permission?(subject, manage_own_clients_permission()) ->
queryable
|> Device.Query.by_account_id(subject.account.id)
|> Device.Query.by_actor_id(subject.actor.id)
|> Client.Query.by_account_id(subject.account.id)
|> Client.Query.by_actor_id(subject.actor.id)
end
end
end

View File

@@ -1,7 +1,7 @@
defmodule Domain.Devices.Device do
defmodule Domain.Clients.Client do
use Domain, :schema
schema "devices" do
schema "clients" do
field :external_id, :string
field :name, :string

View File

@@ -1,7 +1,7 @@
defmodule Domain.Devices.Device.Changeset do
defmodule Domain.Clients.Client.Changeset do
use Domain, :changeset
alias Domain.{Version, Auth}
alias Domain.Devices
alias Domain.Clients
@upsert_fields ~w[external_id name public_key]a
@conflict_replace_fields ~w[public_key
@@ -20,7 +20,7 @@ defmodule Domain.Devices.Device.Changeset do
def upsert_on_conflict, do: {:replace, @conflict_replace_fields}
def upsert(%Auth.Identity{} = identity, %Auth.Context{} = context, attrs) do
%Devices.Device{}
%Clients.Client{}
|> cast(attrs, @upsert_fields)
|> put_default_value(:name, &generate_name/0)
|> put_change(:identity_id, identity.id)
@@ -32,30 +32,30 @@ defmodule Domain.Devices.Device.Changeset do
|> validate_required(@required_fields)
|> validate_base64(:public_key)
|> validate_length(:public_key, is: @key_length)
|> unique_constraint(:ipv4, name: :devices_account_id_ipv4_index)
|> unique_constraint(:ipv6, name: :devices_account_id_ipv6_index)
|> unique_constraint(:ipv4, name: :clients_account_id_ipv4_index)
|> unique_constraint(:ipv6, name: :clients_account_id_ipv6_index)
|> put_change(:last_seen_at, DateTime.utc_now())
|> put_device_version()
|> put_client_version()
end
def finalize_upsert(%Devices.Device{} = device, ipv4, ipv6) do
device
def finalize_upsert(%Clients.Client{} = client, ipv4, ipv6) do
client
|> change()
|> put_change(:ipv4, ipv4)
|> put_change(:ipv6, ipv6)
|> unique_constraint(:ipv4, name: :devices_account_id_ipv4_index)
|> unique_constraint(:ipv6, name: :devices_account_id_ipv6_index)
|> unique_constraint(:ipv4, name: :clients_account_id_ipv4_index)
|> unique_constraint(:ipv6, name: :clients_account_id_ipv6_index)
end
def update(%Devices.Device{} = device, attrs) do
device
def update(%Clients.Client{} = client, attrs) do
client
|> cast(attrs, @update_fields)
|> validate_required(@required_fields)
|> changeset()
end
def delete(%Devices.Device{} = device) do
device
def delete(%Clients.Client{} = client) do
client
|> change()
|> put_default_value(:deleted_at, DateTime.utc_now())
end
@@ -68,10 +68,10 @@ defmodule Domain.Devices.Device.Changeset do
|> unique_constraint([:actor_id, :name])
|> unique_constraint([:actor_id, :public_key])
|> unique_constraint(:external_id)
|> unique_constraint(:name, name: :devices_account_id_actor_id_name_index)
|> unique_constraint(:name, name: :clients_account_id_actor_id_name_index)
end
defp put_device_version(changeset) do
defp put_client_version(changeset) do
with {_data_or_changes, user_agent} when not is_nil(user_agent) <-
fetch_field(changeset, :last_seen_user_agent),
{:ok, version} <- Version.fetch_version(user_agent) do

View File

@@ -1,40 +1,40 @@
defmodule Domain.Devices.Device.Query do
defmodule Domain.Clients.Client.Query do
use Domain, :query
def all do
from(devices in Domain.Devices.Device, as: :devices)
|> where([devices: devices], is_nil(devices.deleted_at))
from(clients in Domain.Clients.Client, as: :clients)
|> where([clients: clients], is_nil(clients.deleted_at))
end
def by_id(queryable \\ all(), id) do
where(queryable, [devices: devices], devices.id == ^id)
where(queryable, [clients: clients], clients.id == ^id)
end
def by_actor_id(queryable \\ all(), actor_id) do
where(queryable, [devices: devices], devices.actor_id == ^actor_id)
where(queryable, [clients: clients], clients.actor_id == ^actor_id)
end
def by_account_id(queryable \\ all(), account_id) do
where(queryable, [devices: devices], devices.account_id == ^account_id)
where(queryable, [clients: clients], clients.account_id == ^account_id)
end
def returning_all(queryable \\ all()) do
select(queryable, [devices: devices], devices)
select(queryable, [clients: clients], clients)
end
def with_preloaded_actor(queryable \\ all()) do
with_named_binding(queryable, :actor, fn queryable, binding ->
queryable
|> join(:inner, [devices: devices], actor in assoc(devices, ^binding), as: ^binding)
|> preload([devices: devices, actor: actor], actor: actor)
|> join(:inner, [clients: clients], actor in assoc(clients, ^binding), as: ^binding)
|> preload([clients: clients, actor: actor], actor: actor)
end)
end
def with_preloaded_identity(queryable \\ all()) do
with_named_binding(queryable, :identity, fn queryable, binding ->
queryable
|> join(:inner, [devices: devices], identity in assoc(devices, ^binding), as: ^binding)
|> preload([devices: devices, identity: identity], identity: identity)
|> join(:inner, [clients: clients], identity in assoc(clients, ^binding), as: ^binding)
|> preload([clients: clients, identity: identity], identity: identity)
end)
end
end

View File

@@ -1,4 +1,4 @@
defmodule Domain.Devices.Presence do
defmodule Domain.Clients.Presence do
use Phoenix.Presence,
otp_app: :domain,
pubsub_server: Domain.PubSub

View File

@@ -147,7 +147,7 @@ defmodule Domain.NameGenerator do
recommendation response selection storage version alcohol argument complaint contract
emphasis highway loss membership possession preparation steak union agreement cancer currency
employment engineering entry interaction mixture preference region republic tradition virus
actor classroom delivery device difficulty drama election engine football guidance hotel
actor classroom delivery client difficulty drama election engine football guidance hotel
owner priority protection suggestion tension variation anxiety atmosphere awareness bath
bread candidate climate comparison confusion construction elevator emotion employee employer
guest height leadership mall manager operation recording sample transportation charity cousin

View File

@@ -116,7 +116,7 @@ defmodule Domain.Resources do
# {:ok, actors} = list_authorized_actors(resource)
# Phoenix.PubSub.broadcast(
# Domain.PubSub,
# "actor_device:#{subject.actor.id}",
# "actor_client:#{subject.actor.id}",
# {:resource_added, resource.id}
# )
@@ -151,7 +151,7 @@ defmodule Domain.Resources do
{:ok, resource} ->
# Phoenix.PubSub.broadcast(
# Domain.PubSub,
# "actor_device:#{resource.actor_id}",
# "actor_client:#{resource.actor_id}",
# {:resource_updated, resource.id}
# )
@@ -172,7 +172,7 @@ defmodule Domain.Resources do
{:ok, resource} ->
# Phoenix.PubSub.broadcast(
# Domain.PubSub,
# "actor_device:#{resource.actor_id}",
# "actor_client:#{resource.actor_id}",
# {:resource_removed, resource.id}
# )

View File

@@ -39,8 +39,8 @@
# :ok
# end
# def add_device do
# PostHog.capture("add_device", common_fields())
# def add_client do
# PostHog.capture("add_client", common_fields())
# :ok
# end
@@ -54,8 +54,8 @@
# :ok
# end
# def delete_device do
# PostHog.capture("delete_device", common_fields())
# def delete_client do
# PostHog.capture("delete_client", common_fields())
# :ok
# end
@@ -94,8 +94,8 @@
# :ok
# end
# # How far back to count handshakes as an active device
# # @active_device_window 86_400
# # How far back to count handshakes as an active client
# # @active_client_window 86_400
# def ping_data do
# %{
# local_auth_enabled: {_, local_auth_enabled},
@@ -108,12 +108,12 @@
# common_fields() ++
# [
# # devices_active_within_24h: Devices.count_active_within(@active_device_window),
# # clients_active_within_24h: Clients.count_active_within(@active_client_window),
# # admin_count: Users.count_by_role(:account_admin_user),
# # actor_count: Users.count(),
# in_docker: in_docker?(),
# # device_count: Devices.count(),
# # max_devices_for_actors: Devices.count_maximum_for_a_actor(),
# # client_count: Clients.count(),
# # max_clients_for_actors: Clients.count_maximum_for_a_actor(),
# # actors_with_mfa: MFA.count_actors_with_mfa_enabled(),
# # actors_with_mfa_totp: MFA.count_actors_with_totp_method(),
# local_authentication: local_auth_enabled,

View File

@@ -0,0 +1,34 @@
defmodule Domain.Repo.Migrations.RenameDevicesToClients do
use Ecto.Migration
def change do
rename(table(:configurations), :devices_upstream_dns, to: :clients_upstream_dns)
execute("""
ALTER INDEX devices_account_id_ipv4_index
RENAME TO clients_account_id_ipv4_index
""")
execute("""
ALTER INDEX devices_account_id_ipv6_index
RENAME TO clients_account_id_ipv6_index
""")
execute("""
ALTER INDEX devices_account_id_actor_id_external_id_index
RENAME TO clients_account_id_actor_id_external_id_index
""")
execute("""
ALTER INDEX devices_account_id_actor_id_name_index
RENAME TO clients_account_id_actor_id_name_index
""")
execute("""
ALTER INDEX devices_account_id_actor_id_public_key_index
RENAME TO clients_account_id_actor_id_public_key_index
""")
rename(table(:devices), to: table(:clients))
end
end

View File

@@ -222,7 +222,7 @@ IO.puts(
IO.puts("")
_user_iphone =
Domain.Devices.upsert_device(
Domain.Clients.upsert_client(
%{
name: "FZ User iPhone",
external_id: Ecto.UUID.generate(),
@@ -233,7 +233,7 @@ _user_iphone =
)
_admin_iphone =
Domain.Devices.upsert_device(
Domain.Clients.upsert_client(
%{
name: "FZ Admin iPhone",
external_id: Ecto.UUID.generate(),
@@ -243,7 +243,7 @@ _admin_iphone =
admin_subject
)
IO.puts("Devices created")
IO.puts("Clients created")
IO.puts("")
IO.puts("Created Actor Groups: ")
@@ -453,6 +453,6 @@ IO.puts("")
{:ok, unprivileged_subject_client_token} =
Auth.create_client_token_from_subject(unprivileged_subject)
IO.puts("Created device tokens:")
IO.puts("Created client tokens:")
IO.puts(" #{unprivileged_actor_email} token: #{unprivileged_subject_client_token}")
IO.puts("")

View File

@@ -1810,7 +1810,7 @@ defmodule Domain.ActorsTest do
assert is_nil(other_actor.deleted_at)
end
test "deletes actor identities and devices" do
test "deletes actor identities and clients" do
account = Fixtures.Accounts.create_account()
actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
identity = Fixtures.Auth.create_identity(account: account, actor: actor)
@@ -1818,12 +1818,12 @@ defmodule Domain.ActorsTest do
actor_to_delete = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
Fixtures.Auth.create_identity(account: account, actor: actor_to_delete)
Fixtures.Devices.create_device(account: account, actor: actor_to_delete)
Fixtures.Clients.create_client(account: account, actor: actor_to_delete)
assert {:ok, actor} = delete_actor(actor_to_delete, subject)
assert actor.deleted_at
assert Repo.aggregate(Domain.Devices.Device.Query.all(), :count) == 0
assert Repo.aggregate(Domain.Clients.Client.Query.all(), :count) == 0
assert Repo.aggregate(Domain.Auth.Identity.Query.all(), :count) == 1
end

View File

@@ -57,8 +57,8 @@ defmodule Domain.Config.DefinitionTest do
end
test "inserts a function which returns definition doc" do
assert {:ok, doc} = fetch_doc(Domain.Config.Definitions, :devices_upstream_dns)
assert doc =~ "Comma-separated list of upstream DNS servers to use for devices."
assert {:ok, doc} = fetch_doc(Domain.Config.Definitions, :clients_upstream_dns)
assert doc =~ "Comma-separated list of upstream DNS servers to use for clients."
assert fetch_doc(Foo, :bar) ==
{:error, :module_not_found}

View File

@@ -37,10 +37,10 @@ defmodule Domain.Config.ResolverTest do
test "returns variable from database" do
env_configurations = %{}
db_configurations = %Domain.Config.Configuration{devices_upstream_dns: "1.2.3.4"}
db_configurations = %Domain.Config.Configuration{clients_upstream_dns: "1.2.3.4"}
assert resolve(:devices_upstream_dns, env_configurations, db_configurations, []) ==
{:ok, {{:db, :devices_upstream_dns}, "1.2.3.4"}}
assert resolve(:clients_upstream_dns, env_configurations, db_configurations, []) ==
{:ok, {{:db, :clients_upstream_dns}, "1.2.3.4"}}
end
test "precedence" do

View File

@@ -90,9 +90,9 @@ defmodule Domain.ConfigTest do
end
test "returns source and config values", %{account: account} do
assert fetch_resolved_configs!(account.id, [:devices_upstream_dns, :devices_upstream_dns]) ==
assert fetch_resolved_configs!(account.id, [:clients_upstream_dns, :clients_upstream_dns]) ==
%{
devices_upstream_dns: [%Postgrex.INET{address: {1, 1, 1, 1}, netmask: nil}]
clients_upstream_dns: [%Postgrex.INET{address: {1, 1, 1, 1}, netmask: nil}]
}
end
@@ -137,10 +137,10 @@ defmodule Domain.ConfigTest do
end
test "returns source and config values", %{account: account} do
assert fetch_resolved_configs_with_sources!(account.id, [:devices_upstream_dns]) ==
assert fetch_resolved_configs_with_sources!(account.id, [:clients_upstream_dns]) ==
%{
devices_upstream_dns:
{{:db, :devices_upstream_dns},
clients_upstream_dns:
{{:db, :clients_upstream_dns},
[%Postgrex.INET{address: {1, 1, 1, 1}, netmask: nil}]}
}
end
@@ -350,7 +350,7 @@ defmodule Domain.ConfigTest do
} do
assert get_account_config_by_account_id(account.id) == %Domain.Config.Configuration{
account_id: account.id,
devices_upstream_dns: []
clients_upstream_dns: []
}
end
end
@@ -387,7 +387,7 @@ defmodule Domain.ConfigTest do
assert config == %Domain.Config.Configuration{
account_id: account.id,
devices_upstream_dns: []
clients_upstream_dns: []
}
end
@@ -442,13 +442,13 @@ defmodule Domain.ConfigTest do
config = get_account_config_by_account_id(account.id)
attrs = %{
devices_upstream_dns: ["!!!"]
clients_upstream_dns: ["!!!"]
}
assert {:error, changeset} = update_config(config, attrs)
assert errors_on(changeset) == %{
devices_upstream_dns: [
clients_upstream_dns: [
"!!! is not a valid FQDN",
"must be one of: Elixir.Domain.Types.IP, string"
]
@@ -456,20 +456,20 @@ defmodule Domain.ConfigTest do
end
test "returns error when trying to change overridden value", %{account: account} do
put_system_env_override(:devices_upstream_dns, ["1.2.3.4"])
put_system_env_override(:clients_upstream_dns, ["1.2.3.4"])
config = get_account_config_by_account_id(account.id)
attrs = %{
devices_upstream_dns: ["4.1.2.3"]
clients_upstream_dns: ["4.1.2.3"]
}
assert {:error, changeset} = update_config(config, attrs)
assert errors_on(changeset) ==
%{
devices_upstream_dns: [
"cannot be changed; it is overridden by DEVICES_UPSTREAM_DNS environment variable"
clients_upstream_dns: [
"cannot be changed; it is overridden by CLIENTS_UPSTREAM_DNS environment variable"
]
}
end
@@ -478,27 +478,27 @@ defmodule Domain.ConfigTest do
config = get_account_config_by_account_id(account.id)
attrs = %{
devices_upstream_dns: [" foobar.com", "google.com "]
clients_upstream_dns: [" foobar.com", "google.com "]
}
assert {:ok, config} = update_config(config, attrs)
assert config.devices_upstream_dns == ["foobar.com", "google.com"]
assert config.clients_upstream_dns == ["foobar.com", "google.com"]
end
test "changes database config value when it did not exist", %{account: account} do
config = get_account_config_by_account_id(account.id)
attrs = %{devices_upstream_dns: ["foobar.com", "google.com"]}
attrs = %{clients_upstream_dns: ["foobar.com", "google.com"]}
assert {:ok, config} = update_config(config, attrs)
assert config.devices_upstream_dns == attrs.devices_upstream_dns
assert config.clients_upstream_dns == attrs.clients_upstream_dns
end
test "changes database config value when it existed", %{account: account} do
Fixtures.Config.upsert_configuration(account: account)
config = get_account_config_by_account_id(account.id)
attrs = %{devices_upstream_dns: ["foobar.com", "google.com"]}
attrs = %{clients_upstream_dns: ["foobar.com", "google.com"]}
assert {:ok, config} = update_config(config, attrs)
assert config.devices_upstream_dns == attrs.devices_upstream_dns
assert config.clients_upstream_dns == attrs.clients_upstream_dns
end
end
end

View File

@@ -1,7 +1,7 @@
defmodule Domain.DevicesTest do
defmodule Domain.ClientsTest do
use Domain.DataCase, async: true
import Domain.Devices
alias Domain.Devices
import Domain.Clients
alias Domain.Clients
setup do
account = Fixtures.Accounts.create_account()
@@ -29,11 +29,11 @@ defmodule Domain.DevicesTest do
end
describe "count_by_account_id/0" do
test "counts devices for an account", %{account: account} do
Fixtures.Devices.create_device(account: account)
Fixtures.Devices.create_device(account: account)
Fixtures.Devices.create_device(account: account)
Fixtures.Devices.create_device()
test "counts clients for an account", %{account: account} do
Fixtures.Clients.create_client(account: account)
Fixtures.Clients.create_client(account: account)
Fixtures.Clients.create_client(account: account)
Fixtures.Clients.create_client()
assert count_by_account_id(account.id) == 3
end
@@ -44,258 +44,258 @@ defmodule Domain.DevicesTest do
assert count_by_actor_id(Ecto.UUID.generate()) == 0
end
test "returns count of devices for a actor" do
device = Fixtures.Devices.create_device()
assert count_by_actor_id(device.actor_id) == 1
test "returns count of clients for a actor" do
client = Fixtures.Clients.create_client()
assert count_by_actor_id(client.actor_id) == 1
end
end
describe "fetch_device_by_id/2" do
describe "fetch_client_by_id/2" do
test "returns error when UUID is invalid", %{unprivileged_subject: subject} do
assert fetch_device_by_id("foo", subject) == {:error, :not_found}
assert fetch_client_by_id("foo", subject) == {:error, :not_found}
end
test "does not return deleted devices", %{
test "does not return deleted clients", %{
unprivileged_actor: actor,
unprivileged_subject: subject
} do
device =
Fixtures.Devices.create_device(actor: actor)
|> Fixtures.Devices.delete_device()
client =
Fixtures.Clients.create_client(actor: actor)
|> Fixtures.Clients.delete_client()
assert fetch_device_by_id(device.id, subject) == {:error, :not_found}
assert fetch_client_by_id(client.id, subject) == {:error, :not_found}
end
test "returns device by id", %{unprivileged_actor: actor, unprivileged_subject: subject} do
device = Fixtures.Devices.create_device(actor: actor)
assert fetch_device_by_id(device.id, subject) == {:ok, device}
test "returns client by id", %{unprivileged_actor: actor, unprivileged_subject: subject} do
client = Fixtures.Clients.create_client(actor: actor)
assert fetch_client_by_id(client.id, subject) == {:ok, client}
end
test "returns device that belongs to another actor with manage permission", %{
test "returns client that belongs to another actor with manage permission", %{
account: account,
unprivileged_subject: subject
} do
device = Fixtures.Devices.create_device(account: account)
client = Fixtures.Clients.create_client(account: account)
subject =
subject
|> Fixtures.Auth.remove_permissions()
|> Fixtures.Auth.add_permission(Devices.Authorizer.manage_devices_permission())
|> Fixtures.Auth.add_permission(Clients.Authorizer.manage_clients_permission())
assert fetch_device_by_id(device.id, subject) == {:ok, device}
assert fetch_client_by_id(client.id, subject) == {:ok, client}
end
test "does not returns device that belongs to another account with manage permission", %{
test "does not returns client that belongs to another account with manage permission", %{
unprivileged_subject: subject
} do
device = Fixtures.Devices.create_device()
client = Fixtures.Clients.create_client()
subject =
subject
|> Fixtures.Auth.remove_permissions()
|> Fixtures.Auth.add_permission(Devices.Authorizer.manage_devices_permission())
|> Fixtures.Auth.add_permission(Clients.Authorizer.manage_clients_permission())
assert fetch_device_by_id(device.id, subject) == {:error, :not_found}
assert fetch_client_by_id(client.id, subject) == {:error, :not_found}
end
test "does not return device that belongs to another actor with manage_own permission", %{
test "does not return client that belongs to another actor with manage_own permission", %{
unprivileged_subject: subject
} do
device = Fixtures.Devices.create_device()
client = Fixtures.Clients.create_client()
subject =
subject
|> Fixtures.Auth.remove_permissions()
|> Fixtures.Auth.add_permission(Devices.Authorizer.manage_own_devices_permission())
|> Fixtures.Auth.add_permission(Clients.Authorizer.manage_own_clients_permission())
assert fetch_device_by_id(device.id, subject) == {:error, :not_found}
assert fetch_client_by_id(client.id, subject) == {:error, :not_found}
end
test "returns error when device does not exist", %{unprivileged_subject: subject} do
assert fetch_device_by_id(Ecto.UUID.generate(), subject) ==
test "returns error when client does not exist", %{unprivileged_subject: subject} do
assert fetch_client_by_id(Ecto.UUID.generate(), subject) ==
{:error, :not_found}
end
test "returns error when subject has no permission to view devices", %{
test "returns error when subject has no permission to view clients", %{
unprivileged_subject: subject
} do
subject = Fixtures.Auth.remove_permissions(subject)
assert fetch_device_by_id(Ecto.UUID.generate(), subject) ==
assert fetch_client_by_id(Ecto.UUID.generate(), subject) ==
{:error,
{:unauthorized,
[
missing_permissions: [
{:one_of,
[
Devices.Authorizer.manage_devices_permission(),
Devices.Authorizer.manage_own_devices_permission()
Clients.Authorizer.manage_clients_permission(),
Clients.Authorizer.manage_own_clients_permission()
]}
]
]}}
end
end
describe "list_devices/1" do
test "returns empty list when there are no devices", %{admin_subject: subject} do
assert list_devices(subject) == {:ok, []}
describe "list_clients/1" do
test "returns empty list when there are no clients", %{admin_subject: subject} do
assert list_clients(subject) == {:ok, []}
end
test "does not list deleted devices", %{
test "does not list deleted clients", %{
unprivileged_actor: actor,
unprivileged_subject: subject
} do
Fixtures.Devices.create_device(actor: actor)
|> Fixtures.Devices.delete_device()
Fixtures.Clients.create_client(actor: actor)
|> Fixtures.Clients.delete_client()
assert list_devices(subject) == {:ok, []}
assert list_clients(subject) == {:ok, []}
end
test "does not list devices in other accounts", %{
test "does not list clients in other accounts", %{
unprivileged_subject: subject
} do
Fixtures.Devices.create_device()
Fixtures.Clients.create_client()
assert list_devices(subject) == {:ok, []}
assert list_clients(subject) == {:ok, []}
end
test "shows all devices owned by a actor for unprivileged subject", %{
test "shows all clients owned by a actor for unprivileged subject", %{
unprivileged_actor: actor,
admin_actor: other_actor,
unprivileged_subject: subject
} do
device = Fixtures.Devices.create_device(actor: actor)
Fixtures.Devices.create_device(actor: other_actor)
client = Fixtures.Clients.create_client(actor: actor)
Fixtures.Clients.create_client(actor: other_actor)
assert list_devices(subject) == {:ok, [device]}
assert list_clients(subject) == {:ok, [client]}
end
test "shows all devices for admin subject", %{
test "shows all clients for admin subject", %{
unprivileged_actor: other_actor,
admin_actor: admin_actor,
admin_subject: subject
} do
Fixtures.Devices.create_device(actor: admin_actor)
Fixtures.Devices.create_device(actor: other_actor)
Fixtures.Clients.create_client(actor: admin_actor)
Fixtures.Clients.create_client(actor: other_actor)
assert {:ok, devices} = list_devices(subject)
assert length(devices) == 2
assert {:ok, clients} = list_clients(subject)
assert length(clients) == 2
end
test "returns error when subject has no permission to manage devices", %{
test "returns error when subject has no permission to manage clients", %{
unprivileged_subject: subject
} do
subject = Fixtures.Auth.remove_permissions(subject)
assert list_devices(subject) ==
assert list_clients(subject) ==
{:error,
{:unauthorized,
[
missing_permissions: [
{:one_of,
[
Devices.Authorizer.manage_devices_permission(),
Devices.Authorizer.manage_own_devices_permission()
Clients.Authorizer.manage_clients_permission(),
Clients.Authorizer.manage_own_clients_permission()
]}
]
]}}
end
end
describe "list_devices_by_actor_id/2" do
test "returns empty list when there are no devices for a given actor", %{
describe "list_clients_by_actor_id/2" do
test "returns empty list when there are no clients for a given actor", %{
admin_actor: actor,
admin_subject: subject
} do
assert list_devices_by_actor_id(Ecto.UUID.generate(), subject) == {:ok, []}
assert list_devices_by_actor_id(actor.id, subject) == {:ok, []}
Fixtures.Devices.create_device()
assert list_devices_by_actor_id(actor.id, subject) == {:ok, []}
assert list_clients_by_actor_id(Ecto.UUID.generate(), subject) == {:ok, []}
assert list_clients_by_actor_id(actor.id, subject) == {:ok, []}
Fixtures.Clients.create_client()
assert list_clients_by_actor_id(actor.id, subject) == {:ok, []}
end
test "returns error when actor id is invalid", %{admin_subject: subject} do
assert list_devices_by_actor_id("foo", subject) == {:error, :not_found}
assert list_clients_by_actor_id("foo", subject) == {:error, :not_found}
end
test "does not list deleted devices", %{
test "does not list deleted clients", %{
unprivileged_actor: actor,
unprivileged_identity: identity,
unprivileged_subject: subject
} do
Fixtures.Devices.create_device(identity: identity)
|> Fixtures.Devices.delete_device()
Fixtures.Clients.create_client(identity: identity)
|> Fixtures.Clients.delete_client()
assert list_devices_by_actor_id(actor.id, subject) == {:ok, []}
assert list_clients_by_actor_id(actor.id, subject) == {:ok, []}
end
test "does not deleted devices for actors in other accounts", %{
test "does not deleted clients for actors in other accounts", %{
unprivileged_subject: unprivileged_subject,
admin_subject: admin_subject
} do
actor = Fixtures.Actors.create_actor(type: :account_user)
Fixtures.Devices.create_device(actor: actor)
Fixtures.Clients.create_client(actor: actor)
assert list_devices_by_actor_id(actor.id, unprivileged_subject) == {:ok, []}
assert list_devices_by_actor_id(actor.id, admin_subject) == {:ok, []}
assert list_clients_by_actor_id(actor.id, unprivileged_subject) == {:ok, []}
assert list_clients_by_actor_id(actor.id, admin_subject) == {:ok, []}
end
test "shows only devices owned by a actor for unprivileged subject", %{
test "shows only clients owned by a actor for unprivileged subject", %{
unprivileged_actor: actor,
admin_actor: other_actor,
unprivileged_subject: subject
} do
device = Fixtures.Devices.create_device(actor: actor)
Fixtures.Devices.create_device(actor: other_actor)
client = Fixtures.Clients.create_client(actor: actor)
Fixtures.Clients.create_client(actor: other_actor)
assert list_devices_by_actor_id(actor.id, subject) == {:ok, [device]}
assert list_devices_by_actor_id(other_actor.id, subject) == {:ok, []}
assert list_clients_by_actor_id(actor.id, subject) == {:ok, [client]}
assert list_clients_by_actor_id(other_actor.id, subject) == {:ok, []}
end
test "shows all devices owned by another actor for admin subject", %{
test "shows all clients owned by another actor for admin subject", %{
unprivileged_actor: other_actor,
admin_actor: admin_actor,
admin_subject: subject
} do
Fixtures.Devices.create_device(actor: admin_actor)
Fixtures.Devices.create_device(actor: other_actor)
Fixtures.Clients.create_client(actor: admin_actor)
Fixtures.Clients.create_client(actor: other_actor)
assert {:ok, [_device]} = list_devices_by_actor_id(admin_actor.id, subject)
assert {:ok, [_device]} = list_devices_by_actor_id(other_actor.id, subject)
assert {:ok, [_client]} = list_clients_by_actor_id(admin_actor.id, subject)
assert {:ok, [_client]} = list_clients_by_actor_id(other_actor.id, subject)
end
test "returns error when subject has no permission to manage devices", %{
test "returns error when subject has no permission to manage clients", %{
unprivileged_subject: subject
} do
subject = Fixtures.Auth.remove_permissions(subject)
assert list_devices_by_actor_id(Ecto.UUID.generate(), subject) ==
assert list_clients_by_actor_id(Ecto.UUID.generate(), subject) ==
{:error,
{:unauthorized,
[
missing_permissions: [
{:one_of,
[
Devices.Authorizer.manage_devices_permission(),
Devices.Authorizer.manage_own_devices_permission()
Clients.Authorizer.manage_clients_permission(),
Clients.Authorizer.manage_own_clients_permission()
]}
]
]}}
end
end
describe "change_device/1" do
describe "change_client/1" do
test "returns changeset with given changes", %{admin_actor: actor} do
device = Fixtures.Devices.create_device(actor: actor)
device_attrs = Fixtures.Devices.device_attrs()
client = Fixtures.Clients.create_client(actor: actor)
client_attrs = Fixtures.Clients.client_attrs()
assert changeset = change_device(device, device_attrs)
assert %Ecto.Changeset{data: %Domain.Devices.Device{}} = changeset
assert changeset = change_client(client, client_attrs)
assert %Ecto.Changeset{data: %Domain.Clients.Client{}} = changeset
assert changeset.changes == %{name: device_attrs.name}
assert changeset.changes == %{name: client_attrs.name}
end
end
describe "upsert_device/2" do
describe "upsert_client/2" do
test "returns errors on invalid attrs", %{
admin_subject: subject
} do
@@ -306,7 +306,7 @@ defmodule Domain.DevicesTest do
ipv6: "fd01::10000"
}
assert {:error, changeset} = upsert_device(attrs, subject)
assert {:error, changeset} = upsert_client(attrs, subject)
assert errors_on(changeset) == %{
public_key: ["should be 44 character(s)", "must be a base64-encoded string"],
@@ -314,39 +314,39 @@ defmodule Domain.DevicesTest do
}
end
test "allows creating device with just required attributes", %{
test "allows creating client with just required attributes", %{
admin_actor: actor,
admin_identity: identity,
admin_subject: subject
} do
attrs =
Fixtures.Devices.device_attrs()
Fixtures.Clients.client_attrs()
|> Map.delete(:name)
assert {:ok, device} = upsert_device(attrs, subject)
assert {:ok, client} = upsert_client(attrs, subject)
assert device.name
assert client.name
assert device.public_key == attrs.public_key
assert client.public_key == attrs.public_key
assert device.actor_id == actor.id
assert device.identity_id == identity.id
assert device.account_id == actor.account_id
assert client.actor_id == actor.id
assert client.identity_id == identity.id
assert client.account_id == actor.account_id
refute is_nil(device.ipv4)
refute is_nil(device.ipv6)
refute is_nil(client.ipv4)
refute is_nil(client.ipv6)
assert device.last_seen_remote_ip == %Postgrex.INET{address: subject.context.remote_ip}
assert device.last_seen_user_agent == subject.context.user_agent
assert device.last_seen_version == "0.7.412"
assert device.last_seen_at
assert client.last_seen_remote_ip == %Postgrex.INET{address: subject.context.remote_ip}
assert client.last_seen_user_agent == subject.context.user_agent
assert client.last_seen_version == "0.7.412"
assert client.last_seen_at
end
test "updates device when it already exists", %{
test "updates client when it already exists", %{
admin_subject: subject
} do
device = Fixtures.Devices.create_device(subject: subject)
attrs = Fixtures.Devices.device_attrs(external_id: device.external_id)
client = Fixtures.Clients.create_client(subject: subject)
attrs = Fixtures.Clients.client_attrs(external_id: client.external_id)
subject = %{
subject
@@ -357,40 +357,40 @@ defmodule Domain.DevicesTest do
}
}
assert {:ok, updated_device} = upsert_device(attrs, subject)
assert {:ok, updated_client} = upsert_client(attrs, subject)
assert Repo.aggregate(Devices.Device, :count, :id) == 1
assert Repo.aggregate(Clients.Client, :count, :id) == 1
assert updated_device.name
assert updated_device.last_seen_remote_ip.address == subject.context.remote_ip
assert updated_device.last_seen_remote_ip != device.last_seen_remote_ip
assert updated_device.last_seen_user_agent == subject.context.user_agent
assert updated_device.last_seen_user_agent != device.last_seen_user_agent
assert updated_device.last_seen_version == "0.7.411"
assert updated_device.public_key != device.public_key
assert updated_device.public_key == attrs.public_key
assert updated_client.name
assert updated_client.last_seen_remote_ip.address == subject.context.remote_ip
assert updated_client.last_seen_remote_ip != client.last_seen_remote_ip
assert updated_client.last_seen_user_agent == subject.context.user_agent
assert updated_client.last_seen_user_agent != client.last_seen_user_agent
assert updated_client.last_seen_version == "0.7.411"
assert updated_client.public_key != client.public_key
assert updated_client.public_key == attrs.public_key
assert updated_device.actor_id == device.actor_id
assert updated_device.identity_id == device.identity_id
assert updated_device.ipv4 == device.ipv4
assert updated_device.ipv6 == device.ipv6
assert updated_device.last_seen_at
assert updated_device.last_seen_at != device.last_seen_at
assert updated_client.actor_id == client.actor_id
assert updated_client.identity_id == client.identity_id
assert updated_client.ipv4 == client.ipv4
assert updated_client.ipv6 == client.ipv6
assert updated_client.last_seen_at
assert updated_client.last_seen_at != client.last_seen_at
end
test "does not reserve additional addresses on update", %{
admin_subject: subject
} do
device = Fixtures.Devices.create_device(subject: subject)
client = Fixtures.Clients.create_client(subject: subject)
attrs =
Fixtures.Devices.device_attrs(
external_id: device.external_id,
Fixtures.Clients.client_attrs(
external_id: client.external_id,
last_seen_user_agent: "iOS/12.5 (iPhone) connlib/0.7.411",
last_seen_remote_ip: %Postgrex.INET{address: {100, 64, 100, 100}}
)
assert {:ok, updated_device} = upsert_device(attrs, subject)
assert {:ok, updated_client} = upsert_client(attrs, subject)
addresses =
Domain.Network.Address
@@ -400,26 +400,26 @@ defmodule Domain.DevicesTest do
end)
assert length(addresses) == 2
assert %{address: updated_device.ipv4, type: :ipv4} in addresses
assert %{address: updated_device.ipv6, type: :ipv6} in addresses
assert %{address: updated_client.ipv4, type: :ipv4} in addresses
assert %{address: updated_client.ipv6, type: :ipv6} in addresses
end
test "allows unprivileged actor to create a device for himself", %{
test "allows unprivileged actor to create a client for himself", %{
admin_subject: subject
} do
attrs =
Fixtures.Devices.device_attrs()
Fixtures.Clients.client_attrs()
|> Map.delete(:name)
assert {:ok, _device} = upsert_device(attrs, subject)
assert {:ok, _client} = upsert_client(attrs, subject)
end
test "does not allow to reuse IP addresses", %{
account: account,
admin_subject: subject
} do
attrs = Fixtures.Devices.device_attrs(account: account)
assert {:ok, device} = upsert_device(attrs, subject)
attrs = Fixtures.Clients.client_attrs(account: account)
assert {:ok, client} = upsert_client(attrs, subject)
addresses =
Domain.Network.Address
@@ -429,15 +429,15 @@ defmodule Domain.DevicesTest do
end)
assert length(addresses) == 2
assert %{address: device.ipv4, type: :ipv4} in addresses
assert %{address: device.ipv6, type: :ipv6} in addresses
assert %{address: client.ipv4, type: :ipv4} in addresses
assert %{address: client.ipv6, type: :ipv6} in addresses
assert_raise Ecto.ConstraintError, fn ->
Fixtures.Network.create_address(address: device.ipv4, account: account)
Fixtures.Network.create_address(address: client.ipv4, account: account)
end
assert_raise Ecto.ConstraintError, fn ->
Fixtures.Network.create_address(address: device.ipv6, account: account)
Fixtures.Network.create_address(address: client.ipv6, account: account)
end
end
@@ -445,101 +445,101 @@ defmodule Domain.DevicesTest do
account: account,
admin_subject: subject
} do
attrs = Fixtures.Devices.device_attrs(account: account)
assert {:ok, device} = upsert_device(attrs, subject)
attrs = Fixtures.Clients.client_attrs(account: account)
assert {:ok, client} = upsert_client(attrs, subject)
assert %Domain.Network.Address{} = Fixtures.Network.create_address(address: device.ipv4)
assert %Domain.Network.Address{} = Fixtures.Network.create_address(address: device.ipv6)
assert %Domain.Network.Address{} = Fixtures.Network.create_address(address: client.ipv4)
assert %Domain.Network.Address{} = Fixtures.Network.create_address(address: client.ipv6)
end
test "returns error when subject has no permission to create devices", %{
test "returns error when subject has no permission to create clients", %{
admin_subject: subject
} do
subject = Fixtures.Auth.remove_permissions(subject)
assert upsert_device(%{}, subject) ==
assert upsert_client(%{}, subject) ==
{:error,
{:unauthorized,
[missing_permissions: [Devices.Authorizer.manage_own_devices_permission()]]}}
[missing_permissions: [Clients.Authorizer.manage_own_clients_permission()]]}}
end
end
describe "update_device/3" do
test "allows admin actor to update own devices", %{admin_actor: actor, admin_subject: subject} do
device = Fixtures.Devices.create_device(actor: actor)
describe "update_client/3" do
test "allows admin actor to update own clients", %{admin_actor: actor, admin_subject: subject} do
client = Fixtures.Clients.create_client(actor: actor)
attrs = %{name: "new name"}
assert {:ok, device} = update_device(device, attrs, subject)
assert {:ok, client} = update_client(client, attrs, subject)
assert device.name == attrs.name
assert client.name == attrs.name
end
test "allows admin actor to update other actors devices", %{
test "allows admin actor to update other actors clients", %{
account: account,
admin_subject: subject
} do
device = Fixtures.Devices.create_device(account: account)
client = Fixtures.Clients.create_client(account: account)
attrs = %{name: "new name"}
assert {:ok, device} = update_device(device, attrs, subject)
assert {:ok, client} = update_client(client, attrs, subject)
assert device.name == attrs.name
assert client.name == attrs.name
end
test "allows unprivileged actor to update own devices", %{
test "allows unprivileged actor to update own clients", %{
unprivileged_actor: actor,
unprivileged_subject: subject
} do
device = Fixtures.Devices.create_device(actor: actor)
client = Fixtures.Clients.create_client(actor: actor)
attrs = %{name: "new name"}
assert {:ok, device} = update_device(device, attrs, subject)
assert {:ok, client} = update_client(client, attrs, subject)
assert device.name == attrs.name
assert client.name == attrs.name
end
test "does not allow unprivileged actor to update other actors devices", %{
test "does not allow unprivileged actor to update other actors clients", %{
account: account,
unprivileged_subject: subject
} do
device = Fixtures.Devices.create_device(account: account)
client = Fixtures.Clients.create_client(account: account)
attrs = %{name: "new name"}
assert update_device(device, attrs, subject) ==
assert update_client(client, attrs, subject) ==
{:error,
{:unauthorized,
[missing_permissions: [Devices.Authorizer.manage_devices_permission()]]}}
[missing_permissions: [Clients.Authorizer.manage_clients_permission()]]}}
end
test "does not allow admin actor to update devices in other accounts", %{
test "does not allow admin actor to update clients in other accounts", %{
admin_subject: subject
} do
device = Fixtures.Devices.create_device()
client = Fixtures.Clients.create_client()
attrs = %{name: "new name"}
assert update_device(device, attrs, subject) == {:error, :not_found}
assert update_client(client, attrs, subject) == {:error, :not_found}
end
test "does not allow to reset required fields to empty values", %{
admin_actor: actor,
admin_subject: subject
} do
device = Fixtures.Devices.create_device(actor: actor)
client = Fixtures.Clients.create_client(actor: actor)
attrs = %{name: nil, public_key: nil}
assert {:error, changeset} = update_device(device, attrs, subject)
assert {:error, changeset} = update_client(client, attrs, subject)
assert errors_on(changeset) == %{name: ["can't be blank"]}
end
test "returns error on invalid attrs", %{admin_actor: actor, admin_subject: subject} do
device = Fixtures.Devices.create_device(actor: actor)
client = Fixtures.Clients.create_client(actor: actor)
attrs = %{
name: String.duplicate("a", 256)
}
assert {:error, changeset} = update_device(device, attrs, subject)
assert {:error, changeset} = update_client(client, attrs, subject)
assert errors_on(changeset) == %{
name: ["should be at most 255 character(s)"]
@@ -550,145 +550,145 @@ defmodule Domain.DevicesTest do
admin_actor: actor,
admin_subject: subject
} do
device = Fixtures.Devices.create_device(actor: actor)
client = Fixtures.Clients.create_client(actor: actor)
fields = Devices.Device.__schema__(:fields) -- [:name]
fields = Clients.Client.__schema__(:fields) -- [:name]
value = -1
for field <- fields do
assert {:ok, updated_device} = update_device(device, %{field => value}, subject)
assert updated_device == device
assert {:ok, updated_client} = update_client(client, %{field => value}, subject)
assert updated_client == client
end
end
test "returns error when subject has no permission to update devices", %{
test "returns error when subject has no permission to update clients", %{
admin_actor: actor,
admin_subject: subject
} do
device = Fixtures.Devices.create_device(actor: actor)
client = Fixtures.Clients.create_client(actor: actor)
subject = Fixtures.Auth.remove_permissions(subject)
assert update_device(device, %{}, subject) ==
assert update_client(client, %{}, subject) ==
{:error,
{:unauthorized,
[missing_permissions: [Devices.Authorizer.manage_own_devices_permission()]]}}
[missing_permissions: [Clients.Authorizer.manage_own_clients_permission()]]}}
device = Fixtures.Devices.create_device()
client = Fixtures.Clients.create_client()
assert update_device(device, %{}, subject) ==
assert update_client(client, %{}, subject) ==
{:error,
{:unauthorized,
[missing_permissions: [Devices.Authorizer.manage_devices_permission()]]}}
[missing_permissions: [Clients.Authorizer.manage_clients_permission()]]}}
end
end
describe "delete_device/2" do
describe "delete_client/2" do
test "returns error on state conflict", %{admin_actor: actor, admin_subject: subject} do
device = Fixtures.Devices.create_device(actor: actor)
client = Fixtures.Clients.create_client(actor: actor)
assert {:ok, deleted} = delete_device(device, subject)
assert delete_device(deleted, subject) == {:error, :not_found}
assert delete_device(device, subject) == {:error, :not_found}
assert {:ok, deleted} = delete_client(client, subject)
assert delete_client(deleted, subject) == {:error, :not_found}
assert delete_client(client, subject) == {:error, :not_found}
end
test "admin can delete own devices", %{admin_actor: actor, admin_subject: subject} do
device = Fixtures.Devices.create_device(actor: actor)
test "admin can delete own clients", %{admin_actor: actor, admin_subject: subject} do
client = Fixtures.Clients.create_client(actor: actor)
assert {:ok, deleted} = delete_device(device, subject)
assert {:ok, deleted} = delete_client(client, subject)
assert deleted.deleted_at
end
test "admin can delete other people devices", %{
test "admin can delete other people clients", %{
unprivileged_actor: actor,
admin_subject: subject
} do
device = Fixtures.Devices.create_device(actor: actor)
client = Fixtures.Clients.create_client(actor: actor)
assert {:ok, deleted} = delete_device(device, subject)
assert {:ok, deleted} = delete_client(client, subject)
assert deleted.deleted_at
end
test "admin can not delete devices in other accounts", %{
test "admin can not delete clients in other accounts", %{
admin_subject: subject
} do
device = Fixtures.Devices.create_device()
client = Fixtures.Clients.create_client()
assert delete_device(device, subject) == {:error, :not_found}
assert delete_client(client, subject) == {:error, :not_found}
end
test "unprivileged can delete own devices", %{
test "unprivileged can delete own clients", %{
account: account,
unprivileged_actor: actor,
unprivileged_subject: subject
} do
device = Fixtures.Devices.create_device(account: account, actor: actor)
client = Fixtures.Clients.create_client(account: account, actor: actor)
assert {:ok, deleted} = delete_device(device, subject)
assert {:ok, deleted} = delete_client(client, subject)
assert deleted.deleted_at
end
test "unprivileged can not delete other people devices", %{
test "unprivileged can not delete other people clients", %{
account: account,
unprivileged_subject: subject
} do
device = Fixtures.Devices.create_device()
client = Fixtures.Clients.create_client()
assert delete_device(device, subject) ==
assert delete_client(client, subject) ==
{:error,
{:unauthorized,
[missing_permissions: [Devices.Authorizer.manage_devices_permission()]]}}
[missing_permissions: [Clients.Authorizer.manage_clients_permission()]]}}
device = Fixtures.Devices.create_device(account: account)
client = Fixtures.Clients.create_client(account: account)
assert delete_device(device, subject) ==
assert delete_client(client, subject) ==
{:error,
{:unauthorized,
[missing_permissions: [Devices.Authorizer.manage_devices_permission()]]}}
[missing_permissions: [Clients.Authorizer.manage_clients_permission()]]}}
assert Repo.aggregate(Devices.Device, :count) == 2
assert Repo.aggregate(Clients.Client, :count) == 2
end
test "returns error when subject has no permission to delete devices", %{
test "returns error when subject has no permission to delete clients", %{
admin_actor: actor,
admin_subject: subject
} do
device = Fixtures.Devices.create_device(actor: actor)
client = Fixtures.Clients.create_client(actor: actor)
subject = Fixtures.Auth.remove_permissions(subject)
assert delete_device(device, subject) ==
assert delete_client(client, subject) ==
{:error,
{:unauthorized,
[missing_permissions: [Devices.Authorizer.manage_own_devices_permission()]]}}
[missing_permissions: [Clients.Authorizer.manage_own_clients_permission()]]}}
device = Fixtures.Devices.create_device()
client = Fixtures.Clients.create_client()
assert delete_device(device, subject) ==
assert delete_client(client, subject) ==
{:error,
{:unauthorized,
[missing_permissions: [Devices.Authorizer.manage_devices_permission()]]}}
[missing_permissions: [Clients.Authorizer.manage_clients_permission()]]}}
end
end
describe "delete_actor_devices/1" do
test "removes all devices that belong to an actor" do
describe "delete_actor_clients/1" do
test "removes all clients that belong to an actor" do
actor = Fixtures.Actors.create_actor()
Fixtures.Devices.create_device(actor: actor)
Fixtures.Devices.create_device(actor: actor)
Fixtures.Devices.create_device(actor: actor)
Fixtures.Clients.create_client(actor: actor)
Fixtures.Clients.create_client(actor: actor)
Fixtures.Clients.create_client(actor: actor)
assert Repo.aggregate(Devices.Device.Query.all(), :count) == 3
assert delete_actor_devices(actor) == :ok
assert Repo.aggregate(Devices.Device.Query.all(), :count) == 0
assert Repo.aggregate(Clients.Client.Query.all(), :count) == 3
assert delete_actor_clients(actor) == :ok
assert Repo.aggregate(Clients.Client.Query.all(), :count) == 0
end
test "does not remove devices that belong to another actor" do
test "does not remove clients that belong to another actor" do
actor = Fixtures.Actors.create_actor()
Fixtures.Devices.create_device()
Fixtures.Clients.create_client()
assert delete_actor_devices(actor) == :ok
assert Repo.aggregate(Devices.Device.Query.all(), :count) == 1
assert delete_actor_clients(actor) == :ok
assert Repo.aggregate(Clients.Client.Query.all(), :count) == 1
end
end
end

View File

@@ -37,7 +37,7 @@ defmodule Domain.Network.Address.QueryTest do
assert Repo.one(queryable) == %Postgrex.INET{address: {10, 3, 3, 3}}
end
test "forward scans available address after offset it it's assigned to a device", %{
test "forward scans available address after offset it it's assigned to a client", %{
account: account
} do
cidr = string_to_cidr("10.3.4.0/29")
@@ -141,7 +141,7 @@ defmodule Domain.Network.Address.QueryTest do
assert Repo.one(queryable) == %Postgrex.INET{address: {64_768, 0, 0, 0, 0, 0, 64, 0}}
end
test "works when netmask allows a large number of devices", %{account: account} do
test "works when netmask allows a large number of clients", %{account: account} do
cidr = string_to_cidr("fd00::/70")
offset = 9_223_372_036_854_775_807

View File

@@ -4,7 +4,7 @@ defmodule Domain.Fixtures.Config do
def configuration_attrs(attrs \\ %{}) do
Enum.into(attrs, %{
devices_upstream_dns: ["1.1.1.1"]
clients_upstream_dns: ["1.1.1.1"]
})
end

View File

@@ -1,17 +1,17 @@
defmodule Domain.Fixtures.Devices do
defmodule Domain.Fixtures.Clients do
use Domain.Fixture
alias Domain.Devices
alias Domain.Clients
def device_attrs(attrs \\ %{}) do
def client_attrs(attrs \\ %{}) do
Enum.into(attrs, %{
external_id: Ecto.UUID.generate(),
name: "device-#{unique_integer()}",
name: "client-#{unique_integer()}",
public_key: unique_public_key()
})
end
def create_device(attrs \\ %{}) do
attrs = device_attrs(attrs)
def create_client(attrs \\ %{}) do
attrs = client_attrs(attrs)
{account, attrs} =
pop_assoc_fixture(attrs, :account, fn assoc_attrs ->
@@ -47,20 +47,20 @@ defmodule Domain.Fixtures.Devices do
|> Fixtures.Auth.create_subject()
end)
{:ok, device} = Devices.upsert_device(attrs, subject)
%{device | online?: false}
{:ok, client} = Clients.upsert_client(attrs, subject)
%{client | online?: false}
end
def delete_device(device) do
device = Repo.preload(device, :account)
def delete_client(client) do
client = Repo.preload(client, :account)
subject =
Fixtures.Auth.create_subject(
account: device.account,
account: client.account,
actor: [type: :account_admin_user]
)
{:ok, device} = Devices.delete_device(device, subject)
device
{:ok, client} = Clients.delete_client(client, subject)
client
end
end

View File

@@ -17,7 +17,7 @@ defmodule Domain.Mocks.OpenIDConnect do
"issuer" => "#{endpoint}/",
"authorization_endpoint" => "#{endpoint}/authorize",
"token_endpoint" => "#{endpoint}/oauth/token",
"device_authorization_endpoint" => "#{endpoint}/oauth/device/code",
"client_authorization_endpoint" => "#{endpoint}/oauth/client/code",
"userinfo_endpoint" => "#{endpoint}/userinfo",
"mfa_challenge_endpoint" => "#{endpoint}/mfa/challenge",
"jwks_uri" => "#{endpoint}/.well-known/jwks.json",

View File

@@ -11,8 +11,8 @@
<.sidebar_item navigate={~p"/#{@account}/groups"} icon="hero-user-group-solid">
Groups
</.sidebar_item>
<.sidebar_item navigate={~p"/#{@account}/devices"} icon="hero-device-phone-mobile-solid">
Devices
<.sidebar_item navigate={~p"/#{@account}/clients"} icon="hero-client-phone-mobile-solid">
Clients
</.sidebar_item>
<.sidebar_item

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="viewport" content="width=client-width, initial-scale=1" />
<!-- iOS App Favicon -->
<link rel="apple-touch-icon" sizes="180x180" href={~p"/images/apple-touch-icon.png"} />
<link rel="icon" type="image/png" sizes="32x32" href={~p"/images/favicon-32x32.png"} />

View File

@@ -236,7 +236,7 @@ defmodule Web.TableComponents do
Renders a table with 2 columns and generic styling.
The component will likely be used when displaying the properties of an
individual resource (e.g. Gateway, Resource, Device, etc...)
individual resource (e.g. Gateway, Resource, Client, etc...)
The component renders a table that is meant to be viewed vertically, with
the first column being the label and the second column being the value.
@@ -277,7 +277,7 @@ defmodule Web.TableComponents do
the header and the second column will be the value.
The component will likely be used when displaying the properties of an
individual resource (e.g. Gateway, Resource, Device, etc...)
individual resource (e.g. Gateway, Resource, Client, etc...)
This component is intended to be used with the `vertical_table` component.

View File

@@ -1,28 +1,28 @@
defmodule Web.Devices.Edit do
defmodule Web.Clients.Edit do
use Web, :live_view
alias Domain.Devices
alias Domain.Clients
def mount(%{"id" => id}, _session, socket) do
with {:ok, device} <- Devices.fetch_device_by_id(id, socket.assigns.subject) do
changeset = Devices.change_device(device)
{:ok, assign(socket, device: device, form: to_form(changeset))}
with {:ok, client} <- Clients.fetch_client_by_id(id, socket.assigns.subject) do
changeset = Clients.change_client(client)
{:ok, assign(socket, client: client, form: to_form(changeset))}
else
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
end
end
def handle_event("change", %{"device" => attrs}, socket) do
def handle_event("change", %{"client" => attrs}, socket) do
changeset =
Devices.change_device(socket.assigns.device, attrs)
Clients.change_client(socket.assigns.client, attrs)
|> Map.put(:action, :insert)
{:noreply, assign(socket, form: to_form(changeset))}
end
def handle_event("submit", %{"device" => attrs}, socket) do
with {:ok, device} <-
Devices.update_device(socket.assigns.device, attrs, socket.assigns.subject) do
socket = redirect(socket, to: ~p"/#{socket.assigns.account}/devices/#{device}")
def handle_event("submit", %{"client" => attrs}, socket) do
with {:ok, client} <-
Clients.update_client(socket.assigns.client, attrs, socket.assigns.subject) do
socket = redirect(socket, to: ~p"/#{socket.assigns.account}/clients/#{client}")
{:noreply, socket}
else
{:error, changeset} ->
@@ -33,23 +33,23 @@ defmodule Web.Devices.Edit do
def render(assigns) do
~H"""
<.breadcrumbs home_path={~p"/#{@account}/dashboard"}>
<.breadcrumb path={~p"/#{@account}/devices"}>Devices</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/devices/#{@device}"}>
<%= @device.name %>
<.breadcrumb path={~p"/#{@account}/clients"}>Clients</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/clients/#{@client}"}>
<%= @client.name %>
</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/devices/#{@device}/edit"}>
<.breadcrumb path={~p"/#{@account}/clients/#{@client}/edit"}>
Edit
</.breadcrumb>
</.breadcrumbs>
<.header>
<:title>
Editing device <code>Engineering</code>
Editing client <code>Engineering</code>
</:title>
</.header>
<!-- Update Group -->
<section class="bg-white dark:bg-gray-900">
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
<h2 class="mb-4 text-xl font-bold text-gray-900 dark:text-white">Edit device details</h2>
<h2 class="mb-4 text-xl font-bold text-gray-900 dark:text-white">Edit client details</h2>
<.form for={@form} phx-change={:change} phx-submit={:submit}>
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
<div>

View File

@@ -1,11 +1,11 @@
defmodule Web.Devices.Index do
defmodule Web.Clients.Index do
use Web, :live_view
alias Domain.Devices
alias Domain.Clients
def mount(_params, _session, socket) do
with {:ok, devices} <- Devices.list_devices(socket.assigns.subject, preload: :actor) do
{:ok, assign(socket, devices: devices)}
with {:ok, clients} <- Clients.list_clients(socket.assigns.subject, preload: :actor) do
{:ok, assign(socket, clients: clients)}
else
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
end
@@ -14,45 +14,45 @@ defmodule Web.Devices.Index do
def render(assigns) do
~H"""
<.breadcrumbs home_path={~p"/#{@account}/dashboard"}>
<.breadcrumb path={~p"/#{@account}/devices"}>Devices</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/clients"}>Clients</.breadcrumb>
</.breadcrumbs>
<.header>
<:title>
All devices
All clients
</:title>
</.header>
<!-- Devices Table -->
<!-- Clients Table -->
<div class="bg-white dark:bg-gray-800 overflow-hidden">
<div :if={Enum.empty?(@devices)} class="text-center align-middle pb-8 pt-4">
<h3 class="mt-2 text-lg font-semibold text-gray-900">There are no devices to display.</h3>
<div :if={Enum.empty?(@clients)} class="text-center align-middle pb-8 pt-4">
<h3 class="mt-2 text-lg font-semibold text-gray-900">There are no clients to display.</h3>
<div class="mt-6">
Devices are created automatically when user connects to a Resource.
Clients are created automatically when user connects to a Resource.
</div>
</div>
<!--<.resource_filter />-->
<.table :if={not Enum.empty?(@devices)} id="devices" rows={@devices} row_id={&"device-#{&1.id}"}>
<:col :let={device} label="NAME" sortable="true">
<.table :if={not Enum.empty?(@clients)} id="clients" rows={@clients} row_id={&"client-#{&1.id}"}>
<:col :let={client} label="NAME" sortable="true">
<.link
navigate={~p"/#{@account}/devices/#{device.id}"}
navigate={~p"/#{@account}/clients/#{client.id}"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
<%= device.name %>
<%= client.name %>
</.link>
</:col>
<:col :let={device} label="USER" sortable="true">
<:col :let={client} label="USER" sortable="true">
<.link
navigate={~p"/#{@account}/actors/#{device.actor.id}"}
navigate={~p"/#{@account}/actors/#{client.actor.id}"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
<%= device.actor.name %>
<%= client.actor.name %>
</.link>
</:col>
<:col :let={device} label="STATUS" sortable="true">
<.connection_status schema={device} />
<:col :let={client} label="STATUS" sortable="true">
<.connection_status schema={client} />
</:col>
</.table>
<!--<.paginator page={3} total_pages={100} collection_base_path={~p"/#{@account}/devices"} />-->
<!--<.paginator page={3} total_pages={100} collection_base_path={~p"/#{@account}/clients"} />-->
</div>
"""
end

View File

@@ -1,81 +1,81 @@
defmodule Web.Devices.Show do
defmodule Web.Clients.Show do
use Web, :live_view
alias Domain.Devices
alias Domain.Clients
def mount(%{"id" => id}, _session, socket) do
with {:ok, device} <- Devices.fetch_device_by_id(id, socket.assigns.subject, preload: :actor) do
{:ok, assign(socket, device: device)}
with {:ok, client} <- Clients.fetch_client_by_id(id, socket.assigns.subject, preload: :actor) do
{:ok, assign(socket, client: client)}
else
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
end
end
def handle_event("delete", _params, socket) do
{:ok, _device} = Devices.delete_device(socket.assigns.device, socket.assigns.subject)
{:noreply, redirect(socket, to: ~p"/#{socket.assigns.account}/devices")}
{:ok, _client} = Clients.delete_client(socket.assigns.client, socket.assigns.subject)
{:noreply, redirect(socket, to: ~p"/#{socket.assigns.account}/clients")}
end
def render(assigns) do
~H"""
<.breadcrumbs home_path={~p"/#{@account}/dashboard"}>
<.breadcrumb path={~p"/#{@account}/devices"}>Devices</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/devices/#{@device.id}"}>
<%= @device.name %>
<.breadcrumb path={~p"/#{@account}/clients"}>Clients</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/clients/#{@client.id}"}>
<%= @client.name %>
</.breadcrumb>
</.breadcrumbs>
<.header>
<:title>
Device Details
Client Details
</:title>
<:actions>
<.edit_button navigate={~p"/#{@account}/devices/#{@device}/edit"}>
Edit Device
<.edit_button navigate={~p"/#{@account}/clients/#{@client}/edit"}>
Edit Client
</.edit_button>
</:actions>
</.header>
<!-- Device Details -->
<div id="device" class="bg-white dark:bg-gray-800 overflow-hidden">
<!-- Client Details -->
<div id="client" class="bg-white dark:bg-gray-800 overflow-hidden">
<.vertical_table>
<.vertical_table_row>
<:label>Identifier</:label>
<:value><%= @device.id %></:value>
<:value><%= @client.id %></:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Name</:label>
<:value><%= @device.name %></:value>
<:value><%= @client.name %></:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Owner</:label>
<:value>
<.link
navigate={~p"/#{@account}/actors/#{@device.actor.id}"}
navigate={~p"/#{@account}/actors/#{@client.actor.id}"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
<%= @device.actor.name %>
<%= @client.actor.name %>
</.link>
</:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Created</:label>
<:value>
<.relative_datetime datetime={@device.inserted_at} />
<.relative_datetime datetime={@client.inserted_at} />
</:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Last Seen</:label>
<:value>
<.relative_datetime datetime={@device.last_seen_at} />
<.relative_datetime datetime={@client.last_seen_at} />
</:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Remote IPv4</:label>
<:value><code><%= @device.ipv4 %></code></:value>
<:value><code><%= @client.ipv4 %></code></:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Remote IPv6</:label>
<:value><code><%= @device.ipv6 %></code></:value>
<:value><code><%= @client.ipv6 %></code></:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Transfer</:label>
@@ -83,11 +83,11 @@ defmodule Web.Devices.Show do
</.vertical_table_row>
<.vertical_table_row>
<:label>Client Version</:label>
<:value><%= @device.last_seen_version %></:value>
<:value><%= @client.last_seen_version %></:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>User Agent</:label>
<:value><%= @device.last_seen_user_agent %></:value>
<:value><%= @client.last_seen_user_agent %></:value>
</.vertical_table_row>
</.vertical_table>
</div>
@@ -100,11 +100,11 @@ defmodule Web.Devices.Show do
<.delete_button
phx-click="delete"
data-confirm={
"Are you sure want to delete this device? " <>
"Are you sure want to delete this client? " <>
"User still will be able to create a new one by reconnecting to the Firezone."
}
>
Delete Device
Delete Client
</.delete_button>
</:actions>
</.header>

View File

@@ -103,7 +103,7 @@ defmodule Web.Policies.Show do
Authorized at
</th>
<th scope="col" class="px-6 py-3">
Device
Client
</th>
<th scope="col" class="px-6 py-3">
User
@@ -121,7 +121,7 @@ defmodule Web.Policies.Show do
<td class="px-6 py-4">
<.link
class="text-blue-600 dark:text-blue-500 hover:underline"
navigate={~p"/#{@account}/devices/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
navigate={~p"/#{@account}/clients/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
>
2425BD07A38D
</.link>

View File

@@ -12,7 +12,7 @@ defmodule Web.Settings.DNS do
</:title>
</.header>
<p class="ml-4 mb-4 font-medium bg-gray-50 dark:bg-gray-800 text-gray-600 dark:text-gray-500">
Configure the default resolver used by connected Devices in your Firezone network. Queries for
Configure the default resolver used by connected Clients in your Firezone network. Queries for
defined Resources will <strong>always</strong>
use Firezone's internal DNS. All other queries will
use the resolver configured below.
@@ -29,7 +29,7 @@ defmodule Web.Settings.DNS do
</p>
<section class="bg-white dark:bg-gray-900">
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
<h2 class="mb-4 text-xl font-bold text-gray-900 dark:text-white">Device DNS</h2>
<h2 class="mb-4 text-xl font-bold text-gray-900 dark:text-white">Client DNS</h2>
<form action="#">
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
<div>

View File

@@ -127,7 +127,7 @@ defmodule Web.Router do
live "/:id", Show
end
scope "/devices", Devices do
scope "/clients", Clients do
live "/", Index
live "/:id", Show
live "/:id/edit", Edit

View File

@@ -1,4 +1,4 @@
defmodule Web.Live.Devices.EditTest do
defmodule Web.Live.Clients.EditTest do
use Web.ConnCase, async: true
setup do
@@ -6,22 +6,22 @@ defmodule Web.Live.Devices.EditTest do
actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
identity = Fixtures.Auth.create_identity(account: account, actor: actor)
device = Fixtures.Devices.create_device(account: account, actor: actor, identity: identity)
client = Fixtures.Clients.create_client(account: account, actor: actor, identity: identity)
%{
account: account,
actor: actor,
identity: identity,
device: device
client: client
}
end
test "redirects to sign in page for unauthorized user", %{
account: account,
device: device,
client: client,
conn: conn
} do
assert live(conn, ~p"/#{account}/devices/#{device}/edit") ==
assert live(conn, ~p"/#{account}/clients/#{client}/edit") ==
{:error,
{:redirect,
%{
@@ -30,80 +30,80 @@ defmodule Web.Live.Devices.EditTest do
}}}
end
test "renders not found error when device is deleted", %{
test "renders not found error when client is deleted", %{
account: account,
identity: identity,
device: device,
client: client,
conn: conn
} do
device = Fixtures.Devices.delete_device(device)
client = Fixtures.Clients.delete_client(client)
assert_raise Web.LiveErrors.NotFoundError, fn ->
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/devices/#{device}/edit")
|> live(~p"/#{account}/clients/#{client}/edit")
end
end
test "renders breadcrumbs item", %{
account: account,
identity: identity,
device: device,
client: client,
conn: conn
} do
{:ok, _lv, html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/devices/#{device}/edit")
|> live(~p"/#{account}/clients/#{client}/edit")
assert item = Floki.find(html, "[aria-label='Breadcrumb']")
breadcrumbs = String.trim(Floki.text(item))
assert breadcrumbs =~ "Devices"
assert breadcrumbs =~ device.name
assert breadcrumbs =~ "Clients"
assert breadcrumbs =~ client.name
assert breadcrumbs =~ "Edit"
end
test "renders form", %{
account: account,
identity: identity,
device: device,
client: client,
conn: conn
} do
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/devices/#{device}/edit")
|> live(~p"/#{account}/clients/#{client}/edit")
form = form(lv, "form")
assert find_inputs(form) == [
"device[name]"
"client[name]"
]
end
test "renders changeset errors on input change", %{
account: account,
identity: identity,
device: device,
client: client,
conn: conn
} do
attrs = Fixtures.Devices.device_attrs() |> Map.take([:name])
attrs = Fixtures.Clients.client_attrs() |> Map.take([:name])
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/devices/#{device}/edit")
|> live(~p"/#{account}/clients/#{client}/edit")
lv
|> form("form", device: attrs)
|> validate_change(%{device: %{name: String.duplicate("a", 256)}}, fn form, _html ->
|> form("form", client: attrs)
|> validate_change(%{client: %{name: String.duplicate("a", 256)}}, fn form, _html ->
assert form_validation_errors(form) == %{
"device[name]" => ["should be at most 255 character(s)"]
"client[name]" => ["should be at most 255 character(s)"]
}
end)
|> validate_change(%{device: %{name: ""}}, fn form, _html ->
|> validate_change(%{client: %{name: ""}}, fn form, _html ->
assert form_validation_errors(form) == %{
"device[name]" => ["can't be blank"]
"client[name]" => ["can't be blank"]
}
end)
end
@@ -112,44 +112,44 @@ defmodule Web.Live.Devices.EditTest do
account: account,
actor: actor,
identity: identity,
device: device,
client: client,
conn: conn
} do
other_device = Fixtures.Devices.create_device(account: account, actor: actor)
attrs = %{name: other_device.name}
other_client = Fixtures.Clients.create_client(account: account, actor: actor)
attrs = %{name: other_client.name}
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/devices/#{device}/edit")
|> live(~p"/#{account}/clients/#{client}/edit")
assert lv
|> form("form", device: attrs)
|> form("form", client: attrs)
|> render_submit()
|> form_validation_errors() == %{
"device[name]" => ["has already been taken"]
"client[name]" => ["has already been taken"]
}
end
test "creates a new device on valid attrs", %{
test "creates a new client on valid attrs", %{
account: account,
identity: identity,
device: device,
client: client,
conn: conn
} do
attrs = Fixtures.Devices.device_attrs() |> Map.take([:name])
attrs = Fixtures.Clients.client_attrs() |> Map.take([:name])
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/devices/#{device}/edit")
|> live(~p"/#{account}/clients/#{client}/edit")
assert lv
|> form("form", device: attrs)
|> form("form", client: attrs)
|> render_submit() ==
{:error, {:redirect, %{to: ~p"/#{account}/devices/#{device}"}}}
{:error, {:redirect, %{to: ~p"/#{account}/clients/#{client}"}}}
assert device = Repo.get_by(Domain.Devices.Device, id: device.id)
assert device.name == attrs.name
assert client = Repo.get_by(Domain.Clients.Client, id: client.id)
assert client.name == attrs.name
end
end

View File

@@ -1,4 +1,4 @@
defmodule Web.Live.Devices.IndexTest do
defmodule Web.Live.Clients.IndexTest do
use Web.ConnCase, async: true
setup do
@@ -12,7 +12,7 @@ defmodule Web.Live.Devices.IndexTest do
end
test "redirects to sign in page for unauthorized user", %{account: account, conn: conn} do
assert live(conn, ~p"/#{account}/devices") ==
assert live(conn, ~p"/#{account}/clients") ==
{:error,
{:redirect,
%{
@@ -29,14 +29,14 @@ defmodule Web.Live.Devices.IndexTest do
{:ok, _lv, html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/devices")
|> live(~p"/#{account}/clients")
assert item = Floki.find(html, "[aria-label='Breadcrumb']")
breadcrumbs = String.trim(Floki.text(item))
assert breadcrumbs =~ "Devices"
assert breadcrumbs =~ "Clients"
end
test "renders empty table when there are no devices", %{
test "renders empty table when there are no clients", %{
account: account,
identity: identity,
conn: conn
@@ -44,39 +44,39 @@ defmodule Web.Live.Devices.IndexTest do
{:ok, _lv, html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/devices")
|> live(~p"/#{account}/clients")
assert html =~ "There are no devices to display."
assert html =~ "There are no clients to display."
refute html =~ "tbody"
end
test "renders devices table", %{
test "renders clients table", %{
account: account,
identity: identity,
conn: conn
} do
online_device = Fixtures.Devices.create_device(account: account)
offline_device = Fixtures.Devices.create_device(account: account)
online_client = Fixtures.Clients.create_client(account: account)
offline_client = Fixtures.Clients.create_client(account: account)
:ok = Domain.Devices.connect_device(online_device)
:ok = Domain.Clients.connect_client(online_client)
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/devices")
|> live(~p"/#{account}/clients")
lv
|> element("#devices")
|> element("#clients")
|> render()
|> table_to_map()
|> with_table_row("name", online_device.name, fn row ->
|> with_table_row("name", online_client.name, fn row ->
assert row["status"] == "Online"
name = Repo.preload(online_device, :actor).actor.name
name = Repo.preload(online_client, :actor).actor.name
assert row["user"] =~ name
end)
|> with_table_row("name", offline_device.name, fn row ->
|> with_table_row("name", offline_client.name, fn row ->
assert row["status"] == "Offline"
name = Repo.preload(offline_device, :actor).actor.name
name = Repo.preload(offline_client, :actor).actor.name
assert row["user"] =~ name
end)
end

View File

@@ -1,4 +1,4 @@
defmodule Web.Live.Devices.ShowTest do
defmodule Web.Live.Clients.ShowTest do
use Web.ConnCase, async: true
setup do
@@ -7,23 +7,23 @@ defmodule Web.Live.Devices.ShowTest do
identity = Fixtures.Auth.create_identity(account: account, actor: actor)
subject = Fixtures.Auth.create_subject(account: account, actor: actor, identity: identity)
device = Fixtures.Devices.create_device(account: account, actor: actor, identity: identity)
client = Fixtures.Clients.create_client(account: account, actor: actor, identity: identity)
%{
account: account,
actor: actor,
identity: identity,
subject: subject,
device: device
client: client
}
end
test "redirects to sign in page for unauthorized user", %{
account: account,
device: device,
client: client,
conn: conn
} do
assert live(conn, ~p"/#{account}/devices/#{device}") ==
assert live(conn, ~p"/#{account}/clients/#{client}") ==
{:error,
{:redirect,
%{
@@ -32,41 +32,41 @@ defmodule Web.Live.Devices.ShowTest do
}}}
end
test "renders not found error when device is deleted", %{
test "renders not found error when client is deleted", %{
account: account,
device: device,
client: client,
identity: identity,
conn: conn
} do
device = Fixtures.Devices.delete_device(device)
client = Fixtures.Clients.delete_client(client)
assert_raise Web.LiveErrors.NotFoundError, fn ->
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/devices/#{device}")
|> live(~p"/#{account}/clients/#{client}")
end
end
test "renders breadcrumbs item", %{
account: account,
device: device,
client: client,
identity: identity,
conn: conn
} do
{:ok, _lv, html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/devices/#{device}")
|> live(~p"/#{account}/clients/#{client}")
assert item = Floki.find(html, "[aria-label='Breadcrumb']")
breadcrumbs = String.trim(Floki.text(item))
assert breadcrumbs =~ "Devices"
assert breadcrumbs =~ device.name
assert breadcrumbs =~ "Clients"
assert breadcrumbs =~ client.name
end
test "renders device details", %{
test "renders client details", %{
account: account,
device: device,
client: client,
actor: actor,
identity: identity,
conn: conn
@@ -74,79 +74,79 @@ defmodule Web.Live.Devices.ShowTest do
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/devices/#{device}")
|> live(~p"/#{account}/clients/#{client}")
table =
lv
|> element("#device")
|> element("#client")
|> render()
|> vertical_table_to_map()
assert table["identifier"] == device.id
assert table["name"] == device.name
assert table["identifier"] == client.id
assert table["name"] == client.name
assert table["owner"] =~ actor.name
assert table["created"]
assert table["last seen"]
assert table["remote ipv4"] =~ to_string(device.ipv4)
assert table["remote ipv6"] =~ to_string(device.ipv6)
assert table["client version"] =~ device.last_seen_version
assert table["user agent"] =~ device.last_seen_user_agent
assert table["remote ipv4"] =~ to_string(client.ipv4)
assert table["remote ipv6"] =~ to_string(client.ipv6)
assert table["client version"] =~ client.last_seen_version
assert table["user agent"] =~ client.last_seen_user_agent
end
test "renders device owner", %{
test "renders client owner", %{
account: account,
device: device,
client: client,
identity: identity,
conn: conn
} do
actor = Repo.preload(device, :actor).actor
actor = Repo.preload(client, :actor).actor
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/devices/#{device}")
|> live(~p"/#{account}/clients/#{client}")
assert lv
|> element("#device")
|> element("#client")
|> render()
|> vertical_table_to_map()
|> Map.fetch!("owner") =~ actor.name
end
test "allows editing devices", %{
test "allows editing clients", %{
account: account,
device: device,
client: client,
identity: identity,
conn: conn
} do
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/devices/#{device}")
|> live(~p"/#{account}/clients/#{client}")
assert lv
|> element("a", "Edit Device")
|> element("a", "Edit Client")
|> render_click() ==
{:error,
{:live_redirect, %{to: ~p"/#{account}/devices/#{device}/edit", kind: :push}}}
{:live_redirect, %{to: ~p"/#{account}/clients/#{client}/edit", kind: :push}}}
end
test "allows deleting devices", %{
test "allows deleting clients", %{
account: account,
device: device,
client: client,
identity: identity,
conn: conn
} do
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/devices/#{device}")
|> live(~p"/#{account}/clients/#{client}")
assert lv
|> element("button", "Delete Device")
|> element("button", "Delete Client")
|> render_click() ==
{:error, {:redirect, %{to: ~p"/#{account}/devices"}}}
{:error, {:redirect, %{to: ~p"/#{account}/clients"}}}
assert Repo.get(Domain.Devices.Device, device.id).deleted_at
assert Repo.get(Domain.Clients.Client, client.id).deleted_at
end
end

View File

@@ -29,7 +29,7 @@ config :domain, Domain.Repo,
migration_timestamps: [type: :timestamptz],
start_apps_before_migration: [:ssl, :logger_json]
config :domain, Domain.Devices, upstream_dns: ["1.1.1.1"]
config :domain, Domain.Clients, upstream_dns: ["1.1.1.1"]
config :domain, Domain.Gateways,
gateway_ipv4_masquerade: true,

View File

@@ -28,7 +28,7 @@ if config_env() == :prod do
path: external_url_path
} = URI.parse(external_url)
config :domain, Domain.Devices, upstream_dns: compile_config!(:devices_upstream_dns)
config :domain, Domain.Clients, upstream_dns: compile_config!(:clients_upstream_dns)
config :domain, Domain.Gateways,
gateway_ipv4_masquerade: compile_config!(:gateway_ipv4_masquerade),

View File

@@ -280,7 +280,7 @@ impl<CB: Callbacks + 'static> ControlSession<Messages, CB> for ControlPlane<CB>
}
fn socket_path() -> &'static str {
"device"
"client"
}
fn retry_strategy() -> ExponentialBackoff {

View File

@@ -139,7 +139,7 @@ mod test {
fn connection_ready_deserialization() {
let message = r#"{
"ref": "0",
"topic": "device",
"topic": "client",
"event": "phx_reply",
"payload": {
"status": "ok",
@@ -160,7 +160,7 @@ mod test {
#[test]
fn init_phoenix_message() {
let m = PhoenixMessage::new(
"device",
"client",
IngressMessages::Init(InitClient {
interface: Interface {
ipv4: "100.72.112.111".parse().unwrap(),
@@ -210,7 +210,7 @@ mod test {
]
},
"ref": null,
"topic": "device"
"topic": "client"
}"#;
let ingress_message: PhoenixMessage<IngressMessages, ReplyMessages> =
serde_json::from_str(message).unwrap();
@@ -220,7 +220,7 @@ mod test {
#[test]
fn list_relays_message() {
let m = PhoenixMessage::<EgressMessages, ()>::new(
"device",
"client",
EgressMessages::PrepareConnection {
resource_id: "f16ecfa0-a94f-4bfd-a2ef-1cc1f2ef3da3".parse().unwrap(),
connected_gateway_ids: vec![],
@@ -235,7 +235,7 @@ mod test {
"connected_gateway_ids": []
},
"ref":null,
"topic": "device"
"topic": "client"
}
"#;
let egress_message = serde_json::from_str(message).unwrap();
@@ -245,7 +245,7 @@ mod test {
#[test]
fn connection_details_reply() {
let m = PhoenixMessage::<IngressMessages, ReplyMessages>::new_reply(
"device",
"client",
ReplyMessages::ConnectionDetails(ConnectionDetails {
gateway_id: "73037362-715d-4a83-a749-f18eadd970e6".parse().unwrap(),
gateway_remote_ip: "172.28.0.1".parse().unwrap(),
@@ -279,7 +279,7 @@ mod test {
let message = r#"
{
"ref":null,
"topic":"device",
"topic":"client",
"event": "phx_reply",
"payload": {
"response": {

View File

@@ -1,3 +0,0 @@
pub struct DeviceRef {
device_ref: std::os::fd::RawFd,
}

View File

@@ -1,3 +0,0 @@
pub struct DeviceRef {
device_ref: std::os::windows::io::RawHandle,
}

View File

@@ -40,9 +40,9 @@ pub struct RequestConnection {
/// Resource id the request is for.
pub resource_id: Id,
/// The preshared key the client generated for the connection that it is trying to establish.
pub device_preshared_key: Key,
pub client_preshared_key: Key,
/// Client's local RTC Session Description that the client will use for this connection.
pub device_rtc_session_description: RTCSessionDescription,
pub client_rtc_session_description: RTCSessionDescription,
}
/// Represent a request to reuse an existing gateway connection from a client to a given resource.
@@ -61,7 +61,7 @@ pub struct ReuseConnection {
impl PartialEq for RequestConnection {
fn eq(&self, other: &Self) -> bool {
self.resource_id == other.resource_id
&& self.device_preshared_key == other.device_preshared_key
&& self.client_preshared_key == other.client_preshared_key
}
}

View File

@@ -82,10 +82,10 @@ impl<CB: Callbacks + 'static> ControlPlane<CB> {
tokio::spawn(async move {
match tunnel
.set_peer_connection_request(
connection_request.device.rtc_session_description,
connection_request.device.peer.into(),
connection_request.client.rtc_session_description,
connection_request.client.peer.into(),
connection_request.relays,
connection_request.device.id,
connection_request.client.id,
connection_request.expires_at,
connection_request.resource,
)
@@ -100,12 +100,12 @@ impl<CB: Callbacks + 'static> ControlPlane<CB> {
}))
.await
{
tunnel.cleanup_connection(connection_request.device.id);
tunnel.cleanup_connection(connection_request.client.id);
let _ = tunnel.callbacks().on_error(&err);
}
}
Err(err) => {
tunnel.cleanup_connection(connection_request.device.id);
tunnel.cleanup_connection(connection_request.client.id);
let _ = tunnel.callbacks().on_error(&err);
}
}
@@ -116,12 +116,12 @@ impl<CB: Callbacks + 'static> ControlPlane<CB> {
fn allow_access(
&self,
AllowAccess {
device_id,
client_id,
resource,
expires_at,
}: AllowAccess,
) {
self.tunnel.allow_access(resource, device_id, expires_at)
self.tunnel.allow_access(resource, client_id, expires_at)
}
#[tracing::instrument(level = "trace", skip(self))]

View File

@@ -19,7 +19,7 @@ pub struct Actor {
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Device {
pub struct Client {
pub id: Id,
pub rtc_session_description: RTCSessionDescription,
pub peer: Peer,
@@ -27,20 +27,20 @@ pub struct Device {
// rtc_sdp is ignored from eq since RTCSessionDescription doesn't implement this
// this will probably be changed in the future.
impl PartialEq for Device {
impl PartialEq for Client {
fn eq(&self, other: &Self) -> bool {
self.id == other.id && self.peer == other.peer
}
}
impl Eq for Device {}
impl Eq for Client {}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
pub struct RequestConnection {
pub actor: Actor,
pub relays: Vec<Relay>,
pub resource: ResourceDescription,
pub device: Device,
pub client: Client,
#[serde(rename = "ref")]
pub reference: String,
#[serde(with = "ts_seconds")]
@@ -73,7 +73,7 @@ pub struct RemoveResource {
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
pub struct AllowAccess {
pub device_id: Id,
pub client_id: Id,
pub resource: ResourceDescription,
#[serde(with = "ts_seconds")]
pub expires_at: DateTime<Utc>,
@@ -122,7 +122,7 @@ mod test {
"topic": "gateway",
"event": "request_connection",
"payload": {
"device": {
"client": {
"id": "3a25ff38-f8d7-47de-9b30-c7c40c206083",
"peer": {
"ipv6": "fd00:2021:1111::3a:ab1b",

View File

@@ -402,8 +402,8 @@ where
Ok(Request::NewConnection(RequestConnection {
resource_id,
gateway_id,
device_preshared_key: Key(preshared_key.to_bytes()),
device_rtc_session_description: local_description,
client_preshared_key: Key(preshared_key.to_bytes()),
client_rtc_session_description: local_description,
}))
}