mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
Fix some of TODOs left from IAM PR (#1627)
This commit is contained in:
@@ -1,102 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
# This script should be run from the app root
|
||||
|
||||
function print_logs() {
|
||||
sudo cat /var/log/firezone/nginx/current
|
||||
sudo cat /var/log/firezone/postgresql/current
|
||||
sudo cat /var/log/firezone/phoenix/current
|
||||
sudo cat /var/log/firezone/wireguard/current
|
||||
}
|
||||
|
||||
trap print_logs EXIT
|
||||
|
||||
# Disable telemetry
|
||||
sudo mkdir -p /opt/firezone/
|
||||
sudo touch /opt/firezone/.disable-telemetry
|
||||
|
||||
if type rpm > /dev/null; then
|
||||
sudo -E rpm -i omnibus/pkg/firezone*.rpm
|
||||
elif type dpkg > /dev/null; then
|
||||
sudo -E dpkg -i omnibus/pkg/firezone*.deb
|
||||
else
|
||||
echo 'Neither rpm nor dpkg found'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Fixes setcap not found on centos 7
|
||||
PATH=/usr/sbin/:$PATH
|
||||
|
||||
# Disable connectivity checks
|
||||
conf="/opt/firezone/embedded/cookbooks/firezone/attributes/default.rb"
|
||||
search="default\['firezone']\['connectivity_checks']\['enabled'] = true"
|
||||
replace="default['firezone']['connectivity_checks']['enabled'] = false"
|
||||
sudo -E sed -i "s/$search/$replace/" $conf
|
||||
|
||||
# Disable telemetry
|
||||
search="default\['firezone']\['telemetry']\['enabled'] = true"
|
||||
search="default['firezone']['telemetry']['enabled'] = false"
|
||||
sudo -E sed -i "s/$search/$replace/" $conf
|
||||
|
||||
# Bootstrap config
|
||||
sudo -E firezone-ctl reconfigure
|
||||
|
||||
# Wait for app to fully boot
|
||||
sleep 5
|
||||
|
||||
# Helpful for debugging
|
||||
print_logs
|
||||
|
||||
# Create admin; requires application to be up
|
||||
sudo -E firezone-ctl create-or-reset-admin
|
||||
|
||||
# XXX: Add more commands here to test
|
||||
|
||||
echo "Trying to load homepage"
|
||||
page=$(curl -L -i -vvv -k https://localhost)
|
||||
echo $page
|
||||
|
||||
echo "Testing for sign in button"
|
||||
echo $page | grep '<a class="button" href="/auth/identity">Sign in with email</a>'
|
||||
|
||||
echo "Testing telemetry_id survives reconfigures"
|
||||
tid1=`sudo cat /var/opt/firezone/cache/telemetry_id`
|
||||
sudo firezone-ctl reconfigure
|
||||
tid2=`sudo cat /var/opt/firezone/cache/telemetry_id`
|
||||
|
||||
if [ "$tid1" = "$tid2" ]; then
|
||||
echo "telemetry_ids match!"
|
||||
else
|
||||
echo "telemetry_ids differ:"
|
||||
echo $tid1
|
||||
echo $tid2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
fz_bin="/opt/firezone/embedded/service/firezone/bin/firezone"
|
||||
ok_res=":ok"
|
||||
|
||||
echo "Testing FzVpn.Interface module works with WireGuard"
|
||||
set_interface=`sudo $fz_bin rpc "IO.inspect(FzVpn.Interface.set(\"wg-fz-test\", %{}))"`
|
||||
del_interface=`sudo $fz_bin rpc "IO.inspect(FzVpn.Interface.delete(\"wg-fz-test\"))"`
|
||||
|
||||
if [[ "$set_interface" != $ok_res || "$del_interface" != $ok_res ]]; then
|
||||
echo "WireGuard test failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing Firewall Rules"
|
||||
user_id="5" # Picking a high enough user_id so there is no overlap
|
||||
device="%{ip: \"10.0.0.1\", ip6: \"fd00::3:2:1\", user_id: $user_id}"
|
||||
rule="%{destination: \"10.0.0.2\", user_id: $user_id, action: :drop, port_type: nil, port_range: nil}"
|
||||
add_user=`sudo $fz_bin rpc "IO.inspect(FzWall.CLI.Live.add_user($user_id))"`
|
||||
add_device=`sudo $fz_bin rpc "IO.inspect(FzWall.CLI.Live.add_device($device))"`
|
||||
add_rule=`sudo $fz_bin rpc "IO.inspect(FzWall.CLI.Live.add_rule($rule))"`
|
||||
del_rule=`sudo $fz_bin rpc "IO.inspect(FzWall.CLI.Live.delete_rule($rule))"`
|
||||
del_device=`sudo $fz_bin rpc "IO.inspect(FzWall.CLI.Live.delete_device($device))"`
|
||||
del_user=`sudo $fz_bin rpc "IO.inspect(FzWall.CLI.Live.delete_user($user_id))"`
|
||||
|
||||
if [[ "$add_user" != $ok_res || "$add_device" != $ok_res || "$add_rule" != '""' || "$del_rule" != '""' || "$del_device" != $ok_res || "$del_user" != $ok_res ]]; then
|
||||
echo "Firewall test failed!"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,6 +1,8 @@
|
||||
apps/web/assets/node_modules
|
||||
apps/web/priv/static/dist
|
||||
apps/web/priv/cert
|
||||
apps/api/priv/static/dist
|
||||
apps/api/priv/cert
|
||||
_build
|
||||
**/cover
|
||||
docs
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -66,8 +66,5 @@ apps/*/screenshots
|
||||
# WG configs generated in acceptance tests
|
||||
*.conf
|
||||
|
||||
# Auto generated private key
|
||||
apps/web/priv/wg_dev_private_key
|
||||
|
||||
# Uploads
|
||||
apps/web/priv/static/uploads
|
||||
|
||||
@@ -2,6 +2,7 @@ ARG ELIXIR_VERSION=1.14.3
|
||||
ARG OTP_VERSION=25.2.1
|
||||
ARG ALPINE_VERSION=3.16.3
|
||||
|
||||
ARG APP_NAME="web"
|
||||
ARG BUILDER_IMAGE="firezone/elixir:${ELIXIR_VERSION}-otp-${OTP_VERSION}"
|
||||
ARG RUNNER_IMAGE="alpine:${ALPINE_VERSION}"
|
||||
|
||||
@@ -42,12 +43,11 @@ COPY apps apps
|
||||
ARG VERSION=0.0.0-docker
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
# compile assets
|
||||
RUN mix tailwind.install --if-missing \
|
||||
&& mix esbuild.install --if-missing \
|
||||
&& mix tailwind default --minify \
|
||||
&& mix esbuild default --minify \
|
||||
&& mix phx.digest
|
||||
# Install and compile assets
|
||||
RUN cd apps/web \
|
||||
&& mix assets.setup \
|
||||
&& mix assets.deploy \
|
||||
&& cd ../../
|
||||
|
||||
# Compile the release
|
||||
RUN mix compile
|
||||
@@ -70,6 +70,6 @@ WORKDIR /app
|
||||
ENV MIX_ENV="prod"
|
||||
|
||||
# Only copy the final release from the build stage
|
||||
COPY --from=builder /app/_build/${MIX_ENV}/rel/firezone ./
|
||||
COPY --from=builder /app/_build/${MIX_ENV}/rel/${APP_NAME} ./
|
||||
|
||||
CMD ["/app/bin/server"]
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
defmodule API.Client.Views.Interface do
|
||||
alias Domain.Clients
|
||||
|
||||
def render(%Clients.Client{} = client) do
|
||||
%{
|
||||
upstream_dns: Domain.Config.fetch_config!(:default_client_dns),
|
||||
ipv4: client.ipv4,
|
||||
ipv6: client.ipv6
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -1,10 +1,10 @@
|
||||
defmodule API.Client.Channel do
|
||||
defmodule API.Device.Channel do
|
||||
use API, :channel
|
||||
alias API.Client.Views
|
||||
alias Domain.{Clients, Resources, Gateways, Relays}
|
||||
alias API.Device.Views
|
||||
alias Domain.{Devices, Resources, Gateways, Relays}
|
||||
|
||||
@impl true
|
||||
def join("client", _payload, socket) do
|
||||
def join("device", _payload, socket) do
|
||||
expires_in =
|
||||
DateTime.diff(socket.assigns.subject.expires_at, DateTime.utc_now(), :millisecond)
|
||||
|
||||
@@ -24,10 +24,10 @@ defmodule API.Client.Channel do
|
||||
:ok =
|
||||
push(socket, "init", %{
|
||||
resources: Views.Resource.render_many(resources),
|
||||
interface: Views.Interface.render(socket.assigns.client)
|
||||
interface: Views.Interface.render(socket.assigns.device)
|
||||
})
|
||||
|
||||
:ok = Clients.connect_client(socket.assigns.client)
|
||||
:ok = Devices.connect_device(socket.assigns.device)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
@@ -93,8 +93,8 @@ defmodule API.Client.Channel do
|
||||
"request_connection",
|
||||
%{
|
||||
"resource_id" => resource_id,
|
||||
"client_rtc_session_description" => client_rtc_session_description,
|
||||
"client_preshared_key" => preshared_key
|
||||
"device_rtc_session_description" => device_rtc_session_description,
|
||||
"device_preshared_key" => preshared_key
|
||||
},
|
||||
socket
|
||||
) do
|
||||
@@ -109,11 +109,11 @@ defmodule API.Client.Channel do
|
||||
API.Gateway.Socket.id(gateway),
|
||||
{:request_connection, {self(), socket_ref(socket)},
|
||||
%{
|
||||
client_id: socket.assigns.client.id,
|
||||
device_id: socket.assigns.device.id,
|
||||
resource_id: resource_id,
|
||||
authorization_expires_at: socket.assigns.expires_at,
|
||||
client_rtc_session_description: client_rtc_session_description,
|
||||
client_preshared_key: preshared_key
|
||||
device_rtc_session_description: device_rtc_session_description,
|
||||
device_preshared_key: preshared_key
|
||||
}}
|
||||
)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
defmodule API.Client.Socket do
|
||||
defmodule API.Device.Socket do
|
||||
use Phoenix.Socket
|
||||
alias Domain.{Auth, Clients}
|
||||
alias Domain.{Auth, Devices}
|
||||
|
||||
## Channels
|
||||
|
||||
channel "client:*", API.Client.Channel
|
||||
channel "device:*", API.Device.Channel
|
||||
|
||||
## Authentication
|
||||
|
||||
@@ -13,11 +13,11 @@ defmodule API.Client.Socket do
|
||||
%{user_agent: user_agent, peer_data: %{address: remote_ip}} = connect_info
|
||||
|
||||
with {:ok, subject} <- Auth.sign_in(token, user_agent, remote_ip),
|
||||
{:ok, client} <- Clients.upsert_client(attrs, subject) do
|
||||
{:ok, device} <- Devices.upsert_device(attrs, subject) do
|
||||
socket =
|
||||
socket
|
||||
|> assign(:subject, subject)
|
||||
|> assign(:client, client)
|
||||
|> assign(:device, device)
|
||||
|
||||
{:ok, socket}
|
||||
else
|
||||
@@ -31,5 +31,5 @@ defmodule API.Client.Socket do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def id(socket), do: "client:#{socket.assigns.client.id}"
|
||||
def id(socket), do: "device:#{socket.assigns.device.id}"
|
||||
end
|
||||
15
apps/api/lib/api/device/views/interface.ex
Normal file
15
apps/api/lib/api/device/views/interface.ex
Normal file
@@ -0,0 +1,15 @@
|
||||
defmodule API.Device.Views.Interface do
|
||||
alias Domain.Devices
|
||||
|
||||
def render(%Devices.Device{} = device) do
|
||||
upstream_dns =
|
||||
Devices.fetch_device_config!(device)
|
||||
|> Keyword.fetch!(:upstream_dns)
|
||||
|
||||
%{
|
||||
upstream_dns: upstream_dns,
|
||||
ipv4: device.ipv4,
|
||||
ipv6: device.ipv6
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
defmodule API.Client.Views.Relay do
|
||||
defmodule API.Device.Views.Relay do
|
||||
alias Domain.Relays
|
||||
|
||||
def render_many(relays, expires_at) do
|
||||
@@ -1,4 +1,4 @@
|
||||
defmodule API.Client.Views.Resource do
|
||||
defmodule API.Device.Views.Resource do
|
||||
alias Domain.Resources
|
||||
|
||||
def render_many(resources) do
|
||||
@@ -17,7 +17,7 @@ defmodule API.Endpoint do
|
||||
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
|
||||
|
||||
socket "/gateway", API.Gateway.Socket, API.Sockets.options()
|
||||
socket "/client", API.Client.Socket, API.Sockets.options()
|
||||
socket "/device", API.Device.Socket, API.Sockets.options()
|
||||
socket "/relay", API.Relay.Socket, API.Sockets.options()
|
||||
|
||||
def external_trusted_proxies do
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
defmodule API.Gateway.Channel do
|
||||
use API, :channel
|
||||
alias API.Gateway.Views
|
||||
alias Domain.{Clients, Resources, Relays, Gateways}
|
||||
alias Domain.{Devices, Resources, Relays, Gateways}
|
||||
|
||||
@impl true
|
||||
def join("gateway", _payload, socket) do
|
||||
@@ -26,14 +26,14 @@ defmodule API.Gateway.Channel do
|
||||
|
||||
def handle_info({:request_connection, {channel_pid, socket_ref}, attrs}, socket) do
|
||||
%{
|
||||
client_id: client_id,
|
||||
device_id: device_id,
|
||||
resource_id: resource_id,
|
||||
authorization_expires_at: authorization_expires_at,
|
||||
client_rtc_session_description: rtc_session_description,
|
||||
client_preshared_key: preshared_key
|
||||
device_rtc_session_description: rtc_session_description,
|
||||
device_preshared_key: preshared_key
|
||||
} = attrs
|
||||
|
||||
client = Clients.fetch_client_by_id!(client_id, preload: [:actor])
|
||||
device = Devices.fetch_device_by_id!(device_id, preload: [:actor])
|
||||
resource = Resources.fetch_resource_by_id!(resource_id)
|
||||
{:ok, relays} = Relays.list_connected_relays_for_resource(resource)
|
||||
|
||||
@@ -41,10 +41,10 @@ defmodule API.Gateway.Channel do
|
||||
|
||||
push(socket, "request_connection", %{
|
||||
ref: ref,
|
||||
actor: Views.Actor.render(client.actor),
|
||||
actor: Views.Actor.render(device.actor),
|
||||
relays: Views.Relay.render_many(relays, authorization_expires_at),
|
||||
resource: Views.Resource.render(resource),
|
||||
client: Views.Client.render(client, rtc_session_description, preshared_key),
|
||||
device: Views.Device.render(device, rtc_session_description, preshared_key),
|
||||
expires_at: DateTime.to_unix(authorization_expires_at, :second)
|
||||
})
|
||||
|
||||
@@ -81,7 +81,7 @@ defmodule API.Gateway.Channel do
|
||||
# "ended_at" => ended_at,
|
||||
# "metrics" => [
|
||||
# %{
|
||||
# "client_id" => client_id,
|
||||
# "device_id" => device_id,
|
||||
# "resource_id" => resource_id,
|
||||
# "rx_bytes" => 0,
|
||||
# "tx_packets" => 0
|
||||
|
||||
@@ -8,32 +8,17 @@ defmodule API.Gateway.Socket do
|
||||
|
||||
## Authentication
|
||||
|
||||
def encode_token!(%Gateways.Token{value: value} = token) when not is_nil(value) do
|
||||
body = {token.id, token.value}
|
||||
config = fetch_config!()
|
||||
key_base = Keyword.fetch!(config, :key_base)
|
||||
salt = Keyword.fetch!(config, :salt)
|
||||
Plug.Crypto.sign(key_base, salt, body)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def connect(%{"token" => encrypted_secret} = attrs, socket, connect_info) do
|
||||
%{user_agent: user_agent, peer_data: %{address: remote_ip}} = connect_info
|
||||
|
||||
config = fetch_config!()
|
||||
key_base = Keyword.fetch!(config, :key_base)
|
||||
salt = Keyword.fetch!(config, :salt)
|
||||
max_age = Keyword.fetch!(config, :max_age)
|
||||
|
||||
attrs =
|
||||
attrs
|
||||
|> Map.take(~w[external_id name_suffix public_key])
|
||||
|> Map.put("last_seen_user_agent", user_agent)
|
||||
|> Map.put("last_seen_remote_ip", remote_ip)
|
||||
|
||||
with {:ok, {id, secret}} <-
|
||||
Plug.Crypto.verify(key_base, salt, encrypted_secret, max_age: max_age),
|
||||
{:ok, token} <- Gateways.use_token_by_id_and_secret(id, secret),
|
||||
with {:ok, token} <- Gateways.authorize_gateway(encrypted_secret),
|
||||
{:ok, gateway} <- Gateways.upsert_gateway(token, attrs) do
|
||||
socket =
|
||||
socket
|
||||
@@ -44,11 +29,7 @@ defmodule API.Gateway.Socket do
|
||||
end
|
||||
|
||||
def connect(_params, _socket, _connect_info) do
|
||||
{:error, :invalid}
|
||||
end
|
||||
|
||||
defp fetch_config! do
|
||||
Domain.Config.fetch_env!(:api, __MODULE__)
|
||||
{:error, :missing_token}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
defmodule API.Gateway.Views.Client do
|
||||
alias Domain.Clients
|
||||
defmodule API.Gateway.Views.Device do
|
||||
alias Domain.Devices
|
||||
|
||||
def render(%Clients.Client{} = client, client_rtc_session_description, preshared_key) do
|
||||
def render(%Devices.Device{} = device, device_rtc_session_description, preshared_key) do
|
||||
%{
|
||||
id: client.id,
|
||||
rtc_session_description: client_rtc_session_description,
|
||||
id: device.id,
|
||||
rtc_session_description: device_rtc_session_description,
|
||||
peer: %{
|
||||
persistent_keepalive: 25,
|
||||
public_key: client.public_key,
|
||||
public_key: device.public_key,
|
||||
preshared_key: preshared_key,
|
||||
ipv4: client.ipv4,
|
||||
ipv6: client.ipv6
|
||||
ipv4: device.ipv4,
|
||||
ipv6: device.ipv6
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
defmodule API.Gateway.Views.Relay do
|
||||
def render_many(relays, expires_at) do
|
||||
Enum.flat_map(relays, &API.Client.Views.Relay.render(&1, expires_at))
|
||||
Enum.flat_map(relays, &API.Device.Views.Relay.render(&1, expires_at))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,32 +8,17 @@ defmodule API.Relay.Socket do
|
||||
|
||||
## Authentication
|
||||
|
||||
def encode_token!(%Relays.Token{value: value} = token) when not is_nil(value) do
|
||||
body = {token.id, token.value}
|
||||
config = fetch_config!()
|
||||
key_base = Keyword.fetch!(config, :key_base)
|
||||
salt = Keyword.fetch!(config, :salt)
|
||||
Plug.Crypto.sign(key_base, salt, body)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def connect(%{"token" => encrypted_secret} = attrs, socket, connect_info) do
|
||||
%{user_agent: user_agent, peer_data: %{address: remote_ip}} = connect_info
|
||||
|
||||
config = fetch_config!()
|
||||
key_base = Keyword.fetch!(config, :key_base)
|
||||
salt = Keyword.fetch!(config, :salt)
|
||||
max_age = Keyword.fetch!(config, :max_age)
|
||||
|
||||
attrs =
|
||||
attrs
|
||||
|> Map.take(~w[ipv4 ipv6])
|
||||
|> Map.put("last_seen_user_agent", user_agent)
|
||||
|> Map.put("last_seen_remote_ip", remote_ip)
|
||||
|
||||
with {:ok, {id, secret}} <-
|
||||
Plug.Crypto.verify(key_base, salt, encrypted_secret, max_age: max_age),
|
||||
{:ok, token} <- Relays.use_token_by_id_and_secret(id, secret),
|
||||
with {:ok, token} <- Relays.authorize_relay(encrypted_secret),
|
||||
{:ok, relay} <- Relays.upsert_relay(token, attrs) do
|
||||
socket =
|
||||
socket
|
||||
@@ -44,11 +29,7 @@ defmodule API.Relay.Socket do
|
||||
end
|
||||
|
||||
def connect(_params, _socket, _connect_info) do
|
||||
{:error, :invalid}
|
||||
end
|
||||
|
||||
defp fetch_config! do
|
||||
Domain.Config.fetch_env!(:api, __MODULE__)
|
||||
{:error, :missing_token}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
defmodule API.Client.ChannelTest do
|
||||
defmodule API.Device.ChannelTest do
|
||||
use API.ChannelCase
|
||||
alias Domain.{AccountsFixtures, ActorsFixtures, AuthFixtures, ResourcesFixtures}
|
||||
alias Domain.{ClientsFixtures, RelaysFixtures, GatewaysFixtures}
|
||||
alias Domain.{ConfigFixtures, DevicesFixtures, RelaysFixtures, GatewaysFixtures}
|
||||
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
ConfigFixtures.upsert_configuration(account: account, devices_upstream_dns: ["1.1.1.1"])
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(actor: actor, account: account)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
client = ClientsFixtures.create_client(subject: subject)
|
||||
device = DevicesFixtures.create_device(subject: subject)
|
||||
gateway = GatewaysFixtures.create_gateway(account: account)
|
||||
|
||||
resource =
|
||||
@@ -20,20 +21,20 @@ defmodule API.Client.ChannelTest do
|
||||
expires_at = DateTime.utc_now() |> DateTime.add(30, :second)
|
||||
|
||||
{:ok, _reply, socket} =
|
||||
API.Client.Socket
|
||||
|> socket("client:#{client.id}", %{
|
||||
client: client,
|
||||
API.Device.Socket
|
||||
|> socket("device:#{device.id}", %{
|
||||
device: device,
|
||||
subject: subject,
|
||||
expires_at: expires_at
|
||||
})
|
||||
|> subscribe_and_join(API.Client.Channel, "client")
|
||||
|> subscribe_and_join(API.Device.Channel, "device")
|
||||
|
||||
%{
|
||||
account: account,
|
||||
actor: actor,
|
||||
identity: identity,
|
||||
subject: subject,
|
||||
client: client,
|
||||
device: device,
|
||||
gateway: gateway,
|
||||
resource: resource,
|
||||
socket: socket
|
||||
@@ -41,30 +42,30 @@ defmodule API.Client.ChannelTest do
|
||||
end
|
||||
|
||||
describe "join/3" do
|
||||
test "tracks presence after join", %{client: client} do
|
||||
presence = Domain.Clients.Presence.list("clients")
|
||||
test "tracks presence after join", %{device: device} do
|
||||
presence = Domain.Devices.Presence.list("devices")
|
||||
|
||||
assert %{metas: [%{online_at: online_at, phx_ref: _ref}]} = Map.fetch!(presence, client.id)
|
||||
assert %{metas: [%{online_at: online_at, phx_ref: _ref}]} = Map.fetch!(presence, device.id)
|
||||
assert is_number(online_at)
|
||||
end
|
||||
|
||||
test "expires the channel when token is expired", %{client: client, subject: subject} do
|
||||
test "expires the channel when token is expired", %{device: device, subject: subject} do
|
||||
expires_at = DateTime.utc_now() |> DateTime.add(25, :millisecond)
|
||||
subject = %{subject | expires_at: expires_at}
|
||||
|
||||
{:ok, _reply, _socket} =
|
||||
API.Client.Socket
|
||||
|> socket("client:#{client.id}", %{
|
||||
client: client,
|
||||
API.Device.Socket
|
||||
|> socket("device:#{device.id}", %{
|
||||
device: device,
|
||||
subject: subject
|
||||
})
|
||||
|> subscribe_and_join(API.Client.Channel, "client")
|
||||
|> subscribe_and_join(API.Device.Channel, "device")
|
||||
|
||||
assert_push "token_expired", %{}, 250
|
||||
end
|
||||
|
||||
test "sends list of resources after join", %{
|
||||
client: client,
|
||||
device: device,
|
||||
resource: resource
|
||||
} do
|
||||
assert_push "init", %{resources: resources, interface: interface}
|
||||
@@ -79,11 +80,10 @@ defmodule API.Client.ChannelTest do
|
||||
]
|
||||
|
||||
assert interface == %{
|
||||
ipv4: client.ipv4,
|
||||
ipv6: client.ipv6,
|
||||
ipv4: device.ipv4,
|
||||
ipv6: device.ipv6,
|
||||
upstream_dns: [
|
||||
%Postgrex.INET{address: {1, 1, 1, 1}, netmask: nil},
|
||||
%Postgrex.INET{address: {1, 0, 0, 1}, netmask: nil}
|
||||
%Postgrex.INET{address: {1, 1, 1, 1}}
|
||||
]
|
||||
}
|
||||
end
|
||||
@@ -154,8 +154,8 @@ defmodule API.Client.ChannelTest do
|
||||
test "returns error when resource is not found", %{socket: socket} do
|
||||
attrs = %{
|
||||
"resource_id" => Ecto.UUID.generate(),
|
||||
"client_rtc_session_description" => "RTC_SD",
|
||||
"client_preshared_key" => "PSK"
|
||||
"device_rtc_session_description" => "RTC_SD",
|
||||
"device_preshared_key" => "PSK"
|
||||
}
|
||||
|
||||
ref = push(socket, "request_connection", attrs)
|
||||
@@ -168,8 +168,8 @@ defmodule API.Client.ChannelTest do
|
||||
} do
|
||||
attrs = %{
|
||||
"resource_id" => resource.id,
|
||||
"client_rtc_session_description" => "RTC_SD",
|
||||
"client_preshared_key" => "PSK"
|
||||
"device_rtc_session_description" => "RTC_SD",
|
||||
"device_preshared_key" => "PSK"
|
||||
}
|
||||
|
||||
ref = push(socket, "request_connection", attrs)
|
||||
@@ -183,8 +183,8 @@ defmodule API.Client.ChannelTest do
|
||||
} do
|
||||
attrs = %{
|
||||
"resource_id" => resource.id,
|
||||
"client_rtc_session_description" => "RTC_SD",
|
||||
"client_preshared_key" => "PSK"
|
||||
"device_rtc_session_description" => "RTC_SD",
|
||||
"device_preshared_key" => "PSK"
|
||||
}
|
||||
|
||||
gateway = GatewaysFixtures.create_gateway(account: account)
|
||||
@@ -197,20 +197,20 @@ defmodule API.Client.ChannelTest do
|
||||
test "broadcasts request_connection to the gateways and then returns connect message", %{
|
||||
resource: resource,
|
||||
gateway: gateway,
|
||||
client: client,
|
||||
device: device,
|
||||
socket: socket
|
||||
} do
|
||||
public_key = gateway.public_key
|
||||
resource_id = resource.id
|
||||
client_id = client.id
|
||||
device_id = device.id
|
||||
|
||||
:ok = Domain.Gateways.connect_gateway(gateway)
|
||||
Phoenix.PubSub.subscribe(Domain.PubSub, API.Gateway.Socket.id(gateway))
|
||||
|
||||
attrs = %{
|
||||
"resource_id" => resource.id,
|
||||
"client_rtc_session_description" => "RTC_SD",
|
||||
"client_preshared_key" => "PSK"
|
||||
"device_rtc_session_description" => "RTC_SD",
|
||||
"device_preshared_key" => "PSK"
|
||||
}
|
||||
|
||||
ref = push(socket, "request_connection", attrs)
|
||||
@@ -219,9 +219,9 @@ defmodule API.Client.ChannelTest do
|
||||
|
||||
assert %{
|
||||
resource_id: ^resource_id,
|
||||
client_id: ^client_id,
|
||||
client_preshared_key: "PSK",
|
||||
client_rtc_session_description: "RTC_SD",
|
||||
device_id: ^device_id,
|
||||
device_preshared_key: "PSK",
|
||||
device_rtc_session_description: "RTC_SD",
|
||||
authorization_expires_at: authorization_expires_at
|
||||
} = payload
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
defmodule API.Client.SocketTest do
|
||||
defmodule API.Device.SocketTest do
|
||||
use API.ChannelCase, async: true
|
||||
import API.Client.Socket, only: [id: 1]
|
||||
alias API.Client.Socket
|
||||
import API.Device.Socket, only: [id: 1]
|
||||
alias API.Device.Socket
|
||||
alias Domain.Auth
|
||||
alias Domain.{AuthFixtures, ClientsFixtures}
|
||||
alias Domain.{AuthFixtures, DevicesFixtures}
|
||||
|
||||
@connect_info %{
|
||||
user_agent: "iOS/12.7 (iPhone) connlib/0.1.1",
|
||||
@@ -20,41 +20,41 @@ defmodule API.Client.SocketTest do
|
||||
assert connect(Socket, attrs, @connect_info) == {:error, :invalid}
|
||||
end
|
||||
|
||||
test "creates a new client" do
|
||||
test "creates a new device" do
|
||||
subject = AuthFixtures.create_subject()
|
||||
{:ok, token} = Auth.create_session_token_from_subject(subject)
|
||||
|
||||
attrs = connect_attrs(token: token)
|
||||
|
||||
assert {:ok, socket} = connect(Socket, attrs, connect_info(subject))
|
||||
assert client = Map.fetch!(socket.assigns, :client)
|
||||
assert device = Map.fetch!(socket.assigns, :device)
|
||||
|
||||
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"
|
||||
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"
|
||||
end
|
||||
|
||||
test "updates existing client" do
|
||||
test "updates existing device" do
|
||||
subject = AuthFixtures.create_subject()
|
||||
existing_client = ClientsFixtures.create_client(subject: subject)
|
||||
existing_device = DevicesFixtures.create_device(subject: subject)
|
||||
{:ok, token} = Auth.create_session_token_from_subject(subject)
|
||||
|
||||
attrs = connect_attrs(token: token, external_id: existing_client.external_id)
|
||||
attrs = connect_attrs(token: token, external_id: existing_device.external_id)
|
||||
|
||||
assert {:ok, socket} = connect(Socket, attrs, connect_info(subject))
|
||||
assert client = Repo.one(Domain.Clients.Client)
|
||||
assert client.id == socket.assigns.client.id
|
||||
assert device = Repo.one(Domain.Devices.Device)
|
||||
assert device.id == socket.assigns.device.id
|
||||
end
|
||||
end
|
||||
|
||||
describe "id/1" do
|
||||
test "creates a channel for a client" do
|
||||
client = ClientsFixtures.create_client()
|
||||
socket = socket(API.Client.Socket, "", %{client: client})
|
||||
test "creates a channel for a device" do
|
||||
device = DevicesFixtures.create_device()
|
||||
socket = socket(API.Device.Socket, "", %{device: device})
|
||||
|
||||
assert id(socket) == "client:#{client.id}"
|
||||
assert id(socket) == "device:#{device.id}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -66,7 +66,7 @@ defmodule API.Client.SocketTest do
|
||||
end
|
||||
|
||||
defp connect_attrs(attrs) do
|
||||
ClientsFixtures.client_attrs()
|
||||
DevicesFixtures.device_attrs()
|
||||
|> Map.take(~w[external_id public_key]a)
|
||||
|> Map.merge(Enum.into(attrs, %{}))
|
||||
|> Enum.into(%{}, fn {k, v} -> {to_string(k), v} end)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
defmodule API.Gateway.ChannelTest do
|
||||
use API.ChannelCase
|
||||
alias Domain.{AccountsFixtures, ActorsFixtures, AuthFixtures, ResourcesFixtures}
|
||||
alias Domain.{ClientsFixtures, RelaysFixtures, GatewaysFixtures}
|
||||
alias Domain.{DevicesFixtures, RelaysFixtures, GatewaysFixtures}
|
||||
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(actor: actor, account: account)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
client = ClientsFixtures.create_client(subject: subject)
|
||||
device = DevicesFixtures.create_device(subject: subject)
|
||||
gateway = GatewaysFixtures.create_gateway(account: account)
|
||||
|
||||
resource =
|
||||
@@ -29,7 +29,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
actor: actor,
|
||||
identity: identity,
|
||||
subject: subject,
|
||||
client: client,
|
||||
device: device,
|
||||
gateway: gateway,
|
||||
resource: resource,
|
||||
relay: relay,
|
||||
@@ -63,7 +63,7 @@ defmodule API.Gateway.ChannelTest do
|
||||
|
||||
describe "handle_info/2 :request_connection" do
|
||||
test "pushes request_connection message", %{
|
||||
client: client,
|
||||
device: device,
|
||||
resource: resource,
|
||||
relay: relay,
|
||||
socket: socket
|
||||
@@ -81,18 +81,18 @@ defmodule API.Gateway.ChannelTest do
|
||||
socket.channel_pid,
|
||||
{:request_connection, {channel_pid, socket_ref},
|
||||
%{
|
||||
client_id: client.id,
|
||||
device_id: device.id,
|
||||
resource_id: resource.id,
|
||||
authorization_expires_at: expires_at,
|
||||
client_rtc_session_description: rtc_session_description,
|
||||
client_preshared_key: preshared_key
|
||||
device_rtc_session_description: rtc_session_description,
|
||||
device_preshared_key: preshared_key
|
||||
}}
|
||||
)
|
||||
|
||||
assert_push "request_connection", payload
|
||||
|
||||
assert is_binary(payload.ref)
|
||||
assert payload.actor == %{id: client.actor_id}
|
||||
assert payload.actor == %{id: device.actor_id}
|
||||
|
||||
ipv4_stun_uri = "stun:#{relay.ipv4}:#{relay.port}"
|
||||
ipv4_turn_uri = "turn:#{relay.ipv4}:#{relay.port}"
|
||||
@@ -138,14 +138,14 @@ defmodule API.Gateway.ChannelTest do
|
||||
ipv6: resource.ipv6
|
||||
}
|
||||
|
||||
assert payload.client == %{
|
||||
id: client.id,
|
||||
assert payload.device == %{
|
||||
id: device.id,
|
||||
peer: %{
|
||||
ipv4: client.ipv4,
|
||||
ipv6: client.ipv6,
|
||||
ipv4: device.ipv4,
|
||||
ipv6: device.ipv6,
|
||||
persistent_keepalive: 25,
|
||||
preshared_key: preshared_key,
|
||||
public_key: client.public_key
|
||||
public_key: device.public_key
|
||||
},
|
||||
rtc_session_description: rtc_session_description
|
||||
}
|
||||
@@ -155,8 +155,8 @@ defmodule API.Gateway.ChannelTest do
|
||||
end
|
||||
|
||||
describe "handle_in/3 connection_ready" do
|
||||
test "forwards RFC session description to the client channel", %{
|
||||
client: client,
|
||||
test "forwards RFC session description to the device channel", %{
|
||||
device: device,
|
||||
resource: resource,
|
||||
relay: relay,
|
||||
gateway: gateway,
|
||||
@@ -176,11 +176,11 @@ defmodule API.Gateway.ChannelTest do
|
||||
socket.channel_pid,
|
||||
{:request_connection, {channel_pid, socket_ref},
|
||||
%{
|
||||
client_id: client.id,
|
||||
device_id: device.id,
|
||||
resource_id: resource.id,
|
||||
authorization_expires_at: expires_at,
|
||||
client_rtc_session_description: rtc_session_description,
|
||||
client_preshared_key: preshared_key
|
||||
device_rtc_session_description: rtc_session_description,
|
||||
device_preshared_key: preshared_key
|
||||
}}
|
||||
)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ defmodule API.Gateway.SocketTest do
|
||||
use API.ChannelCase, async: true
|
||||
import API.Gateway.Socket, except: [connect: 3]
|
||||
alias API.Gateway.Socket
|
||||
alias Domain.Gateways
|
||||
alias Domain.GatewaysFixtures
|
||||
|
||||
@connlib_version "0.1.1"
|
||||
@@ -11,28 +12,14 @@ defmodule API.Gateway.SocketTest do
|
||||
peer_data: %{address: {189, 172, 73, 153}}
|
||||
}
|
||||
|
||||
describe "encode_token!/1" do
|
||||
test "returns encoded token" do
|
||||
token = GatewaysFixtures.create_token()
|
||||
assert encrypted_secret = encode_token!(token)
|
||||
|
||||
config = Application.fetch_env!(:api, Socket)
|
||||
key_base = Keyword.fetch!(config, :key_base)
|
||||
salt = Keyword.fetch!(config, :salt)
|
||||
|
||||
assert Plug.Crypto.verify(key_base, salt, encrypted_secret) ==
|
||||
{:ok, {token.id, token.value}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "connect/3" do
|
||||
test "returns error when token is missing" do
|
||||
assert connect(Socket, %{}, @connect_info) == {:error, :invalid}
|
||||
assert connect(Socket, %{}, @connect_info) == {:error, :missing_token}
|
||||
end
|
||||
|
||||
test "creates a new gateway" do
|
||||
token = GatewaysFixtures.create_token()
|
||||
encrypted_secret = encode_token!(token)
|
||||
encrypted_secret = Gateways.encode_token!(token)
|
||||
|
||||
attrs = connect_attrs(token: encrypted_secret)
|
||||
|
||||
@@ -49,7 +36,7 @@ defmodule API.Gateway.SocketTest do
|
||||
test "updates existing gateway" do
|
||||
token = GatewaysFixtures.create_token()
|
||||
existing_gateway = GatewaysFixtures.create_gateway(token: token)
|
||||
encrypted_secret = encode_token!(token)
|
||||
encrypted_secret = Gateways.encode_token!(token)
|
||||
|
||||
attrs = connect_attrs(token: encrypted_secret, external_id: existing_gateway.external_id)
|
||||
|
||||
@@ -60,7 +47,7 @@ defmodule API.Gateway.SocketTest do
|
||||
|
||||
test "returns error when token is invalid" do
|
||||
attrs = connect_attrs(token: "foo")
|
||||
assert connect(Socket, attrs, @connect_info) == {:error, :invalid}
|
||||
assert connect(Socket, attrs, @connect_info) == {:error, :invalid_token}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ defmodule API.Relay.SocketTest do
|
||||
use API.ChannelCase, async: true
|
||||
import API.Relay.Socket, except: [connect: 3]
|
||||
alias API.Relay.Socket
|
||||
alias Domain.Relays
|
||||
alias Domain.RelaysFixtures
|
||||
|
||||
@connlib_version "0.1.1"
|
||||
@@ -11,28 +12,14 @@ defmodule API.Relay.SocketTest do
|
||||
peer_data: %{address: {189, 172, 73, 153}}
|
||||
}
|
||||
|
||||
describe "encode_token!/1" do
|
||||
test "returns encoded token" do
|
||||
token = RelaysFixtures.create_token()
|
||||
assert encrypted_secret = encode_token!(token)
|
||||
|
||||
config = Application.fetch_env!(:api, Socket)
|
||||
key_base = Keyword.fetch!(config, :key_base)
|
||||
salt = Keyword.fetch!(config, :salt)
|
||||
|
||||
assert Plug.Crypto.verify(key_base, salt, encrypted_secret) ==
|
||||
{:ok, {token.id, token.value}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "connect/3" do
|
||||
test "returns error when token is missing" do
|
||||
assert connect(Socket, %{}, @connect_info) == {:error, :invalid}
|
||||
assert connect(Socket, %{}, @connect_info) == {:error, :missing_token}
|
||||
end
|
||||
|
||||
test "creates a new relay" do
|
||||
token = RelaysFixtures.create_token()
|
||||
encrypted_secret = encode_token!(token)
|
||||
encrypted_secret = Relays.encode_token!(token)
|
||||
|
||||
attrs = connect_attrs(token: encrypted_secret)
|
||||
|
||||
@@ -49,7 +36,7 @@ defmodule API.Relay.SocketTest do
|
||||
test "updates existing relay" do
|
||||
token = RelaysFixtures.create_token()
|
||||
existing_relay = RelaysFixtures.create_relay(token: token)
|
||||
encrypted_secret = encode_token!(token)
|
||||
encrypted_secret = Relays.encode_token!(token)
|
||||
|
||||
attrs = connect_attrs(token: encrypted_secret, ipv4: existing_relay.ipv4)
|
||||
|
||||
@@ -60,7 +47,7 @@ defmodule API.Relay.SocketTest do
|
||||
|
||||
test "returns error when token is invalid" do
|
||||
attrs = connect_attrs(token: "foo")
|
||||
assert connect(Socket, attrs, @connect_info) == {:error, :invalid}
|
||||
assert connect(Socket, attrs, @connect_info) == {:error, :invalid_token}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ defmodule API.ChannelCase do
|
||||
use Domain.CaseTemplate
|
||||
|
||||
@presences [
|
||||
Domain.Clients.Presence,
|
||||
Domain.Devices.Presence,
|
||||
Domain.Gateways.Presence,
|
||||
Domain.Relays.Presence
|
||||
]
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
defmodule Domain.Actors do
|
||||
alias Domain.{Repo, Auth, Validator, Telemetry}
|
||||
alias Domain.{Repo, Auth, Validator}
|
||||
alias Domain.Actors.{Authorizer, Actor}
|
||||
require Ecto.Query
|
||||
|
||||
def fetch_count_by_role(role, %Auth.Subject{} = subject) do
|
||||
def fetch_count_by_type(type, %Auth.Subject{} = subject) do
|
||||
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()) do
|
||||
Actor.Query.by_role(role)
|
||||
Actor.Query.by_type(type)
|
||||
|> Authorizer.for_subject(subject)
|
||||
|> Repo.aggregate(:count)
|
||||
end
|
||||
@@ -61,7 +61,7 @@ defmodule Domain.Actors do
|
||||
:ok <- Auth.ensure_has_access_to(subject, provider),
|
||||
changeset = Actor.Changeset.create_changeset(provider, attrs),
|
||||
{:ok, data} <- Ecto.Changeset.apply_action(changeset, :validate) do
|
||||
granted_permissions = Auth.fetch_role_permissions!(data.role)
|
||||
granted_permissions = Auth.fetch_type_permissions!(data.type)
|
||||
|
||||
if MapSet.subset?(granted_permissions, subject.permissions) do
|
||||
create_actor(provider, provider_identifier, attrs)
|
||||
@@ -84,7 +84,6 @@ defmodule Domain.Actors do
|
||||
|> Repo.transaction()
|
||||
|> case do
|
||||
{:ok, %{actor: actor, identity: identity}} ->
|
||||
Telemetry.add_actor()
|
||||
{:ok, %{actor | identities: [identity]}}
|
||||
|
||||
{:error, _step, changeset, _effects_so_far} ->
|
||||
@@ -92,26 +91,26 @@ defmodule Domain.Actors do
|
||||
end
|
||||
end
|
||||
|
||||
def change_actor_role(%Actor{} = actor, role, %Auth.Subject{} = subject) do
|
||||
def change_actor_type(%Actor{} = actor, type, %Auth.Subject{} = subject) do
|
||||
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_actors_permission()) do
|
||||
Actor.Query.by_id(actor.id)
|
||||
|> Authorizer.for_subject(subject)
|
||||
|> Repo.fetch_and_update(
|
||||
with: fn actor ->
|
||||
changeset = Actor.Changeset.set_actor_role(actor, role)
|
||||
changeset = Actor.Changeset.set_actor_type(actor, type)
|
||||
|
||||
cond do
|
||||
changeset.data.role != :admin ->
|
||||
changeset.data.type != :admin ->
|
||||
changeset
|
||||
|
||||
changeset.changes.role == :admin ->
|
||||
changeset.changes.type == :admin ->
|
||||
changeset
|
||||
|
||||
other_enabled_admins_exist?(actor) ->
|
||||
changeset
|
||||
|
||||
true ->
|
||||
:cant_remove_admin_role
|
||||
:cant_remove_admin_type
|
||||
end
|
||||
end
|
||||
)
|
||||
@@ -158,8 +157,12 @@ defmodule Domain.Actors do
|
||||
end
|
||||
end
|
||||
|
||||
defp other_enabled_admins_exist?(%Actor{role: :admin, account_id: account_id, id: id}) do
|
||||
Actor.Query.by_role(:admin)
|
||||
defp other_enabled_admins_exist?(%Actor{
|
||||
type: :account_admin_user,
|
||||
account_id: account_id,
|
||||
id: id
|
||||
}) do
|
||||
Actor.Query.by_type(:account_admin_user)
|
||||
|> Actor.Query.not_disabled()
|
||||
|> Actor.Query.by_account_id(account_id)
|
||||
|> Actor.Query.by_id({:not, id})
|
||||
|
||||
@@ -2,8 +2,7 @@ defmodule Domain.Actors.Actor do
|
||||
use Domain, :schema
|
||||
|
||||
schema "actors" do
|
||||
field :type, Ecto.Enum, values: [:user, :service_account]
|
||||
field :role, Ecto.Enum, values: [:unprivileged, :admin]
|
||||
field :type, Ecto.Enum, values: [:account_user, :account_admin_user, :service_account]
|
||||
|
||||
# TODO:
|
||||
# field :first_name, :string
|
||||
@@ -14,9 +13,6 @@ defmodule Domain.Actors.Actor do
|
||||
# belongs_to :group, Domain.Actors.Group
|
||||
belongs_to :account, Domain.Accounts.Account
|
||||
|
||||
# has_many :oidc_connections, Domain.Auth.OIDC.Connection
|
||||
has_many :api_tokens, Domain.ApiTokens.ApiToken
|
||||
|
||||
field :disabled_at, :utc_datetime_usec
|
||||
field :deleted_at, :utc_datetime_usec
|
||||
timestamps()
|
||||
|
||||
@@ -5,15 +5,15 @@ defmodule Domain.Actors.Actor.Changeset do
|
||||
|
||||
def create_changeset(%Auth.Provider{} = provider, attrs) do
|
||||
%Actors.Actor{}
|
||||
|> cast(attrs, ~w[type role]a)
|
||||
|> validate_required(~w[type role]a)
|
||||
|> cast(attrs, ~w[type]a)
|
||||
|> validate_required(~w[type]a)
|
||||
|> put_change(:account_id, provider.account_id)
|
||||
end
|
||||
|
||||
def set_actor_role(actor, role) do
|
||||
def set_actor_type(actor, type) when type in [:account_user, :account_admin_user] do
|
||||
actor
|
||||
|> change()
|
||||
|> put_change(:role, role)
|
||||
|> put_change(:type, type)
|
||||
end
|
||||
|
||||
def disable_actor(actor) do
|
||||
|
||||
@@ -20,8 +20,8 @@ defmodule Domain.Actors.Actor.Query do
|
||||
where(queryable, [actors: actors], actors.account_id == ^account_id)
|
||||
end
|
||||
|
||||
def by_role(queryable \\ all(), role) do
|
||||
where(queryable, [actors: actors], actors.role == ^role)
|
||||
def by_type(queryable \\ all(), type) do
|
||||
where(queryable, [actors: actors], actors.type == ^type)
|
||||
end
|
||||
|
||||
def not_disabled(queryable \\ all()) do
|
||||
|
||||
@@ -6,14 +6,14 @@ defmodule Domain.Actors.Authorizer do
|
||||
def edit_own_profile_permission, do: build(Actor, :edit_own_profile)
|
||||
|
||||
@impl Domain.Auth.Authorizer
|
||||
def list_permissions_for_role(:admin) do
|
||||
def list_permissions_for_role(:account_admin_user) do
|
||||
[
|
||||
manage_actors_permission(),
|
||||
edit_own_profile_permission()
|
||||
]
|
||||
end
|
||||
|
||||
def list_permissions_for_role(:unprivileged) do
|
||||
def list_permissions_for_role(:account_user) do
|
||||
[
|
||||
edit_own_profile_permission()
|
||||
]
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
defmodule Domain.ApiTokens do
|
||||
alias Domain.{Repo, Validator, Auth}
|
||||
alias Domain.Actors
|
||||
alias Domain.ApiTokens.Authorizer
|
||||
alias Domain.ApiTokens.ApiToken
|
||||
|
||||
def count_by_actor_id(actor_id) do
|
||||
ApiToken.Query.by_actor_id(actor_id)
|
||||
|> Repo.aggregate(:count)
|
||||
end
|
||||
|
||||
def list_api_tokens(%Auth.Subject{} = subject) do
|
||||
required_permissions =
|
||||
{:one_of,
|
||||
[
|
||||
Authorizer.manage_api_tokens_permission(),
|
||||
Authorizer.manage_own_api_tokens_permission()
|
||||
]}
|
||||
|
||||
with :ok <- Auth.ensure_has_permissions(subject, required_permissions) do
|
||||
ApiToken.Query.all()
|
||||
|> Authorizer.for_subject(subject)
|
||||
|> Repo.list()
|
||||
end
|
||||
end
|
||||
|
||||
def list_api_tokens_by_actor_id(actor_id, %Auth.Subject{} = subject) do
|
||||
required_permissions =
|
||||
{:one_of,
|
||||
[
|
||||
Authorizer.manage_api_tokens_permission(),
|
||||
Authorizer.manage_own_api_tokens_permission()
|
||||
]}
|
||||
|
||||
with true <- Validator.valid_uuid?(actor_id),
|
||||
:ok <- Auth.ensure_has_permissions(subject, required_permissions) do
|
||||
ApiToken.Query.by_actor_id(actor_id)
|
||||
|> Authorizer.for_subject(subject)
|
||||
|> Repo.list()
|
||||
else
|
||||
false -> {:ok, []}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_api_token_by_id(id, %Auth.Subject{} = subject) do
|
||||
required_permissions =
|
||||
{:one_of,
|
||||
[
|
||||
Authorizer.manage_api_tokens_permission(),
|
||||
Authorizer.manage_own_api_tokens_permission()
|
||||
]}
|
||||
|
||||
with true <- Validator.valid_uuid?(id),
|
||||
:ok <- Auth.ensure_has_permissions(subject, required_permissions) do
|
||||
ApiToken.Query.by_id(id)
|
||||
|> Authorizer.for_subject(subject)
|
||||
|> Repo.fetch()
|
||||
else
|
||||
false -> {:error, :not_found}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_unexpired_api_token_by_id(id, %Auth.Subject{} = subject) do
|
||||
required_permissions =
|
||||
{:one_of,
|
||||
[
|
||||
Authorizer.manage_api_tokens_permission(),
|
||||
Authorizer.manage_own_api_tokens_permission()
|
||||
]}
|
||||
|
||||
with true <- Validator.valid_uuid?(id),
|
||||
:ok <- Auth.ensure_has_permissions(subject, required_permissions) do
|
||||
ApiToken.Query.by_id(id)
|
||||
|> Authorizer.for_subject(subject)
|
||||
|> ApiToken.Query.not_expired()
|
||||
|> Repo.fetch()
|
||||
else
|
||||
false -> {:error, :not_found}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_unexpired_api_token_by_id(id) do
|
||||
if Validator.valid_uuid?(id) do
|
||||
ApiToken.Query.by_id(id)
|
||||
|> ApiToken.Query.not_expired()
|
||||
|> Repo.fetch()
|
||||
else
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def new_api_token(attrs \\ %{}) do
|
||||
ApiToken.Changeset.changeset(attrs)
|
||||
end
|
||||
|
||||
def create_api_token(attrs, %Auth.Subject{} = subject) do
|
||||
with :ok <-
|
||||
Auth.ensure_has_permissions(
|
||||
subject,
|
||||
Authorizer.manage_own_api_tokens_permission()
|
||||
) do
|
||||
create_api_token(subject.actor, attrs)
|
||||
end
|
||||
end
|
||||
|
||||
def create_api_token(%Actors.Actor{} = actor, attrs) do
|
||||
count_by_actor_id = count_by_actor_id(actor.id)
|
||||
changeset = ApiToken.Changeset.create_changeset(actor, attrs, max: count_by_actor_id)
|
||||
|
||||
with {:ok, api_token} <- Repo.insert(changeset) do
|
||||
Domain.Telemetry.create_api_token()
|
||||
{:ok, api_token}
|
||||
end
|
||||
end
|
||||
|
||||
def api_token_expired?(%ApiToken{} = api_token) do
|
||||
DateTime.diff(api_token.expires_at, DateTime.utc_now()) < 0
|
||||
end
|
||||
|
||||
def delete_api_token_by_id(api_token_id, %Auth.Subject{} = subject) do
|
||||
with {:ok, api_token} <- fetch_api_token_by_id(api_token_id, subject),
|
||||
:ok <- Authorizer.ensure_can_manage(subject, api_token) do
|
||||
{:ok, Repo.delete!(api_token)}
|
||||
else
|
||||
{:error, :not_found} -> {:error, :not_found}
|
||||
{:error, {:unauthorized, context}} -> {:error, {:unauthorized, context}}
|
||||
{:error, :unauthorized} -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,14 +0,0 @@
|
||||
defmodule Domain.ApiTokens.ApiToken do
|
||||
use Domain, :schema
|
||||
|
||||
schema "api_tokens" do
|
||||
field :expires_at, :utc_datetime_usec
|
||||
|
||||
# Developer-friendly way to set expires_at
|
||||
field :expires_in, :integer, virtual: true, default: 30
|
||||
|
||||
belongs_to :actor, Domain.Actors.Actor
|
||||
|
||||
timestamps(updated_at: false)
|
||||
end
|
||||
end
|
||||
@@ -1,44 +0,0 @@
|
||||
defmodule Domain.ApiTokens.ApiToken.Changeset do
|
||||
use Domain, :changeset
|
||||
alias Domain.ApiTokens.ApiToken
|
||||
|
||||
@max_per_user 25
|
||||
|
||||
def create_changeset(user, attrs, opts \\ []) do
|
||||
changeset(attrs)
|
||||
|> put_change(:user_id, user.id)
|
||||
|> assoc_constraint(:user)
|
||||
|> maybe_validate_count_per_user(@max_per_user, opts[:max])
|
||||
end
|
||||
|
||||
def changeset(api_token \\ %ApiToken{}, attrs) do
|
||||
api_token
|
||||
|> cast(attrs, ~w[
|
||||
expires_in
|
||||
expires_at
|
||||
]a)
|
||||
|> validate_required([:expires_in])
|
||||
|> validate_number(:expires_in, greater_than_or_equal_to: 1, less_than_or_equal_to: 90)
|
||||
|> resolve_expires_at()
|
||||
|> validate_required(:expires_at)
|
||||
end
|
||||
|
||||
def max_per_user, do: @max_per_user
|
||||
|
||||
defp resolve_expires_at(changeset) do
|
||||
expires_at =
|
||||
DateTime.utc_now()
|
||||
|> DateTime.add(get_field(changeset, :expires_in), :day)
|
||||
|
||||
put_change(changeset, :expires_at, expires_at)
|
||||
end
|
||||
|
||||
defp maybe_validate_count_per_user(changeset, max, num) when is_integer(num) and num >= max do
|
||||
# XXX: This suffers from a race condition because the count happens in a separate transaction.
|
||||
# At the moment it's not a big concern. Fixing it would require locking against INSERTs or DELETEs
|
||||
# while counts are happening.
|
||||
add_error(changeset, :base, "token limit of #{@max_per_user} reached")
|
||||
end
|
||||
|
||||
defp maybe_validate_count_per_user(changeset, _, _), do: changeset
|
||||
end
|
||||
@@ -1,19 +0,0 @@
|
||||
defmodule Domain.ApiTokens.ApiToken.Query do
|
||||
use Domain, :query
|
||||
|
||||
def all do
|
||||
from(api_tokens in Domain.ApiTokens.ApiToken, as: :api_tokens)
|
||||
end
|
||||
|
||||
def by_id(queryable \\ all(), id) do
|
||||
where(queryable, [api_tokens: api_tokens], api_tokens.id == ^id)
|
||||
end
|
||||
|
||||
def by_actor_id(queryable \\ all(), actor_id) do
|
||||
where(queryable, [api_tokens: api_tokens], api_tokens.actor_id == ^actor_id)
|
||||
end
|
||||
|
||||
def not_expired(queryable \\ all()) do
|
||||
where(queryable, [api_tokens: api_tokens], api_tokens.expires_at >= fragment("NOW()"))
|
||||
end
|
||||
end
|
||||
@@ -1,50 +0,0 @@
|
||||
defmodule Domain.ApiTokens.Authorizer do
|
||||
use Domain.Auth.Authorizer
|
||||
alias Domain.ApiTokens.ApiToken
|
||||
|
||||
def manage_own_api_tokens_permission, do: build(ApiToken, :manage_own)
|
||||
def manage_api_tokens_permission, do: build(ApiToken, :manage)
|
||||
|
||||
@impl Domain.Auth.Authorizer
|
||||
def list_permissions_for_role(:admin) do
|
||||
[
|
||||
manage_own_api_tokens_permission(),
|
||||
manage_api_tokens_permission()
|
||||
]
|
||||
end
|
||||
|
||||
def list_permissions_for_role(_role) do
|
||||
[]
|
||||
end
|
||||
|
||||
@impl Domain.Auth.Authorizer
|
||||
def for_subject(queryable, %Subject{} = subject) do
|
||||
cond do
|
||||
has_permission?(subject, manage_api_tokens_permission()) ->
|
||||
queryable
|
||||
|
||||
has_permission?(subject, manage_own_api_tokens_permission()) ->
|
||||
%{actor_id: actor_id} = subject.identity
|
||||
ApiToken.Query.by_actor_id(queryable, actor_id)
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_can_manage(%Subject{} = subject, %ApiToken{} = api_token) do
|
||||
cond do
|
||||
has_permission?(subject, manage_api_tokens_permission()) ->
|
||||
:ok
|
||||
|
||||
has_permission?(subject, manage_own_api_tokens_permission()) ->
|
||||
%{type: :user, id: actor_id} = subject.identity
|
||||
|
||||
if api_token.actor_id == actor_id do
|
||||
:ok
|
||||
else
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
|
||||
true ->
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,14 +2,9 @@ defmodule Domain.Application do
|
||||
use Application
|
||||
|
||||
def start(_type, _args) do
|
||||
result =
|
||||
Supervisor.start_link(children(), strategy: :one_for_one, name: __MODULE__.Supervisor)
|
||||
|
||||
:ok = after_start()
|
||||
result
|
||||
Supervisor.start_link(children(), strategy: :one_for_one, name: __MODULE__.Supervisor)
|
||||
end
|
||||
|
||||
# TODO: when app starts for migrations set env to disable connectivity checks and telemetry
|
||||
def children do
|
||||
[
|
||||
# Infrastructure services
|
||||
@@ -20,20 +15,10 @@ defmodule Domain.Application do
|
||||
Domain.Auth,
|
||||
Domain.Relays,
|
||||
Domain.Gateways,
|
||||
Domain.Clients,
|
||||
Domain.Devices
|
||||
|
||||
# Observability
|
||||
Domain.Telemetry
|
||||
# Domain.Telemetry
|
||||
]
|
||||
end
|
||||
|
||||
if Mix.env() == :prod do
|
||||
defp after_start do
|
||||
Domain.Config.validate_runtime_config!()
|
||||
end
|
||||
else
|
||||
defp after_start do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,8 +7,8 @@ defmodule Domain.Auth do
|
||||
alias Domain.Auth.{Adapters, Provider}
|
||||
|
||||
@default_session_duration_hours %{
|
||||
admin: 3,
|
||||
unprivileged: 24 * 7
|
||||
account_admin_user: 3,
|
||||
account_user: 24 * 7
|
||||
}
|
||||
|
||||
def start_link(opts) do
|
||||
@@ -25,6 +25,11 @@ defmodule Domain.Auth do
|
||||
|
||||
# Providers
|
||||
|
||||
def fetch_provider_by_id(id) do
|
||||
Provider.Query.by_id(id)
|
||||
|> Repo.fetch()
|
||||
end
|
||||
|
||||
def create_provider(%Accounts.Account{} = account, attrs, %Subject{} = subject) do
|
||||
with :ok <- ensure_has_permissions(subject, Authorizer.manage_providers_permission()),
|
||||
:ok <- Accounts.ensure_has_access_to(subject, account) do
|
||||
@@ -182,9 +187,8 @@ defmodule Domain.Auth do
|
||||
end
|
||||
|
||||
def sign_in(session_token, user_agent, remote_ip) do
|
||||
with {:ok, identity_id} <- verify_session_token(session_token, user_agent, remote_ip),
|
||||
{:ok, expires_at} <- fetch_session_token_expires_at(session_token),
|
||||
{:ok, identity} <- fetch_identity_by_id(identity_id) do
|
||||
with {:ok, identity, expires_at} <-
|
||||
verify_session_token(session_token, user_agent, remote_ip) do
|
||||
{:ok, build_subject(identity, expires_at, user_agent, remote_ip)}
|
||||
else
|
||||
{:error, :not_found} -> {:error, :unauthorized}
|
||||
@@ -209,7 +213,7 @@ defmodule Domain.Auth do
|
||||
|> Repo.update!()
|
||||
|
||||
identity_with_preloads = Repo.preload(identity, [:account, :actor])
|
||||
permissions = fetch_role_permissions!(identity_with_preloads.actor.role)
|
||||
permissions = fetch_type_permissions!(identity_with_preloads.actor.type)
|
||||
|
||||
%Subject{
|
||||
identity: identity,
|
||||
@@ -222,7 +226,7 @@ defmodule Domain.Auth do
|
||||
end
|
||||
|
||||
defp build_subject_expires_at(%Actors.Actor{} = actor, expires_at) do
|
||||
default_session_duration_hours = Map.fetch!(@default_session_duration_hours, actor.role)
|
||||
default_session_duration_hours = Map.fetch!(@default_session_duration_hours, actor.type)
|
||||
expires_at || DateTime.utc_now() |> DateTime.add(default_session_duration_hours, :hour)
|
||||
end
|
||||
|
||||
@@ -238,6 +242,16 @@ defmodule Domain.Auth do
|
||||
{:ok, Plug.Crypto.sign(key_base, salt, payload, max_age: max_age)}
|
||||
end
|
||||
|
||||
def create_access_token_for_identity(%Identity{} = identity) do
|
||||
config = fetch_config!()
|
||||
key_base = Keyword.fetch!(config, :key_base)
|
||||
salt = Keyword.fetch!(config, :salt)
|
||||
payload = {:identity, identity.id, identity.provider_virtual_state.secret, :ignore}
|
||||
{:ok, expires_at, 0} = DateTime.from_iso8601(identity.provider_state["expires_at"])
|
||||
max_age = DateTime.diff(expires_at, DateTime.utc_now(), :second)
|
||||
{:ok, Plug.Crypto.sign(key_base, salt, payload, max_age: max_age)}
|
||||
end
|
||||
|
||||
def fetch_session_token_expires_at(token, opts \\ []) do
|
||||
config = fetch_config!()
|
||||
key_base = Keyword.fetch!(config, :key_base)
|
||||
@@ -258,9 +272,6 @@ defmodule Domain.Auth do
|
||||
end
|
||||
end
|
||||
|
||||
defp session_token_payload(%Subject{identity: %Identity{} = identity, context: context}),
|
||||
do: {:identity, identity.id, session_context_payload(context.remote_ip, context.user_agent)}
|
||||
|
||||
defp session_context_payload(remote_ip, user_agent)
|
||||
when is_tuple(remote_ip) and is_binary(user_agent) do
|
||||
:crypto.hash(:sha256, :erlang.term_to_binary({remote_ip, user_agent}))
|
||||
@@ -271,23 +282,51 @@ defmodule Domain.Auth do
|
||||
key_base = Keyword.fetch!(config, :key_base)
|
||||
salt = Keyword.fetch!(config, :salt)
|
||||
|
||||
context_payload = session_context_payload(remote_ip, user_agent)
|
||||
|
||||
case Plug.Crypto.verify(key_base, salt, token) do
|
||||
{:ok, {:identity, identity_id, ^context_payload}} ->
|
||||
{:ok, identity_id}
|
||||
|
||||
{:ok, {_type, _id, _context_payload}} ->
|
||||
{:error, :unauthorized_browser}
|
||||
|
||||
{:error, :invalid} ->
|
||||
{:error, :invalid_token}
|
||||
|
||||
{:error, :expired} ->
|
||||
{:error, :expired_token}
|
||||
{:ok, payload} -> verify_session_token_payload(token, payload, user_agent, remote_ip)
|
||||
{:error, :invalid} -> {:error, :invalid_token}
|
||||
{:error, :expired} -> {:error, :expired_token}
|
||||
end
|
||||
end
|
||||
|
||||
defp verify_session_token_payload(
|
||||
_token,
|
||||
{:identity, identity_id, secret, :ignore},
|
||||
_user_agent,
|
||||
_remote_ip
|
||||
) do
|
||||
with {:ok, identity} <- fetch_identity_by_id(identity_id),
|
||||
{:ok, provider} <- fetch_provider_by_id(identity.provider_id),
|
||||
{:ok, identity, expires_at} <-
|
||||
Adapters.verify_secret(provider, identity, secret) do
|
||||
{:ok, identity, expires_at}
|
||||
else
|
||||
{:error, :invalid_secret} -> {:error, :invalid_token}
|
||||
{:error, :expired_secret} -> {:error, :expired_token}
|
||||
{:error, :not_found} -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
defp verify_session_token_payload(
|
||||
token,
|
||||
{:identity, identity_id, context_payload},
|
||||
user_agent,
|
||||
remote_ip
|
||||
) do
|
||||
with {:ok, identity} <- fetch_identity_by_id(identity_id),
|
||||
true <- context_payload == session_context_payload(remote_ip, user_agent),
|
||||
{:ok, expires_at} <- fetch_session_token_expires_at(token) do
|
||||
{:ok, identity, expires_at}
|
||||
else
|
||||
false -> {:error, :unauthorized_browser}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
defp session_token_payload(%Subject{identity: %Identity{} = identity, context: context}) do
|
||||
{:identity, identity.id, session_context_payload(context.remote_ip, context.user_agent)}
|
||||
end
|
||||
|
||||
defp fetch_config! do
|
||||
Config.fetch_env!(:domain, __MODULE__)
|
||||
end
|
||||
@@ -311,11 +350,11 @@ defmodule Domain.Auth do
|
||||
ensure_has_permissions(subject, required_permissions) == :ok
|
||||
end
|
||||
|
||||
def fetch_role_permissions!(%Role{} = role),
|
||||
do: role.permissions
|
||||
def fetch_type_permissions!(%Role{} = type),
|
||||
do: type.permissions
|
||||
|
||||
def fetch_role_permissions!(role_name) when is_atom(role_name),
|
||||
do: role_name |> Roles.build() |> fetch_role_permissions!()
|
||||
def fetch_type_permissions!(type_name) when is_atom(type_name),
|
||||
do: type_name |> Roles.build() |> fetch_type_permissions!()
|
||||
|
||||
# Authorization
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ defmodule Domain.Auth.Adapters do
|
||||
@adapters %{
|
||||
email: Domain.Auth.Adapters.Email,
|
||||
openid_connect: Domain.Auth.Adapters.OpenIDConnect,
|
||||
userpass: Domain.Auth.Adapters.UserPass
|
||||
userpass: Domain.Auth.Adapters.UserPass,
|
||||
token: Domain.Auth.Adapters.Token
|
||||
}
|
||||
|
||||
@adapter_names Map.keys(@adapters)
|
||||
|
||||
@@ -52,8 +52,8 @@ defmodule Domain.Auth.Adapters.Email do
|
||||
|
||||
{
|
||||
%{
|
||||
sign_in_token_hash: Domain.Crypto.hash(sign_in_token),
|
||||
sign_in_token_created_at: DateTime.utc_now()
|
||||
"sign_in_token_hash" => Domain.Crypto.hash(sign_in_token),
|
||||
"sign_in_token_created_at" => DateTime.utc_now()
|
||||
},
|
||||
%{
|
||||
sign_in_token: sign_in_token
|
||||
@@ -109,6 +109,11 @@ defmodule Domain.Auth.Adapters.Email do
|
||||
end
|
||||
end
|
||||
|
||||
defp sign_in_token_expired?(%DateTime{} = sign_in_token_created_at) do
|
||||
now = DateTime.utc_now()
|
||||
DateTime.diff(now, sign_in_token_created_at, :second) > @sign_in_token_expiration_seconds
|
||||
end
|
||||
|
||||
defp sign_in_token_expired?(sign_in_token_created_at) do
|
||||
now = DateTime.utc_now()
|
||||
|
||||
|
||||
123
apps/domain/lib/domain/auth/adapters/token.ex
Normal file
123
apps/domain/lib/domain/auth/adapters/token.ex
Normal file
@@ -0,0 +1,123 @@
|
||||
defmodule Domain.Auth.Adapters.Token do
|
||||
@moduledoc """
|
||||
This is not recommended to use in production,
|
||||
it's only for development, testing, and small home labs.
|
||||
"""
|
||||
use Supervisor
|
||||
alias Domain.Repo
|
||||
alias Domain.Auth.{Identity, Provider, Adapter}
|
||||
alias Domain.Auth.Adapters.Token.State
|
||||
|
||||
@behaviour Adapter
|
||||
|
||||
def start_link(_init_arg) do
|
||||
Supervisor.start_link(__MODULE__, nil, name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_init_arg) do
|
||||
children = []
|
||||
|
||||
Supervisor.init(children, strategy: :one_for_one)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def identity_changeset(%Provider{} = _provider, %Ecto.Changeset{} = changeset) do
|
||||
changeset
|
||||
|> Domain.Validator.trim_change(:provider_identifier)
|
||||
|> put_hash_and_expiration()
|
||||
end
|
||||
|
||||
defp put_hash_and_expiration(changeset) do
|
||||
secret = Domain.Crypto.rand_token(32)
|
||||
secret_hash = Domain.Crypto.hash(secret)
|
||||
|
||||
data = Map.get(changeset.data, :provider_virtual_state) || %{}
|
||||
attrs = Ecto.Changeset.get_change(changeset, :provider_virtual_state) || %{}
|
||||
|
||||
Ecto.embedded_load(State, data, :json)
|
||||
|> State.Changeset.changeset(attrs)
|
||||
|> Ecto.Changeset.put_change(:secret_hash, secret_hash)
|
||||
|> case do
|
||||
%{valid?: false} = nested_changeset ->
|
||||
{changeset, _original_type} =
|
||||
Domain.Changeset.inject_embedded_changeset(
|
||||
changeset,
|
||||
:provider_virtual_state,
|
||||
nested_changeset
|
||||
)
|
||||
|
||||
changeset
|
||||
|
||||
%{valid?: true} = nested_changeset ->
|
||||
expires_at = Ecto.Changeset.fetch_change!(nested_changeset, :expires_at)
|
||||
|
||||
changeset
|
||||
|> Ecto.Changeset.put_change(:provider_state, %{
|
||||
"expires_at" => DateTime.to_iso8601(expires_at),
|
||||
"secret_hash" => secret_hash
|
||||
})
|
||||
|> Ecto.Changeset.put_change(:provider_virtual_state, %{
|
||||
secret: secret
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def ensure_provisioned(%Ecto.Changeset{} = changeset) do
|
||||
changeset
|
||||
end
|
||||
|
||||
@impl true
|
||||
def ensure_deprovisioned(%Ecto.Changeset{} = changeset) do
|
||||
changeset
|
||||
end
|
||||
|
||||
@impl true
|
||||
def verify_secret(%Identity{} = identity, secret) when is_binary(secret) do
|
||||
Identity.Query.by_id(identity.id)
|
||||
|> Repo.fetch_and_update(
|
||||
with: fn identity ->
|
||||
secret_hash = identity.provider_state["secret_hash"]
|
||||
secret_expires_at = identity.provider_state["expires_at"]
|
||||
|
||||
cond do
|
||||
is_nil(secret_hash) ->
|
||||
:invalid_secret
|
||||
|
||||
is_nil(secret_expires_at) ->
|
||||
:invalid_secret
|
||||
|
||||
sign_in_token_expired?(secret_expires_at) ->
|
||||
:expired_secret
|
||||
|
||||
not Domain.Crypto.equal?(secret, secret_hash) ->
|
||||
:invalid_secret
|
||||
|
||||
true ->
|
||||
Ecto.Changeset.change(identity)
|
||||
end
|
||||
end
|
||||
)
|
||||
|> case do
|
||||
{:ok, identity} ->
|
||||
{:ok, expires_at, 0} = DateTime.from_iso8601(identity.provider_state["expires_at"])
|
||||
{:ok, identity, expires_at}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp sign_in_token_expired?(secret_expires_at) do
|
||||
now = DateTime.utc_now()
|
||||
|
||||
case DateTime.from_iso8601(secret_expires_at) do
|
||||
{:ok, secret_expires_at, 0} ->
|
||||
DateTime.diff(secret_expires_at, now, :second) < 0
|
||||
|
||||
{:error, _reason} ->
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
9
apps/domain/lib/domain/auth/adapters/token/state.ex
Normal file
9
apps/domain/lib/domain/auth/adapters/token/state.ex
Normal file
@@ -0,0 +1,9 @@
|
||||
defmodule Domain.Auth.Adapters.Token.State do
|
||||
use Domain, :schema
|
||||
|
||||
@primary_key false
|
||||
embedded_schema do
|
||||
field :secret_hash, :string, redact: true
|
||||
field :expires_at, :utc_datetime_usec
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
defmodule Domain.Auth.Adapters.Token.State.Changeset do
|
||||
use Domain, :changeset
|
||||
alias Domain.Auth.Adapters.Token.State
|
||||
|
||||
@fields ~w[expires_at]a
|
||||
|
||||
def create_changeset(attrs) do
|
||||
changeset(%State{}, attrs)
|
||||
end
|
||||
|
||||
def changeset(struct, attrs) do
|
||||
struct
|
||||
|> cast(attrs, @fields)
|
||||
|> validate_required(@fields)
|
||||
|> validate_datetime(:expires_at, greater_than: DateTime.utc_now())
|
||||
end
|
||||
end
|
||||
@@ -49,7 +49,7 @@ defmodule Domain.Auth.Adapters.UserPass do
|
||||
password_hash = Ecto.Changeset.fetch_change!(nested_changeset, :password_hash)
|
||||
|
||||
changeset
|
||||
|> Ecto.Changeset.put_change(:provider_state, %{password_hash: password_hash})
|
||||
|> Ecto.Changeset.put_change(:provider_state, %{"password_hash" => password_hash})
|
||||
|> Ecto.Changeset.put_change(:provider_virtual_state, %{})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -37,14 +37,14 @@ defmodule Domain.Auth.Authorizer do
|
||||
def manage_identities_permission, do: build(Auth.Identity, :manage)
|
||||
def manage_own_identities_permission, do: build(Auth.Identity, :manage_own)
|
||||
|
||||
def list_permissions_for_role(:admin) do
|
||||
def list_permissions_for_role(:account_admin_user) do
|
||||
[
|
||||
manage_providers_permission(),
|
||||
manage_identities_permission()
|
||||
]
|
||||
end
|
||||
|
||||
def list_permissions_for_role(:unprivileged) do
|
||||
def list_permissions_for_role(:account_user) do
|
||||
[
|
||||
manage_own_identities_permission()
|
||||
]
|
||||
@@ -60,11 +60,9 @@ defmodule Domain.Auth.Authorizer do
|
||||
Auth.Identity.Query.by_account_id(queryable, subject.account.id)
|
||||
|
||||
Auth.has_permission?(subject, manage_own_identities_permission()) ->
|
||||
%{type: :user, id: actor_id} = subject.actor
|
||||
|
||||
queryable
|
||||
|> Auth.Identity.Query.by_account_id(subject.account.id)
|
||||
|> Auth.Identity.Query.by_actor_id(actor_id)
|
||||
|> Auth.Identity.Query.by_actor_id(subject.actor.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ defmodule Domain.Auth.Provider do
|
||||
schema "auth_providers" do
|
||||
field :name, :string
|
||||
|
||||
field :adapter, Ecto.Enum, values: ~w[email openid_connect userpass]a
|
||||
field :adapter, Ecto.Enum, values: ~w[email openid_connect userpass token]a
|
||||
field :adapter_config, :map
|
||||
|
||||
belongs_to :account, Domain.Accounts.Account
|
||||
|
||||
@@ -3,8 +3,8 @@ defmodule Domain.Auth.Roles do
|
||||
|
||||
def list_roles do
|
||||
[
|
||||
build(:admin),
|
||||
build(:unprivileged)
|
||||
build(:account_admin_user),
|
||||
build(:account_user)
|
||||
]
|
||||
end
|
||||
|
||||
@@ -12,8 +12,7 @@ defmodule Domain.Auth.Roles do
|
||||
[
|
||||
Domain.Auth.Authorizer,
|
||||
Domain.Config.Authorizer,
|
||||
Domain.ApiTokens.Authorizer,
|
||||
Domain.Clients.Authorizer,
|
||||
Domain.Devices.Authorizer,
|
||||
Domain.Gateways.Authorizer,
|
||||
Domain.Relays.Authorizer,
|
||||
Domain.Actors.Authorizer,
|
||||
|
||||
@@ -91,9 +91,17 @@ defmodule Domain.Changeset do
|
||||
|> get_change(field)
|
||||
|> apply_action!(:dump)
|
||||
|> Ecto.embedded_dump(:json)
|
||||
|> atom_keys_to_string()
|
||||
|
||||
changeset = %{changeset | types: Map.put(changeset.types, field, original_type)}
|
||||
|
||||
put_change(changeset, field, map)
|
||||
end
|
||||
|
||||
# We dump atoms to strings because if we persist to Postgres and read it,
|
||||
# the map will be returned with string keys, and we want to make sure that
|
||||
# the map handling is unified across the codebase.
|
||||
defp atom_keys_to_string(map) do
|
||||
for {k, v} <- map, into: %{}, do: {to_string(k), v}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
defmodule Domain.Clients do
|
||||
use Supervisor
|
||||
alias Domain.{Repo, Auth, Validator}
|
||||
alias Domain.Actors
|
||||
alias Domain.Clients.{Client, Authorizer, Presence}
|
||||
|
||||
def start_link(opts) do
|
||||
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
|
||||
def init(_opts) do
|
||||
children = [
|
||||
Presence
|
||||
]
|
||||
|
||||
Supervisor.init(children, strategy: :one_for_one)
|
||||
end
|
||||
|
||||
def count_by_account_id(account_id) do
|
||||
Client.Query.by_account_id(account_id)
|
||||
|> Repo.aggregate(:count)
|
||||
end
|
||||
|
||||
def count_by_actor_id(actor_id) do
|
||||
Client.Query.by_actor_id(actor_id)
|
||||
|> Repo.aggregate(:count)
|
||||
end
|
||||
|
||||
def fetch_client_by_id(id, %Auth.Subject{} = subject) do
|
||||
required_permissions =
|
||||
{:one_of,
|
||||
[
|
||||
Authorizer.manage_clients_permission(),
|
||||
Authorizer.manage_own_clients_permission()
|
||||
]}
|
||||
|
||||
with :ok <- Auth.ensure_has_permissions(subject, required_permissions),
|
||||
true <- Validator.valid_uuid?(id) do
|
||||
Client.Query.by_id(id)
|
||||
|> Authorizer.for_subject(subject)
|
||||
|> Repo.fetch()
|
||||
else
|
||||
false -> {:error, :not_found}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_client_by_id!(id, opts \\ []) do
|
||||
{preload, _opts} = Keyword.pop(opts, :preload, [])
|
||||
|
||||
Client.Query.by_id(id)
|
||||
|> Repo.one!()
|
||||
|> Repo.preload(preload)
|
||||
end
|
||||
|
||||
def list_clients(%Auth.Subject{} = subject) do
|
||||
required_permissions =
|
||||
{:one_of,
|
||||
[
|
||||
Authorizer.manage_clients_permission(),
|
||||
Authorizer.manage_own_clients_permission()
|
||||
]}
|
||||
|
||||
with :ok <- Auth.ensure_has_permissions(subject, required_permissions) do
|
||||
Client.Query.all()
|
||||
|> Authorizer.for_subject(subject)
|
||||
|> Repo.list()
|
||||
end
|
||||
end
|
||||
|
||||
def list_clients_for_actor(%Actors.Actor{} = actor, %Auth.Subject{} = subject) do
|
||||
list_clients_by_actor_id(actor.id, subject)
|
||||
end
|
||||
|
||||
def list_clients_by_actor_id(actor_id, %Auth.Subject{} = subject) do
|
||||
required_permissions =
|
||||
{:one_of,
|
||||
[
|
||||
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
|
||||
Client.Query.by_actor_id(actor_id)
|
||||
|> Authorizer.for_subject(subject)
|
||||
|> Repo.list()
|
||||
else
|
||||
false -> {:error, :not_found}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
def change_client(%Client{} = client, attrs \\ %{}) do
|
||||
Client.Changeset.update_changeset(client, attrs)
|
||||
end
|
||||
|
||||
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_changeset(identity, subject.context, attrs)
|
||||
|
||||
Ecto.Multi.new()
|
||||
|> 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(:client_with_address, fn
|
||||
%{client: %Client{} = client, ipv4: ipv4, ipv6: ipv6} ->
|
||||
Client.Changeset.finalize_upsert_changeset(client, ipv4, ipv6)
|
||||
end)
|
||||
|> Repo.transaction()
|
||||
|> case do
|
||||
{: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, %{client: %Client{} = client} ->
|
||||
if address = Map.get(client, type) do
|
||||
{:ok, address}
|
||||
else
|
||||
{:ok, Domain.Network.fetch_next_available_address!(client.account_id, type)}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
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: &Client.Changeset.update_changeset(&1, attrs))
|
||||
end
|
||||
end
|
||||
|
||||
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: &Client.Changeset.delete_changeset/1)
|
||||
end
|
||||
end
|
||||
|
||||
def authorize_actor_client_management(%Actors.Actor{} = actor, %Auth.Subject{} = subject) do
|
||||
authorize_actor_client_management(actor.id, subject)
|
||||
end
|
||||
|
||||
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_client_management(_actor_id, %Auth.Subject{} = subject) do
|
||||
Auth.ensure_has_permissions(subject, Authorizer.manage_clients_permission())
|
||||
end
|
||||
|
||||
def connect_client(%Client{} = client) do
|
||||
Phoenix.PubSub.subscribe(Domain.PubSub, "actor:#{client.actor_id}")
|
||||
|
||||
{:ok, _} =
|
||||
Presence.track(self(), "clients", client.id, %{
|
||||
online_at: System.system_time(:second)
|
||||
})
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
@@ -1,41 +0,0 @@
|
||||
defmodule Domain.Clients.Authorizer do
|
||||
use Domain.Auth.Authorizer
|
||||
alias Domain.Clients.Client
|
||||
|
||||
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(:admin) do
|
||||
[
|
||||
manage_own_clients_permission(),
|
||||
manage_clients_permission()
|
||||
]
|
||||
end
|
||||
|
||||
def list_permissions_for_role(:unprivileged) do
|
||||
[
|
||||
manage_own_clients_permission()
|
||||
]
|
||||
end
|
||||
|
||||
def list_permissions_for_role(_) do
|
||||
[]
|
||||
end
|
||||
|
||||
@impl Domain.Auth.Authorizer
|
||||
def for_subject(queryable, %Subject{} = subject) do
|
||||
cond do
|
||||
has_permission?(subject, manage_clients_permission()) ->
|
||||
Client.Query.by_account_id(queryable, subject.account.id)
|
||||
|
||||
has_permission?(subject, manage_own_clients_permission()) ->
|
||||
%{type: :user, id: actor_id} = subject.actor
|
||||
|
||||
queryable
|
||||
|> Client.Query.by_account_id(subject.account.id)
|
||||
|> Client.Query.by_actor_id(actor_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,40 +0,0 @@
|
||||
defmodule Domain.Clients.Client.Query do
|
||||
use Domain, :query
|
||||
|
||||
def all do
|
||||
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, [clients: clients], clients.id == ^id)
|
||||
end
|
||||
|
||||
def by_actor_id(queryable \\ all(), actor_id) do
|
||||
where(queryable, [clients: clients], clients.actor_id == ^actor_id)
|
||||
end
|
||||
|
||||
def by_account_id(queryable \\ all(), account_id) do
|
||||
where(queryable, [clients: clients], clients.account_id == ^account_id)
|
||||
end
|
||||
|
||||
def returning_all(queryable \\ all()) do
|
||||
select(queryable, [clients: clients], clients)
|
||||
end
|
||||
|
||||
def with_preloaded_actor(queryable \\ all()) do
|
||||
with_named_binding(queryable, :actor, fn queryable, binding ->
|
||||
queryable
|
||||
|> 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, [clients: clients], identity in assoc(clients, ^binding), as: ^binding)
|
||||
|> preload([clients: clients, identity: identity], identity: identity)
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -4,22 +4,16 @@ defmodule Domain.Config do
|
||||
alias Domain.Config.{Definition, Definitions, Validator, Errors, Fetcher}
|
||||
alias Domain.Config.Configuration
|
||||
|
||||
def fetch_source_and_config!(key) do
|
||||
db_config = maybe_fetch_db_config!(key)
|
||||
env_config = System.get_env()
|
||||
|
||||
case Fetcher.fetch_source_and_config(Definitions, key, db_config, env_config) do
|
||||
{:ok, source, config} ->
|
||||
{source, config}
|
||||
|
||||
{:error, reason} ->
|
||||
Errors.raise_error!(reason)
|
||||
def fetch_resolved_configs!(account_id, keys, opts \\ []) do
|
||||
for {key, {_source, value}} <-
|
||||
fetch_resolved_configs_with_sources!(account_id, keys, opts),
|
||||
into: %{} do
|
||||
{key, value}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_source_and_configs!(keys) when is_list(keys) do
|
||||
db_config = maybe_fetch_db_config!(keys)
|
||||
env_config = System.get_env()
|
||||
def fetch_resolved_configs_with_sources!(account_id, keys, opts \\ []) do
|
||||
{db_config, env_config} = maybe_load_sources(account_id, opts, keys)
|
||||
|
||||
for key <- keys, into: %{} do
|
||||
case Fetcher.fetch_source_and_config(Definitions, key, db_config, env_config) do
|
||||
@@ -32,46 +26,24 @@ defmodule Domain.Config do
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_config(key) do
|
||||
db_config = maybe_fetch_db_config!(key)
|
||||
env_config = System.get_env()
|
||||
defp maybe_load_sources(account_id, opts, keys) when is_list(keys) do
|
||||
ignored_sources = Keyword.get(opts, :ignore_sources, []) |> List.wrap()
|
||||
|
||||
with {:ok, _source, config} <-
|
||||
Fetcher.fetch_source_and_config(Definitions, key, db_config, env_config) do
|
||||
{:ok, config}
|
||||
end
|
||||
end
|
||||
one_of_keys_is_stored_in_db? =
|
||||
Enum.any?(keys, &(&1 in Domain.Config.Configuration.__schema__(:fields)))
|
||||
|
||||
def fetch_config!(key) do
|
||||
case fetch_config(key) do
|
||||
{:ok, config} ->
|
||||
config
|
||||
db_config =
|
||||
if :db not in ignored_sources and one_of_keys_is_stored_in_db?,
|
||||
do: get_account_config_by_account_id(account_id),
|
||||
else: %{}
|
||||
|
||||
{:error, reason} ->
|
||||
Errors.raise_error!(reason)
|
||||
end
|
||||
end
|
||||
# credo:disable-for-lines:4
|
||||
env_config =
|
||||
if :env not in ignored_sources,
|
||||
do: System.get_env(),
|
||||
else: %{}
|
||||
|
||||
def fetch_configs!(keys) do
|
||||
for {key, {_source, value}} <- fetch_source_and_configs!(keys), into: %{} do
|
||||
{key, value}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_fetch_db_config!(keys) when is_list(keys) do
|
||||
if Enum.any?(keys, &(&1 in Domain.Config.Configuration.__schema__(:fields))) do
|
||||
fetch_db_config!()
|
||||
else
|
||||
%{}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_fetch_db_config!(key) do
|
||||
if key in Domain.Config.Configuration.__schema__(:fields) do
|
||||
fetch_db_config!()
|
||||
else
|
||||
%{}
|
||||
end
|
||||
{db_config, env_config}
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -92,59 +64,32 @@ defmodule Domain.Config do
|
||||
end
|
||||
end
|
||||
|
||||
def validate_runtime_config!(
|
||||
module \\ Definitions,
|
||||
db_config \\ fetch_db_config!(),
|
||||
env_config \\ System.get_env()
|
||||
) do
|
||||
module.configs()
|
||||
|> Enum.flat_map(fn {module, key} ->
|
||||
case Fetcher.fetch_source_and_config(module, key, db_config, env_config) do
|
||||
{:ok, _source, _value} -> []
|
||||
{:error, reason} -> [reason]
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
[] -> :ok
|
||||
errors -> Errors.raise_error!(errors)
|
||||
## Configuration stored in database
|
||||
|
||||
def get_account_config_by_account_id(account_id) do
|
||||
queryable = Configuration.Query.by_account_id(account_id)
|
||||
Repo.one(queryable) || %Configuration{account_id: account_id}
|
||||
end
|
||||
|
||||
def fetch_account_config(%Auth.Subject{} = subject) do
|
||||
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_permission()) do
|
||||
{:ok, get_account_config_by_account_id(subject.account.id)}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_db_config! do
|
||||
Repo.one!(Configuration)
|
||||
def change_account_config(%Configuration{} = configuration, attrs \\ %{}) do
|
||||
Configuration.Changeset.changeset(configuration, attrs)
|
||||
end
|
||||
|
||||
def fetch_db_config(%Auth.Subject{} = subject) do
|
||||
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.configure_permission()) do
|
||||
{:ok, fetch_db_config!()}
|
||||
def update_config(%Configuration{} = configuration, attrs, %Auth.Subject{} = subject) do
|
||||
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.manage_permission()) do
|
||||
update_config(configuration, attrs)
|
||||
end
|
||||
end
|
||||
|
||||
def change_config(%Configuration{} = config \\ fetch_db_config!(), attrs \\ %{}) do
|
||||
Configuration.Changeset.changeset(config, attrs)
|
||||
end
|
||||
|
||||
def update_config(%Configuration{} = config, attrs, %Auth.Subject{} = subject) do
|
||||
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.configure_permission()) do
|
||||
update_config(config, attrs)
|
||||
end
|
||||
end
|
||||
|
||||
def update_config(%Configuration{} = config, attrs) do
|
||||
changeset = Configuration.Changeset.changeset(config, attrs)
|
||||
|
||||
with {:ok, config} <- Repo.update(changeset) do
|
||||
# Domain.Auth.SAML.StartProxy.refresh(config.saml_identity_providers)
|
||||
{:ok, config}
|
||||
end
|
||||
end
|
||||
|
||||
def put_config!(key, value) do
|
||||
with {:ok, config} <- update_config(fetch_db_config!(), %{key => value}) do
|
||||
config
|
||||
else
|
||||
{:error, reason} -> raise "cannot update config: #{inspect(reason)}"
|
||||
end
|
||||
def update_config(%Configuration{} = configuration, attrs) do
|
||||
Configuration.Changeset.changeset(configuration, attrs)
|
||||
|> Repo.insert_or_update()
|
||||
end
|
||||
|
||||
def config_changeset(changeset, schema_key, config_key \\ nil) do
|
||||
@@ -168,10 +113,7 @@ defmodule Domain.Config do
|
||||
end
|
||||
end
|
||||
|
||||
def vpn_sessions_expire? do
|
||||
freq = fetch_config!(:vpn_session_duration)
|
||||
0 < freq && freq < Configuration.Changeset.max_vpn_session_duration()
|
||||
end
|
||||
## Test helpers
|
||||
|
||||
if Mix.env() != :test do
|
||||
defdelegate fetch_env!(app, key), to: Application
|
||||
|
||||
@@ -2,12 +2,12 @@ defmodule Domain.Config.Authorizer do
|
||||
use Domain.Auth.Authorizer
|
||||
alias Domain.Config.Configuration
|
||||
|
||||
def configure_permission, do: build(Configuration, :manage)
|
||||
def manage_permission, do: build(Configuration, :manage)
|
||||
|
||||
@impl Domain.Auth.Authorizer
|
||||
def list_permissions_for_role(:admin) do
|
||||
def list_permissions_for_role(:account_admin_user) do
|
||||
[
|
||||
configure_permission()
|
||||
manage_permission()
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
@@ -3,37 +3,12 @@ defmodule Domain.Config.Configuration do
|
||||
alias Domain.Config.Logo
|
||||
|
||||
schema "configurations" do
|
||||
# field :upstream_dns, {:array, :string}, default: []
|
||||
|
||||
field :allow_unprivileged_device_management, :boolean
|
||||
field :allow_unprivileged_device_configuration, :boolean
|
||||
|
||||
field :local_auth_enabled, :boolean
|
||||
field :disable_vpn_on_oidc_error, :boolean
|
||||
|
||||
# The defaults for these fields are set in the following migration:
|
||||
# apps/domain/priv/repo/migrations/20221224210654_fix_sites_nullable_fields.exs
|
||||
#
|
||||
# This will be changing in 0.8 and again when we have client apps,
|
||||
# so this works for the time being. The important thing is allowing users
|
||||
# to update these fields via the REST API since they were removed as
|
||||
# environment variables in the above migration. This is important for users
|
||||
# wishing to configure Firezone with automated Infrastructure tools like
|
||||
# Terraform.
|
||||
field :default_client_persistent_keepalive, :integer
|
||||
field :default_client_mtu, :integer
|
||||
field :default_client_endpoint, :string
|
||||
field :default_client_dns, {:array, :string}, default: []
|
||||
field :default_client_allowed_ips, {:array, Domain.Types.INET}, default: []
|
||||
|
||||
# XXX: Remove when this feature is refactored into config expiration feature
|
||||
# and WireGuard keys are decoupled from devices to facilitate rotation.
|
||||
#
|
||||
# See https://github.com/firezone/firezone/issues/1236
|
||||
field :vpn_session_duration, :integer, read_after_writes: true
|
||||
field :devices_upstream_dns, {:array, :string}, default: []
|
||||
|
||||
embeds_one :logo, Logo, on_replace: :delete
|
||||
|
||||
belongs_to :account, Domain.Accounts.Account
|
||||
|
||||
timestamps()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,46 +2,28 @@ defmodule Domain.Config.Configuration.Changeset do
|
||||
use Domain, :changeset
|
||||
import Domain.Config, only: [config_changeset: 2]
|
||||
|
||||
# Postgres max int size is 4 bytes
|
||||
@max_vpn_session_duration 2_147_483_647
|
||||
@fields ~w[devices_upstream_dns]a
|
||||
|
||||
@fields ~w[
|
||||
local_auth_enabled
|
||||
allow_unprivileged_device_management
|
||||
allow_unprivileged_device_configuration
|
||||
default_client_persistent_keepalive
|
||||
default_client_mtu
|
||||
default_client_endpoint
|
||||
default_client_dns
|
||||
default_client_allowed_ips
|
||||
vpn_session_duration
|
||||
]a
|
||||
|
||||
@spec changeset(
|
||||
{map, map}
|
||||
| %{
|
||||
:__struct__ => atom | %{:__changeset__ => map, optional(any) => any},
|
||||
optional(atom) => any
|
||||
},
|
||||
:invalid | %{optional(:__struct__) => none, optional(atom | binary) => any}
|
||||
) :: any
|
||||
def changeset(configuration, attrs) do
|
||||
changeset =
|
||||
configuration
|
||||
|> cast(attrs, @fields)
|
||||
|> cast_embed(:logo)
|
||||
|> trim_change(:default_client_dns)
|
||||
|> trim_change(:default_client_endpoint)
|
||||
|> trim_change(:devices_upstream_dns)
|
||||
|
||||
Enum.reduce(@fields, changeset, fn field, changeset ->
|
||||
config_changeset(changeset, field)
|
||||
end)
|
||||
|> ensure_no_overridden_changes()
|
||||
|> ensure_no_overridden_changes(configuration.account_id)
|
||||
end
|
||||
|
||||
defp ensure_no_overridden_changes(changeset) do
|
||||
defp ensure_no_overridden_changes(changeset, account_id) do
|
||||
changed_keys = Map.keys(changeset.changes)
|
||||
configs = Domain.Config.fetch_source_and_configs!(changed_keys)
|
||||
|
||||
configs =
|
||||
Domain.Config.fetch_resolved_configs_with_sources!(account_id, changed_keys,
|
||||
ignore_sources: :db
|
||||
)
|
||||
|
||||
Enum.reduce(changed_keys, changeset, fn key, changeset ->
|
||||
case Map.fetch!(configs, key) do
|
||||
@@ -58,6 +40,4 @@ defmodule Domain.Config.Configuration.Changeset do
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def max_vpn_session_duration, do: @max_vpn_session_duration
|
||||
end
|
||||
|
||||
15
apps/domain/lib/domain/config/configuration/query.ex
Normal file
15
apps/domain/lib/domain/config/configuration/query.ex
Normal file
@@ -0,0 +1,15 @@
|
||||
defmodule Domain.Config.Configuration.Query do
|
||||
use Domain, :query
|
||||
|
||||
def all do
|
||||
from(configurations in Domain.Config.Configuration, as: :configurations)
|
||||
end
|
||||
|
||||
def by_id(queryable \\ all(), id) do
|
||||
where(queryable, [configurations: configurations], configurations.id == ^id)
|
||||
end
|
||||
|
||||
def by_account_id(queryable \\ all(), account_id) do
|
||||
where(queryable, [configurations: configurations], configurations.account_id == ^account_id)
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,3 @@
|
||||
# TODO: clean up unused definitions
|
||||
defmodule Domain.Config.Definitions do
|
||||
@moduledoc """
|
||||
Most day-to-day config of Firezone can be done via the Firezone Web UI,
|
||||
@@ -40,7 +39,8 @@ defmodule Domain.Config.Definitions do
|
||||
:external_url,
|
||||
:phoenix_secure_cookies,
|
||||
:phoenix_listen_address,
|
||||
:phoenix_http_port,
|
||||
:phoenix_http_web_port,
|
||||
:phoenix_http_api_port,
|
||||
:phoenix_http_protocol_options,
|
||||
:phoenix_external_trusted_proxies,
|
||||
:phoenix_private_clients
|
||||
@@ -57,17 +57,6 @@ defmodule Domain.Config.Definitions do
|
||||
:database_ssl_opts,
|
||||
:database_parameters
|
||||
]},
|
||||
{"Admin Setup",
|
||||
"""
|
||||
Options responsible for initial admin provisioning and resetting the admin password.
|
||||
|
||||
For more details see [troubleshooting guide](/docs/administer/troubleshoot/#admin-login-isnt-working).
|
||||
""",
|
||||
[
|
||||
:reset_admin_on_boot,
|
||||
:default_admin_email,
|
||||
:default_admin_password
|
||||
]},
|
||||
{"Secrets and Encryption",
|
||||
"""
|
||||
Your secrets should be generated during installation automatically and persisted to `.env` file.
|
||||
@@ -75,8 +64,12 @@ defmodule Domain.Config.Definitions do
|
||||
All secrets should be a **base64-encoded string**.
|
||||
""",
|
||||
[
|
||||
:guardian_secret_key,
|
||||
:database_encryption_key,
|
||||
:auth_token_key_base,
|
||||
:auth_token_salt,
|
||||
:relays_auth_token_key_base,
|
||||
:relays_auth_token_salt,
|
||||
:gateways_auth_token_key_base,
|
||||
:gateways_auth_token_salt,
|
||||
:secret_key_base,
|
||||
:live_view_signing_salt,
|
||||
:cookie_signing_salt,
|
||||
@@ -84,38 +77,25 @@ defmodule Domain.Config.Definitions do
|
||||
]},
|
||||
{"Devices",
|
||||
[
|
||||
:allow_unprivileged_device_management,
|
||||
:allow_unprivileged_device_configuration,
|
||||
:vpn_session_duration,
|
||||
:default_client_persistent_keepalive,
|
||||
:default_client_mtu,
|
||||
:default_client_endpoint,
|
||||
:default_client_dns,
|
||||
:default_client_allowed_ips
|
||||
:devices_upstream_dns
|
||||
]},
|
||||
# {"Limits",
|
||||
# [
|
||||
# :max_devices_per_user
|
||||
# ]},
|
||||
{"Authorization",
|
||||
"""
|
||||
Providers:
|
||||
|
||||
* `openid_connect` is used to authenticate users via OpenID Connect, this is recommended for production use;
|
||||
* `email` is used to authenticate users via magic links sent to the email;
|
||||
* `token` is used to authenticate service accounts using an API token;
|
||||
* `userpass` is used to authenticate users with username and password, should be used
|
||||
with extreme care and is not recommended for production use.
|
||||
""",
|
||||
[
|
||||
:local_auth_enabled
|
||||
:auth_provider_adapters
|
||||
]},
|
||||
{"WireGuard",
|
||||
{"Gateways",
|
||||
[
|
||||
:wireguard_port,
|
||||
:wireguard_ipv4_enabled,
|
||||
:wireguard_ipv4_masquerade,
|
||||
:wireguard_ipv4_network,
|
||||
:wireguard_ipv4_address,
|
||||
:wireguard_ipv6_enabled,
|
||||
:wireguard_ipv6_masquerade,
|
||||
:wireguard_ipv6_network,
|
||||
:wireguard_ipv6_address,
|
||||
:wireguard_private_key_path,
|
||||
:wireguard_interface_name,
|
||||
:gateway_egress_interface,
|
||||
:gateway_nft_path
|
||||
:gateway_ipv4_masquerade,
|
||||
:gateway_ipv6_masquerade
|
||||
]},
|
||||
{"Outbound Emails",
|
||||
[
|
||||
@@ -123,11 +103,6 @@ defmodule Domain.Config.Definitions do
|
||||
:outbound_email_adapter,
|
||||
:outbound_email_adapter_opts
|
||||
]},
|
||||
{"Connectivity Checks",
|
||||
[
|
||||
:connectivity_checks_enabled,
|
||||
:connectivity_checks_interval
|
||||
]},
|
||||
{"Telemetry",
|
||||
[
|
||||
:telemetry_enabled,
|
||||
@@ -169,7 +144,20 @@ defmodule Domain.Config.Definitions do
|
||||
@doc """
|
||||
Internal port to listen on for the Phoenix web server.
|
||||
"""
|
||||
defconfig(:phoenix_http_port, :integer,
|
||||
defconfig(:phoenix_http_web_port, :integer,
|
||||
default: 13_000,
|
||||
changeset: fn changeset, key ->
|
||||
Ecto.Changeset.validate_number(changeset, key,
|
||||
greater_than: 0,
|
||||
less_than_or_equal_to: 65_535
|
||||
)
|
||||
end
|
||||
)
|
||||
|
||||
@doc """
|
||||
Internal port to listen on for the Phoenix api server.
|
||||
"""
|
||||
defconfig(:phoenix_http_api_port, :integer,
|
||||
default: 13_000,
|
||||
changeset: fn changeset, key ->
|
||||
Ecto.Changeset.validate_number(changeset, key,
|
||||
@@ -288,59 +276,54 @@ defmodule Domain.Config.Definitions do
|
||||
dump: &Dumper.keyword/1
|
||||
)
|
||||
|
||||
##############################################
|
||||
## Admin Setup
|
||||
##############################################
|
||||
|
||||
@doc """
|
||||
Set this variable to `true` to create or reset the admin password every time Firezone
|
||||
starts. By default, the admin password is only set when Firezone is installed.
|
||||
|
||||
Note: This **will not** change the status of local authentication.
|
||||
"""
|
||||
defconfig(:reset_admin_on_boot, :boolean, default: false)
|
||||
|
||||
@doc """
|
||||
Primary administrator email.
|
||||
"""
|
||||
defconfig(:default_admin_email, :string,
|
||||
default: nil,
|
||||
sensitive: true,
|
||||
legacy_keys: [{:env, "ADMIN_EMAIL", "0.9"}],
|
||||
changeset: fn changeset, key ->
|
||||
changeset
|
||||
|> Domain.Validator.trim_change(key)
|
||||
|> Domain.Validator.validate_email(key)
|
||||
end
|
||||
)
|
||||
|
||||
@doc """
|
||||
Default password that will be used for creating or resetting the primary administrator account.
|
||||
"""
|
||||
defconfig(:default_admin_password, :string,
|
||||
default: nil,
|
||||
sensitive: true,
|
||||
changeset: fn changeset, key ->
|
||||
Ecto.Changeset.validate_length(changeset, key, min: 5)
|
||||
end
|
||||
)
|
||||
|
||||
##############################################
|
||||
## Secrets
|
||||
##############################################
|
||||
|
||||
@doc """
|
||||
Secret key used for signing JWTs.
|
||||
Secret which is used to encode and sign auth tokens.
|
||||
"""
|
||||
defconfig(:guardian_secret_key, :string,
|
||||
defconfig(:auth_token_key_base, :string,
|
||||
sensitive: true,
|
||||
changeset: &Domain.Validator.validate_base64/2
|
||||
)
|
||||
|
||||
@doc """
|
||||
Secret key used for encrypting sensitive data in the database.
|
||||
Salt which is used to encode and sign auth tokens.
|
||||
"""
|
||||
defconfig(:database_encryption_key, :string,
|
||||
defconfig(:auth_token_salt, :string,
|
||||
sensitive: true,
|
||||
changeset: &Domain.Validator.validate_base64/2
|
||||
)
|
||||
|
||||
@doc """
|
||||
Secret which is used to encode and sign relays auth tokens.
|
||||
"""
|
||||
defconfig(:relays_auth_token_key_base, :string,
|
||||
sensitive: true,
|
||||
changeset: &Domain.Validator.validate_base64/2
|
||||
)
|
||||
|
||||
@doc """
|
||||
Salt which is used to encode and sign relays auth tokens.
|
||||
"""
|
||||
defconfig(:relays_auth_token_salt, :string,
|
||||
sensitive: true,
|
||||
changeset: &Domain.Validator.validate_base64/2
|
||||
)
|
||||
|
||||
@doc """
|
||||
Secret which is used to encode and sign gateways auth tokens.
|
||||
"""
|
||||
defconfig(:gateways_auth_token_key_base, :string,
|
||||
sensitive: true,
|
||||
changeset: &Domain.Validator.validate_base64/2
|
||||
)
|
||||
|
||||
@doc """
|
||||
Salt which is used to encode and sign gateways auth tokens.
|
||||
"""
|
||||
defconfig(:gateways_auth_token_salt, :string,
|
||||
sensitive: true,
|
||||
changeset: &Domain.Validator.validate_base64/2
|
||||
)
|
||||
@@ -382,88 +365,15 @@ defmodule Domain.Config.Definitions do
|
||||
##############################################
|
||||
|
||||
@doc """
|
||||
Enable or disable management of devices on unprivileged accounts.
|
||||
"""
|
||||
defconfig(:allow_unprivileged_device_management, :boolean, default: true)
|
||||
|
||||
@doc """
|
||||
Enable or disable configuration of device network settings for unprivileged users.
|
||||
"""
|
||||
defconfig(:allow_unprivileged_device_configuration, :boolean, default: true)
|
||||
|
||||
@doc """
|
||||
Optionally require users to periodically authenticate to the Firezone web UI in order to keep their VPN sessions active.
|
||||
"""
|
||||
defconfig(:vpn_session_duration, :integer,
|
||||
default: 0,
|
||||
changeset: fn changeset, key ->
|
||||
Ecto.Changeset.validate_number(changeset, key,
|
||||
greater_than_or_equal_to: 0,
|
||||
less_than_or_equal_to: 2_147_483_647
|
||||
)
|
||||
end
|
||||
)
|
||||
|
||||
@doc """
|
||||
Interval for WireGuard [persistent keepalive](https://www.wireguard.com/quickstart/#nat-and-firewall-traversal-persistence).
|
||||
|
||||
If you experience NAT or firewall traversal problems, you can enable this to send a keepalive packet every 25 seconds.
|
||||
Otherwise, keep it disabled with a 0 default value.
|
||||
"""
|
||||
defconfig(:default_client_persistent_keepalive, :integer,
|
||||
default: 25,
|
||||
changeset: fn changeset, key ->
|
||||
Ecto.Changeset.validate_number(changeset, key,
|
||||
greater_than_or_equal_to: 0,
|
||||
less_than_or_equal_to: 120
|
||||
)
|
||||
end
|
||||
)
|
||||
|
||||
@doc """
|
||||
WireGuard interface MTU for devices. 1280 is a safe bet for most networks.
|
||||
Leave this blank to omit this field from generated configs.
|
||||
"""
|
||||
defconfig(:default_client_mtu, :integer,
|
||||
default: 1280,
|
||||
legacy_keys: [{:env, "WIREGUARD_MTU", "0.8"}],
|
||||
changeset: fn changeset, key ->
|
||||
Ecto.Changeset.validate_number(changeset, key,
|
||||
greater_than_or_equal_to: 576,
|
||||
less_than_or_equal_to: 1500
|
||||
)
|
||||
end
|
||||
)
|
||||
|
||||
@doc """
|
||||
IPv4, IPv6 address, or FQDN that devices will be configured to connect to. Defaults to this server's FQDN.
|
||||
"""
|
||||
defconfig(:default_client_endpoint, {:one_of, [Types.IPPort, :string]},
|
||||
default: fn ->
|
||||
external_uri = URI.parse(compile_config!(:external_url))
|
||||
wireguard_port = compile_config!(:wireguard_port)
|
||||
"#{external_uri.host}:#{wireguard_port}"
|
||||
end,
|
||||
changeset: fn
|
||||
Types.IPPort, changeset, _key ->
|
||||
changeset
|
||||
|
||||
:string, changeset, key ->
|
||||
changeset
|
||||
|> Domain.Validator.trim_change(key)
|
||||
|> Domain.Validator.validate_fqdn(key, allow_port: true)
|
||||
end
|
||||
)
|
||||
|
||||
@doc """
|
||||
Comma-separated list of DNS servers to use for devices.
|
||||
Comma-separated list of upstream DNS servers to use for devices.
|
||||
|
||||
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.
|
||||
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.
|
||||
"""
|
||||
defconfig(
|
||||
:default_client_dns,
|
||||
:devices_upstream_dns,
|
||||
{:array, ",", {:one_of, [Types.IP, :string]}, validate_unique: true},
|
||||
default: [],
|
||||
changeset: fn
|
||||
@@ -477,45 +387,27 @@ defmodule Domain.Config.Definitions do
|
||||
end
|
||||
)
|
||||
|
||||
##############################################
|
||||
## Userpass / SAML / OIDC / Magic Link authentication
|
||||
##############################################
|
||||
|
||||
@doc """
|
||||
Configures the default AllowedIPs setting for devices.
|
||||
Enable or disable the authentication methods for all users.
|
||||
|
||||
AllowedIPs determines which destination IPs get routed through Firezone.
|
||||
|
||||
Specify a comma-separated list of IPs or CIDRs here to achieve split tunneling, or use
|
||||
`0.0.0.0/0, ::/0` to route all device traffic through this Firezone server.
|
||||
It will affect on which auth providers can be created per an account but will not disable
|
||||
already active providers when setting is changed.
|
||||
"""
|
||||
defconfig(
|
||||
:default_client_allowed_ips,
|
||||
{:array, ",", {:one_of, [Types.CIDR, Types.IP]}, validate_unique: true},
|
||||
default: "0.0.0.0/0, ::/0"
|
||||
:auth_provider_adapters,
|
||||
{
|
||||
:array,
|
||||
",",
|
||||
{:parameterized, Ecto.Enum,
|
||||
Ecto.Enum.init(values: ~w[email openid_connect userpass token]a)}
|
||||
},
|
||||
default: ~w[email openid_connect token]a
|
||||
)
|
||||
|
||||
##############################################
|
||||
## Limits
|
||||
##############################################
|
||||
|
||||
defconfig(:max_devices_per_user, :integer,
|
||||
default: 10,
|
||||
changeset: fn changeset, key ->
|
||||
Ecto.Changeset.validate_number(changeset, key,
|
||||
greater_than_or_equal_to: 0,
|
||||
less_than_or_equal_to: 100
|
||||
)
|
||||
end
|
||||
)
|
||||
|
||||
##############################################
|
||||
## Userpass / SAML / OIDC authentication
|
||||
##############################################
|
||||
|
||||
@doc """
|
||||
Enable or disable the local authentication method for all users.
|
||||
"""
|
||||
# XXX: This should be replaced with auth_methods config which accepts a list
|
||||
# of enabled methods.
|
||||
defconfig(:local_auth_enabled, :boolean, default: true)
|
||||
|
||||
##############################################
|
||||
## Telemetry
|
||||
##############################################
|
||||
@@ -536,91 +428,11 @@ defmodule Domain.Config.Definitions do
|
||||
)
|
||||
|
||||
##############################################
|
||||
## Connectivity Checks
|
||||
## Gateways
|
||||
##############################################
|
||||
|
||||
@doc """
|
||||
Enable / disable periodic checking for egress connectivity. Determines the instance's public IP to populate `Endpoint` fields.
|
||||
"""
|
||||
defconfig(:connectivity_checks_enabled, :boolean, default: true)
|
||||
|
||||
@doc """
|
||||
Periodicity in seconds to check for egress connectivity.
|
||||
"""
|
||||
defconfig(:connectivity_checks_interval, :integer,
|
||||
default: 43_200,
|
||||
changeset: fn changeset, key ->
|
||||
Ecto.Changeset.validate_number(changeset, key,
|
||||
greater_than_or_equal_to: 60,
|
||||
less_than_or_equal_to: 86_400
|
||||
)
|
||||
end
|
||||
)
|
||||
|
||||
##############################################
|
||||
## WireGuard
|
||||
##############################################
|
||||
|
||||
@doc """
|
||||
A port on which WireGuard will listen for incoming connections.
|
||||
"""
|
||||
defconfig(:wireguard_port, :integer,
|
||||
default: 51_820,
|
||||
changeset: fn changeset, key ->
|
||||
Ecto.Changeset.validate_number(changeset, key,
|
||||
greater_than: 0,
|
||||
less_than_or_equal_to: 65_535
|
||||
)
|
||||
end
|
||||
)
|
||||
|
||||
@doc """
|
||||
Enable or disable IPv4 support for WireGuard.
|
||||
"""
|
||||
defconfig(:wireguard_ipv4_enabled, :boolean, default: true)
|
||||
defconfig(:wireguard_ipv4_masquerade, :boolean, default: true)
|
||||
|
||||
defconfig(:wireguard_ipv4_network, Types.CIDR,
|
||||
default: "10.3.2.0/24",
|
||||
changeset: &Domain.Validator.validate_ip_type_inclusion(&1, &2, [:ipv4])
|
||||
)
|
||||
|
||||
defconfig(:wireguard_ipv4_address, Types.IP,
|
||||
default: "10.3.2.1",
|
||||
changeset: &Domain.Validator.validate_ip_type_inclusion(&1, &2, [:ipv4])
|
||||
)
|
||||
|
||||
@doc """
|
||||
Enable or disable IPv6 support for WireGuard.
|
||||
"""
|
||||
defconfig(:wireguard_ipv6_enabled, :boolean, default: true)
|
||||
defconfig(:wireguard_ipv6_masquerade, :boolean, default: true)
|
||||
|
||||
defconfig(:wireguard_ipv6_network, Types.CIDR,
|
||||
default: "fd00::3:2:0/120",
|
||||
changeset: &Domain.Validator.validate_ip_type_inclusion(&1, &2, [:ipv6])
|
||||
)
|
||||
|
||||
defconfig(:wireguard_ipv6_address, Types.IP,
|
||||
default: "fd00::3:2:1",
|
||||
changeset: &Domain.Validator.validate_ip_type_inclusion(&1, &2, [:ipv6])
|
||||
)
|
||||
|
||||
defconfig(:wireguard_private_key_path, :string,
|
||||
default: "/var/firezone/private_key"
|
||||
# We don't check if the file exists, because it is generated on
|
||||
# the first boot.
|
||||
# changeset: &Domain.Validator.validate_file(&1, &2)
|
||||
)
|
||||
|
||||
defconfig(:wireguard_interface_name, :string, default: "wg-firezone")
|
||||
|
||||
defconfig(:gateway_egress_interface, :string,
|
||||
legacy_keys: [{:env, "EGRESS_INTERFACE", "0.8"}],
|
||||
default: "eth0"
|
||||
)
|
||||
|
||||
defconfig(:gateway_nft_path, :string, default: "nft", legacy_keys: [{:env, "NFT_PATH", "0.8"}])
|
||||
defconfig(:gateway_ipv4_masquerade, :boolean, default: true)
|
||||
defconfig(:gateway_ipv6_masquerade, :boolean, default: true)
|
||||
|
||||
##############################################
|
||||
## HTTP Client Settings
|
||||
|
||||
178
apps/domain/lib/domain/devices.ex
Normal file
178
apps/domain/lib/domain/devices.ex
Normal file
@@ -0,0 +1,178 @@
|
||||
defmodule Domain.Devices do
|
||||
use Supervisor
|
||||
alias Domain.{Repo, Auth, Validator}
|
||||
alias Domain.Actors
|
||||
alias Domain.Devices.{Device, Authorizer, Presence}
|
||||
|
||||
def start_link(opts) do
|
||||
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
|
||||
def init(_opts) do
|
||||
children = [
|
||||
Presence
|
||||
]
|
||||
|
||||
Supervisor.init(children, strategy: :one_for_one)
|
||||
end
|
||||
|
||||
def count_by_account_id(account_id) do
|
||||
Device.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)
|
||||
|> Repo.aggregate(:count)
|
||||
end
|
||||
|
||||
def fetch_device_by_id(id, %Auth.Subject{} = subject) do
|
||||
required_permissions =
|
||||
{:one_of,
|
||||
[
|
||||
Authorizer.manage_devices_permission(),
|
||||
Authorizer.manage_own_devices_permission()
|
||||
]}
|
||||
|
||||
with :ok <- Auth.ensure_has_permissions(subject, required_permissions),
|
||||
true <- Validator.valid_uuid?(id) do
|
||||
Device.Query.by_id(id)
|
||||
|> Authorizer.for_subject(subject)
|
||||
|> Repo.fetch()
|
||||
else
|
||||
false -> {:error, :not_found}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_device_by_id!(id, opts \\ []) do
|
||||
{preload, _opts} = Keyword.pop(opts, :preload, [])
|
||||
|
||||
Device.Query.by_id(id)
|
||||
|> Repo.one!()
|
||||
|> Repo.preload(preload)
|
||||
end
|
||||
|
||||
def list_devices(%Auth.Subject{} = subject) do
|
||||
required_permissions =
|
||||
{:one_of,
|
||||
[
|
||||
Authorizer.manage_devices_permission(),
|
||||
Authorizer.manage_own_devices_permission()
|
||||
]}
|
||||
|
||||
with :ok <- Auth.ensure_has_permissions(subject, required_permissions) do
|
||||
Device.Query.all()
|
||||
|> Authorizer.for_subject(subject)
|
||||
|> Repo.list()
|
||||
end
|
||||
end
|
||||
|
||||
def list_devices_for_actor(%Actors.Actor{} = actor, %Auth.Subject{} = subject) do
|
||||
list_devices_by_actor_id(actor.id, subject)
|
||||
end
|
||||
|
||||
def list_devices_by_actor_id(actor_id, %Auth.Subject{} = subject) do
|
||||
required_permissions =
|
||||
{:one_of,
|
||||
[
|
||||
Authorizer.manage_devices_permission(),
|
||||
Authorizer.manage_own_devices_permission()
|
||||
]}
|
||||
|
||||
with :ok <- Auth.ensure_has_permissions(subject, required_permissions),
|
||||
true <- Validator.valid_uuid?(actor_id) do
|
||||
Device.Query.by_actor_id(actor_id)
|
||||
|> Authorizer.for_subject(subject)
|
||||
|> Repo.list()
|
||||
else
|
||||
false -> {:error, :not_found}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
def change_device(%Device{} = device, attrs \\ %{}) do
|
||||
Device.Changeset.update_changeset(device, 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_changeset(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(),
|
||||
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_changeset(device, ipv4, ipv6)
|
||||
end)
|
||||
|> Repo.transaction()
|
||||
|> case do
|
||||
{:ok, %{device_with_address: device}} -> {:ok, device}
|
||||
{:error, :device, 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
|
||||
{:ok, address}
|
||||
else
|
||||
{:ok, Domain.Network.fetch_next_available_address!(device.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)
|
||||
|> Authorizer.for_subject(subject)
|
||||
|> Repo.fetch_and_update(with: &Device.Changeset.update_changeset(&1, attrs))
|
||||
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)
|
||||
|> Authorizer.for_subject(subject)
|
||||
|> Repo.fetch_and_update(with: &Device.Changeset.delete_changeset/1)
|
||||
end
|
||||
end
|
||||
|
||||
def authorize_actor_device_management(%Actors.Actor{} = actor, %Auth.Subject{} = subject) do
|
||||
authorize_actor_device_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())
|
||||
end
|
||||
|
||||
def authorize_actor_device_management(_actor_id, %Auth.Subject{} = subject) do
|
||||
Auth.ensure_has_permissions(subject, Authorizer.manage_devices_permission())
|
||||
end
|
||||
|
||||
def connect_device(%Device{} = device) do
|
||||
Phoenix.PubSub.subscribe(Domain.PubSub, "actor:#{device.actor_id}")
|
||||
|
||||
{:ok, _} =
|
||||
Presence.track(self(), "devices", device.id, %{
|
||||
online_at: System.system_time(:second)
|
||||
})
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def fetch_device_config!(%Device{} = device) do
|
||||
%{
|
||||
devices_upstream_dns: upstream_dns
|
||||
} = Domain.Config.fetch_resolved_configs!(device.account_id, [:devices_upstream_dns])
|
||||
|
||||
[upstream_dns: upstream_dns]
|
||||
end
|
||||
end
|
||||
39
apps/domain/lib/domain/devices/authorizer.ex
Normal file
39
apps/domain/lib/domain/devices/authorizer.ex
Normal file
@@ -0,0 +1,39 @@
|
||||
defmodule Domain.Devices.Authorizer do
|
||||
use Domain.Auth.Authorizer
|
||||
alias Domain.Devices.Device
|
||||
|
||||
def manage_own_devices_permission, do: build(Device, :manage_own)
|
||||
def manage_devices_permission, do: build(Device, :manage)
|
||||
|
||||
@impl Domain.Auth.Authorizer
|
||||
|
||||
def list_permissions_for_role(:account_admin_user) do
|
||||
[
|
||||
manage_own_devices_permission(),
|
||||
manage_devices_permission()
|
||||
]
|
||||
end
|
||||
|
||||
def list_permissions_for_role(:account_user) do
|
||||
[
|
||||
manage_own_devices_permission()
|
||||
]
|
||||
end
|
||||
|
||||
def list_permissions_for_role(_) do
|
||||
[]
|
||||
end
|
||||
|
||||
@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_own_devices_permission()) ->
|
||||
queryable
|
||||
|> Device.Query.by_account_id(subject.account.id)
|
||||
|> Device.Query.by_actor_id(subject.actor.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,7 @@
|
||||
defmodule Domain.Clients.Client do
|
||||
defmodule Domain.Devices.Device do
|
||||
use Domain, :schema
|
||||
|
||||
schema "clients" do
|
||||
schema "devices" do
|
||||
field :external_id, :string
|
||||
|
||||
field :name, :string
|
||||
@@ -1,7 +1,7 @@
|
||||
defmodule Domain.Clients.Client.Changeset do
|
||||
defmodule Domain.Devices.Device.Changeset do
|
||||
use Domain, :changeset
|
||||
alias Domain.{Version, Auth}
|
||||
alias Domain.Clients
|
||||
alias Domain.Devices
|
||||
|
||||
@upsert_fields ~w[external_id name public_key]a
|
||||
@conflict_replace_fields ~w[public_key
|
||||
@@ -19,7 +19,7 @@ defmodule Domain.Clients.Client.Changeset do
|
||||
def upsert_on_conflict, do: {:replace, @conflict_replace_fields}
|
||||
|
||||
def upsert_changeset(%Auth.Identity{} = identity, %Auth.Context{} = context, attrs) do
|
||||
%Clients.Client{}
|
||||
%Devices.Device{}
|
||||
|> cast(attrs, @upsert_fields)
|
||||
|> put_default_value(:name, &generate_name/0)
|
||||
|> put_change(:identity_id, identity.id)
|
||||
@@ -31,30 +31,30 @@ defmodule Domain.Clients.Client.Changeset do
|
||||
|> validate_required(@required_fields)
|
||||
|> validate_base64(:public_key)
|
||||
|> validate_length(:public_key, is: @key_length)
|
||||
|> unique_constraint(:ipv4, name: :clients_account_id_ipv4_index)
|
||||
|> unique_constraint(:ipv6, name: :clients_account_id_ipv6_index)
|
||||
|> unique_constraint(:ipv4, name: :devices_account_id_ipv4_index)
|
||||
|> unique_constraint(:ipv6, name: :devices_account_id_ipv6_index)
|
||||
|> put_change(:last_seen_at, DateTime.utc_now())
|
||||
|> put_client_version()
|
||||
|> put_device_version()
|
||||
end
|
||||
|
||||
def finalize_upsert_changeset(%Clients.Client{} = client, ipv4, ipv6) do
|
||||
client
|
||||
def finalize_upsert_changeset(%Devices.Device{} = device, ipv4, ipv6) do
|
||||
device
|
||||
|> change()
|
||||
|> put_change(:ipv4, ipv4)
|
||||
|> put_change(:ipv6, ipv6)
|
||||
|> unique_constraint(:ipv4, name: :clients_account_id_ipv4_index)
|
||||
|> unique_constraint(:ipv6, name: :clients_account_id_ipv6_index)
|
||||
|> unique_constraint(:ipv4, name: :devices_account_id_ipv4_index)
|
||||
|> unique_constraint(:ipv6, name: :devices_account_id_ipv6_index)
|
||||
end
|
||||
|
||||
def update_changeset(%Clients.Client{} = client, attrs) do
|
||||
client
|
||||
def update_changeset(%Devices.Device{} = device, attrs) do
|
||||
device
|
||||
|> cast(attrs, @update_fields)
|
||||
|> changeset()
|
||||
|> validate_required(@required_fields)
|
||||
end
|
||||
|
||||
def delete_changeset(%Clients.Client{} = client) do
|
||||
client
|
||||
def delete_changeset(%Devices.Device{} = device) do
|
||||
device
|
||||
|> change()
|
||||
|> put_default_value(:deleted_at, DateTime.utc_now())
|
||||
end
|
||||
@@ -69,7 +69,7 @@ defmodule Domain.Clients.Client.Changeset do
|
||||
|> unique_constraint(:external_id)
|
||||
end
|
||||
|
||||
defp put_client_version(changeset) do
|
||||
defp put_device_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
|
||||
40
apps/domain/lib/domain/devices/device/query.ex
Normal file
40
apps/domain/lib/domain/devices/device/query.ex
Normal file
@@ -0,0 +1,40 @@
|
||||
defmodule Domain.Devices.Device.Query do
|
||||
use Domain, :query
|
||||
|
||||
def all do
|
||||
from(devices in Domain.Devices.Device, as: :devices)
|
||||
|> where([devices: devices], is_nil(devices.deleted_at))
|
||||
end
|
||||
|
||||
def by_id(queryable \\ all(), id) do
|
||||
where(queryable, [devices: devices], devices.id == ^id)
|
||||
end
|
||||
|
||||
def by_actor_id(queryable \\ all(), actor_id) do
|
||||
where(queryable, [devices: devices], devices.actor_id == ^actor_id)
|
||||
end
|
||||
|
||||
def by_account_id(queryable \\ all(), account_id) do
|
||||
where(queryable, [devices: devices], devices.account_id == ^account_id)
|
||||
end
|
||||
|
||||
def returning_all(queryable \\ all()) do
|
||||
select(queryable, [devices: devices], devices)
|
||||
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)
|
||||
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)
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
defmodule Domain.Clients.Presence do
|
||||
defmodule Domain.Devices.Presence do
|
||||
use Phoenix.Presence,
|
||||
otp_app: :domain,
|
||||
pubsub_server: Domain.PubSub
|
||||
@@ -199,6 +199,28 @@ defmodule Domain.Gateways do
|
||||
end
|
||||
end
|
||||
|
||||
def encode_token!(%Token{value: value} = token) when not is_nil(value) do
|
||||
body = {token.id, token.value}
|
||||
config = fetch_config!()
|
||||
key_base = Keyword.fetch!(config, :key_base)
|
||||
salt = Keyword.fetch!(config, :salt)
|
||||
Plug.Crypto.sign(key_base, salt, body)
|
||||
end
|
||||
|
||||
def authorize_gateway(encrypted_secret) do
|
||||
config = fetch_config!()
|
||||
key_base = Keyword.fetch!(config, :key_base)
|
||||
salt = Keyword.fetch!(config, :salt)
|
||||
|
||||
with {:ok, {id, secret}} <- Plug.Crypto.verify(key_base, salt, encrypted_secret),
|
||||
{:ok, token} <- use_token_by_id_and_secret(id, secret) do
|
||||
{:ok, token}
|
||||
else
|
||||
{:error, :invalid} -> {:error, :invalid_token}
|
||||
{:error, :not_found} -> {:error, :invalid_token}
|
||||
end
|
||||
end
|
||||
|
||||
def connect_gateway(%Gateway{} = gateway) do
|
||||
{:ok, _} =
|
||||
Presence.track(self(), "gateways", gateway.id, %{
|
||||
@@ -207,4 +229,12 @@ defmodule Domain.Gateways do
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def fetch_gateway_config!(%Gateway{} = _gateway) do
|
||||
Application.fetch_env!(:domain, __MODULE__)
|
||||
end
|
||||
|
||||
defp fetch_config! do
|
||||
Domain.Config.fetch_env!(:domain, __MODULE__)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ defmodule Domain.Gateways.Authorizer do
|
||||
|
||||
@impl Domain.Auth.Authorizer
|
||||
|
||||
def list_permissions_for_role(:admin) do
|
||||
def list_permissions_for_role(:account_admin_user) do
|
||||
[
|
||||
manage_gateways_permission()
|
||||
]
|
||||
|
||||
@@ -183,6 +183,28 @@ defmodule Domain.Relays do
|
||||
end
|
||||
end
|
||||
|
||||
def encode_token!(%Token{value: value} = token) when not is_nil(value) do
|
||||
body = {token.id, token.value}
|
||||
config = fetch_config!()
|
||||
key_base = Keyword.fetch!(config, :key_base)
|
||||
salt = Keyword.fetch!(config, :salt)
|
||||
Plug.Crypto.sign(key_base, salt, body)
|
||||
end
|
||||
|
||||
def authorize_relay(encrypted_secret) do
|
||||
config = fetch_config!()
|
||||
key_base = Keyword.fetch!(config, :key_base)
|
||||
salt = Keyword.fetch!(config, :salt)
|
||||
|
||||
with {:ok, {id, secret}} <- Plug.Crypto.verify(key_base, salt, encrypted_secret),
|
||||
{:ok, token} <- use_token_by_id_and_secret(id, secret) do
|
||||
{:ok, token}
|
||||
else
|
||||
{:error, :invalid} -> {:error, :invalid_token}
|
||||
{:error, :not_found} -> {:error, :invalid_token}
|
||||
end
|
||||
end
|
||||
|
||||
def connect_relay(%Relay{} = relay, secret) do
|
||||
{:ok, _} =
|
||||
Presence.track(self(), "relays", relay.id, %{
|
||||
@@ -192,4 +214,8 @@ defmodule Domain.Relays do
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp fetch_config! do
|
||||
Domain.Config.fetch_env!(:domain, __MODULE__)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ defmodule Domain.Relays.Authorizer do
|
||||
|
||||
@impl Domain.Auth.Authorizer
|
||||
|
||||
def list_permissions_for_role(:admin) do
|
||||
def list_permissions_for_role(:account_admin_user) do
|
||||
[
|
||||
manage_relays_permission()
|
||||
]
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
defmodule Domain.Release do
|
||||
# alias Domain.{ApiTokens, Users}
|
||||
require Logger
|
||||
|
||||
# @app :domain
|
||||
@repos Application.compile_env!(:domain, :ecto_repos)
|
||||
|
||||
def migrate do
|
||||
@@ -10,92 +8,4 @@ defmodule Domain.Release do
|
||||
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
|
||||
end
|
||||
end
|
||||
|
||||
# def create_admin_user do
|
||||
# start_domain_app()
|
||||
|
||||
# email = email()
|
||||
|
||||
# with {:ok, _user} <- Users.fetch_user_by_email(email) do
|
||||
# change_password(email(), default_password())
|
||||
# {:ok, user} = reset_role(email(), :admin)
|
||||
|
||||
# # Notify the user
|
||||
# Logger.info(
|
||||
# "Password for user specified by DEFAULT_ADMIN_EMAIL reset to DEFAULT_ADMIN_PASSWORD!"
|
||||
# )
|
||||
|
||||
# {:ok, user}
|
||||
# else
|
||||
# {:error, :not_found} ->
|
||||
# with {:ok, user} <-
|
||||
# Users.create_user(:admin, %{
|
||||
# email: email(),
|
||||
# password: default_password(),
|
||||
# password_confirmation: default_password()
|
||||
# }) do
|
||||
# # Notify the user
|
||||
# Logger.info(
|
||||
# "An admin user specified by DEFAULT_ADMIN_EMAIL is created with a DEFAULT_ADMIN_PASSWORD!"
|
||||
# )
|
||||
|
||||
# {:ok, user}
|
||||
# else
|
||||
# {:error, changeset} ->
|
||||
# Logger.error("Failed to create admin user: #{inspect(changeset.errors)}")
|
||||
# {:error, changeset}
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
|
||||
# def create_api_token(device \\ :stdio) do
|
||||
# start_domain_app()
|
||||
|
||||
# device
|
||||
# |> IO.write(default_admin_user() |> mint_jwt())
|
||||
# end
|
||||
|
||||
# def change_password(email, password) do
|
||||
# params = %{
|
||||
# "password" => password,
|
||||
# "password_confirmation" => password
|
||||
# }
|
||||
|
||||
# {:ok, user} = Users.fetch_user_by_email(email)
|
||||
# {:ok, _user} = Users.update_user(user, params)
|
||||
# end
|
||||
|
||||
# def reset_role(email, role) do
|
||||
# {:ok, user} = Users.fetch_user_by_email(email)
|
||||
# Users.update_user(user, %{role: role})
|
||||
# end
|
||||
|
||||
# defp email do
|
||||
# Domain.Config.fetch_env!(:domain, :admin_email)
|
||||
# end
|
||||
|
||||
# defp default_admin_user do
|
||||
# case Users.fetch_user_by_email(email()) do
|
||||
# {:ok, user} -> user
|
||||
# {:error, :not_found} -> nil
|
||||
# end
|
||||
# end
|
||||
|
||||
# defp mint_jwt(%Users.User{} = user) do
|
||||
# {:ok, api_token} = ApiTokens.create_api_token(user, %{})
|
||||
# {:ok, secret, _claims} = Web.Auth.JSON.Authentication.fz_encode_and_sign(api_token)
|
||||
# secret
|
||||
# end
|
||||
|
||||
# defp start_domain_app do
|
||||
# # Load the app
|
||||
# :ok = Application.ensure_loaded(@app)
|
||||
|
||||
# # Start the app dependencies
|
||||
# {:ok, _apps} = Application.ensure_all_started(@app)
|
||||
# end
|
||||
|
||||
# defp default_password do
|
||||
# Domain.Config.fetch_env!(:domain, :default_admin_password)
|
||||
# end
|
||||
end
|
||||
|
||||
@@ -51,7 +51,7 @@ defmodule Domain.Resources do
|
||||
# {:ok, actors} = list_authorized_actors(resource)
|
||||
# Phoenix.PubSub.broadcast(
|
||||
# Domain.PubSub,
|
||||
# "actor_client:#{subject.actor.id}",
|
||||
# "actor_device:#{subject.actor.id}",
|
||||
# {:resource_added, resource.id}
|
||||
# )
|
||||
|
||||
@@ -82,7 +82,7 @@ defmodule Domain.Resources do
|
||||
{:ok, resource} ->
|
||||
# Phoenix.PubSub.broadcast(
|
||||
# Domain.PubSub,
|
||||
# "actor_client:#{resource.actor_id}",
|
||||
# "actor_device:#{resource.actor_id}",
|
||||
# {:resource_updated, resource.id}
|
||||
# )
|
||||
|
||||
@@ -103,7 +103,7 @@ defmodule Domain.Resources do
|
||||
{:ok, resource} ->
|
||||
# Phoenix.PubSub.broadcast(
|
||||
# Domain.PubSub,
|
||||
# "actor_client:#{resource.actor_id}",
|
||||
# "actor_device:#{resource.actor_id}",
|
||||
# {:resource_removed, resource.id}
|
||||
# )
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ defmodule Domain.Resources.Authorizer do
|
||||
def manage_resources_permission, do: build(Resource, :manage)
|
||||
|
||||
@impl Domain.Auth.Authorizer
|
||||
def list_permissions_for_role(:admin) do
|
||||
def list_permissions_for_role(:account_admin_user) do
|
||||
[
|
||||
manage_resources_permission()
|
||||
]
|
||||
|
||||
@@ -12,7 +12,7 @@ defmodule Domain.Sandbox do
|
||||
sandbox.allow(metadata, Ecto.Adapters.SQL.Sandbox)
|
||||
end
|
||||
else
|
||||
def allow(_metadata) do
|
||||
def allow(_sandbox, _metadata) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,194 +1,189 @@
|
||||
defmodule Domain.Telemetry do
|
||||
@moduledoc """
|
||||
Functions for various telemetry events.
|
||||
"""
|
||||
use Supervisor
|
||||
alias Domain.Telemetry.{Timer, PostHog}
|
||||
require Logger
|
||||
# TODO: when app starts for migrations set env to disable connectivity checks and telemetry
|
||||
# defmodule Domain.Telemetry do
|
||||
# @moduledoc """
|
||||
# Functions for various telemetry events.
|
||||
# """
|
||||
# use Supervisor
|
||||
# alias Domain.Telemetry.{Timer, PostHog}
|
||||
# require Logger
|
||||
|
||||
def start_link(opts) do
|
||||
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
# def start_link(opts) do
|
||||
# Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
# end
|
||||
|
||||
def init(_opts) do
|
||||
config = Domain.Config.fetch_env!(:domain, Domain.Telemetry)
|
||||
# def init(_opts) do
|
||||
# config = Domain.Config.fetch_env!(:domain, Domain.Telemetry)
|
||||
|
||||
if Keyword.fetch!(config, :enabled) == true do
|
||||
children = [Timer]
|
||||
Supervisor.init(children, strategy: :one_for_one)
|
||||
else
|
||||
:ignore
|
||||
end
|
||||
end
|
||||
# if Keyword.fetch!(config, :enabled) == true do
|
||||
# children = [Timer]
|
||||
# Supervisor.init(children, strategy: :one_for_one)
|
||||
# else
|
||||
# :ignore
|
||||
# end
|
||||
# end
|
||||
|
||||
def create_api_token do
|
||||
PostHog.capture("add_api_token", common_fields())
|
||||
:ok
|
||||
end
|
||||
# def create_api_token do
|
||||
# PostHog.capture("add_api_token", common_fields())
|
||||
# :ok
|
||||
# end
|
||||
|
||||
def delete_api_token(api_token) do
|
||||
PostHog.capture(
|
||||
"delete_api_token",
|
||||
common_fields() ++
|
||||
[
|
||||
api_token_created_at: api_token.inserted_at
|
||||
]
|
||||
)
|
||||
# def delete_api_token(api_token) do
|
||||
# PostHog.capture(
|
||||
# "delete_api_token",
|
||||
# common_fields() ++
|
||||
# [
|
||||
# api_token_created_at: api_token.inserted_at
|
||||
# ]
|
||||
# )
|
||||
|
||||
:ok
|
||||
end
|
||||
# :ok
|
||||
# end
|
||||
|
||||
def add_device do
|
||||
PostHog.capture("add_device", common_fields())
|
||||
:ok
|
||||
end
|
||||
# def add_device do
|
||||
# PostHog.capture("add_device", common_fields())
|
||||
# :ok
|
||||
# end
|
||||
|
||||
def add_actor do
|
||||
PostHog.capture("add_actor", common_fields())
|
||||
:ok
|
||||
end
|
||||
# def add_actor do
|
||||
# PostHog.capture("add_actor", common_fields())
|
||||
# :ok
|
||||
# end
|
||||
|
||||
def add_rule do
|
||||
PostHog.capture("add_rule", common_fields())
|
||||
:ok
|
||||
end
|
||||
# def add_rule do
|
||||
# PostHog.capture("add_rule", common_fields())
|
||||
# :ok
|
||||
# end
|
||||
|
||||
def delete_device do
|
||||
PostHog.capture("delete_device", common_fields())
|
||||
:ok
|
||||
end
|
||||
# def delete_device do
|
||||
# PostHog.capture("delete_device", common_fields())
|
||||
# :ok
|
||||
# end
|
||||
|
||||
def delete_actor do
|
||||
PostHog.capture("delete_actor", common_fields())
|
||||
:ok
|
||||
end
|
||||
# def delete_actor do
|
||||
# PostHog.capture("delete_actor", common_fields())
|
||||
# :ok
|
||||
# end
|
||||
|
||||
def delete_rule do
|
||||
PostHog.capture("delete_rule", common_fields())
|
||||
:ok
|
||||
end
|
||||
# def delete_rule do
|
||||
# PostHog.capture("delete_rule", common_fields())
|
||||
# :ok
|
||||
# end
|
||||
|
||||
def login do
|
||||
PostHog.capture("login", common_fields())
|
||||
:ok
|
||||
end
|
||||
# def login do
|
||||
# PostHog.capture("login", common_fields())
|
||||
# :ok
|
||||
# end
|
||||
|
||||
def enable_actor do
|
||||
PostHog.capture("enable_actor", common_fields())
|
||||
:ok
|
||||
end
|
||||
# def enable_actor do
|
||||
# PostHog.capture("enable_actor", common_fields())
|
||||
# :ok
|
||||
# end
|
||||
|
||||
def disable_actor do
|
||||
PostHog.capture("disable_actor", common_fields())
|
||||
:ok
|
||||
end
|
||||
# def disable_actor do
|
||||
# PostHog.capture("disable_actor", common_fields())
|
||||
# :ok
|
||||
# end
|
||||
|
||||
def domain_started do
|
||||
PostHog.capture("domain_started", common_fields())
|
||||
:ok
|
||||
end
|
||||
# def domain_started do
|
||||
# PostHog.capture("domain_started", common_fields())
|
||||
# :ok
|
||||
# end
|
||||
|
||||
def ping do
|
||||
PostHog.capture("ping", ping_data())
|
||||
:ok
|
||||
end
|
||||
# def ping do
|
||||
# PostHog.capture("ping", ping_data())
|
||||
# :ok
|
||||
# end
|
||||
|
||||
# How far back to count handshakes as an active device
|
||||
# @active_device_window 86_400
|
||||
def ping_data do
|
||||
%{
|
||||
allow_unprivileged_device_management: {_, allow_unprivileged_device_management},
|
||||
allow_unprivileged_device_configuration: {_, allow_unprivileged_device_configuration},
|
||||
local_auth_enabled: {_, local_auth_enabled},
|
||||
logo: {_, logo}
|
||||
} =
|
||||
Domain.Config.fetch_source_and_configs!([
|
||||
:allow_unprivileged_device_management,
|
||||
:allow_unprivileged_device_configuration,
|
||||
:local_auth_enabled,
|
||||
:logo
|
||||
])
|
||||
# # How far back to count handshakes as an active device
|
||||
# # @active_device_window 86_400
|
||||
# def ping_data do
|
||||
# %{
|
||||
# local_auth_enabled: {_, local_auth_enabled},
|
||||
# logo: {_, logo}
|
||||
# } =
|
||||
# Domain.Config.fetch_resolved_configs_with_sources!([
|
||||
# :local_auth_enabled,
|
||||
# :logo
|
||||
# ])
|
||||
|
||||
common_fields() ++
|
||||
[
|
||||
# devices_active_within_24h: Devices.count_active_within(@active_device_window),
|
||||
# admin_count: Users.count_by_role(:admin),
|
||||
# actor_count: Users.count(),
|
||||
in_docker: in_docker?(),
|
||||
# device_count: Devices.count(),
|
||||
# max_devices_for_actors: Devices.count_maximum_for_a_actor(),
|
||||
# actors_with_mfa: MFA.count_actors_with_mfa_enabled(),
|
||||
# actors_with_mfa_totp: MFA.count_actors_with_totp_method(),
|
||||
unprivileged_device_management: allow_unprivileged_device_management,
|
||||
unprivileged_device_configuration: allow_unprivileged_device_configuration,
|
||||
local_authentication: local_auth_enabled,
|
||||
# outbound_email: Web.Mailer.active?(),
|
||||
external_database:
|
||||
external_database?(Map.new(Domain.Config.fetch_env!(:domain, Domain.Repo))),
|
||||
logo_type: Domain.Config.Logo.type(logo)
|
||||
]
|
||||
end
|
||||
# common_fields() ++
|
||||
# [
|
||||
# # devices_active_within_24h: Devices.count_active_within(@active_device_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(),
|
||||
# # actors_with_mfa: MFA.count_actors_with_mfa_enabled(),
|
||||
# # actors_with_mfa_totp: MFA.count_actors_with_totp_method(),
|
||||
# local_authentication: local_auth_enabled,
|
||||
# # outbound_email: Web.Mailer.active?(),
|
||||
# external_database:
|
||||
# external_database?(Map.new(Domain.Config.fetch_env!(:domain, Domain.Repo))),
|
||||
# logo_type: Domain.Config.Logo.type(logo)
|
||||
# ]
|
||||
# end
|
||||
|
||||
defp in_docker? do
|
||||
File.exists?("/.dockerenv")
|
||||
end
|
||||
# defp in_docker? do
|
||||
# File.exists?("/.dockerenv")
|
||||
# end
|
||||
|
||||
defp common_fields do
|
||||
[
|
||||
distinct_id: id(),
|
||||
fqdn: fqdn(),
|
||||
version: version(),
|
||||
kernel_version: "#{os_type()} #{os_version()}"
|
||||
]
|
||||
end
|
||||
# defp common_fields do
|
||||
# [
|
||||
# distinct_id: id(),
|
||||
# fqdn: fqdn(),
|
||||
# version: version(),
|
||||
# kernel_version: "#{os_type()} #{os_version()}"
|
||||
# ]
|
||||
# end
|
||||
|
||||
def id do
|
||||
Domain.Config.fetch_env!(:domain, __MODULE__)
|
||||
|> Keyword.fetch!(:id)
|
||||
end
|
||||
# def id do
|
||||
# Domain.Config.fetch_env!(:domain, __MODULE__)
|
||||
# |> Keyword.fetch!(:id)
|
||||
# end
|
||||
|
||||
defp fqdn do
|
||||
:web
|
||||
|> Domain.Config.fetch_env!(Web.Endpoint)
|
||||
|> Keyword.get(:url)
|
||||
|> Keyword.get(:host)
|
||||
end
|
||||
# defp fqdn do
|
||||
# :web
|
||||
# |> Domain.Config.fetch_env!(Web.Endpoint)
|
||||
# |> Keyword.get(:url)
|
||||
# |> Keyword.get(:host)
|
||||
# end
|
||||
|
||||
defp version do
|
||||
Application.spec(:domain, :vsn) |> to_string()
|
||||
end
|
||||
# defp version do
|
||||
# Application.spec(:domain, :vsn) |> to_string()
|
||||
# end
|
||||
|
||||
defp external_database?(repo_conf) when is_map_key(repo_conf, :hostname) do
|
||||
is_external_db?(repo_conf.hostname)
|
||||
end
|
||||
# defp external_database?(repo_conf) when is_map_key(repo_conf, :hostname) do
|
||||
# is_external_db?(repo_conf.hostname)
|
||||
# end
|
||||
|
||||
defp external_database?(repo_conf) when is_map_key(repo_conf, :url) do
|
||||
%{host: host} = URI.parse(repo_conf.url)
|
||||
# defp external_database?(repo_conf) when is_map_key(repo_conf, :url) do
|
||||
# %{host: host} = URI.parse(repo_conf.url)
|
||||
|
||||
is_external_db?(host)
|
||||
end
|
||||
# is_external_db?(host)
|
||||
# end
|
||||
|
||||
defp is_external_db?(host) do
|
||||
host != "localhost" && host != "127.0.0.1"
|
||||
end
|
||||
# defp is_external_db?(host) do
|
||||
# host != "localhost" && host != "127.0.0.1"
|
||||
# end
|
||||
|
||||
defp os_type do
|
||||
case :os.type() do
|
||||
{:unix, type} ->
|
||||
"#{type}"
|
||||
# defp os_type do
|
||||
# case :os.type() do
|
||||
# {:unix, type} ->
|
||||
# "#{type}"
|
||||
|
||||
_ ->
|
||||
"other"
|
||||
end
|
||||
end
|
||||
# _ ->
|
||||
# "other"
|
||||
# end
|
||||
# end
|
||||
|
||||
defp os_version do
|
||||
case :os.version() do
|
||||
{major, minor, patch} ->
|
||||
"#{major}.#{minor}.#{patch}"
|
||||
# defp os_version do
|
||||
# case :os.version() do
|
||||
# {major, minor, patch} ->
|
||||
# "#{major}.#{minor}.#{patch}"
|
||||
|
||||
_ ->
|
||||
"0.0.0"
|
||||
end
|
||||
end
|
||||
end
|
||||
# _ ->
|
||||
# "0.0.0"
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
defmodule Domain.Telemetry.Timer do
|
||||
use GenServer
|
||||
alias Domain.Telemetry
|
||||
# defmodule Domain.Telemetry.Timer do
|
||||
# use GenServer
|
||||
# alias Domain.Telemetry
|
||||
|
||||
@initial_delay 60 * 1_000
|
||||
@interval 43_200
|
||||
# @initial_delay 60 * 1_000
|
||||
# @interval 43_200
|
||||
|
||||
def start_link(opts) do
|
||||
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
# def start_link(opts) do
|
||||
# GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
# end
|
||||
|
||||
@impl GenServer
|
||||
def init(_opts) do
|
||||
# Send ping after 1 minute
|
||||
:timer.send_after(@initial_delay, :start_interval)
|
||||
# @impl GenServer
|
||||
# def init(_opts) do
|
||||
# # Send ping after 1 minute
|
||||
# :timer.send_after(@initial_delay, :start_interval)
|
||||
|
||||
{:ok, %{}}
|
||||
end
|
||||
# {:ok, %{}}
|
||||
# end
|
||||
|
||||
@impl GenServer
|
||||
def handle_info(:start_interval, state) do
|
||||
# Continue pinging twice a day
|
||||
:timer.send_interval(@interval * 1_000, :tick)
|
||||
# @impl GenServer
|
||||
# def handle_info(:start_interval, state) do
|
||||
# # Continue pinging twice a day
|
||||
# :timer.send_interval(@interval * 1_000, :tick)
|
||||
|
||||
:ok = Telemetry.ping()
|
||||
# :ok = Telemetry.ping()
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
# {:noreply, state}
|
||||
# end
|
||||
|
||||
@impl GenServer
|
||||
def handle_info(:tick, state) do
|
||||
:ok = Telemetry.ping()
|
||||
# @impl GenServer
|
||||
# def handle_info(:tick, state) do
|
||||
# :ok = Telemetry.ping()
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
# {:noreply, state}
|
||||
# end
|
||||
# end
|
||||
|
||||
@@ -296,6 +296,16 @@ defmodule Domain.Validator do
|
||||
end
|
||||
end
|
||||
|
||||
def validate_datetime(changeset, field, greater_than: greater_than) do
|
||||
validate_change(changeset, field, fn _current_field, value ->
|
||||
if DateTime.compare(value, greater_than) == :gt do
|
||||
[]
|
||||
else
|
||||
[{field, "must be greater than #{inspect(greater_than)}"}]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Applies a validation function for every elements of the list.
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
defmodule Domain.Repo.Migrations.CreateClients do
|
||||
defmodule Domain.Repo.Migrations.RecreateDevices do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:clients, primary_key: false) do
|
||||
drop(table(:devices))
|
||||
|
||||
create table(:devices, primary_key: false) do
|
||||
add(:id, :uuid, primary_key: true)
|
||||
add(:external_id, :string, null: false)
|
||||
|
||||
@@ -40,28 +42,28 @@ defmodule Domain.Repo.Migrations.CreateClients do
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
end
|
||||
|
||||
# Used to list clients for a user
|
||||
create(index(:clients, [:user_id], where: "deleted_at IS NULL"))
|
||||
# Used to list devices for a user
|
||||
create(index(:devices, [:user_id], where: "deleted_at IS NULL"))
|
||||
|
||||
# Used for upserts
|
||||
create(
|
||||
index(:clients, [:account_id, :user_id, :external_id],
|
||||
index(:devices, [:account_id, :user_id, :external_id],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL"
|
||||
)
|
||||
)
|
||||
|
||||
# Used to enforce unique IPv4 and IPv6 addresses.
|
||||
create(index(:clients, [:account_id, :ipv4], unique: true, where: "deleted_at IS NULL"))
|
||||
create(index(:clients, [:account_id, :ipv6], unique: true, where: "deleted_at IS NULL"))
|
||||
create(index(:devices, [:account_id, :ipv4], unique: true, where: "deleted_at IS NULL"))
|
||||
create(index(:devices, [:account_id, :ipv6], unique: true, where: "deleted_at IS NULL"))
|
||||
|
||||
# Used to enforce unique names and public keys.
|
||||
create(
|
||||
index(:clients, [:account_id, :user_id, :name], unique: true, where: "deleted_at IS NULL")
|
||||
index(:devices, [:account_id, :user_id, :name], unique: true, where: "deleted_at IS NULL")
|
||||
)
|
||||
|
||||
create(
|
||||
index(:clients, [:account_id, :user_id, :public_key],
|
||||
index(:devices, [:account_id, :user_id, :public_key],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL"
|
||||
)
|
||||
@@ -1,7 +0,0 @@
|
||||
defmodule Domain.Repo.Migrations.RemoveDevices do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
drop(table(:devices))
|
||||
end
|
||||
end
|
||||
@@ -11,30 +11,30 @@ defmodule Domain.Repo.Migrations.RemoveUsers do
|
||||
|
||||
create(index(:api_tokens, [:actor_id]))
|
||||
|
||||
## Clients
|
||||
## Devices
|
||||
|
||||
alter table(:clients) do
|
||||
alter table(:devices) do
|
||||
remove(:user_id, references(:users, type: :binary_id), null: false)
|
||||
add(:actor_id, references(:actors, type: :binary_id), null: false)
|
||||
add(:identity_id, references(:auth_identities, type: :binary_id), null: false)
|
||||
end
|
||||
|
||||
create(
|
||||
index(:clients, [:account_id, :actor_id, :external_id],
|
||||
index(:devices, [:account_id, :actor_id, :external_id],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL"
|
||||
)
|
||||
)
|
||||
|
||||
create(
|
||||
index(:clients, [:account_id, :actor_id, :name],
|
||||
index(:devices, [:account_id, :actor_id, :name],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL"
|
||||
)
|
||||
)
|
||||
|
||||
create(
|
||||
index(:clients, [:account_id, :actor_id, :public_key],
|
||||
index(:devices, [:account_id, :actor_id, :public_key],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
defmodule Domain.Repo.Migrations.DropApiTokens do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
drop(table(:api_tokens))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
defmodule Domain.Repo.Migrations.RemoveActorsRole do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:actors) do
|
||||
remove(:role)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
defmodule Domain.Repo.Migrations.CleanupConfigurations do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
execute("delete from configurations")
|
||||
|
||||
alter table(:configurations) do
|
||||
remove(:allow_unprivileged_device_management)
|
||||
remove(:allow_unprivileged_device_configuration)
|
||||
remove(:local_auth_enabled)
|
||||
remove(:disable_vpn_on_oidc_error)
|
||||
remove(:default_client_persistent_keepalive)
|
||||
remove(:default_client_mtu)
|
||||
remove(:default_client_endpoint)
|
||||
remove(:default_client_dns)
|
||||
remove(:default_client_allowed_ips)
|
||||
remove(:vpn_session_duration)
|
||||
|
||||
add(:devices_upstream_dns, {:array, :string}, default: [])
|
||||
|
||||
add(:account_id, references(:accounts, type: :binary_id), null: false)
|
||||
end
|
||||
|
||||
create(index(:configurations, [:account_id], unique: true))
|
||||
end
|
||||
end
|
||||
@@ -35,14 +35,12 @@ admin_actor_email = "firezone@localhost"
|
||||
|
||||
{:ok, unprivileged_actor} =
|
||||
Actors.create_actor(email_provider, unprivileged_actor_email, %{
|
||||
type: :user,
|
||||
role: :unprivileged
|
||||
type: :account_user
|
||||
})
|
||||
|
||||
{:ok, admin_actor} =
|
||||
Actors.create_actor(email_provider, admin_actor_email, %{
|
||||
type: :user,
|
||||
role: :admin
|
||||
type: :account_admin_user
|
||||
})
|
||||
|
||||
{:ok, _unprivileged_actor_userpass_identity} =
|
||||
@@ -70,11 +68,12 @@ admin_subject =
|
||||
|
||||
IO.puts("Created users: ")
|
||||
|
||||
for {role, login, password, email_token} <- [
|
||||
{:unprivileged, unprivileged_actor_email, "Firezone1234", unprivileged_actor_token},
|
||||
{:admin, admin_actor_email, "Firezone1234", admin_actor_token}
|
||||
for {type, login, password, email_token} <- [
|
||||
{unprivileged_actor.type, unprivileged_actor_email, "Firezone1234",
|
||||
unprivileged_actor_token},
|
||||
{admin_actor.type, admin_actor_email, "Firezone1234", admin_actor_token}
|
||||
] do
|
||||
IO.puts(" #{login}, #{role}, password: #{password}, email token: #{email_token}")
|
||||
IO.puts(" #{login}, #{type}, password: #{password}, email token: #{email_token}")
|
||||
end
|
||||
|
||||
IO.puts("")
|
||||
|
||||
@@ -5,10 +5,10 @@ defmodule Domain.ActorsTest do
|
||||
alias Domain.Actors
|
||||
alias Domain.{AccountsFixtures, AuthFixtures, ActorsFixtures}
|
||||
|
||||
describe "fetch_count_by_role/0" do
|
||||
describe "fetch_count_by_type/0" do
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -19,37 +19,37 @@ defmodule Domain.ActorsTest do
|
||||
}
|
||||
end
|
||||
|
||||
test "returns correct count of not deleted actors by role", %{
|
||||
test "returns correct count of not deleted actors by type", %{
|
||||
account: account,
|
||||
subject: subject
|
||||
} do
|
||||
assert fetch_count_by_role(:admin, subject) == 1
|
||||
assert fetch_count_by_role(:unprivileged, subject) == 0
|
||||
assert fetch_count_by_type(:account_admin_user, subject) == 1
|
||||
assert fetch_count_by_type(:account_user, subject) == 0
|
||||
|
||||
ActorsFixtures.create_actor(role: :admin)
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
ActorsFixtures.create_actor(type: :account_admin_user)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
assert {:ok, _actor} = delete_actor(actor, subject)
|
||||
assert fetch_count_by_role(:admin, subject) == 1
|
||||
assert fetch_count_by_role(:unprivileged, subject) == 0
|
||||
assert fetch_count_by_type(:account_admin_user, subject) == 1
|
||||
assert fetch_count_by_type(:account_user, subject) == 0
|
||||
|
||||
ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
assert fetch_count_by_role(:admin, subject) == 2
|
||||
assert fetch_count_by_role(:unprivileged, subject) == 0
|
||||
ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
assert fetch_count_by_type(:account_admin_user, subject) == 2
|
||||
assert fetch_count_by_type(:account_user, subject) == 0
|
||||
|
||||
ActorsFixtures.create_actor(role: :unprivileged)
|
||||
ActorsFixtures.create_actor(role: :unprivileged, account: account)
|
||||
assert fetch_count_by_role(:admin, subject) == 2
|
||||
assert fetch_count_by_role(:unprivileged, subject) == 1
|
||||
ActorsFixtures.create_actor(type: :account_user)
|
||||
ActorsFixtures.create_actor(type: :account_user, account: account)
|
||||
assert fetch_count_by_type(:account_admin_user, subject) == 2
|
||||
assert fetch_count_by_type(:account_user, subject) == 1
|
||||
|
||||
for _ <- 1..5, do: ActorsFixtures.create_actor(role: :unprivileged, account: account)
|
||||
assert fetch_count_by_role(:admin, subject) == 2
|
||||
assert fetch_count_by_role(:unprivileged, subject) == 6
|
||||
for _ <- 1..5, do: ActorsFixtures.create_actor(type: :account_user, account: account)
|
||||
assert fetch_count_by_type(:account_admin_user, subject) == 2
|
||||
assert fetch_count_by_type(:account_user, subject) == 6
|
||||
end
|
||||
|
||||
test "returns error when subject can not view actors", %{subject: subject} do
|
||||
subject = AuthFixtures.remove_permissions(subject)
|
||||
|
||||
assert fetch_count_by_role(:foo, subject) ==
|
||||
assert fetch_count_by_type(:foo, subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[missing_permissions: [Actors.Authorizer.manage_actors_permission()]]}}
|
||||
@@ -69,7 +69,7 @@ defmodule Domain.ActorsTest do
|
||||
|
||||
test "returns own actor" do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -79,7 +79,7 @@ defmodule Domain.ActorsTest do
|
||||
|
||||
test "returns non own actor" do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -91,7 +91,7 @@ defmodule Domain.ActorsTest do
|
||||
|
||||
test "returns error when actor is in another account" do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -121,7 +121,7 @@ defmodule Domain.ActorsTest do
|
||||
end
|
||||
|
||||
test "returns actor" do
|
||||
actor = ActorsFixtures.create_actor(role: :admin)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user)
|
||||
assert {:ok, returned_actor} = fetch_actor_by_id(actor.id)
|
||||
assert returned_actor.id == actor.id
|
||||
end
|
||||
@@ -141,7 +141,7 @@ defmodule Domain.ActorsTest do
|
||||
end
|
||||
|
||||
test "returns actor" do
|
||||
actor = ActorsFixtures.create_actor(role: :admin)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user)
|
||||
assert returned_actor = fetch_actor_by_id!(actor.id)
|
||||
assert returned_actor.id == actor.id
|
||||
end
|
||||
@@ -166,11 +166,11 @@ defmodule Domain.ActorsTest do
|
||||
assert list_actors(subject, hydrate: []) == {:ok, []}
|
||||
end
|
||||
|
||||
test "returns list of actors in all roles" do
|
||||
test "returns list of actors in all types" do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor1 = ActorsFixtures.create_actor(account: account, role: :admin)
|
||||
actor2 = ActorsFixtures.create_actor(account: account, role: :unprivileged)
|
||||
ActorsFixtures.create_actor(role: :unprivileged)
|
||||
actor1 = ActorsFixtures.create_actor(account: account, type: :account_admin_user)
|
||||
actor2 = ActorsFixtures.create_actor(account: account, type: :account_user)
|
||||
ActorsFixtures.create_actor(type: :account_user)
|
||||
|
||||
identity1 = AuthFixtures.create_identity(account: account, actor: actor1)
|
||||
subject = AuthFixtures.create_subject(identity1)
|
||||
@@ -212,7 +212,6 @@ defmodule Domain.ActorsTest do
|
||||
refute changeset.valid?
|
||||
|
||||
assert errors_on(changeset) == %{
|
||||
role: ["can't be blank"],
|
||||
type: ["can't be blank"]
|
||||
}
|
||||
end
|
||||
@@ -221,13 +220,12 @@ defmodule Domain.ActorsTest do
|
||||
provider: provider,
|
||||
provider_identifier: provider_identifier
|
||||
} do
|
||||
attrs = ActorsFixtures.actor_attrs(role: :foo, type: :bar)
|
||||
attrs = ActorsFixtures.actor_attrs(type: :foo)
|
||||
|
||||
assert {:error, changeset} = create_actor(provider, provider_identifier, attrs)
|
||||
refute changeset.valid?
|
||||
|
||||
assert errors_on(changeset) == %{
|
||||
role: ["is invalid"],
|
||||
type: ["is invalid"]
|
||||
}
|
||||
end
|
||||
@@ -242,21 +240,10 @@ defmodule Domain.ActorsTest do
|
||||
assert errors_on(changeset) == %{provider_identifier: ["has already been taken"]}
|
||||
end
|
||||
|
||||
test "creates an actor in given role", %{
|
||||
provider: provider
|
||||
} do
|
||||
for role <- [:admin, :unprivileged] do
|
||||
attrs = ActorsFixtures.actor_attrs(role: role)
|
||||
provider_identifier = AuthFixtures.random_provider_identifier(provider)
|
||||
assert {:ok, actor} = create_actor(provider, provider_identifier, attrs)
|
||||
assert actor.role == role
|
||||
end
|
||||
end
|
||||
|
||||
test "creates an actor in given type", %{
|
||||
provider: provider
|
||||
} do
|
||||
for type <- [:user, :service_account] do
|
||||
for type <- [:account_user, :account_admin_user, :service_account] do
|
||||
attrs = ActorsFixtures.actor_attrs(type: type)
|
||||
provider_identifier = AuthFixtures.random_provider_identifier(provider)
|
||||
assert {:ok, actor} = create_actor(provider, provider_identifier, attrs)
|
||||
@@ -273,7 +260,7 @@ defmodule Domain.ActorsTest do
|
||||
assert {:ok, actor} = create_actor(provider, provider_identifier, attrs)
|
||||
|
||||
assert actor.type == attrs.type
|
||||
assert actor.role == attrs.role
|
||||
assert actor.type == attrs.type
|
||||
assert is_nil(actor.disabled_at)
|
||||
assert is_nil(actor.deleted_at)
|
||||
|
||||
@@ -310,7 +297,7 @@ defmodule Domain.ActorsTest do
|
||||
provider: provider,
|
||||
provider_identifier: provider_identifier
|
||||
} do
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
|
||||
subject =
|
||||
AuthFixtures.create_identity(account: account, actor: actor)
|
||||
@@ -336,7 +323,7 @@ defmodule Domain.ActorsTest do
|
||||
provider: provider,
|
||||
provider_identifier: provider_identifier
|
||||
} do
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
|
||||
subject =
|
||||
AuthFixtures.create_identity(account: account, actor: actor)
|
||||
@@ -354,22 +341,22 @@ defmodule Domain.ActorsTest do
|
||||
MapSet.difference(admin_permissions, MapSet.new(required_permissions))
|
||||
|> MapSet.to_list()
|
||||
|
||||
attrs = %{type: :user, role: :admin}
|
||||
attrs = %{type: :account_admin_user}
|
||||
|
||||
assert create_actor(provider, provider_identifier, attrs, subject) ==
|
||||
{:error, {:unauthorized, privilege_escalation: missing_permissions}}
|
||||
|
||||
attrs = %{"type" => "user", "role" => "admin"}
|
||||
attrs = %{"type" => "account_admin_user"}
|
||||
|
||||
assert create_actor(provider, provider_identifier, attrs, subject) ==
|
||||
{:error, {:unauthorized, privilege_escalation: missing_permissions}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "change_actor_role/3" do
|
||||
describe "change_actor_type/3" do
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -380,25 +367,29 @@ defmodule Domain.ActorsTest do
|
||||
}
|
||||
end
|
||||
|
||||
test "allows admin to change other actors role", %{account: account, subject: subject} do
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
assert {:ok, %{role: :unprivileged}} = change_actor_role(actor, :unprivileged, subject)
|
||||
assert {:ok, %{role: :admin}} = change_actor_role(actor, :admin, subject)
|
||||
test "allows admin to change other actors type", %{account: account, subject: subject} do
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
assert {:ok, %{type: :account_user}} = change_actor_type(actor, :account_user, subject)
|
||||
|
||||
actor = ActorsFixtures.create_actor(role: :unprivileged, account: account)
|
||||
assert {:ok, %{role: :unprivileged}} = change_actor_role(actor, :unprivileged, subject)
|
||||
assert {:ok, %{role: :admin}} = change_actor_role(actor, :admin, subject)
|
||||
assert {:ok, %{type: :account_admin_user}} =
|
||||
change_actor_type(actor, :account_admin_user, subject)
|
||||
|
||||
actor = ActorsFixtures.create_actor(type: :account_user, account: account)
|
||||
assert {:ok, %{type: :account_user}} = change_actor_type(actor, :account_user, subject)
|
||||
|
||||
assert {:ok, %{type: :account_admin_user}} =
|
||||
change_actor_type(actor, :account_admin_user, subject)
|
||||
end
|
||||
|
||||
test "returns error when subject can not manage roles", %{account: account} do
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
test "returns error when subject can not manage types", %{account: account} do
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
|
||||
subject =
|
||||
AuthFixtures.create_identity(account: account, actor: actor)
|
||||
|> AuthFixtures.create_subject()
|
||||
|> AuthFixtures.remove_permissions()
|
||||
|
||||
assert change_actor_role(actor, :foo, subject) ==
|
||||
assert change_actor_type(actor, :foo, subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[missing_permissions: [Actors.Authorizer.manage_actors_permission()]]}}
|
||||
@@ -408,8 +399,8 @@ defmodule Domain.ActorsTest do
|
||||
describe "disable_actor/2" do
|
||||
test "disables a given actor" do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
other_actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
other_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -425,7 +416,7 @@ defmodule Domain.ActorsTest do
|
||||
|
||||
test "returns error when trying to disable the last admin actor" do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(account: account, role: :admin)
|
||||
actor = ActorsFixtures.create_actor(account: account, type: :account_admin_user)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -434,8 +425,8 @@ defmodule Domain.ActorsTest do
|
||||
|
||||
test "last admin check ignores admins in other accounts" do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
ActorsFixtures.create_actor(role: :admin)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
ActorsFixtures.create_actor(type: :account_admin_user)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -444,8 +435,8 @@ defmodule Domain.ActorsTest do
|
||||
|
||||
test "last admin check ignores disabled admins" do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
other_actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
other_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
{:ok, _other_actor} = disable_actor(other_actor, subject)
|
||||
@@ -462,8 +453,8 @@ defmodule Domain.ActorsTest do
|
||||
|
||||
account = AccountsFixtures.create_account()
|
||||
|
||||
actor_one = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor_two = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor_one = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
actor_two = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
|
||||
subject_one = AuthFixtures.create_subject(actor_one)
|
||||
subject_two = AuthFixtures.create_subject(actor_two)
|
||||
@@ -480,8 +471,8 @@ defmodule Domain.ActorsTest do
|
||||
|
||||
test "does not do anything when an actor is disabled twice" do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
other_actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
other_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -492,8 +483,8 @@ defmodule Domain.ActorsTest do
|
||||
|
||||
test "does not allow to disable actors in other accounts" do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
other_actor = ActorsFixtures.create_actor(role: :admin)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
other_actor = ActorsFixtures.create_actor(type: :account_admin_user)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -502,7 +493,7 @@ defmodule Domain.ActorsTest do
|
||||
|
||||
test "returns error when subject can not disable actors" do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
|
||||
subject =
|
||||
AuthFixtures.create_identity(account: account, actor: actor)
|
||||
@@ -519,8 +510,8 @@ defmodule Domain.ActorsTest do
|
||||
describe "enable_actor/2" do
|
||||
test "enables a given actor" do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
other_actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
other_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -538,8 +529,8 @@ defmodule Domain.ActorsTest do
|
||||
|
||||
test "does not do anything when an actor is already enabled" do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
other_actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
other_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -552,8 +543,8 @@ defmodule Domain.ActorsTest do
|
||||
|
||||
test "does not allow to enable actors in other accounts" do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
other_actor = ActorsFixtures.create_actor(role: :admin)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
other_actor = ActorsFixtures.create_actor(type: :account_admin_user)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -562,7 +553,7 @@ defmodule Domain.ActorsTest do
|
||||
|
||||
test "returns error when subject can not enable actors" do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
|
||||
subject =
|
||||
AuthFixtures.create_identity(account: account, actor: actor)
|
||||
@@ -579,8 +570,8 @@ defmodule Domain.ActorsTest do
|
||||
describe "delete_actor/2" do
|
||||
test "deletes a given actor" do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
other_actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
other_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -596,7 +587,7 @@ defmodule Domain.ActorsTest do
|
||||
|
||||
test "returns error when trying to delete the last admin actor" do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -605,8 +596,8 @@ defmodule Domain.ActorsTest do
|
||||
|
||||
test "last admin check ignores admins in other accounts" do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
ActorsFixtures.create_actor(role: :admin)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
ActorsFixtures.create_actor(type: :account_admin_user)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -615,8 +606,8 @@ defmodule Domain.ActorsTest do
|
||||
|
||||
test "last admin check ignores disabled admins" do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
other_actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
other_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
{:ok, _other_actor} = disable_actor(other_actor, subject)
|
||||
@@ -633,8 +624,8 @@ defmodule Domain.ActorsTest do
|
||||
|
||||
account = AccountsFixtures.create_account()
|
||||
|
||||
actor_one = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor_two = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor_one = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
actor_two = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
|
||||
subject_one = AuthFixtures.create_subject(actor_one)
|
||||
subject_two = AuthFixtures.create_subject(actor_two)
|
||||
@@ -651,8 +642,8 @@ defmodule Domain.ActorsTest do
|
||||
|
||||
test "does not allow to delete an actor twice" do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
other_actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
other_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -662,8 +653,8 @@ defmodule Domain.ActorsTest do
|
||||
|
||||
test "does not allow to delete actors in other accounts" do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
other_actor = ActorsFixtures.create_actor(role: :admin)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
other_actor = ActorsFixtures.create_actor(type: :account_admin_user)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -672,7 +663,7 @@ defmodule Domain.ActorsTest do
|
||||
|
||||
test "returns error when subject can not delete actors" do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
|
||||
subject =
|
||||
AuthFixtures.create_identity(account: account, actor: actor)
|
||||
|
||||
@@ -1,439 +0,0 @@
|
||||
# defmodule Domain.ApiTokensTest do
|
||||
# use Domain.DataCase, async: true
|
||||
# import Domain.ApiTokens
|
||||
# alias Domain.ApiTokens.{ApiToken, Authorizer}
|
||||
# alias Domain.{AccountsFixtures, AuthFixtures, ActorsFixtures, ApiTokensFixtures}
|
||||
|
||||
# setup do
|
||||
# account = AccountsFixtures.create_account()
|
||||
# actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
# identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
# subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
# %{actor: actor, subject: subject}
|
||||
# end
|
||||
|
||||
# describe "count_by_actor_id/1" do
|
||||
# test "returns 0 when no actor exist" do
|
||||
# assert count_by_actor_id(Ecto.UUID.generate()) == 0
|
||||
# end
|
||||
|
||||
# test "returns the number of api_tokens for a actor" do
|
||||
# actor = ActorsFixtures.create_actor(role: :admin)
|
||||
# assert count_by_actor_id(actor.id) == 0
|
||||
|
||||
# ApiTokensFixtures.create_api_token(actor: actor)
|
||||
# assert count_by_actor_id(actor.id) == 1
|
||||
|
||||
# ApiTokensFixtures.create_api_token(actor: actor)
|
||||
# assert count_by_actor_id(actor.id) == 2
|
||||
# end
|
||||
# end
|
||||
|
||||
# describe "list_api_tokens/1" do
|
||||
# test "returns empty list when there are no api tokens", %{subject: subject} do
|
||||
# assert list_api_tokens(subject) == {:ok, []}
|
||||
# end
|
||||
|
||||
# test "does not return api tokens when actor has no access to them", %{subject: subject} do
|
||||
# subject =
|
||||
# subject
|
||||
# |> AuthFixtures.remove_permissions()
|
||||
# |> AuthFixtures.add_permission(Authorizer.manage_own_api_tokens_permission())
|
||||
|
||||
# ApiTokensFixtures.create_api_token()
|
||||
# assert list_api_tokens(subject) == {:ok, []}
|
||||
# end
|
||||
|
||||
# test "returns other actor api tokens when subject has manage permission", %{subject: subject} do
|
||||
# subject =
|
||||
# subject
|
||||
# |> AuthFixtures.remove_permissions()
|
||||
# |> AuthFixtures.add_permission(Authorizer.manage_own_api_tokens_permission())
|
||||
# |> AuthFixtures.add_permission(Authorizer.manage_api_tokens_permission())
|
||||
|
||||
# api_token = ApiTokensFixtures.create_api_token()
|
||||
|
||||
# assert list_api_tokens(subject) == {:ok, [api_token]}
|
||||
# end
|
||||
|
||||
# test "returns all api tokens for a actor", %{actor: actor, subject: subject} do
|
||||
# api_token = ApiTokensFixtures.create_api_token(actor: actor)
|
||||
# assert list_api_tokens(subject) == {:ok, [api_token]}
|
||||
|
||||
# ApiTokensFixtures.create_api_token(actor: actor)
|
||||
# assert {:ok, api_tokens} = list_api_tokens(subject)
|
||||
# assert length(api_tokens) == 2
|
||||
# end
|
||||
|
||||
# test "returns error when subject has no permission to view api tokens", %{subject: subject} do
|
||||
# subject = AuthFixtures.remove_permissions(subject)
|
||||
|
||||
# assert list_api_tokens(subject) ==
|
||||
# {:error,
|
||||
# {:unauthorized,
|
||||
# [
|
||||
# missing_permissions: [
|
||||
# {:one_of,
|
||||
# [
|
||||
# Authorizer.manage_api_tokens_permission(),
|
||||
# Authorizer.manage_own_api_tokens_permission()
|
||||
# ]}
|
||||
# ]
|
||||
# ]}}
|
||||
# end
|
||||
# end
|
||||
|
||||
# describe "list_api_tokens_by_actor_id/2" do
|
||||
# test "returns api token that belongs to another actor with manage permission", %{
|
||||
# subject: subject
|
||||
# } do
|
||||
# api_token = ApiTokensFixtures.create_api_token()
|
||||
|
||||
# subject =
|
||||
# subject
|
||||
# |> AuthFixtures.remove_permissions()
|
||||
# |> AuthFixtures.add_permission(Authorizer.manage_own_api_tokens_permission())
|
||||
# |> AuthFixtures.add_permission(Authorizer.manage_api_tokens_permission())
|
||||
|
||||
# assert list_api_tokens_by_actor_id(api_token.actor_id, subject) ==
|
||||
# {:ok, [api_token]}
|
||||
# end
|
||||
|
||||
# test "does not return api token that belongs to another actor with manage_own permission", %{
|
||||
# subject: subject
|
||||
# } do
|
||||
# api_token = ApiTokensFixtures.create_api_token()
|
||||
|
||||
# subject =
|
||||
# subject
|
||||
# |> AuthFixtures.remove_permissions()
|
||||
# |> AuthFixtures.add_permission(Authorizer.manage_own_api_tokens_permission())
|
||||
|
||||
# assert list_api_tokens_by_actor_id(api_token.actor_id, subject) == {:ok, []}
|
||||
# end
|
||||
|
||||
# test "returns api tokens scoped to a actor", %{actor: actor, subject: subject} do
|
||||
# ApiTokensFixtures.create_api_token(actor: actor)
|
||||
# ApiTokensFixtures.create_api_token(actor: actor)
|
||||
|
||||
# assert {:ok, api_tokens} = list_api_tokens_by_actor_id(actor.id, subject)
|
||||
# assert length(api_tokens) == 2
|
||||
# end
|
||||
|
||||
# test "returns error when api token does not exist", %{subject: subject} do
|
||||
# assert list_api_tokens_by_actor_id(Ecto.UUID.generate(), subject) == {:ok, []}
|
||||
# end
|
||||
|
||||
# test "returns error when actor ID is not a valid UUID", %{subject: subject} do
|
||||
# assert list_api_tokens_by_actor_id("foo", subject) == {:ok, []}
|
||||
# end
|
||||
|
||||
# test "returns error when subject has no permission to view api tokens", %{subject: subject} do
|
||||
# subject = AuthFixtures.remove_permissions(subject)
|
||||
|
||||
# assert list_api_tokens_by_actor_id(Ecto.UUID.generate(), subject) ==
|
||||
# {:error,
|
||||
# {:unauthorized,
|
||||
# [
|
||||
# missing_permissions: [
|
||||
# {:one_of,
|
||||
# [
|
||||
# Authorizer.manage_api_tokens_permission(),
|
||||
# Authorizer.manage_own_api_tokens_permission()
|
||||
# ]}
|
||||
# ]
|
||||
# ]}}
|
||||
# end
|
||||
# end
|
||||
|
||||
# describe "fetch_api_token_by_id/2" do
|
||||
# test "returns error when UUID is invalid", %{subject: subject} do
|
||||
# assert fetch_api_token_by_id("foo", subject) == {:error, :not_found}
|
||||
# end
|
||||
|
||||
# test "returns api token by id", %{actor: actor, subject: subject} do
|
||||
# api_token = ApiTokensFixtures.create_api_token(actor: actor)
|
||||
# assert fetch_api_token_by_id(api_token.id, subject) == {:ok, api_token}
|
||||
# end
|
||||
|
||||
# test "returns api token that belongs to another actor with manage permission", %{
|
||||
# subject: subject
|
||||
# } do
|
||||
# api_token = ApiTokensFixtures.create_api_token()
|
||||
|
||||
# subject =
|
||||
# subject
|
||||
# |> AuthFixtures.remove_permissions()
|
||||
# |> AuthFixtures.add_permission(Authorizer.manage_own_api_tokens_permission())
|
||||
# |> AuthFixtures.add_permission(Authorizer.manage_api_tokens_permission())
|
||||
|
||||
# assert fetch_api_token_by_id(api_token.id, subject) == {:ok, api_token}
|
||||
# end
|
||||
|
||||
# test "does not return api token that belongs to another actor with manage_own permission", %{
|
||||
# subject: subject
|
||||
# } do
|
||||
# api_token = ApiTokensFixtures.create_api_token()
|
||||
|
||||
# subject =
|
||||
# subject
|
||||
# |> AuthFixtures.remove_permissions()
|
||||
# |> AuthFixtures.add_permission(Authorizer.manage_own_api_tokens_permission())
|
||||
|
||||
# assert fetch_api_token_by_id(api_token.id, subject) == {:error, :not_found}
|
||||
# end
|
||||
|
||||
# test "returns error when api token does not exist", %{subject: subject} do
|
||||
# assert fetch_api_token_by_id(Ecto.UUID.generate(), subject) ==
|
||||
# {:error, :not_found}
|
||||
# end
|
||||
|
||||
# test "returns error when subject has no permission to view api tokens", %{subject: subject} do
|
||||
# subject = AuthFixtures.remove_permissions(subject)
|
||||
|
||||
# assert fetch_api_token_by_id(Ecto.UUID.generate(), subject) ==
|
||||
# {:error,
|
||||
# {:unauthorized,
|
||||
# [
|
||||
# missing_permissions: [
|
||||
# {:one_of,
|
||||
# [
|
||||
# Authorizer.manage_api_tokens_permission(),
|
||||
# Authorizer.manage_own_api_tokens_permission()
|
||||
# ]}
|
||||
# ]
|
||||
# ]}}
|
||||
# end
|
||||
# end
|
||||
|
||||
# describe "fetch_unexpired_api_token_by_id/2" do
|
||||
# test "returns error when UUID is invalid", %{subject: subject} do
|
||||
# assert fetch_unexpired_api_token_by_id("foo", subject) == {:error, :not_found}
|
||||
# end
|
||||
|
||||
# test "returns api token by id", %{actor: actor, subject: subject} do
|
||||
# api_token = ApiTokensFixtures.create_api_token(actor: actor)
|
||||
# assert fetch_unexpired_api_token_by_id(api_token.id, subject) == {:ok, api_token}
|
||||
# end
|
||||
|
||||
# test "returns error for expired token", %{actor: actor, subject: subject} do
|
||||
# api_token =
|
||||
# ApiTokensFixtures.create_api_token(actor: actor, expires_in: 1)
|
||||
# |> ApiTokensFixtures.expire_api_token()
|
||||
|
||||
# assert fetch_unexpired_api_token_by_id(api_token.id, subject) ==
|
||||
# {:error, :not_found}
|
||||
# end
|
||||
|
||||
# test "returns api token that belongs to another actor with manage permission", %{
|
||||
# subject: subject
|
||||
# } do
|
||||
# api_token = ApiTokensFixtures.create_api_token()
|
||||
|
||||
# subject =
|
||||
# subject
|
||||
# |> AuthFixtures.remove_permissions()
|
||||
# |> AuthFixtures.add_permission(Authorizer.manage_own_api_tokens_permission())
|
||||
# |> AuthFixtures.add_permission(Authorizer.manage_api_tokens_permission())
|
||||
|
||||
# assert fetch_unexpired_api_token_by_id(api_token.id, subject) == {:ok, api_token}
|
||||
# end
|
||||
|
||||
# test "does not return api token that belongs to another actor with manage_own permission", %{
|
||||
# subject: subject
|
||||
# } do
|
||||
# api_token = ApiTokensFixtures.create_api_token()
|
||||
|
||||
# subject =
|
||||
# subject
|
||||
# |> AuthFixtures.remove_permissions()
|
||||
# |> AuthFixtures.add_permission(Authorizer.manage_own_api_tokens_permission())
|
||||
|
||||
# assert fetch_unexpired_api_token_by_id(api_token.id, subject) ==
|
||||
# {:error, :not_found}
|
||||
# end
|
||||
|
||||
# test "returns error when api token does not exist", %{subject: subject} do
|
||||
# assert fetch_unexpired_api_token_by_id(Ecto.UUID.generate(), subject) ==
|
||||
# {:error, :not_found}
|
||||
# end
|
||||
|
||||
# test "returns error when subject has no permission to view api tokens", %{subject: subject} do
|
||||
# subject = AuthFixtures.remove_permissions(subject)
|
||||
|
||||
# assert fetch_unexpired_api_token_by_id(Ecto.UUID.generate(), subject) ==
|
||||
# {:error,
|
||||
# {:unauthorized,
|
||||
# [
|
||||
# missing_permissions: [
|
||||
# {:one_of,
|
||||
# [
|
||||
# Authorizer.manage_api_tokens_permission(),
|
||||
# Authorizer.manage_own_api_tokens_permission()
|
||||
# ]}
|
||||
# ]
|
||||
# ]}}
|
||||
# end
|
||||
# end
|
||||
|
||||
# describe "fetch_unexpired_api_token_by_id/1" do
|
||||
# test "fetches the unexpired token" do
|
||||
# api_token = ApiTokensFixtures.create_api_token()
|
||||
# assert fetch_unexpired_api_token_by_id(api_token.id) == {:ok, api_token}
|
||||
# end
|
||||
|
||||
# test "returns error for expired token" do
|
||||
# api_token =
|
||||
# ApiTokensFixtures.create_api_token(%{"expires_in" => 1})
|
||||
# |> ApiTokensFixtures.expire_api_token()
|
||||
|
||||
# assert fetch_unexpired_api_token_by_id(api_token.id) == {:error, :not_found}
|
||||
# end
|
||||
# end
|
||||
|
||||
# describe "new_api_token/1" do
|
||||
# test "returns api token changeset" do
|
||||
# assert %Ecto.Changeset{data: %ApiToken{}, changes: changes} = new_api_token()
|
||||
# assert Map.has_key?(changes, :expires_at)
|
||||
# end
|
||||
# end
|
||||
|
||||
# describe "create_api_token/2" do
|
||||
# test "creates an api_token", %{actor: actor, subject: subject} do
|
||||
# attrs = %{
|
||||
# "expires_in" => 1
|
||||
# }
|
||||
|
||||
# assert {:ok, %ApiToken{} = api_token} = create_api_token(attrs, subject)
|
||||
|
||||
# # Within 10 seconds
|
||||
# assert_in_delta DateTime.to_unix(api_token.expires_at),
|
||||
# DateTime.to_unix(DateTime.add(DateTime.utc_now(), 1, :day)),
|
||||
# 10
|
||||
|
||||
# assert api_token.actor_id == actor.id
|
||||
# assert api_token.expires_in == 1
|
||||
# end
|
||||
|
||||
# test "returns changeset error on invalid data", %{subject: subject} do
|
||||
# attrs = %{
|
||||
# "expires_in" => 0
|
||||
# }
|
||||
|
||||
# assert {:error, %Ecto.Changeset{} = changeset} = create_api_token(attrs, subject)
|
||||
|
||||
# assert changeset.valid? == false
|
||||
# assert errors_on(changeset) == %{expires_in: ["must be greater than or equal to 1"]}
|
||||
# end
|
||||
|
||||
# test "returns error when subject has no permission to create api tokens", %{subject: subject} do
|
||||
# attrs = %{
|
||||
# "expires_in" => 0
|
||||
# }
|
||||
|
||||
# subject = AuthFixtures.remove_permissions(subject)
|
||||
|
||||
# assert create_api_token(attrs, subject) ==
|
||||
# {:error,
|
||||
# {:unauthorized,
|
||||
# [missing_permissions: [Authorizer.manage_own_api_tokens_permission()]]}}
|
||||
# end
|
||||
# end
|
||||
|
||||
# describe "api_token_expired?/1" do
|
||||
# test "returns true when expired" do
|
||||
# api_token =
|
||||
# ApiTokensFixtures.create_api_token(%{"expires_in" => 1})
|
||||
# |> ApiTokensFixtures.expire_api_token()
|
||||
|
||||
# assert api_token_expired?(api_token) == true
|
||||
# end
|
||||
|
||||
# test "returns false when not expired" do
|
||||
# api_token = ApiTokensFixtures.create_api_token(%{"expires_in" => 1})
|
||||
# assert api_token_expired?(api_token) == false
|
||||
# end
|
||||
# end
|
||||
|
||||
# describe "delete_api_token_by_id/1" do
|
||||
# test "deletes the api token that belongs to a subject actor", %{
|
||||
# actor: actor,
|
||||
# subject: subject
|
||||
# } do
|
||||
# api_token = ApiTokensFixtures.create_api_token(actor: actor)
|
||||
|
||||
# assert {:ok, deleted_api_token} = delete_api_token_by_id(api_token.id, subject)
|
||||
|
||||
# assert deleted_api_token.id == api_token.id
|
||||
# refute Repo.one(ApiToken)
|
||||
# end
|
||||
|
||||
# test "deletes api token that belongs to another actor with manage permission", %{
|
||||
# subject: subject
|
||||
# } do
|
||||
# api_token = ApiTokensFixtures.create_api_token()
|
||||
|
||||
# subject =
|
||||
# subject
|
||||
# |> AuthFixtures.remove_permissions()
|
||||
# |> AuthFixtures.add_permission(Authorizer.manage_own_api_tokens_permission())
|
||||
# |> AuthFixtures.add_permission(Authorizer.manage_api_tokens_permission())
|
||||
|
||||
# assert {:ok, deleted_api_token} = delete_api_token_by_id(api_token.id, subject)
|
||||
|
||||
# assert deleted_api_token.id == api_token.id
|
||||
# refute Repo.one(ApiToken)
|
||||
# end
|
||||
|
||||
# test "does not delete api token that belongs to another actor with manage_own permission",
|
||||
# %{
|
||||
# subject: subject
|
||||
# } do
|
||||
# api_token = ApiTokensFixtures.create_api_token()
|
||||
|
||||
# subject =
|
||||
# subject
|
||||
# |> AuthFixtures.remove_permissions()
|
||||
# |> AuthFixtures.add_permission(Authorizer.manage_own_api_tokens_permission())
|
||||
|
||||
# assert delete_api_token_by_id(api_token.id, subject) ==
|
||||
# {:error, :not_found}
|
||||
# end
|
||||
|
||||
# test "does not delete api token that belongs to another actor with just view permission", %{
|
||||
# subject: subject
|
||||
# } do
|
||||
# api_token = ApiTokensFixtures.create_api_token()
|
||||
|
||||
# subject =
|
||||
# subject
|
||||
# |> AuthFixtures.remove_permissions()
|
||||
# |> AuthFixtures.add_permission(Authorizer.manage_own_api_tokens_permission())
|
||||
|
||||
# assert delete_api_token_by_id(api_token.id, subject) ==
|
||||
# {:error, :not_found}
|
||||
# end
|
||||
|
||||
# test "returns error when api token does not exist", %{subject: subject} do
|
||||
# assert delete_api_token_by_id(Ecto.UUID.generate(), subject) == {:error, :not_found}
|
||||
# end
|
||||
|
||||
# test "returns error when subject can not view api tokens", %{subject: subject} do
|
||||
# subject = AuthFixtures.remove_permissions(subject)
|
||||
|
||||
# assert delete_api_token_by_id(Ecto.UUID.generate(), subject) ==
|
||||
# {:error,
|
||||
# {:unauthorized,
|
||||
# [
|
||||
# missing_permissions: [
|
||||
# {:one_of,
|
||||
# [
|
||||
# Authorizer.manage_api_tokens_permission(),
|
||||
# Authorizer.manage_own_api_tokens_permission()
|
||||
# ]}
|
||||
# ]
|
||||
# ]}}
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
@@ -22,8 +22,8 @@ defmodule Domain.Auth.Adapters.EmailTest do
|
||||
|
||||
assert %{
|
||||
provider_state: %{
|
||||
sign_in_token_created_at: %DateTime{},
|
||||
sign_in_token_hash: sign_in_token_hash
|
||||
"sign_in_token_created_at" => %DateTime{},
|
||||
"sign_in_token_hash" => sign_in_token_hash
|
||||
},
|
||||
provider_virtual_state: %{sign_in_token: sign_in_token}
|
||||
} = changeset.changes
|
||||
@@ -59,8 +59,8 @@ defmodule Domain.Auth.Adapters.EmailTest do
|
||||
assert {:ok, identity} = request_sign_in_token(identity)
|
||||
|
||||
assert %{
|
||||
sign_in_token_created_at: sign_in_token_created_at,
|
||||
sign_in_token_hash: sign_in_token_hash
|
||||
"sign_in_token_created_at" => sign_in_token_created_at,
|
||||
"sign_in_token_hash" => sign_in_token_hash
|
||||
} = identity.provider_state
|
||||
|
||||
assert %{
|
||||
|
||||
@@ -3,14 +3,14 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
import Domain.Auth.Adapters.OpenIDConnect
|
||||
alias Domain.Auth
|
||||
alias Domain.Auth.Adapters.OpenIDConnect.{PKCE, State}
|
||||
alias Domain.{AccountsFixtures, AuthFixtures, ConfigFixtures}
|
||||
alias Domain.{AccountsFixtures, AuthFixtures}
|
||||
|
||||
describe "identity_changeset/2" do
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
|
||||
{provider, bypass} =
|
||||
ConfigFixtures.start_openid_providers(["google"])
|
||||
AuthFixtures.start_openid_providers(["google"])
|
||||
|> AuthFixtures.create_openid_connect_provider(account: account)
|
||||
|
||||
changeset = %Auth.Identity{} |> Ecto.Changeset.change()
|
||||
@@ -56,7 +56,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
|
||||
test "returns changeset on valid adapter config" do
|
||||
account = AccountsFixtures.create_account()
|
||||
{_bypass, discovery_document_uri} = ConfigFixtures.discovery_document_server()
|
||||
{_bypass, discovery_document_uri} = AuthFixtures.discovery_document_server()
|
||||
|
||||
attrs =
|
||||
AuthFixtures.provider_attrs(
|
||||
@@ -77,11 +77,11 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
assert provider.adapter == attrs.adapter
|
||||
|
||||
assert provider.adapter_config == %{
|
||||
scope: "openid email profile",
|
||||
response_type: "code",
|
||||
client_id: "client_id",
|
||||
client_secret: "client_secret",
|
||||
discovery_document_uri: discovery_document_uri
|
||||
"scope" => "openid email profile",
|
||||
"response_type" => "code",
|
||||
"client_id" => "client_id",
|
||||
"client_secret" => "client_secret",
|
||||
"discovery_document_uri" => discovery_document_uri
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -98,7 +98,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
account = AccountsFixtures.create_account()
|
||||
|
||||
{provider, bypass} =
|
||||
ConfigFixtures.start_openid_providers(["google"])
|
||||
AuthFixtures.start_openid_providers(["google"])
|
||||
|> AuthFixtures.create_openid_connect_provider(account: account)
|
||||
|
||||
assert {:ok, authorization_uri, {state, verifier}} =
|
||||
@@ -138,7 +138,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
account = AccountsFixtures.create_account()
|
||||
|
||||
{provider, bypass} =
|
||||
ConfigFixtures.start_openid_providers(["google"])
|
||||
AuthFixtures.start_openid_providers(["google"])
|
||||
|> AuthFixtures.create_openid_connect_provider(account: account)
|
||||
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider)
|
||||
@@ -153,8 +153,8 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
} do
|
||||
{token, claims} = generate_token(provider, identity)
|
||||
|
||||
ConfigFixtures.expect_refresh_token(bypass, %{"id_token" => token})
|
||||
ConfigFixtures.expect_userinfo(bypass)
|
||||
AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token})
|
||||
AuthFixtures.expect_userinfo(bypass)
|
||||
|
||||
code_verifier = PKCE.code_verifier()
|
||||
redirect_uri = "https://example.com/"
|
||||
@@ -189,7 +189,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
} do
|
||||
{token, _claims} = generate_token(provider, identity)
|
||||
|
||||
ConfigFixtures.expect_refresh_token(bypass, %{
|
||||
AuthFixtures.expect_refresh_token(bypass, %{
|
||||
"token_type" => "Bearer",
|
||||
"id_token" => token,
|
||||
"access_token" => "MY_ACCESS_TOKEN",
|
||||
@@ -197,7 +197,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
"expires_in" => 3600
|
||||
})
|
||||
|
||||
ConfigFixtures.expect_userinfo(bypass)
|
||||
AuthFixtures.expect_userinfo(bypass)
|
||||
|
||||
code_verifier = PKCE.code_verifier()
|
||||
redirect_uri = "https://example.com/"
|
||||
@@ -220,7 +220,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
|
||||
{token, _claims} = generate_token(provider, identity, %{"exp" => forty_seconds_ago})
|
||||
|
||||
ConfigFixtures.expect_refresh_token(bypass, %{"id_token" => token})
|
||||
AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token})
|
||||
|
||||
code_verifier = PKCE.code_verifier()
|
||||
redirect_uri = "https://example.com/"
|
||||
@@ -235,7 +235,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
} do
|
||||
token = "foo"
|
||||
|
||||
ConfigFixtures.expect_refresh_token(bypass, %{"id_token" => token})
|
||||
AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token})
|
||||
|
||||
code_verifier = PKCE.code_verifier()
|
||||
redirect_uri = "https://example.com/"
|
||||
@@ -263,7 +263,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
account = AccountsFixtures.create_account()
|
||||
|
||||
{provider, bypass} =
|
||||
ConfigFixtures.start_openid_providers(["google"])
|
||||
AuthFixtures.start_openid_providers(["google"])
|
||||
|> AuthFixtures.create_openid_connect_provider(account: account)
|
||||
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider)
|
||||
@@ -278,7 +278,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
} do
|
||||
{token, claims} = generate_token(provider, identity)
|
||||
|
||||
ConfigFixtures.expect_refresh_token(bypass, %{
|
||||
AuthFixtures.expect_refresh_token(bypass, %{
|
||||
"token_type" => "Bearer",
|
||||
"id_token" => token,
|
||||
"access_token" => "MY_ACCESS_TOKEN",
|
||||
@@ -286,7 +286,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
"expires_in" => nil
|
||||
})
|
||||
|
||||
ConfigFixtures.expect_userinfo(bypass)
|
||||
AuthFixtures.expect_userinfo(bypass)
|
||||
|
||||
assert {:ok, identity, expires_at} = refresh_token(identity)
|
||||
|
||||
@@ -314,7 +314,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
end
|
||||
|
||||
defp generate_token(provider, identity, claims \\ %{}) do
|
||||
jwk = ConfigFixtures.jwks_attrs()
|
||||
jwk = AuthFixtures.jwks_attrs()
|
||||
|
||||
claims =
|
||||
Map.merge(
|
||||
|
||||
137
apps/domain/test/domain/auth/adapters/token_test.exs
Normal file
137
apps/domain/test/domain/auth/adapters/token_test.exs
Normal file
@@ -0,0 +1,137 @@
|
||||
defmodule Domain.Auth.Adapters.TokenTest do
|
||||
use Domain.DataCase, async: true
|
||||
import Domain.Auth.Adapters.Token
|
||||
alias Domain.Auth
|
||||
alias Domain.{AccountsFixtures, AuthFixtures}
|
||||
|
||||
describe "identity_changeset/2" do
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_token_provider(account: account)
|
||||
|
||||
%{
|
||||
account: account,
|
||||
provider: provider
|
||||
}
|
||||
end
|
||||
|
||||
test "puts secret hash in the provider state", %{provider: provider} do
|
||||
changeset =
|
||||
%Auth.Identity{}
|
||||
|> Ecto.Changeset.change(
|
||||
provider_virtual_state: %{
|
||||
expires_at: DateTime.utc_now() |> DateTime.add(1, :day)
|
||||
}
|
||||
)
|
||||
|
||||
assert %Ecto.Changeset{} = changeset = identity_changeset(provider, changeset)
|
||||
assert %{provider_state: state, provider_virtual_state: virtual_state} = changeset.changes
|
||||
|
||||
assert %{"secret_hash" => secret_hash} = state
|
||||
assert %{secret: secret} = virtual_state
|
||||
assert Domain.Crypto.equal?(secret, secret_hash)
|
||||
end
|
||||
|
||||
test "returns error on invalid attrs", %{provider: provider} do
|
||||
changeset =
|
||||
%Auth.Identity{}
|
||||
|> Ecto.Changeset.change(
|
||||
provider_virtual_state: %{
|
||||
expires_at: DateTime.utc_now()
|
||||
}
|
||||
)
|
||||
|
||||
assert changeset = identity_changeset(provider, changeset)
|
||||
|
||||
refute changeset.valid?
|
||||
|
||||
assert %{
|
||||
provider_virtual_state: %{
|
||||
expires_at: ["must be greater than " <> _]
|
||||
}
|
||||
} = errors_on(changeset)
|
||||
end
|
||||
|
||||
test "trims provider identifier", %{provider: provider} do
|
||||
changeset =
|
||||
%Auth.Identity{}
|
||||
|> Ecto.Changeset.change(
|
||||
provider_identifier: " X ",
|
||||
provider_virtual_state: %{
|
||||
expires_at: DateTime.utc_now() |> DateTime.add(1, :day)
|
||||
}
|
||||
)
|
||||
|
||||
assert %Ecto.Changeset{} = changeset = identity_changeset(provider, changeset)
|
||||
assert changeset.changes.provider_identifier == "X"
|
||||
end
|
||||
end
|
||||
|
||||
describe "ensure_provisioned/1" do
|
||||
test "returns changeset as is" do
|
||||
changeset = %Ecto.Changeset{}
|
||||
assert ensure_provisioned(changeset) == changeset
|
||||
end
|
||||
end
|
||||
|
||||
describe "ensure_deprovisioned/1" do
|
||||
test "returns changeset as is" do
|
||||
changeset = %Ecto.Changeset{}
|
||||
assert ensure_deprovisioned(changeset) == changeset
|
||||
end
|
||||
end
|
||||
|
||||
describe "verify_secret/2" do
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_token_provider(account: account)
|
||||
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
account: account,
|
||||
provider: provider,
|
||||
provider_virtual_state: %{
|
||||
"expires_at" => DateTime.utc_now() |> DateTime.add(1, :day)
|
||||
}
|
||||
)
|
||||
|
||||
%{
|
||||
account: account,
|
||||
provider: provider,
|
||||
identity: identity
|
||||
}
|
||||
end
|
||||
|
||||
test "returns :invalid_secret on invalid secret", %{identity: identity} do
|
||||
assert verify_secret(identity, "foo") == {:error, :invalid_secret}
|
||||
end
|
||||
|
||||
test "returns :expired_secret on expires secret", %{identity: identity} do
|
||||
identity =
|
||||
identity
|
||||
|> Ecto.Changeset.change(
|
||||
provider_state: %{
|
||||
"expires_at" => DateTime.utc_now() |> DateTime.add(-1, :second),
|
||||
"secret_hash" => Domain.Crypto.hash("foo")
|
||||
}
|
||||
)
|
||||
|> Repo.update!()
|
||||
|
||||
assert verify_secret(identity, identity.provider_virtual_state.secret) ==
|
||||
{:error, :expired_secret}
|
||||
end
|
||||
|
||||
test "returns :ok on valid secret", %{identity: identity} do
|
||||
assert {:ok, verified_identity, expires_at} =
|
||||
verify_secret(identity, identity.provider_virtual_state.secret)
|
||||
|
||||
assert verified_identity.provider_state["secret_hash"] ==
|
||||
identity.provider_state["secret_hash"]
|
||||
|
||||
assert verified_identity.provider_state["expires_at"] ==
|
||||
identity.provider_state["expires_at"]
|
||||
|
||||
assert {:ok, ^expires_at, 0} = DateTime.from_iso8601(identity.provider_state["expires_at"])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -28,7 +28,7 @@ defmodule Domain.Auth.Adapters.UserPassTest do
|
||||
assert %Ecto.Changeset{} = changeset = identity_changeset(provider, changeset)
|
||||
assert %{provider_state: state, provider_virtual_state: virtual_state} = changeset.changes
|
||||
|
||||
assert %{password_hash: password_hash} = state
|
||||
assert %{"password_hash" => password_hash} = state
|
||||
assert Domain.Crypto.equal?("Firezone1234", password_hash)
|
||||
|
||||
assert virtual_state == %{}
|
||||
@@ -135,7 +135,7 @@ defmodule Domain.Auth.Adapters.UserPassTest do
|
||||
assert {:ok, verified_identity, nil} = verify_secret(identity, "Firezone1234")
|
||||
|
||||
assert verified_identity.provider_state["password_hash"] ==
|
||||
identity.provider_state.password_hash
|
||||
identity.provider_state["password_hash"]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
# alias Domain.UsersFixtures
|
||||
|
||||
# setup do
|
||||
# user = UsersFixtures.create_user_with_role(:admin)
|
||||
# {bypass, [provider_attrs]} = Domain.ConfigFixtures.start_openid_providers(["google"])
|
||||
# user = UsersFixtures.create_user_with_role(:account_admin_user)
|
||||
# {bypass, [provider_attrs]} = Domain.AuthFixtures.start_openid_providers(["google"])
|
||||
|
||||
# conn =
|
||||
# Repo.insert!(%Domain.Auth.OIDC.Connection{
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
# describe "refresh failed" do
|
||||
# test "disable user", %{user: user, conn: conn, bypass: bypass} do
|
||||
# Domain.ConfigFixtures.expect_refresh_token_failure(bypass)
|
||||
# Domain.AuthFixtures.expect_refresh_token_failure(bypass)
|
||||
|
||||
# assert Refresher.refresh(user.id) == {:stop, :shutdown, user.id}
|
||||
# user = Repo.reload(user)
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
# describe "refresh succeeded" do
|
||||
# test "does not change user", %{user: user, conn: conn, bypass: bypass} do
|
||||
# Domain.ConfigFixtures.expect_refresh_token(bypass)
|
||||
# Domain.AuthFixtures.expect_refresh_token(bypass)
|
||||
|
||||
# assert Refresher.refresh(user.id) == {:stop, :shutdown, user.id}
|
||||
# user = Repo.reload(user)
|
||||
|
||||
@@ -99,7 +99,7 @@ defmodule Domain.AuthTest do
|
||||
account: other_account
|
||||
} do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(account: account, role: :admin)
|
||||
actor = ActorsFixtures.create_actor(account: account, type: :account_admin_user)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -111,7 +111,14 @@ defmodule Domain.AuthTest do
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_email_provider(account: account)
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account, provider: provider)
|
||||
|
||||
actor =
|
||||
ActorsFixtures.create_actor(
|
||||
type: :account_admin_user,
|
||||
account: account,
|
||||
provider: provider
|
||||
)
|
||||
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -227,7 +234,7 @@ defmodule Domain.AuthTest do
|
||||
describe "enable_provider/2" do
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -285,7 +292,14 @@ defmodule Domain.AuthTest do
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_email_provider(account: account)
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account, provider: provider)
|
||||
|
||||
actor =
|
||||
ActorsFixtures.create_actor(
|
||||
type: :account_admin_user,
|
||||
account: account,
|
||||
provider: provider
|
||||
)
|
||||
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -415,14 +429,23 @@ defmodule Domain.AuthTest do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_email_provider(account: account)
|
||||
provider_identifier = AuthFixtures.random_provider_identifier(provider)
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account, provider: provider)
|
||||
|
||||
actor =
|
||||
ActorsFixtures.create_actor(
|
||||
type: :account_admin_user,
|
||||
account: account,
|
||||
provider: provider
|
||||
)
|
||||
|
||||
assert {:ok, identity} = create_identity(actor, provider, provider_identifier)
|
||||
|
||||
assert identity.provider_id == provider.id
|
||||
assert identity.provider_identifier == provider_identifier
|
||||
assert identity.actor_id == actor.id
|
||||
assert %{sign_in_token_created_at: _, sign_in_token_hash: _} = identity.provider_state
|
||||
|
||||
assert %{"sign_in_token_created_at" => _, "sign_in_token_hash" => _} =
|
||||
identity.provider_state
|
||||
|
||||
assert %{sign_in_token: _} = identity.provider_virtual_state
|
||||
assert identity.account_id == provider.account_id
|
||||
assert is_nil(identity.deleted_at)
|
||||
@@ -431,7 +454,13 @@ defmodule Domain.AuthTest do
|
||||
test "returns error when identifier is invalid" do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_email_provider(account: account)
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account, provider: provider)
|
||||
|
||||
actor =
|
||||
ActorsFixtures.create_actor(
|
||||
type: :account_admin_user,
|
||||
account: account,
|
||||
provider: provider
|
||||
)
|
||||
|
||||
provider_identifier = Ecto.UUID.generate()
|
||||
assert {:error, changeset} = create_identity(actor, provider, provider_identifier)
|
||||
@@ -447,7 +476,14 @@ defmodule Domain.AuthTest do
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_email_provider(account: account)
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account, provider: provider)
|
||||
|
||||
actor =
|
||||
ActorsFixtures.create_actor(
|
||||
type: :account_admin_user,
|
||||
account: account,
|
||||
provider: provider
|
||||
)
|
||||
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -493,7 +529,10 @@ defmodule Domain.AuthTest do
|
||||
assert new_identity.provider_identifier == provider_identifier
|
||||
assert new_identity.provider_id == identity.provider_id
|
||||
assert new_identity.actor_id == identity.actor_id
|
||||
assert %{sign_in_token_created_at: _, sign_in_token_hash: _} = new_identity.provider_state
|
||||
|
||||
assert %{"sign_in_token_created_at" => _, "sign_in_token_hash" => _} =
|
||||
new_identity.provider_state
|
||||
|
||||
assert %{sign_in_token: _} = new_identity.provider_virtual_state
|
||||
assert new_identity.account_id == identity.account_id
|
||||
assert is_nil(new_identity.deleted_at)
|
||||
@@ -526,7 +565,14 @@ defmodule Domain.AuthTest do
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_email_provider(account: account)
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account, provider: provider)
|
||||
|
||||
actor =
|
||||
ActorsFixtures.create_actor(
|
||||
type: :account_admin_user,
|
||||
account: account,
|
||||
provider: provider
|
||||
)
|
||||
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -691,13 +737,13 @@ defmodule Domain.AuthTest do
|
||||
assert subject.context.user_agent == user_agent
|
||||
end
|
||||
|
||||
test "returned subject expiration depends on user role", %{
|
||||
test "returned subject expiration depends on user type", %{
|
||||
account: account,
|
||||
provider: provider,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip
|
||||
} do
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor)
|
||||
secret = identity.provider_virtual_state.sign_in_token
|
||||
|
||||
@@ -707,7 +753,7 @@ defmodule Domain.AuthTest do
|
||||
three_hours = 3 * 60 * 60
|
||||
assert_datetime_diff(subject.expires_at, DateTime.utc_now(), three_hours)
|
||||
|
||||
actor = ActorsFixtures.create_actor(role: :unprivileged, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor)
|
||||
secret = identity.provider_virtual_state.sign_in_token
|
||||
|
||||
@@ -724,7 +770,7 @@ defmodule Domain.AuthTest do
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip
|
||||
} do
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
{:ok, _provider} = disable_provider(provider, subject)
|
||||
@@ -742,7 +788,7 @@ defmodule Domain.AuthTest do
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip
|
||||
} do
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
{:ok, _provider} = delete_provider(provider, subject)
|
||||
@@ -805,7 +851,7 @@ defmodule Domain.AuthTest do
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
|
||||
test "returns subject on success", %{
|
||||
test "returns subject on success for session token", %{
|
||||
subject: subject,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip
|
||||
@@ -821,6 +867,37 @@ defmodule Domain.AuthTest do
|
||||
assert DateTime.diff(reconstructed_subject.expires_at, subject.expires_at) <= 1
|
||||
end
|
||||
|
||||
test "returns subject on success for service account token", %{
|
||||
account: account,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip,
|
||||
subject: subject
|
||||
} do
|
||||
one_day = DateTime.utc_now() |> DateTime.add(1, :day)
|
||||
provider = AuthFixtures.create_token_provider(account: account)
|
||||
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
account: account,
|
||||
provider: provider,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip,
|
||||
provider_virtual_state: %{
|
||||
"expires_at" => one_day
|
||||
}
|
||||
)
|
||||
|
||||
{:ok, token} = create_access_token_for_identity(identity)
|
||||
|
||||
assert {:ok, reconstructed_subject} = sign_in(token, user_agent, remote_ip)
|
||||
assert reconstructed_subject.identity.id == identity.id
|
||||
assert reconstructed_subject.actor.id == identity.actor_id
|
||||
assert reconstructed_subject.account.id == identity.account_id
|
||||
assert reconstructed_subject.permissions == subject.permissions
|
||||
assert reconstructed_subject.context == subject.context
|
||||
assert DateTime.diff(reconstructed_subject.expires_at, one_day) <= 1
|
||||
end
|
||||
|
||||
test "updates last signed in fields for identity on success", %{
|
||||
identity: identity,
|
||||
subject: subject,
|
||||
@@ -888,7 +965,7 @@ defmodule Domain.AuthTest do
|
||||
describe "has_permission?/2" do
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -926,9 +1003,9 @@ defmodule Domain.AuthTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_role_permissions!/1" do
|
||||
test "returns permissions for given role" do
|
||||
permissions = fetch_role_permissions!(:admin)
|
||||
describe "fetch_type_permissions!/1" do
|
||||
test "returns permissions for given type" do
|
||||
permissions = fetch_type_permissions!(:account_admin_user)
|
||||
assert Enum.count(permissions) > 0
|
||||
end
|
||||
end
|
||||
@@ -948,7 +1025,7 @@ defmodule Domain.AuthTest do
|
||||
describe "ensure_has_access_to/2" do
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -978,7 +1055,7 @@ defmodule Domain.AuthTest do
|
||||
describe "ensure_has_permissions/2" do
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
|
||||
@@ -1,675 +0,0 @@
|
||||
defmodule Domain.ClientsTest do
|
||||
use Domain.DataCase, async: true
|
||||
import Domain.Clients
|
||||
alias Domain.AccountsFixtures
|
||||
alias Domain.{NetworkFixtures, ActorsFixtures, AuthFixtures, ClientsFixtures}
|
||||
alias Domain.Clients
|
||||
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
|
||||
unprivileged_actor = ActorsFixtures.create_actor(role: :unprivileged, account: account)
|
||||
|
||||
unprivileged_identity =
|
||||
AuthFixtures.create_identity(account: account, actor: unprivileged_actor)
|
||||
|
||||
unprivileged_subject = AuthFixtures.create_subject(unprivileged_identity)
|
||||
|
||||
admin_actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
admin_identity = AuthFixtures.create_identity(account: account, actor: admin_actor)
|
||||
admin_subject = AuthFixtures.create_subject(admin_identity)
|
||||
|
||||
%{
|
||||
account: account,
|
||||
unprivileged_actor: unprivileged_actor,
|
||||
unprivileged_identity: unprivileged_identity,
|
||||
unprivileged_subject: unprivileged_subject,
|
||||
admin_actor: admin_actor,
|
||||
admin_identity: admin_identity,
|
||||
admin_subject: admin_subject
|
||||
}
|
||||
end
|
||||
|
||||
describe "count_by_account_id/0" do
|
||||
test "counts clients for an account", %{account: account} do
|
||||
ClientsFixtures.create_client(account: account)
|
||||
ClientsFixtures.create_client(account: account)
|
||||
ClientsFixtures.create_client(account: account)
|
||||
ClientsFixtures.create_client()
|
||||
|
||||
assert count_by_account_id(account.id) == 3
|
||||
end
|
||||
end
|
||||
|
||||
describe "count_by_actor_id/1" do
|
||||
test "returns 0 if actor does not exist" do
|
||||
assert count_by_actor_id(Ecto.UUID.generate()) == 0
|
||||
end
|
||||
|
||||
test "returns count of clients for a actor" do
|
||||
client = ClientsFixtures.create_client()
|
||||
assert count_by_actor_id(client.actor_id) == 1
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_client_by_id/2" do
|
||||
test "returns error when UUID is invalid", %{unprivileged_subject: subject} do
|
||||
assert fetch_client_by_id("foo", subject) == {:error, :not_found}
|
||||
end
|
||||
|
||||
test "does not return deleted clients", %{
|
||||
unprivileged_actor: actor,
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
client =
|
||||
ClientsFixtures.create_client(actor: actor)
|
||||
|> ClientsFixtures.delete_client()
|
||||
|
||||
assert fetch_client_by_id(client.id, subject) == {:error, :not_found}
|
||||
end
|
||||
|
||||
test "returns client by id", %{unprivileged_actor: actor, unprivileged_subject: subject} do
|
||||
client = ClientsFixtures.create_client(actor: actor)
|
||||
assert fetch_client_by_id(client.id, subject) == {:ok, client}
|
||||
end
|
||||
|
||||
test "returns client that belongs to another actor with manage permission", %{
|
||||
account: account,
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
client = ClientsFixtures.create_client(account: account)
|
||||
|
||||
subject =
|
||||
subject
|
||||
|> AuthFixtures.remove_permissions()
|
||||
|> AuthFixtures.add_permission(Clients.Authorizer.manage_clients_permission())
|
||||
|
||||
assert fetch_client_by_id(client.id, subject) == {:ok, client}
|
||||
end
|
||||
|
||||
test "does not returns client that belongs to another account with manage permission", %{
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
client = ClientsFixtures.create_client()
|
||||
|
||||
subject =
|
||||
subject
|
||||
|> AuthFixtures.remove_permissions()
|
||||
|> AuthFixtures.add_permission(Clients.Authorizer.manage_clients_permission())
|
||||
|
||||
assert fetch_client_by_id(client.id, subject) == {:error, :not_found}
|
||||
end
|
||||
|
||||
test "does not return client that belongs to another actor with manage_own permission", %{
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
client = ClientsFixtures.create_client()
|
||||
|
||||
subject =
|
||||
subject
|
||||
|> AuthFixtures.remove_permissions()
|
||||
|> AuthFixtures.add_permission(Clients.Authorizer.manage_own_clients_permission())
|
||||
|
||||
assert fetch_client_by_id(client.id, subject) == {:error, :not_found}
|
||||
end
|
||||
|
||||
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 clients", %{
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
subject = AuthFixtures.remove_permissions(subject)
|
||||
|
||||
assert fetch_client_by_id(Ecto.UUID.generate(), subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[
|
||||
missing_permissions: [
|
||||
{:one_of,
|
||||
[
|
||||
Clients.Authorizer.manage_clients_permission(),
|
||||
Clients.Authorizer.manage_own_clients_permission()
|
||||
]}
|
||||
]
|
||||
]}}
|
||||
end
|
||||
end
|
||||
|
||||
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 clients", %{
|
||||
unprivileged_actor: actor,
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
ClientsFixtures.create_client(actor: actor)
|
||||
|> ClientsFixtures.delete_client()
|
||||
|
||||
assert list_clients(subject) == {:ok, []}
|
||||
end
|
||||
|
||||
test "does not list clients in other accounts", %{
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
ClientsFixtures.create_client()
|
||||
|
||||
assert list_clients(subject) == {:ok, []}
|
||||
end
|
||||
|
||||
test "shows all clients owned by a actor for unprivileged subject", %{
|
||||
unprivileged_actor: actor,
|
||||
admin_actor: other_actor,
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
client = ClientsFixtures.create_client(actor: actor)
|
||||
ClientsFixtures.create_client(actor: other_actor)
|
||||
|
||||
assert list_clients(subject) == {:ok, [client]}
|
||||
end
|
||||
|
||||
test "shows all clients for admin subject", %{
|
||||
unprivileged_actor: other_actor,
|
||||
admin_actor: admin_actor,
|
||||
admin_subject: subject
|
||||
} do
|
||||
ClientsFixtures.create_client(actor: admin_actor)
|
||||
ClientsFixtures.create_client(actor: other_actor)
|
||||
|
||||
assert {:ok, clients} = list_clients(subject)
|
||||
assert length(clients) == 2
|
||||
end
|
||||
|
||||
test "returns error when subject has no permission to manage clients", %{
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
subject = AuthFixtures.remove_permissions(subject)
|
||||
|
||||
assert list_clients(subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[
|
||||
missing_permissions: [
|
||||
{:one_of,
|
||||
[
|
||||
Clients.Authorizer.manage_clients_permission(),
|
||||
Clients.Authorizer.manage_own_clients_permission()
|
||||
]}
|
||||
]
|
||||
]}}
|
||||
end
|
||||
end
|
||||
|
||||
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_clients_by_actor_id(Ecto.UUID.generate(), subject) == {:ok, []}
|
||||
assert list_clients_by_actor_id(actor.id, subject) == {:ok, []}
|
||||
ClientsFixtures.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_clients_by_actor_id("foo", subject) == {:error, :not_found}
|
||||
end
|
||||
|
||||
test "does not list deleted clients", %{
|
||||
unprivileged_actor: actor,
|
||||
unprivileged_identity: identity,
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
ClientsFixtures.create_client(identity: identity)
|
||||
|> ClientsFixtures.delete_client()
|
||||
|
||||
assert list_clients_by_actor_id(actor.id, subject) == {:ok, []}
|
||||
end
|
||||
|
||||
test "does not deleted clients for actors in other accounts", %{
|
||||
unprivileged_subject: unprivileged_subject,
|
||||
admin_subject: admin_subject
|
||||
} do
|
||||
actor = ActorsFixtures.create_actor(role: :unprivileged)
|
||||
ClientsFixtures.create_client(actor: actor)
|
||||
|
||||
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 clients owned by a actor for unprivileged subject", %{
|
||||
unprivileged_actor: actor,
|
||||
admin_actor: other_actor,
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
client = ClientsFixtures.create_client(actor: actor)
|
||||
ClientsFixtures.create_client(actor: other_actor)
|
||||
|
||||
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 clients owned by another actor for admin subject", %{
|
||||
unprivileged_actor: other_actor,
|
||||
admin_actor: admin_actor,
|
||||
admin_subject: subject
|
||||
} do
|
||||
ClientsFixtures.create_client(actor: admin_actor)
|
||||
ClientsFixtures.create_client(actor: other_actor)
|
||||
|
||||
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 clients", %{
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
subject = AuthFixtures.remove_permissions(subject)
|
||||
|
||||
assert list_clients_by_actor_id(Ecto.UUID.generate(), subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[
|
||||
missing_permissions: [
|
||||
{:one_of,
|
||||
[
|
||||
Clients.Authorizer.manage_clients_permission(),
|
||||
Clients.Authorizer.manage_own_clients_permission()
|
||||
]}
|
||||
]
|
||||
]}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "change_client/1" do
|
||||
test "returns changeset with given changes", %{admin_actor: actor} do
|
||||
client = ClientsFixtures.create_client(actor: actor)
|
||||
client_attrs = ClientsFixtures.client_attrs()
|
||||
|
||||
assert changeset = change_client(client, client_attrs)
|
||||
assert %Ecto.Changeset{data: %Domain.Clients.Client{}} = changeset
|
||||
|
||||
assert changeset.changes == %{name: client_attrs.name}
|
||||
end
|
||||
end
|
||||
|
||||
describe "upsert_client/2" do
|
||||
test "returns errors on invalid attrs", %{
|
||||
admin_subject: subject
|
||||
} do
|
||||
attrs = %{
|
||||
external_id: nil,
|
||||
public_key: "x",
|
||||
ipv4: "1.1.1.256",
|
||||
ipv6: "fd01::10000"
|
||||
}
|
||||
|
||||
assert {:error, changeset} = upsert_client(attrs, subject)
|
||||
|
||||
assert errors_on(changeset) == %{
|
||||
public_key: ["should be 44 character(s)", "must be a base64-encoded string"],
|
||||
external_id: ["can't be blank"]
|
||||
}
|
||||
end
|
||||
|
||||
test "allows creating client with just required attributes", %{
|
||||
admin_actor: actor,
|
||||
admin_identity: identity,
|
||||
admin_subject: subject
|
||||
} do
|
||||
attrs =
|
||||
ClientsFixtures.client_attrs()
|
||||
|> Map.delete(:name)
|
||||
|
||||
assert {:ok, client} = upsert_client(attrs, subject)
|
||||
|
||||
assert client.name
|
||||
|
||||
assert client.public_key == attrs.public_key
|
||||
|
||||
assert client.actor_id == actor.id
|
||||
assert client.identity_id == identity.id
|
||||
assert client.account_id == actor.account_id
|
||||
|
||||
refute is_nil(client.ipv4)
|
||||
refute is_nil(client.ipv6)
|
||||
|
||||
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 client when it already exists", %{
|
||||
admin_subject: subject
|
||||
} do
|
||||
client = ClientsFixtures.create_client(subject: subject)
|
||||
attrs = ClientsFixtures.client_attrs(external_id: client.external_id)
|
||||
|
||||
subject = %{
|
||||
subject
|
||||
| context: %Domain.Auth.Context{
|
||||
subject.context
|
||||
| remote_ip: {100, 64, 100, 101},
|
||||
user_agent: "iOS/12.5 (iPhone) connlib/0.7.411"
|
||||
}
|
||||
}
|
||||
|
||||
assert {:ok, updated_client} = upsert_client(attrs, subject)
|
||||
|
||||
assert Repo.aggregate(Clients.Client, :count, :id) == 1
|
||||
|
||||
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_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
|
||||
client = ClientsFixtures.create_client(subject: subject)
|
||||
|
||||
attrs =
|
||||
ClientsFixtures.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_client} = upsert_client(attrs, subject)
|
||||
|
||||
addresses =
|
||||
Domain.Network.Address
|
||||
|> Repo.all()
|
||||
|> Enum.map(fn %Domain.Network.Address{address: address, type: type} ->
|
||||
%{address: address, type: type}
|
||||
end)
|
||||
|
||||
assert length(addresses) == 2
|
||||
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 client for himself", %{
|
||||
admin_subject: subject
|
||||
} do
|
||||
attrs =
|
||||
ClientsFixtures.client_attrs()
|
||||
|> Map.delete(:name)
|
||||
|
||||
assert {:ok, _client} = upsert_client(attrs, subject)
|
||||
end
|
||||
|
||||
test "does not allow to reuse IP addresses", %{
|
||||
account: account,
|
||||
admin_subject: subject
|
||||
} do
|
||||
attrs = ClientsFixtures.client_attrs(account: account)
|
||||
assert {:ok, client} = upsert_client(attrs, subject)
|
||||
|
||||
addresses =
|
||||
Domain.Network.Address
|
||||
|> Repo.all()
|
||||
|> Enum.map(fn %Domain.Network.Address{address: address, type: type} ->
|
||||
%{address: address, type: type}
|
||||
end)
|
||||
|
||||
assert length(addresses) == 2
|
||||
assert %{address: client.ipv4, type: :ipv4} in addresses
|
||||
assert %{address: client.ipv6, type: :ipv6} in addresses
|
||||
|
||||
assert_raise Ecto.ConstraintError, fn ->
|
||||
NetworkFixtures.create_address(address: client.ipv4, account: account)
|
||||
end
|
||||
|
||||
assert_raise Ecto.ConstraintError, fn ->
|
||||
NetworkFixtures.create_address(address: client.ipv6, account: account)
|
||||
end
|
||||
end
|
||||
|
||||
test "ip addresses are unique per account", %{
|
||||
account: account,
|
||||
admin_subject: subject
|
||||
} do
|
||||
attrs = ClientsFixtures.client_attrs(account: account)
|
||||
assert {:ok, client} = upsert_client(attrs, subject)
|
||||
|
||||
assert %Domain.Network.Address{} = NetworkFixtures.create_address(address: client.ipv4)
|
||||
assert %Domain.Network.Address{} = NetworkFixtures.create_address(address: client.ipv6)
|
||||
end
|
||||
|
||||
test "returns error when subject has no permission to create clients", %{
|
||||
admin_subject: subject
|
||||
} do
|
||||
subject = AuthFixtures.remove_permissions(subject)
|
||||
|
||||
assert upsert_client(%{}, subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[missing_permissions: [Clients.Authorizer.manage_own_clients_permission()]]}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "update_client/3" do
|
||||
test "allows admin actor to update own clients", %{admin_actor: actor, admin_subject: subject} do
|
||||
client = ClientsFixtures.create_client(actor: actor)
|
||||
attrs = %{name: "new name"}
|
||||
|
||||
assert {:ok, client} = update_client(client, attrs, subject)
|
||||
|
||||
assert client.name == attrs.name
|
||||
end
|
||||
|
||||
test "allows admin actor to update other actors clients", %{
|
||||
account: account,
|
||||
admin_subject: subject
|
||||
} do
|
||||
client = ClientsFixtures.create_client(account: account)
|
||||
attrs = %{name: "new name"}
|
||||
|
||||
assert {:ok, client} = update_client(client, attrs, subject)
|
||||
|
||||
assert client.name == attrs.name
|
||||
end
|
||||
|
||||
test "allows unprivileged actor to update own clients", %{
|
||||
unprivileged_actor: actor,
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
client = ClientsFixtures.create_client(actor: actor)
|
||||
attrs = %{name: "new name"}
|
||||
|
||||
assert {:ok, client} = update_client(client, attrs, subject)
|
||||
|
||||
assert client.name == attrs.name
|
||||
end
|
||||
|
||||
test "does not allow unprivileged actor to update other actors clients", %{
|
||||
account: account,
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
client = ClientsFixtures.create_client(account: account)
|
||||
attrs = %{name: "new name"}
|
||||
|
||||
assert update_client(client, attrs, subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[missing_permissions: [Clients.Authorizer.manage_clients_permission()]]}}
|
||||
end
|
||||
|
||||
test "does not allow admin actor to update clients in other accounts", %{
|
||||
admin_subject: subject
|
||||
} do
|
||||
client = ClientsFixtures.create_client()
|
||||
attrs = %{name: "new name"}
|
||||
|
||||
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
|
||||
client = ClientsFixtures.create_client(actor: actor)
|
||||
attrs = %{name: nil, public_key: nil}
|
||||
|
||||
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
|
||||
client = ClientsFixtures.create_client(actor: actor)
|
||||
|
||||
attrs = %{
|
||||
name: String.duplicate("a", 256)
|
||||
}
|
||||
|
||||
assert {:error, changeset} = update_client(client, attrs, subject)
|
||||
|
||||
assert errors_on(changeset) == %{
|
||||
name: ["should be at most 255 character(s)"]
|
||||
}
|
||||
end
|
||||
|
||||
test "ignores updates for any field except name", %{
|
||||
admin_actor: actor,
|
||||
admin_subject: subject
|
||||
} do
|
||||
client = ClientsFixtures.create_client(actor: actor)
|
||||
|
||||
fields = Clients.Client.__schema__(:fields) -- [:name]
|
||||
value = -1
|
||||
|
||||
for field <- fields do
|
||||
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 clients", %{
|
||||
admin_actor: actor,
|
||||
admin_subject: subject
|
||||
} do
|
||||
client = ClientsFixtures.create_client(actor: actor)
|
||||
|
||||
subject = AuthFixtures.remove_permissions(subject)
|
||||
|
||||
assert update_client(client, %{}, subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[missing_permissions: [Clients.Authorizer.manage_own_clients_permission()]]}}
|
||||
|
||||
client = ClientsFixtures.create_client()
|
||||
|
||||
assert update_client(client, %{}, subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[missing_permissions: [Clients.Authorizer.manage_clients_permission()]]}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete_client/2" do
|
||||
test "returns error on state conflict", %{admin_actor: actor, admin_subject: subject} do
|
||||
client = ClientsFixtures.create_client(actor: actor)
|
||||
|
||||
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 clients", %{admin_actor: actor, admin_subject: subject} do
|
||||
client = ClientsFixtures.create_client(actor: actor)
|
||||
|
||||
assert {:ok, deleted} = delete_client(client, subject)
|
||||
assert deleted.deleted_at
|
||||
end
|
||||
|
||||
test "admin can delete other people clients", %{
|
||||
unprivileged_actor: actor,
|
||||
admin_subject: subject
|
||||
} do
|
||||
client = ClientsFixtures.create_client(actor: actor)
|
||||
|
||||
assert {:ok, deleted} = delete_client(client, subject)
|
||||
assert deleted.deleted_at
|
||||
end
|
||||
|
||||
test "admin can not delete clients in other accounts", %{
|
||||
admin_subject: subject
|
||||
} do
|
||||
client = ClientsFixtures.create_client()
|
||||
|
||||
assert delete_client(client, subject) == {:error, :not_found}
|
||||
end
|
||||
|
||||
test "unprivileged can delete own clients", %{
|
||||
account: account,
|
||||
unprivileged_actor: actor,
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
client = ClientsFixtures.create_client(account: account, actor: actor)
|
||||
|
||||
assert {:ok, deleted} = delete_client(client, subject)
|
||||
assert deleted.deleted_at
|
||||
end
|
||||
|
||||
test "unprivileged can not delete other people clients", %{
|
||||
account: account,
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
client = ClientsFixtures.create_client()
|
||||
|
||||
assert delete_client(client, subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[missing_permissions: [Clients.Authorizer.manage_clients_permission()]]}}
|
||||
|
||||
client = ClientsFixtures.create_client(account: account)
|
||||
|
||||
assert delete_client(client, subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[missing_permissions: [Clients.Authorizer.manage_clients_permission()]]}}
|
||||
|
||||
assert Repo.aggregate(Clients.Client, :count) == 2
|
||||
end
|
||||
|
||||
test "returns error when subject has no permission to delete clients", %{
|
||||
admin_actor: actor,
|
||||
admin_subject: subject
|
||||
} do
|
||||
client = ClientsFixtures.create_client(actor: actor)
|
||||
|
||||
subject = AuthFixtures.remove_permissions(subject)
|
||||
|
||||
assert delete_client(client, subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[missing_permissions: [Clients.Authorizer.manage_own_clients_permission()]]}}
|
||||
|
||||
client = ClientsFixtures.create_client()
|
||||
|
||||
assert delete_client(client, subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[missing_permissions: [Clients.Authorizer.manage_clients_permission()]]}}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -57,8 +57,8 @@ defmodule Domain.Config.DefinitionTest do
|
||||
end
|
||||
|
||||
test "inserts a function which returns definition doc" do
|
||||
assert fetch_doc(Domain.Config.Definitions, :default_admin_email) ==
|
||||
{:ok, "Primary administrator email.\n"}
|
||||
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 fetch_doc(Foo, :bar) ==
|
||||
{:error, :module_not_found}
|
||||
|
||||
@@ -37,10 +37,10 @@ defmodule Domain.Config.ResolverTest do
|
||||
|
||||
test "returns variable from database" do
|
||||
env_configurations = %{}
|
||||
db_configurations = %Domain.Config.Configuration{default_client_dns: "1.2.3.4"}
|
||||
db_configurations = %Domain.Config.Configuration{devices_upstream_dns: "1.2.3.4"}
|
||||
|
||||
assert resolve(:default_client_dns, env_configurations, db_configurations, []) ==
|
||||
{:ok, {{:db, :default_client_dns}, "1.2.3.4"}}
|
||||
assert resolve(:devices_upstream_dns, env_configurations, db_configurations, []) ==
|
||||
{:ok, {{:db, :devices_upstream_dns}, "1.2.3.4"}}
|
||||
end
|
||||
|
||||
test "precedence" do
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
defmodule Domain.ConfigTest do
|
||||
use Domain.DataCase, async: true
|
||||
import Domain.Config
|
||||
alias Domain.Config
|
||||
alias Domain.{AccountsFixtures, AuthFixtures, ActorsFixtures}
|
||||
alias Domain.ConfigFixtures
|
||||
|
||||
defmodule Test do
|
||||
use Domain.Config.Definition
|
||||
@@ -80,13 +83,22 @@ defmodule Domain.ConfigTest do
|
||||
)
|
||||
end
|
||||
|
||||
describe "fetch_source_and_config!/1" do
|
||||
test "returns source and config value" do
|
||||
assert fetch_source_and_config!(:default_client_mtu) ==
|
||||
{{:db, :default_client_mtu}, 1280}
|
||||
describe "fetch_resolved_configs!/1" do
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
ConfigFixtures.upsert_configuration(account: account)
|
||||
|
||||
%{account: account}
|
||||
end
|
||||
|
||||
test "raises an error when value is missing" do
|
||||
test "returns source and config values", %{account: account} do
|
||||
assert fetch_resolved_configs!(account.id, [:devices_upstream_dns, :devices_upstream_dns]) ==
|
||||
%{
|
||||
devices_upstream_dns: [%Postgrex.INET{address: {1, 1, 1, 1}, netmask: nil}]
|
||||
}
|
||||
end
|
||||
|
||||
test "raises an error when value is missing", %{account: account} do
|
||||
message = """
|
||||
Missing required configuration value for 'external_url'.
|
||||
|
||||
@@ -113,26 +125,29 @@ defmodule Domain.ConfigTest do
|
||||
"""
|
||||
|
||||
assert_raise RuntimeError, message, fn ->
|
||||
fetch_source_and_config!(:external_url)
|
||||
fetch_resolved_configs!(account.id, [:external_url])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_source_and_configs!/1" do
|
||||
test "returns source and config values" do
|
||||
assert fetch_source_and_configs!([:default_client_mtu, :default_client_dns]) ==
|
||||
describe "fetch_resolved_configs_with_sources!/1" do
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
ConfigFixtures.upsert_configuration(account: account)
|
||||
|
||||
%{account: account}
|
||||
end
|
||||
|
||||
test "returns source and config values", %{account: account} do
|
||||
assert fetch_resolved_configs_with_sources!(account.id, [:devices_upstream_dns]) ==
|
||||
%{
|
||||
default_client_dns:
|
||||
{{:db, :default_client_dns},
|
||||
[
|
||||
%Postgrex.INET{address: {1, 1, 1, 1}, netmask: nil},
|
||||
%Postgrex.INET{address: {1, 0, 0, 1}, netmask: nil}
|
||||
]},
|
||||
default_client_mtu: {{:db, :default_client_mtu}, 1280}
|
||||
devices_upstream_dns:
|
||||
{{:db, :devices_upstream_dns},
|
||||
[%Postgrex.INET{address: {1, 1, 1, 1}, netmask: nil}]}
|
||||
}
|
||||
end
|
||||
|
||||
test "raises an error when value is missing" do
|
||||
test "raises an error when value is missing", %{account: account} do
|
||||
message = """
|
||||
Missing required configuration value for 'external_url'.
|
||||
|
||||
@@ -159,11 +174,11 @@ defmodule Domain.ConfigTest do
|
||||
"""
|
||||
|
||||
assert_raise RuntimeError, message, fn ->
|
||||
fetch_source_and_configs!([:external_url])
|
||||
fetch_resolved_configs_with_sources!(account.id, [:external_url])
|
||||
end
|
||||
end
|
||||
|
||||
test "raises an error when value is invalid" do
|
||||
test "raises an error when value is invalid", %{account: account} do
|
||||
put_system_env_override(:external_url, "https://example.com/vpn")
|
||||
|
||||
message = """
|
||||
@@ -187,78 +202,7 @@ defmodule Domain.ConfigTest do
|
||||
"""
|
||||
|
||||
assert_raise RuntimeError, message, fn ->
|
||||
fetch_source_and_configs!([:external_url])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_config/1" do
|
||||
test "returns config value" do
|
||||
assert fetch_config(:default_client_mtu) ==
|
||||
{:ok, 1280}
|
||||
end
|
||||
|
||||
test "returns error when value is missing" do
|
||||
assert fetch_config(:external_url) ==
|
||||
{:error,
|
||||
{{nil, ["is required"]},
|
||||
[module: Domain.Config.Definitions, key: :external_url, source: :not_found]}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_config!/1" do
|
||||
test "returns config value" do
|
||||
assert fetch_config!(:default_client_mtu) ==
|
||||
1280
|
||||
end
|
||||
|
||||
test "raises when value is missing" do
|
||||
assert_raise RuntimeError, fn ->
|
||||
fetch_config!(:external_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_configs!/1" do
|
||||
test "returns source and config values" do
|
||||
assert fetch_configs!([:default_client_mtu, :default_client_dns]) ==
|
||||
%{
|
||||
default_client_dns: [
|
||||
%Postgrex.INET{address: {1, 1, 1, 1}, netmask: nil},
|
||||
%Postgrex.INET{address: {1, 0, 0, 1}, netmask: nil}
|
||||
],
|
||||
default_client_mtu: 1280
|
||||
}
|
||||
end
|
||||
|
||||
test "raises an error when value is missing" do
|
||||
message = """
|
||||
Missing required configuration value for 'external_url'.
|
||||
|
||||
## How to fix?
|
||||
|
||||
### Using environment variables
|
||||
|
||||
You can set this configuration via environment variable by adding it to `.env` file:
|
||||
|
||||
EXTERNAL_URL=YOUR_VALUE
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
The external URL the web UI will be accessible at.
|
||||
|
||||
Must be a valid and public FQDN for ACME SSL issuance to function.
|
||||
|
||||
You can add a path suffix if you want to serve firezone from a non-root path,
|
||||
eg: `https://firezone.mycorp.com/vpn/`.
|
||||
|
||||
|
||||
You can find more information on configuration here: https://www.firezone.dev/docs/reference/env-vars/#environment-variable-listing
|
||||
"""
|
||||
|
||||
assert_raise RuntimeError, message, fn ->
|
||||
fetch_configs!([:external_url])
|
||||
fetch_resolved_configs_with_sources!(account.id, [:external_url])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -390,246 +334,173 @@ defmodule Domain.ConfigTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "validate_runtime_config!/0" do
|
||||
test "raises error on invalid values" do
|
||||
message = """
|
||||
Found 9 configuration errors:
|
||||
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Missing required configuration value for 'url'.
|
||||
|
||||
## How to fix?
|
||||
|
||||
### Using environment variables
|
||||
|
||||
You can set this configuration via environment variable by adding it to `.env` file:
|
||||
|
||||
URL=YOUR_VALUE
|
||||
|
||||
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Missing required configuration value for 'boolean'.
|
||||
|
||||
## How to fix?
|
||||
|
||||
### Using environment variables
|
||||
|
||||
You can set this configuration via environment variable by adding it to `.env` file:
|
||||
|
||||
BOOLEAN=YOUR_VALUE
|
||||
|
||||
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Missing required configuration value for 'json'.
|
||||
|
||||
## How to fix?
|
||||
|
||||
### Using environment variables
|
||||
|
||||
You can set this configuration via environment variable by adding it to `.env` file:
|
||||
|
||||
JSON=YOUR_VALUE
|
||||
|
||||
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Missing required configuration value for 'json_array'.
|
||||
|
||||
## How to fix?
|
||||
|
||||
### Using environment variables
|
||||
|
||||
You can set this configuration via environment variable by adding it to `.env` file:
|
||||
|
||||
JSON_ARRAY=YOUR_VALUE
|
||||
|
||||
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Invalid configuration for 'array' retrieved from default value.
|
||||
|
||||
Errors:
|
||||
|
||||
- `3`: must be less than or equal to 2
|
||||
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Invalid configuration for 'invalid_with_validation' retrieved from default value.
|
||||
|
||||
Errors:
|
||||
|
||||
- `-1`: must be greater than or equal to 0
|
||||
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Missing required configuration value for 'integer'.
|
||||
|
||||
## How to fix?
|
||||
|
||||
### Using environment variables
|
||||
|
||||
You can set this configuration via environment variable by adding it to `.env` file:
|
||||
|
||||
INTEGER=YOUR_VALUE
|
||||
|
||||
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Missing required configuration value for 'one_of'.
|
||||
|
||||
## How to fix?
|
||||
|
||||
### Using environment variables
|
||||
|
||||
You can set this configuration via environment variable by adding it to `.env` file:
|
||||
|
||||
ONE_OF=YOUR_VALUE
|
||||
|
||||
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Missing required configuration value for 'required'.
|
||||
|
||||
## How to fix?
|
||||
|
||||
### Using environment variables
|
||||
|
||||
You can set this configuration via environment variable by adding it to `.env` file:
|
||||
|
||||
REQUIRED=YOUR_VALUE
|
||||
"""
|
||||
|
||||
assert_raise RuntimeError, message, fn ->
|
||||
validate_runtime_config!(Test, %{}, %{})
|
||||
end
|
||||
describe "get_account_config_by_account_id/1" do
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
%{account: account}
|
||||
end
|
||||
|
||||
test "returns :ok when config is valid" do
|
||||
env_config = %{
|
||||
"BOOLEAN" => "true",
|
||||
"ARRAY" => "1",
|
||||
"JSON" => "{\"foo\":\"bar\"}",
|
||||
"JSON_ARRAY" => "[{\"foo\":\"bar\"}]",
|
||||
"INTEGER" => "123",
|
||||
"ONE_OF" => "a",
|
||||
"REQUIRED" => "1.1.1.1",
|
||||
"INVALID_WITH_VALIDATION" => "2",
|
||||
"URL" => "http://example.com"
|
||||
test "returns configuration for an account if it exists", %{
|
||||
account: account
|
||||
} do
|
||||
configuration = ConfigFixtures.upsert_configuration(account: account)
|
||||
assert get_account_config_by_account_id(account.id) == configuration
|
||||
end
|
||||
|
||||
test "returns default configuration for an account if it does not exist", %{
|
||||
account: account
|
||||
} do
|
||||
assert get_account_config_by_account_id(account.id) == %Domain.Config.Configuration{
|
||||
account_id: account.id,
|
||||
devices_upstream_dns: []
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_account_config/1" do
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
%{
|
||||
account: account,
|
||||
actor: actor,
|
||||
identity: identity,
|
||||
subject: subject
|
||||
}
|
||||
end
|
||||
|
||||
assert validate_runtime_config!(Test, %{}, env_config) == :ok
|
||||
test "returns configuration for an account if it exists", %{
|
||||
account: account,
|
||||
subject: subject
|
||||
} do
|
||||
configuration = ConfigFixtures.upsert_configuration(account: account)
|
||||
assert fetch_account_config(subject) == {:ok, configuration}
|
||||
end
|
||||
|
||||
test "returns default configuration for an account if it does not exist", %{
|
||||
account: account,
|
||||
subject: subject
|
||||
} do
|
||||
assert {:ok, config} = fetch_account_config(subject)
|
||||
|
||||
assert config == %Domain.Config.Configuration{
|
||||
account_id: account.id,
|
||||
devices_upstream_dns: []
|
||||
}
|
||||
end
|
||||
|
||||
test "returns error when subject does not have permission to read configuration", %{
|
||||
subject: subject
|
||||
} do
|
||||
subject = AuthFixtures.remove_permissions(subject)
|
||||
|
||||
assert fetch_account_config(subject) ==
|
||||
{:error,
|
||||
{:unauthorized, [missing_permissions: [Config.Authorizer.manage_permission()]]}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_db_config!" do
|
||||
test "returns config from db table" do
|
||||
assert fetch_db_config!() == Repo.one(Domain.Config.Configuration)
|
||||
describe "change_account_config/2" do
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
configuration = ConfigFixtures.upsert_configuration(account: account)
|
||||
|
||||
%{account: account, configuration: configuration}
|
||||
end
|
||||
|
||||
test "returns config changeset", %{configuration: configuration} do
|
||||
assert %Ecto.Changeset{} = change_account_config(configuration)
|
||||
end
|
||||
end
|
||||
|
||||
describe "change_config/2" do
|
||||
test "returns config changeset" do
|
||||
assert %Ecto.Changeset{} = change_config()
|
||||
describe "update_config/3" do
|
||||
test "returns error when subject can not manage account configuration" do
|
||||
account = AccountsFixtures.create_account()
|
||||
config = get_account_config_by_account_id(account.id)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
|
||||
subject =
|
||||
AuthFixtures.create_subject(identity)
|
||||
|> AuthFixtures.remove_permissions()
|
||||
|
||||
assert update_config(config, %{}, subject) ==
|
||||
{:error,
|
||||
{:unauthorized, [missing_permissions: [Config.Authorizer.manage_permission()]]}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "update_config/2" do
|
||||
test "returns error when changeset is invalid" do
|
||||
config = Repo.one(Domain.Config.Configuration)
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
%{account: account}
|
||||
end
|
||||
|
||||
test "returns error when changeset is invalid", %{account: account} do
|
||||
config = get_account_config_by_account_id(account.id)
|
||||
|
||||
attrs = %{
|
||||
local_auth_enabled: 1,
|
||||
allow_unprivileged_device_management: 1,
|
||||
allow_unprivileged_device_configuration: 1,
|
||||
disable_vpn_on_oidc_error: 1,
|
||||
default_client_persistent_keepalive: -1,
|
||||
default_client_mtu: -1,
|
||||
default_client_endpoint: "123",
|
||||
default_client_dns: ["!!!"],
|
||||
default_client_allowed_ips: ["!"],
|
||||
vpn_session_duration: -1
|
||||
devices_upstream_dns: ["!!!"]
|
||||
}
|
||||
|
||||
assert {:error, changeset} = update_config(config, attrs)
|
||||
|
||||
assert errors_on(changeset) == %{
|
||||
default_client_mtu: ["must be greater than or equal to 576"],
|
||||
allow_unprivileged_device_configuration: ["is invalid"],
|
||||
allow_unprivileged_device_management: ["is invalid"],
|
||||
default_client_allowed_ips: ["is invalid"],
|
||||
default_client_dns: [
|
||||
devices_upstream_dns: [
|
||||
"!!! is not a valid FQDN",
|
||||
"must be one of: Elixir.Domain.Types.IP, string"
|
||||
],
|
||||
default_client_persistent_keepalive: ["must be greater than or equal to 0"],
|
||||
local_auth_enabled: ["is invalid"],
|
||||
vpn_session_duration: ["must be greater than or equal to 0"]
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
test "returns error when trying to change overridden value" do
|
||||
put_system_env_override(:local_auth_enabled, false)
|
||||
test "returns error when trying to change overridden value", %{account: account} do
|
||||
put_system_env_override(:devices_upstream_dns, ["1.2.3.4"])
|
||||
|
||||
config = Repo.one(Domain.Config.Configuration)
|
||||
config = get_account_config_by_account_id(account.id)
|
||||
|
||||
attrs = %{
|
||||
local_auth_enabled: false
|
||||
devices_upstream_dns: ["4.1.2.3"]
|
||||
}
|
||||
|
||||
assert {:error, changeset} = update_config(config, attrs)
|
||||
|
||||
assert errors_on(changeset) ==
|
||||
%{
|
||||
local_auth_enabled: [
|
||||
"cannot be changed; it is overridden by LOCAL_AUTH_ENABLED environment variable"
|
||||
devices_upstream_dns: [
|
||||
"cannot be changed; it is overridden by DEVICES_UPSTREAM_DNS environment variable"
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
test "trims binary fields" do
|
||||
config = Repo.one(Domain.Config.Configuration)
|
||||
test "trims binary fields", %{account: account} do
|
||||
config = get_account_config_by_account_id(account.id)
|
||||
|
||||
attrs = %{
|
||||
default_client_dns: [" foobar.com", "google.com "],
|
||||
default_client_endpoint: " 127.0.0.1 "
|
||||
devices_upstream_dns: [" foobar.com", "google.com "]
|
||||
}
|
||||
|
||||
assert {:ok, config} = update_config(config, attrs)
|
||||
assert config.default_client_dns == ["foobar.com", "google.com"]
|
||||
assert config.default_client_endpoint == "127.0.0.1"
|
||||
assert config.devices_upstream_dns == ["foobar.com", "google.com"]
|
||||
end
|
||||
|
||||
test "changes database config value" do
|
||||
config = Repo.one(Domain.Config.Configuration)
|
||||
attrs = %{default_client_dns: ["foobar.com", "google.com"]}
|
||||
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"]}
|
||||
assert {:ok, config} = update_config(config, attrs)
|
||||
assert config.default_client_dns == attrs.default_client_dns
|
||||
end
|
||||
end
|
||||
|
||||
describe "put_config!/2" do
|
||||
test "updates config field in a database" do
|
||||
assert config = put_config!(:default_client_endpoint, " 127.0.0.1")
|
||||
assert config.default_client_endpoint == "127.0.0.1"
|
||||
assert Repo.one(Domain.Config.Configuration).default_client_endpoint == "127.0.0.1"
|
||||
assert config.devices_upstream_dns == attrs.devices_upstream_dns
|
||||
end
|
||||
|
||||
test "raises when config field is not valid" do
|
||||
assert_raise RuntimeError, fn ->
|
||||
put_config!(:default_client_endpoint, "!!!")
|
||||
end
|
||||
test "changes database config value when it existed", %{account: account} do
|
||||
ConfigFixtures.upsert_configuration(account: account)
|
||||
|
||||
config = get_account_config_by_account_id(account.id)
|
||||
attrs = %{devices_upstream_dns: ["foobar.com", "google.com"]}
|
||||
assert {:ok, config} = update_config(config, attrs)
|
||||
assert config.devices_upstream_dns == attrs.devices_upstream_dns
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
675
apps/domain/test/domain/devices_test.exs
Normal file
675
apps/domain/test/domain/devices_test.exs
Normal file
@@ -0,0 +1,675 @@
|
||||
defmodule Domain.DevicesTest do
|
||||
use Domain.DataCase, async: true
|
||||
import Domain.Devices
|
||||
alias Domain.AccountsFixtures
|
||||
alias Domain.{NetworkFixtures, ActorsFixtures, AuthFixtures, DevicesFixtures}
|
||||
alias Domain.Devices
|
||||
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
|
||||
unprivileged_actor = ActorsFixtures.create_actor(type: :account_user, account: account)
|
||||
|
||||
unprivileged_identity =
|
||||
AuthFixtures.create_identity(account: account, actor: unprivileged_actor)
|
||||
|
||||
unprivileged_subject = AuthFixtures.create_subject(unprivileged_identity)
|
||||
|
||||
admin_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
admin_identity = AuthFixtures.create_identity(account: account, actor: admin_actor)
|
||||
admin_subject = AuthFixtures.create_subject(admin_identity)
|
||||
|
||||
%{
|
||||
account: account,
|
||||
unprivileged_actor: unprivileged_actor,
|
||||
unprivileged_identity: unprivileged_identity,
|
||||
unprivileged_subject: unprivileged_subject,
|
||||
admin_actor: admin_actor,
|
||||
admin_identity: admin_identity,
|
||||
admin_subject: admin_subject
|
||||
}
|
||||
end
|
||||
|
||||
describe "count_by_account_id/0" do
|
||||
test "counts devices for an account", %{account: account} do
|
||||
DevicesFixtures.create_device(account: account)
|
||||
DevicesFixtures.create_device(account: account)
|
||||
DevicesFixtures.create_device(account: account)
|
||||
DevicesFixtures.create_device()
|
||||
|
||||
assert count_by_account_id(account.id) == 3
|
||||
end
|
||||
end
|
||||
|
||||
describe "count_by_actor_id/1" do
|
||||
test "returns 0 if actor does not exist" do
|
||||
assert count_by_actor_id(Ecto.UUID.generate()) == 0
|
||||
end
|
||||
|
||||
test "returns count of devices for a actor" do
|
||||
device = DevicesFixtures.create_device()
|
||||
assert count_by_actor_id(device.actor_id) == 1
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_device_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}
|
||||
end
|
||||
|
||||
test "does not return deleted devices", %{
|
||||
unprivileged_actor: actor,
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
device =
|
||||
DevicesFixtures.create_device(actor: actor)
|
||||
|> DevicesFixtures.delete_device()
|
||||
|
||||
assert fetch_device_by_id(device.id, subject) == {:error, :not_found}
|
||||
end
|
||||
|
||||
test "returns device by id", %{unprivileged_actor: actor, unprivileged_subject: subject} do
|
||||
device = DevicesFixtures.create_device(actor: actor)
|
||||
assert fetch_device_by_id(device.id, subject) == {:ok, device}
|
||||
end
|
||||
|
||||
test "returns device that belongs to another actor with manage permission", %{
|
||||
account: account,
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
device = DevicesFixtures.create_device(account: account)
|
||||
|
||||
subject =
|
||||
subject
|
||||
|> AuthFixtures.remove_permissions()
|
||||
|> AuthFixtures.add_permission(Devices.Authorizer.manage_devices_permission())
|
||||
|
||||
assert fetch_device_by_id(device.id, subject) == {:ok, device}
|
||||
end
|
||||
|
||||
test "does not returns device that belongs to another account with manage permission", %{
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
device = DevicesFixtures.create_device()
|
||||
|
||||
subject =
|
||||
subject
|
||||
|> AuthFixtures.remove_permissions()
|
||||
|> AuthFixtures.add_permission(Devices.Authorizer.manage_devices_permission())
|
||||
|
||||
assert fetch_device_by_id(device.id, subject) == {:error, :not_found}
|
||||
end
|
||||
|
||||
test "does not return device that belongs to another actor with manage_own permission", %{
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
device = DevicesFixtures.create_device()
|
||||
|
||||
subject =
|
||||
subject
|
||||
|> AuthFixtures.remove_permissions()
|
||||
|> AuthFixtures.add_permission(Devices.Authorizer.manage_own_devices_permission())
|
||||
|
||||
assert fetch_device_by_id(device.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) ==
|
||||
{:error, :not_found}
|
||||
end
|
||||
|
||||
test "returns error when subject has no permission to view devices", %{
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
subject = AuthFixtures.remove_permissions(subject)
|
||||
|
||||
assert fetch_device_by_id(Ecto.UUID.generate(), subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[
|
||||
missing_permissions: [
|
||||
{:one_of,
|
||||
[
|
||||
Devices.Authorizer.manage_devices_permission(),
|
||||
Devices.Authorizer.manage_own_devices_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, []}
|
||||
end
|
||||
|
||||
test "does not list deleted devices", %{
|
||||
unprivileged_actor: actor,
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
DevicesFixtures.create_device(actor: actor)
|
||||
|> DevicesFixtures.delete_device()
|
||||
|
||||
assert list_devices(subject) == {:ok, []}
|
||||
end
|
||||
|
||||
test "does not list devices in other accounts", %{
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
DevicesFixtures.create_device()
|
||||
|
||||
assert list_devices(subject) == {:ok, []}
|
||||
end
|
||||
|
||||
test "shows all devices owned by a actor for unprivileged subject", %{
|
||||
unprivileged_actor: actor,
|
||||
admin_actor: other_actor,
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
device = DevicesFixtures.create_device(actor: actor)
|
||||
DevicesFixtures.create_device(actor: other_actor)
|
||||
|
||||
assert list_devices(subject) == {:ok, [device]}
|
||||
end
|
||||
|
||||
test "shows all devices for admin subject", %{
|
||||
unprivileged_actor: other_actor,
|
||||
admin_actor: admin_actor,
|
||||
admin_subject: subject
|
||||
} do
|
||||
DevicesFixtures.create_device(actor: admin_actor)
|
||||
DevicesFixtures.create_device(actor: other_actor)
|
||||
|
||||
assert {:ok, devices} = list_devices(subject)
|
||||
assert length(devices) == 2
|
||||
end
|
||||
|
||||
test "returns error when subject has no permission to manage devices", %{
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
subject = AuthFixtures.remove_permissions(subject)
|
||||
|
||||
assert list_devices(subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[
|
||||
missing_permissions: [
|
||||
{:one_of,
|
||||
[
|
||||
Devices.Authorizer.manage_devices_permission(),
|
||||
Devices.Authorizer.manage_own_devices_permission()
|
||||
]}
|
||||
]
|
||||
]}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_devices_by_actor_id/2" do
|
||||
test "returns empty list when there are no devices 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, []}
|
||||
DevicesFixtures.create_device()
|
||||
assert list_devices_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}
|
||||
end
|
||||
|
||||
test "does not list deleted devices", %{
|
||||
unprivileged_actor: actor,
|
||||
unprivileged_identity: identity,
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
DevicesFixtures.create_device(identity: identity)
|
||||
|> DevicesFixtures.delete_device()
|
||||
|
||||
assert list_devices_by_actor_id(actor.id, subject) == {:ok, []}
|
||||
end
|
||||
|
||||
test "does not deleted devices for actors in other accounts", %{
|
||||
unprivileged_subject: unprivileged_subject,
|
||||
admin_subject: admin_subject
|
||||
} do
|
||||
actor = ActorsFixtures.create_actor(type: :account_user)
|
||||
DevicesFixtures.create_device(actor: actor)
|
||||
|
||||
assert list_devices_by_actor_id(actor.id, unprivileged_subject) == {:ok, []}
|
||||
assert list_devices_by_actor_id(actor.id, admin_subject) == {:ok, []}
|
||||
end
|
||||
|
||||
test "shows only devices owned by a actor for unprivileged subject", %{
|
||||
unprivileged_actor: actor,
|
||||
admin_actor: other_actor,
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
device = DevicesFixtures.create_device(actor: actor)
|
||||
DevicesFixtures.create_device(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, []}
|
||||
end
|
||||
|
||||
test "shows all devices owned by another actor for admin subject", %{
|
||||
unprivileged_actor: other_actor,
|
||||
admin_actor: admin_actor,
|
||||
admin_subject: subject
|
||||
} do
|
||||
DevicesFixtures.create_device(actor: admin_actor)
|
||||
DevicesFixtures.create_device(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)
|
||||
end
|
||||
|
||||
test "returns error when subject has no permission to manage devices", %{
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
subject = AuthFixtures.remove_permissions(subject)
|
||||
|
||||
assert list_devices_by_actor_id(Ecto.UUID.generate(), subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[
|
||||
missing_permissions: [
|
||||
{:one_of,
|
||||
[
|
||||
Devices.Authorizer.manage_devices_permission(),
|
||||
Devices.Authorizer.manage_own_devices_permission()
|
||||
]}
|
||||
]
|
||||
]}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "change_device/1" do
|
||||
test "returns changeset with given changes", %{admin_actor: actor} do
|
||||
device = DevicesFixtures.create_device(actor: actor)
|
||||
device_attrs = DevicesFixtures.device_attrs()
|
||||
|
||||
assert changeset = change_device(device, device_attrs)
|
||||
assert %Ecto.Changeset{data: %Domain.Devices.Device{}} = changeset
|
||||
|
||||
assert changeset.changes == %{name: device_attrs.name}
|
||||
end
|
||||
end
|
||||
|
||||
describe "upsert_device/2" do
|
||||
test "returns errors on invalid attrs", %{
|
||||
admin_subject: subject
|
||||
} do
|
||||
attrs = %{
|
||||
external_id: nil,
|
||||
public_key: "x",
|
||||
ipv4: "1.1.1.256",
|
||||
ipv6: "fd01::10000"
|
||||
}
|
||||
|
||||
assert {:error, changeset} = upsert_device(attrs, subject)
|
||||
|
||||
assert errors_on(changeset) == %{
|
||||
public_key: ["should be 44 character(s)", "must be a base64-encoded string"],
|
||||
external_id: ["can't be blank"]
|
||||
}
|
||||
end
|
||||
|
||||
test "allows creating device with just required attributes", %{
|
||||
admin_actor: actor,
|
||||
admin_identity: identity,
|
||||
admin_subject: subject
|
||||
} do
|
||||
attrs =
|
||||
DevicesFixtures.device_attrs()
|
||||
|> Map.delete(:name)
|
||||
|
||||
assert {:ok, device} = upsert_device(attrs, subject)
|
||||
|
||||
assert device.name
|
||||
|
||||
assert device.public_key == attrs.public_key
|
||||
|
||||
assert device.actor_id == actor.id
|
||||
assert device.identity_id == identity.id
|
||||
assert device.account_id == actor.account_id
|
||||
|
||||
refute is_nil(device.ipv4)
|
||||
refute is_nil(device.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
|
||||
end
|
||||
|
||||
test "updates device when it already exists", %{
|
||||
admin_subject: subject
|
||||
} do
|
||||
device = DevicesFixtures.create_device(subject: subject)
|
||||
attrs = DevicesFixtures.device_attrs(external_id: device.external_id)
|
||||
|
||||
subject = %{
|
||||
subject
|
||||
| context: %Domain.Auth.Context{
|
||||
subject.context
|
||||
| remote_ip: {100, 64, 100, 101},
|
||||
user_agent: "iOS/12.5 (iPhone) connlib/0.7.411"
|
||||
}
|
||||
}
|
||||
|
||||
assert {:ok, updated_device} = upsert_device(attrs, subject)
|
||||
|
||||
assert Repo.aggregate(Devices.Device, :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_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
|
||||
end
|
||||
|
||||
test "does not reserve additional addresses on update", %{
|
||||
admin_subject: subject
|
||||
} do
|
||||
device = DevicesFixtures.create_device(subject: subject)
|
||||
|
||||
attrs =
|
||||
DevicesFixtures.device_attrs(
|
||||
external_id: device.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)
|
||||
|
||||
addresses =
|
||||
Domain.Network.Address
|
||||
|> Repo.all()
|
||||
|> Enum.map(fn %Domain.Network.Address{address: address, type: type} ->
|
||||
%{address: address, type: type}
|
||||
end)
|
||||
|
||||
assert length(addresses) == 2
|
||||
assert %{address: updated_device.ipv4, type: :ipv4} in addresses
|
||||
assert %{address: updated_device.ipv6, type: :ipv6} in addresses
|
||||
end
|
||||
|
||||
test "allows unprivileged actor to create a device for himself", %{
|
||||
admin_subject: subject
|
||||
} do
|
||||
attrs =
|
||||
DevicesFixtures.device_attrs()
|
||||
|> Map.delete(:name)
|
||||
|
||||
assert {:ok, _device} = upsert_device(attrs, subject)
|
||||
end
|
||||
|
||||
test "does not allow to reuse IP addresses", %{
|
||||
account: account,
|
||||
admin_subject: subject
|
||||
} do
|
||||
attrs = DevicesFixtures.device_attrs(account: account)
|
||||
assert {:ok, device} = upsert_device(attrs, subject)
|
||||
|
||||
addresses =
|
||||
Domain.Network.Address
|
||||
|> Repo.all()
|
||||
|> Enum.map(fn %Domain.Network.Address{address: address, type: type} ->
|
||||
%{address: address, type: type}
|
||||
end)
|
||||
|
||||
assert length(addresses) == 2
|
||||
assert %{address: device.ipv4, type: :ipv4} in addresses
|
||||
assert %{address: device.ipv6, type: :ipv6} in addresses
|
||||
|
||||
assert_raise Ecto.ConstraintError, fn ->
|
||||
NetworkFixtures.create_address(address: device.ipv4, account: account)
|
||||
end
|
||||
|
||||
assert_raise Ecto.ConstraintError, fn ->
|
||||
NetworkFixtures.create_address(address: device.ipv6, account: account)
|
||||
end
|
||||
end
|
||||
|
||||
test "ip addresses are unique per account", %{
|
||||
account: account,
|
||||
admin_subject: subject
|
||||
} do
|
||||
attrs = DevicesFixtures.device_attrs(account: account)
|
||||
assert {:ok, device} = upsert_device(attrs, subject)
|
||||
|
||||
assert %Domain.Network.Address{} = NetworkFixtures.create_address(address: device.ipv4)
|
||||
assert %Domain.Network.Address{} = NetworkFixtures.create_address(address: device.ipv6)
|
||||
end
|
||||
|
||||
test "returns error when subject has no permission to create devices", %{
|
||||
admin_subject: subject
|
||||
} do
|
||||
subject = AuthFixtures.remove_permissions(subject)
|
||||
|
||||
assert upsert_device(%{}, subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[missing_permissions: [Devices.Authorizer.manage_own_devices_permission()]]}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "update_device/3" do
|
||||
test "allows admin actor to update own devices", %{admin_actor: actor, admin_subject: subject} do
|
||||
device = DevicesFixtures.create_device(actor: actor)
|
||||
attrs = %{name: "new name"}
|
||||
|
||||
assert {:ok, device} = update_device(device, attrs, subject)
|
||||
|
||||
assert device.name == attrs.name
|
||||
end
|
||||
|
||||
test "allows admin actor to update other actors devices", %{
|
||||
account: account,
|
||||
admin_subject: subject
|
||||
} do
|
||||
device = DevicesFixtures.create_device(account: account)
|
||||
attrs = %{name: "new name"}
|
||||
|
||||
assert {:ok, device} = update_device(device, attrs, subject)
|
||||
|
||||
assert device.name == attrs.name
|
||||
end
|
||||
|
||||
test "allows unprivileged actor to update own devices", %{
|
||||
unprivileged_actor: actor,
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
device = DevicesFixtures.create_device(actor: actor)
|
||||
attrs = %{name: "new name"}
|
||||
|
||||
assert {:ok, device} = update_device(device, attrs, subject)
|
||||
|
||||
assert device.name == attrs.name
|
||||
end
|
||||
|
||||
test "does not allow unprivileged actor to update other actors devices", %{
|
||||
account: account,
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
device = DevicesFixtures.create_device(account: account)
|
||||
attrs = %{name: "new name"}
|
||||
|
||||
assert update_device(device, attrs, subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[missing_permissions: [Devices.Authorizer.manage_devices_permission()]]}}
|
||||
end
|
||||
|
||||
test "does not allow admin actor to update devices in other accounts", %{
|
||||
admin_subject: subject
|
||||
} do
|
||||
device = DevicesFixtures.create_device()
|
||||
attrs = %{name: "new name"}
|
||||
|
||||
assert update_device(device, 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 = DevicesFixtures.create_device(actor: actor)
|
||||
attrs = %{name: nil, public_key: nil}
|
||||
|
||||
assert {:error, changeset} = update_device(device, 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 = DevicesFixtures.create_device(actor: actor)
|
||||
|
||||
attrs = %{
|
||||
name: String.duplicate("a", 256)
|
||||
}
|
||||
|
||||
assert {:error, changeset} = update_device(device, attrs, subject)
|
||||
|
||||
assert errors_on(changeset) == %{
|
||||
name: ["should be at most 255 character(s)"]
|
||||
}
|
||||
end
|
||||
|
||||
test "ignores updates for any field except name", %{
|
||||
admin_actor: actor,
|
||||
admin_subject: subject
|
||||
} do
|
||||
device = DevicesFixtures.create_device(actor: actor)
|
||||
|
||||
fields = Devices.Device.__schema__(:fields) -- [:name]
|
||||
value = -1
|
||||
|
||||
for field <- fields do
|
||||
assert {:ok, updated_device} = update_device(device, %{field => value}, subject)
|
||||
assert updated_device == device
|
||||
end
|
||||
end
|
||||
|
||||
test "returns error when subject has no permission to update devices", %{
|
||||
admin_actor: actor,
|
||||
admin_subject: subject
|
||||
} do
|
||||
device = DevicesFixtures.create_device(actor: actor)
|
||||
|
||||
subject = AuthFixtures.remove_permissions(subject)
|
||||
|
||||
assert update_device(device, %{}, subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[missing_permissions: [Devices.Authorizer.manage_own_devices_permission()]]}}
|
||||
|
||||
device = DevicesFixtures.create_device()
|
||||
|
||||
assert update_device(device, %{}, subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[missing_permissions: [Devices.Authorizer.manage_devices_permission()]]}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete_device/2" do
|
||||
test "returns error on state conflict", %{admin_actor: actor, admin_subject: subject} do
|
||||
device = DevicesFixtures.create_device(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}
|
||||
end
|
||||
|
||||
test "admin can delete own devices", %{admin_actor: actor, admin_subject: subject} do
|
||||
device = DevicesFixtures.create_device(actor: actor)
|
||||
|
||||
assert {:ok, deleted} = delete_device(device, subject)
|
||||
assert deleted.deleted_at
|
||||
end
|
||||
|
||||
test "admin can delete other people devices", %{
|
||||
unprivileged_actor: actor,
|
||||
admin_subject: subject
|
||||
} do
|
||||
device = DevicesFixtures.create_device(actor: actor)
|
||||
|
||||
assert {:ok, deleted} = delete_device(device, subject)
|
||||
assert deleted.deleted_at
|
||||
end
|
||||
|
||||
test "admin can not delete devices in other accounts", %{
|
||||
admin_subject: subject
|
||||
} do
|
||||
device = DevicesFixtures.create_device()
|
||||
|
||||
assert delete_device(device, subject) == {:error, :not_found}
|
||||
end
|
||||
|
||||
test "unprivileged can delete own devices", %{
|
||||
account: account,
|
||||
unprivileged_actor: actor,
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
device = DevicesFixtures.create_device(account: account, actor: actor)
|
||||
|
||||
assert {:ok, deleted} = delete_device(device, subject)
|
||||
assert deleted.deleted_at
|
||||
end
|
||||
|
||||
test "unprivileged can not delete other people devices", %{
|
||||
account: account,
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
device = DevicesFixtures.create_device()
|
||||
|
||||
assert delete_device(device, subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[missing_permissions: [Devices.Authorizer.manage_devices_permission()]]}}
|
||||
|
||||
device = DevicesFixtures.create_device(account: account)
|
||||
|
||||
assert delete_device(device, subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[missing_permissions: [Devices.Authorizer.manage_devices_permission()]]}}
|
||||
|
||||
assert Repo.aggregate(Devices.Device, :count) == 2
|
||||
end
|
||||
|
||||
test "returns error when subject has no permission to delete devices", %{
|
||||
admin_actor: actor,
|
||||
admin_subject: subject
|
||||
} do
|
||||
device = DevicesFixtures.create_device(actor: actor)
|
||||
|
||||
subject = AuthFixtures.remove_permissions(subject)
|
||||
|
||||
assert delete_device(device, subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[missing_permissions: [Devices.Authorizer.manage_own_devices_permission()]]}}
|
||||
|
||||
device = DevicesFixtures.create_device()
|
||||
|
||||
assert delete_device(device, subject) ==
|
||||
{:error,
|
||||
{:unauthorized,
|
||||
[missing_permissions: [Devices.Authorizer.manage_devices_permission()]]}}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -7,7 +7,7 @@ defmodule Domain.GatewaysTest do
|
||||
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -721,4 +721,32 @@ defmodule Domain.GatewaysTest do
|
||||
[missing_permissions: [Gateways.Authorizer.manage_gateways_permission()]]}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "encode_token!/1" do
|
||||
test "returns encoded token" do
|
||||
token = GatewaysFixtures.create_token()
|
||||
assert encrypted_secret = encode_token!(token)
|
||||
|
||||
config = Application.fetch_env!(:domain, Domain.Gateways)
|
||||
key_base = Keyword.fetch!(config, :key_base)
|
||||
salt = Keyword.fetch!(config, :salt)
|
||||
|
||||
assert Plug.Crypto.verify(key_base, salt, encrypted_secret) ==
|
||||
{:ok, {token.id, token.value}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "authorize_gateway/1" do
|
||||
test "returns token when encoded secret is valid" do
|
||||
token = GatewaysFixtures.create_token()
|
||||
encoded_token = encode_token!(token)
|
||||
assert {:ok, fetched_token} = authorize_gateway(encoded_token)
|
||||
assert fetched_token.id == token.id
|
||||
assert is_nil(fetched_token.value)
|
||||
end
|
||||
|
||||
test "returns error when secret is invalid" do
|
||||
assert authorize_gateway(Ecto.UUID.generate()) == {:error, :invalid_token}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,7 +7,7 @@ defmodule Domain.RelaysTest do
|
||||
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
@@ -599,4 +599,32 @@ defmodule Domain.RelaysTest do
|
||||
[missing_permissions: [Relays.Authorizer.manage_relays_permission()]]}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "encode_token!/1" do
|
||||
test "returns encoded token" do
|
||||
token = RelaysFixtures.create_token()
|
||||
assert encrypted_secret = encode_token!(token)
|
||||
|
||||
config = Application.fetch_env!(:domain, Domain.Relays)
|
||||
key_base = Keyword.fetch!(config, :key_base)
|
||||
salt = Keyword.fetch!(config, :salt)
|
||||
|
||||
assert Plug.Crypto.verify(key_base, salt, encrypted_secret) ==
|
||||
{:ok, {token.id, token.value}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "authorize_relay/1" do
|
||||
test "returns token when encoded secret is valid" do
|
||||
token = RelaysFixtures.create_token()
|
||||
encoded_token = encode_token!(token)
|
||||
assert {:ok, fetched_token} = authorize_relay(encoded_token)
|
||||
assert fetched_token.id == token.id
|
||||
assert is_nil(fetched_token.value)
|
||||
end
|
||||
|
||||
test "returns error when secret is invalid" do
|
||||
assert authorize_relay(Ecto.UUID.generate()) == {:error, :invalid_token}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
# defmodule Domain.ReleaseTest do
|
||||
# @moduledoc """
|
||||
# XXX: Write more meaningful tests for this module.
|
||||
# Perhaps the best way to test this module is through functional tests.
|
||||
# """
|
||||
# use Domain.DataCase, async: true
|
||||
# alias Domain.{ApiTokens, Users}
|
||||
# alias Domain.Release
|
||||
# alias Domain.UsersFixtures
|
||||
|
||||
# describe "migrate/0" do
|
||||
# test "function runs without error" do
|
||||
# assert Release.migrate()
|
||||
# end
|
||||
# end
|
||||
|
||||
# describe "create_admin_user/0" do
|
||||
# test "creates admin when none exists" do
|
||||
# Release.create_admin_user()
|
||||
# email = Domain.Config.fetch_env!(:domain, :admin_email)
|
||||
# assert {:ok, %Users.User{}} = Users.fetch_user_by_email(email)
|
||||
# end
|
||||
|
||||
# test "reset admin password when user exists" do
|
||||
# {:ok, first_user} = Release.create_admin_user()
|
||||
# {:ok, new_first_user} = Release.change_password(first_user.email, "newpassword1234")
|
||||
# {:ok, second_user} = Release.create_admin_user()
|
||||
|
||||
# assert second_user.password_hash != new_first_user.password_hash
|
||||
# end
|
||||
# end
|
||||
|
||||
# describe "create_api_token/1" do
|
||||
# test "creates api_token_token for default admin user" do
|
||||
# admin_user =
|
||||
# UsersFixtures.create_user_with_role(:admin, %{
|
||||
# email: Domain.Config.fetch_env!(:domain, :admin_email)
|
||||
# })
|
||||
|
||||
# assert :ok = Release.create_api_token()
|
||||
# assert ApiTokens.count_by_user_id(admin_user.id) == 1
|
||||
# end
|
||||
# end
|
||||
|
||||
# describe "change_password/2" do
|
||||
# test "changes password" do
|
||||
# user = UsersFixtures.create_user_with_role(:unprivileged)
|
||||
|
||||
# Release.change_password(user.email, "this password should be different")
|
||||
# assert {:ok, new_user} = Users.fetch_user_by_email(user.email)
|
||||
|
||||
# assert new_user.password_hash != user.password_hash
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
@@ -7,7 +7,7 @@ defmodule Domain.ResourcesTest do
|
||||
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
|
||||
@@ -1,150 +1,150 @@
|
||||
defmodule Domain.TelemetryTest do
|
||||
use Domain.DataCase, async: true
|
||||
# import Domain.TestHelpers
|
||||
# alias Domain.Telemetry
|
||||
# alias Domain.MFAFixtures
|
||||
# defmodule Domain.TelemetryTest do
|
||||
# use Domain.DataCase, async: true
|
||||
# # import Domain.TestHelpers
|
||||
# # alias Domain.Telemetry
|
||||
# # alias Domain.MFAFixtures
|
||||
|
||||
# describe "user" do
|
||||
# setup :create_user
|
||||
# # describe "user" do
|
||||
# # setup :create_user
|
||||
|
||||
# test "count" do
|
||||
# ping_data = Telemetry.ping_data()
|
||||
# # test "count" do
|
||||
# # ping_data = Telemetry.ping_data()
|
||||
|
||||
# assert ping_data[:user_count] == 1
|
||||
# end
|
||||
# # assert ping_data[:user_count] == 1
|
||||
# # end
|
||||
|
||||
# test "count mfa", %{user: user} do
|
||||
# {:ok, [user: other_user]} = create_user(%{})
|
||||
# MFAFixtures.create_totp_method(user: user)
|
||||
# MFAFixtures.create_totp_method(user: other_user)
|
||||
# ping_data = Telemetry.ping_data()
|
||||
# # test "count mfa", %{user: user} do
|
||||
# # {:ok, [user: other_user]} = create_user(%{})
|
||||
# # MFAFixtures.create_totp_method(user: user)
|
||||
# # MFAFixtures.create_totp_method(user: other_user)
|
||||
# # ping_data = Telemetry.ping_data()
|
||||
|
||||
# assert ping_data[:users_with_mfa] == 2
|
||||
# assert ping_data[:users_with_mfa_totp] == 2
|
||||
# end
|
||||
# end
|
||||
# # assert ping_data[:users_with_mfa] == 2
|
||||
# # assert ping_data[:users_with_mfa_totp] == 2
|
||||
# # end
|
||||
# # end
|
||||
|
||||
# describe "device" do
|
||||
# setup [:create_devices, :create_other_user_device]
|
||||
# # describe "device" do
|
||||
# # setup [:create_devices, :create_other_user_device]
|
||||
|
||||
# test "count" do
|
||||
# ping_data = Telemetry.ping_data()
|
||||
# # test "count" do
|
||||
# # ping_data = Telemetry.ping_data()
|
||||
|
||||
# assert ping_data[:device_count] == 6
|
||||
# end
|
||||
# # assert ping_data[:device_count] == 6
|
||||
# # end
|
||||
|
||||
# test "max count for users" do
|
||||
# ping_data = Telemetry.ping_data()
|
||||
# # test "max count for users" do
|
||||
# # ping_data = Telemetry.ping_data()
|
||||
|
||||
# assert ping_data[:max_devices_for_users] == 5
|
||||
# end
|
||||
# end
|
||||
# # assert ping_data[:max_devices_for_users] == 5
|
||||
# # end
|
||||
# # end
|
||||
|
||||
# describe "auth" do
|
||||
# test "count openid providers" do
|
||||
# Domain.ConfigFixtures.start_openid_providers([
|
||||
# "google",
|
||||
# "okta",
|
||||
# "auth0",
|
||||
# "azure",
|
||||
# "onelogin",
|
||||
# "keycloak",
|
||||
# "vault"
|
||||
# ])
|
||||
# # describe "auth" do
|
||||
# # test "count openid providers" do
|
||||
# # Domain.ConfigFixtures.start_openid_providers([
|
||||
# # "google",
|
||||
# # "okta",
|
||||
# # "auth0",
|
||||
# # "azure",
|
||||
# # "onelogin",
|
||||
# # "keycloak",
|
||||
# # "vault"
|
||||
# # ])
|
||||
|
||||
# ping_data = Telemetry.ping_data()
|
||||
# # ping_data = Telemetry.ping_data()
|
||||
|
||||
# assert ping_data[:openid_providers] == 7
|
||||
# end
|
||||
# # assert ping_data[:openid_providers] == 7
|
||||
# # end
|
||||
|
||||
# test "disable vpn on oidc error enabled" do
|
||||
# Domain.Config.put_config!(:disable_vpn_on_oidc_error, true)
|
||||
# # test "disable vpn on oidc error enabled" do
|
||||
# # Domain.Config.put_config!(:disable_vpn_on_oidc_error, true)
|
||||
|
||||
# ping_data = Telemetry.ping_data()
|
||||
# # ping_data = Telemetry.ping_data()
|
||||
|
||||
# assert ping_data[:disable_vpn_on_oidc_error]
|
||||
# end
|
||||
# # assert ping_data[:disable_vpn_on_oidc_error]
|
||||
# # end
|
||||
|
||||
# test "disable vpn on oidc error disabled" do
|
||||
# Domain.Config.put_config!(:disable_vpn_on_oidc_error, false)
|
||||
# # test "disable vpn on oidc error disabled" do
|
||||
# # Domain.Config.put_config!(:disable_vpn_on_oidc_error, false)
|
||||
|
||||
# ping_data = Telemetry.ping_data()
|
||||
# # ping_data = Telemetry.ping_data()
|
||||
|
||||
# refute ping_data[:disable_vpn_on_oidc_error]
|
||||
# end
|
||||
# # refute ping_data[:disable_vpn_on_oidc_error]
|
||||
# # end
|
||||
|
||||
# test "local authentication enabled" do
|
||||
# Domain.Config.put_config!(:local_auth_enabled, true)
|
||||
# # test "local authentication enabled" do
|
||||
# # Domain.Config.put_config!(:local_auth_enabled, true)
|
||||
|
||||
# ping_data = Telemetry.ping_data()
|
||||
# # ping_data = Telemetry.ping_data()
|
||||
|
||||
# assert ping_data[:local_authentication]
|
||||
# end
|
||||
# # assert ping_data[:local_authentication]
|
||||
# # end
|
||||
|
||||
# test "local authentication disabled" do
|
||||
# Domain.Config.put_config!(:local_auth_enabled, false)
|
||||
# # test "local authentication disabled" do
|
||||
# # Domain.Config.put_config!(:local_auth_enabled, false)
|
||||
|
||||
# ping_data = Telemetry.ping_data()
|
||||
# # ping_data = Telemetry.ping_data()
|
||||
|
||||
# refute ping_data[:local_authentication]
|
||||
# end
|
||||
# # refute ping_data[:local_authentication]
|
||||
# # end
|
||||
|
||||
# test "unprivileged device management enabled" do
|
||||
# Domain.Config.put_config!(:allow_unprivileged_device_management, true)
|
||||
# # test "unprivileged device management enabled" do
|
||||
# # Domain.Config.put_config!(:allow_unprivileged_device_management, true)
|
||||
|
||||
# ping_data = Telemetry.ping_data()
|
||||
# # ping_data = Telemetry.ping_data()
|
||||
|
||||
# assert ping_data[:unprivileged_device_management]
|
||||
# end
|
||||
# # assert ping_data[:unprivileged_device_management]
|
||||
# # end
|
||||
|
||||
# test "unprivileged device configuration enabled" do
|
||||
# Domain.Config.put_config!(:allow_unprivileged_device_configuration, true)
|
||||
# # test "unprivileged device configuration enabled" do
|
||||
# # Domain.Config.put_config!(:allow_unprivileged_device_configuration, true)
|
||||
|
||||
# ping_data = Telemetry.ping_data()
|
||||
# # ping_data = Telemetry.ping_data()
|
||||
|
||||
# assert ping_data[:unprivileged_device_configuration]
|
||||
# end
|
||||
# # assert ping_data[:unprivileged_device_configuration]
|
||||
# # end
|
||||
|
||||
# test "unprivileged device configuration disabled" do
|
||||
# Domain.Config.put_config!(:allow_unprivileged_device_configuration, false)
|
||||
# # test "unprivileged device configuration disabled" do
|
||||
# # Domain.Config.put_config!(:allow_unprivileged_device_configuration, false)
|
||||
|
||||
# ping_data = Telemetry.ping_data()
|
||||
# # ping_data = Telemetry.ping_data()
|
||||
|
||||
# refute ping_data[:unprivileged_device_configuration]
|
||||
# end
|
||||
# end
|
||||
# # refute ping_data[:unprivileged_device_configuration]
|
||||
# # end
|
||||
# # end
|
||||
|
||||
# describe "database" do
|
||||
# test "local hostname" do
|
||||
# Domain.Config.put_env_override(:domain, Domain.Repo, hostname: "localhost")
|
||||
# # describe "database" do
|
||||
# # test "local hostname" do
|
||||
# # Domain.Config.put_env_override(:domain, Domain.Repo, hostname: "localhost")
|
||||
|
||||
# ping_data = Telemetry.ping_data()
|
||||
# # ping_data = Telemetry.ping_data()
|
||||
|
||||
# refute ping_data[:external_database]
|
||||
# end
|
||||
# # refute ping_data[:external_database]
|
||||
# # end
|
||||
|
||||
# test "local url" do
|
||||
# Domain.Config.put_env_override(:domain, Domain.Repo, url: "postgres://127.0.0.1")
|
||||
# # test "local url" do
|
||||
# # Domain.Config.put_env_override(:domain, Domain.Repo, url: "postgres://127.0.0.1")
|
||||
|
||||
# ping_data = Telemetry.ping_data()
|
||||
# # ping_data = Telemetry.ping_data()
|
||||
|
||||
# refute ping_data[:external_database]
|
||||
# end
|
||||
# # refute ping_data[:external_database]
|
||||
# # end
|
||||
|
||||
# test "external hostname" do
|
||||
# Domain.Config.put_env_override(:domain, Domain.Repo, hostname: "firezone.dev")
|
||||
# # test "external hostname" do
|
||||
# # Domain.Config.put_env_override(:domain, Domain.Repo, hostname: "firezone.dev")
|
||||
|
||||
# ping_data = Telemetry.ping_data()
|
||||
# # ping_data = Telemetry.ping_data()
|
||||
|
||||
# assert ping_data[:external_database]
|
||||
# end
|
||||
# # assert ping_data[:external_database]
|
||||
# # end
|
||||
|
||||
# test "external url" do
|
||||
# Domain.Config.put_env_override(:domain, Domain.Repo, url: "postgres://firezone.dev")
|
||||
# # test "external url" do
|
||||
# # Domain.Config.put_env_override(:domain, Domain.Repo, url: "postgres://firezone.dev")
|
||||
|
||||
# ping_data = Telemetry.ping_data()
|
||||
# # ping_data = Telemetry.ping_data()
|
||||
|
||||
# assert ping_data[:external_database]
|
||||
# end
|
||||
# end
|
||||
end
|
||||
# # assert ping_data[:external_database]
|
||||
# # end
|
||||
# # end
|
||||
# end
|
||||
|
||||
@@ -5,8 +5,7 @@ defmodule Domain.ActorsFixtures do
|
||||
|
||||
def actor_attrs(attrs \\ %{}) do
|
||||
Enum.into(attrs, %{
|
||||
type: :user,
|
||||
role: :unprivileged
|
||||
type: :account_user
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
defmodule Domain.ApiTokensFixtures do
|
||||
alias Domain.{AccountsFixtures, ActorsFixtures, AuthFixtures}
|
||||
|
||||
def api_token_attrs(attrs \\ %{}) do
|
||||
Enum.into(attrs, %{})
|
||||
end
|
||||
|
||||
def create_api_token(attrs \\ %{}) do
|
||||
attrs = api_token_attrs(attrs)
|
||||
|
||||
{account, attrs} =
|
||||
Map.pop_lazy(attrs, :account, fn ->
|
||||
AccountsFixtures.create_account()
|
||||
end)
|
||||
|
||||
{subject, attrs} =
|
||||
Map.pop_lazy(attrs, :subject, fn ->
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, actor: actor)
|
||||
AuthFixtures.create_subject(identity)
|
||||
end)
|
||||
|
||||
{:ok, api_token} = Domain.ApiTokens.create_api_token(attrs, subject)
|
||||
api_token
|
||||
end
|
||||
|
||||
def expire_api_token(api_token) do
|
||||
one_second_ago = DateTime.utc_now() |> DateTime.add(-1, :second)
|
||||
|
||||
Ecto.Changeset.change(api_token, expires_at: one_second_ago)
|
||||
|> Domain.Repo.update!()
|
||||
end
|
||||
end
|
||||
@@ -15,6 +15,10 @@ defmodule Domain.AuthFixtures do
|
||||
Ecto.UUID.generate()
|
||||
end
|
||||
|
||||
def random_provider_identifier(%Domain.Auth.Provider{adapter: :token}) do
|
||||
Ecto.UUID.generate()
|
||||
end
|
||||
|
||||
def random_provider_identifier(%Domain.Auth.Provider{adapter: :userpass, name: name}) do
|
||||
"user-#{counter()}@#{String.downcase(name)}.com"
|
||||
end
|
||||
@@ -72,6 +76,20 @@ defmodule Domain.AuthFixtures do
|
||||
provider
|
||||
end
|
||||
|
||||
def create_token_provider(attrs \\ %{}) do
|
||||
attrs = Enum.into(attrs, %{})
|
||||
|
||||
{account, _attrs} =
|
||||
Map.pop_lazy(attrs, :account, fn ->
|
||||
AccountsFixtures.create_account()
|
||||
end)
|
||||
|
||||
attrs = provider_attrs(adapter: :token)
|
||||
|
||||
{:ok, provider} = Auth.create_provider(account, attrs)
|
||||
provider
|
||||
end
|
||||
|
||||
def create_identity(attrs \\ %{}) do
|
||||
attrs = Enum.into(attrs, %{})
|
||||
|
||||
@@ -118,7 +136,7 @@ defmodule Domain.AuthFixtures do
|
||||
|
||||
def create_subject do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = create_identity(actor: actor, account: account)
|
||||
create_subject(identity)
|
||||
end
|
||||
@@ -129,7 +147,7 @@ defmodule Domain.AuthFixtures do
|
||||
%Auth.Subject{
|
||||
identity: identity,
|
||||
actor: identity.actor,
|
||||
permissions: Auth.Roles.build(identity.actor.role).permissions,
|
||||
permissions: Auth.Roles.build(identity.actor.type).permissions,
|
||||
account: identity.account,
|
||||
expires_at: DateTime.utc_now() |> DateTime.add(60, :second),
|
||||
context: %Auth.Context{remote_ip: remote_ip(), user_agent: user_agent()}
|
||||
@@ -148,6 +166,292 @@ defmodule Domain.AuthFixtures do
|
||||
%{subject | permissions: MapSet.put(subject.permissions, permission)}
|
||||
end
|
||||
|
||||
def start_openid_providers(provider_names, overrides \\ %{}) do
|
||||
{bypass, discovery_document_url} = discovery_document_server()
|
||||
|
||||
openid_connect_providers_attrs =
|
||||
discovery_document_url
|
||||
|> openid_connect_providers_attrs()
|
||||
|> Enum.filter(&(&1["id"] in provider_names))
|
||||
|> Enum.map(fn config ->
|
||||
config
|
||||
|> Enum.into(%{})
|
||||
|> Map.merge(overrides)
|
||||
end)
|
||||
|
||||
{bypass, openid_connect_providers_attrs}
|
||||
end
|
||||
|
||||
def openid_connect_provider_attrs(overrides \\ %{}) do
|
||||
Enum.into(overrides, %{
|
||||
"id" => "google",
|
||||
"discovery_document_uri" => "https://firezone.example.com/.well-known/openid-configuration",
|
||||
"client_id" => "google-client-id",
|
||||
"client_secret" => "google-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/google/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile",
|
||||
"label" => "OIDC Google",
|
||||
"auto_create_users" => false
|
||||
})
|
||||
end
|
||||
|
||||
defp openid_connect_providers_attrs(discovery_document_url) do
|
||||
[
|
||||
%{
|
||||
"id" => "google",
|
||||
"discovery_document_uri" => discovery_document_url,
|
||||
"client_id" => "google-client-id",
|
||||
"client_secret" => "google-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/google/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile",
|
||||
"label" => "OIDC Google",
|
||||
"auto_create_users" => false
|
||||
},
|
||||
%{
|
||||
"id" => "okta",
|
||||
"discovery_document_uri" => discovery_document_url,
|
||||
"client_id" => "okta-client-id",
|
||||
"client_secret" => "okta-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/okta/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile offline_access",
|
||||
"label" => "OIDC Okta",
|
||||
"auto_create_users" => false
|
||||
},
|
||||
%{
|
||||
"id" => "auth0",
|
||||
"discovery_document_uri" => discovery_document_url,
|
||||
"client_id" => "auth0-client-id",
|
||||
"client_secret" => "auth0-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/auth0/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile",
|
||||
"label" => "OIDC Auth0",
|
||||
"auto_create_users" => false
|
||||
},
|
||||
%{
|
||||
"id" => "azure",
|
||||
"discovery_document_uri" => discovery_document_url,
|
||||
"client_id" => "azure-client-id",
|
||||
"client_secret" => "azure-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/azure/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile offline_access",
|
||||
"label" => "OIDC Azure",
|
||||
"auto_create_users" => false
|
||||
},
|
||||
%{
|
||||
"id" => "onelogin",
|
||||
"discovery_document_uri" => discovery_document_url,
|
||||
"client_id" => "onelogin-client-id",
|
||||
"client_secret" => "onelogin-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/onelogin/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile offline_access",
|
||||
"label" => "OIDC Onelogin",
|
||||
"auto_create_users" => false
|
||||
},
|
||||
%{
|
||||
"id" => "keycloak",
|
||||
"discovery_document_uri" => discovery_document_url,
|
||||
"client_id" => "keycloak-client-id",
|
||||
"client_secret" => "keycloak-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/keycloak/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile offline_access",
|
||||
"label" => "OIDC Keycloak",
|
||||
"auto_create_users" => false
|
||||
},
|
||||
%{
|
||||
"id" => "vault",
|
||||
"discovery_document_uri" => discovery_document_url,
|
||||
"client_id" => "vault-client-id",
|
||||
"client_secret" => "vault-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/vault/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile offline_access",
|
||||
"label" => "OIDC Vault",
|
||||
"auto_create_users" => false
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def jwks_attrs do
|
||||
%{
|
||||
"alg" => "RS256",
|
||||
"d" =>
|
||||
"X8TM24Zqbiha9geYYk_vZpANu16IadJLJLJ7ucTc3JaMbK8NCYNcHMoXKnNYPFxmq-UWAEIwh-2" <>
|
||||
"txOiOxuChVrblpfyE4SBJio1T0AUcCwmm8U6G-CsSHMMzWTt2dMTnArHjdyAIgOVRW5SVzhTT" <>
|
||||
"taf4JY-47S-fbcJ7g0hmBbVih5i1sE2fad4I4qFHT-YFU_pnUHbteR6GQuRW4r03Eon8Aje6a" <>
|
||||
"l2AxcYnfF8_cSOIOpkDgGavTtGYhhZPi2jZ7kPm6QGkNW5CyfEq5PGB6JOihw-XIFiiMzYgx0" <>
|
||||
"52rnzoqALoLheXrI0By4kgHSmcqOOmq7aiOff45rlSbpsR",
|
||||
"e" => "AQAB",
|
||||
"kid" => "example@firezone.dev",
|
||||
"kty" => "RSA",
|
||||
"n" =>
|
||||
"qlKll8no4lPYXNSuTTnacpFHiXwPOv_htCYvIXmiR7CWhiiOHQqj7KWXIW7TGxyoLVIyeRM4mwv" <>
|
||||
"kLI-UgsSMYdEKTT0j7Ydjrr0zCunPu5Gxr2yOmcRaszAzGxJL5DwpA0V40RqMlm5OuwdqS4To" <>
|
||||
"_p9LlLxzMF6RZe1OqslV5RZ4Y8FmrWq6BV98eIziEHL0IKdsAIrrOYkkcLDdQeMNuTp_yNB8X" <>
|
||||
"l2TdWSdsbRomrs2dCtCqZcXTsy2EXDceHvYhgAB33nh_w17WLrZQwMM-7kJk36Kk54jZd7i80" <>
|
||||
"AJf_s_plXn1mEh-L5IAL1vg3a9EOMFUl-lPiGqc3td_ykH",
|
||||
"use" => "sig"
|
||||
}
|
||||
end
|
||||
|
||||
def expect_refresh_token(bypass, attrs \\ %{}) do
|
||||
test_pid = self()
|
||||
|
||||
Bypass.expect(bypass, "POST", "/oauth/token", fn conn ->
|
||||
conn = fetch_conn_params(conn)
|
||||
send(test_pid, {:request, conn})
|
||||
Plug.Conn.resp(conn, 200, Jason.encode!(attrs))
|
||||
end)
|
||||
end
|
||||
|
||||
def expect_refresh_token_failure(bypass, attrs \\ %{}) do
|
||||
test_pid = self()
|
||||
|
||||
Bypass.expect(bypass, "POST", "/oauth/token", fn conn ->
|
||||
conn = fetch_conn_params(conn)
|
||||
send(test_pid, {:request, conn})
|
||||
Plug.Conn.resp(conn, 401, Jason.encode!(attrs))
|
||||
end)
|
||||
end
|
||||
|
||||
def expect_userinfo(bypass, attrs \\ %{}) do
|
||||
test_pid = self()
|
||||
|
||||
Bypass.expect(bypass, "GET", "/userinfo", fn conn ->
|
||||
attrs =
|
||||
Map.merge(
|
||||
%{
|
||||
"sub" => "353690423699814251281",
|
||||
"name" => "Ada Lovelace",
|
||||
"given_name" => "Ada",
|
||||
"family_name" => "Lovelace",
|
||||
"picture" =>
|
||||
"https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg",
|
||||
"email" => "ada@example.com",
|
||||
"email_verified" => true,
|
||||
"locale" => "en"
|
||||
},
|
||||
attrs
|
||||
)
|
||||
|
||||
conn = fetch_conn_params(conn)
|
||||
send(test_pid, {:request, conn})
|
||||
Plug.Conn.resp(conn, 200, Jason.encode!(attrs))
|
||||
end)
|
||||
end
|
||||
|
||||
def discovery_document_server do
|
||||
bypass = Bypass.open()
|
||||
endpoint = "http://localhost:#{bypass.port}"
|
||||
test_pid = self()
|
||||
|
||||
Bypass.stub(bypass, "GET", "/.well-known/jwks.json", fn conn ->
|
||||
attrs = %{"keys" => [jwks_attrs()]}
|
||||
Plug.Conn.resp(conn, 200, Jason.encode!(attrs))
|
||||
end)
|
||||
|
||||
Bypass.stub(bypass, "GET", "/.well-known/openid-configuration", fn conn ->
|
||||
conn = fetch_conn_params(conn)
|
||||
send(test_pid, {:request, conn})
|
||||
|
||||
attrs = %{
|
||||
"issuer" => "#{endpoint}/",
|
||||
"authorization_endpoint" => "#{endpoint}/authorize",
|
||||
"token_endpoint" => "#{endpoint}/oauth/token",
|
||||
"device_authorization_endpoint" => "#{endpoint}/oauth/device/code",
|
||||
"userinfo_endpoint" => "#{endpoint}/userinfo",
|
||||
"mfa_challenge_endpoint" => "#{endpoint}/mfa/challenge",
|
||||
"jwks_uri" => "#{endpoint}/.well-known/jwks.json",
|
||||
"registration_endpoint" => "#{endpoint}/oidc/register",
|
||||
"revocation_endpoint" => "#{endpoint}/oauth/revoke",
|
||||
"end_session_endpoint" => "https://example.com",
|
||||
"scopes_supported" => [
|
||||
"openid",
|
||||
"profile",
|
||||
"offline_access",
|
||||
"name",
|
||||
"given_name",
|
||||
"family_name",
|
||||
"nickname",
|
||||
"email",
|
||||
"email_verified",
|
||||
"picture",
|
||||
"created_at",
|
||||
"identities",
|
||||
"phone",
|
||||
"address"
|
||||
],
|
||||
"response_types_supported" => [
|
||||
"code",
|
||||
"token",
|
||||
"id_token",
|
||||
"code token",
|
||||
"code id_token",
|
||||
"token id_token",
|
||||
"code token id_token"
|
||||
],
|
||||
"code_challenge_methods_supported" => [
|
||||
"S256",
|
||||
"plain"
|
||||
],
|
||||
"response_modes_supported" => [
|
||||
"query",
|
||||
"fragment",
|
||||
"form_post"
|
||||
],
|
||||
"subject_types_supported" => [
|
||||
"public"
|
||||
],
|
||||
"id_token_signing_alg_values_supported" => [
|
||||
"HS256",
|
||||
"RS256"
|
||||
],
|
||||
"token_endpoint_auth_methods_supported" => [
|
||||
"client_secret_basic",
|
||||
"client_secret_post"
|
||||
],
|
||||
"claims_supported" => [
|
||||
"aud",
|
||||
"auth_time",
|
||||
"created_at",
|
||||
"email",
|
||||
"email_verified",
|
||||
"exp",
|
||||
"family_name",
|
||||
"given_name",
|
||||
"iat",
|
||||
"identities",
|
||||
"iss",
|
||||
"name",
|
||||
"nickname",
|
||||
"phone_number",
|
||||
"picture",
|
||||
"sub"
|
||||
],
|
||||
"request_uri_parameter_supported" => false,
|
||||
"request_parameter_supported" => false
|
||||
}
|
||||
|
||||
Plug.Conn.resp(conn, 200, Jason.encode!(attrs))
|
||||
end)
|
||||
|
||||
{bypass, "#{endpoint}/.well-known/openid-configuration"}
|
||||
end
|
||||
|
||||
def fetch_conn_params(conn) do
|
||||
opts = Plug.Parsers.init(parsers: [:urlencoded, :json], pass: ["*/*"], json_decoder: Jason)
|
||||
|
||||
conn
|
||||
|> Plug.Conn.fetch_query_params()
|
||||
|> Plug.Parsers.call(opts)
|
||||
end
|
||||
|
||||
defp counter do
|
||||
System.unique_integer([:positive])
|
||||
end
|
||||
|
||||
@@ -1,350 +1,31 @@
|
||||
defmodule Domain.ConfigFixtures do
|
||||
@moduledoc """
|
||||
Allows for easily updating configuration in tests.
|
||||
"""
|
||||
alias Domain.Repo
|
||||
alias Domain.Config
|
||||
alias Domain.AccountsFixtures
|
||||
|
||||
def configuration_attrs(attrs \\ %{}) do
|
||||
Enum.into(attrs, %{
|
||||
devices_upstream_dns: ["1.1.1.1"]
|
||||
})
|
||||
end
|
||||
|
||||
def upsert_configuration(attrs \\ %{}) do
|
||||
attrs = Enum.into(attrs, %{})
|
||||
|
||||
{account, attrs} =
|
||||
Map.pop_lazy(attrs, :account, fn ->
|
||||
AccountsFixtures.create_account()
|
||||
end)
|
||||
|
||||
attrs = configuration_attrs(attrs)
|
||||
|
||||
def configuration(%Config.Configuration{} = conf \\ Config.fetch_db_config!(), attrs) do
|
||||
{:ok, configuration} =
|
||||
conf
|
||||
|> Config.Configuration.Changeset.changeset(attrs)
|
||||
|> Repo.update()
|
||||
Config.get_account_config_by_account_id(account.id)
|
||||
|> Config.update_config(attrs)
|
||||
|
||||
configuration
|
||||
end
|
||||
|
||||
def start_openid_providers(provider_names, overrides \\ %{}) do
|
||||
{bypass, discovery_document_url} = discovery_document_server()
|
||||
|
||||
openid_connect_providers_attrs =
|
||||
discovery_document_url
|
||||
|> openid_connect_providers_attrs()
|
||||
|> Enum.filter(&(&1["id"] in provider_names))
|
||||
|> Enum.map(fn config ->
|
||||
config
|
||||
|> Enum.into(%{})
|
||||
|> Map.merge(overrides)
|
||||
end)
|
||||
|
||||
{bypass, openid_connect_providers_attrs}
|
||||
end
|
||||
|
||||
def openid_connect_provider_attrs(overrides \\ %{}) do
|
||||
Enum.into(overrides, %{
|
||||
"id" => "google",
|
||||
"discovery_document_uri" => "https://firezone.example.com/.well-known/openid-configuration",
|
||||
"client_id" => "google-client-id",
|
||||
"client_secret" => "google-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/google/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile",
|
||||
"label" => "OIDC Google",
|
||||
"auto_create_users" => false
|
||||
})
|
||||
end
|
||||
|
||||
defp openid_connect_providers_attrs(discovery_document_url) do
|
||||
[
|
||||
%{
|
||||
"id" => "google",
|
||||
"discovery_document_uri" => discovery_document_url,
|
||||
"client_id" => "google-client-id",
|
||||
"client_secret" => "google-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/google/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile",
|
||||
"label" => "OIDC Google",
|
||||
"auto_create_users" => false
|
||||
},
|
||||
%{
|
||||
"id" => "okta",
|
||||
"discovery_document_uri" => discovery_document_url,
|
||||
"client_id" => "okta-client-id",
|
||||
"client_secret" => "okta-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/okta/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile offline_access",
|
||||
"label" => "OIDC Okta",
|
||||
"auto_create_users" => false
|
||||
},
|
||||
%{
|
||||
"id" => "auth0",
|
||||
"discovery_document_uri" => discovery_document_url,
|
||||
"client_id" => "auth0-client-id",
|
||||
"client_secret" => "auth0-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/auth0/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile",
|
||||
"label" => "OIDC Auth0",
|
||||
"auto_create_users" => false
|
||||
},
|
||||
%{
|
||||
"id" => "azure",
|
||||
"discovery_document_uri" => discovery_document_url,
|
||||
"client_id" => "azure-client-id",
|
||||
"client_secret" => "azure-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/azure/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile offline_access",
|
||||
"label" => "OIDC Azure",
|
||||
"auto_create_users" => false
|
||||
},
|
||||
%{
|
||||
"id" => "onelogin",
|
||||
"discovery_document_uri" => discovery_document_url,
|
||||
"client_id" => "onelogin-client-id",
|
||||
"client_secret" => "onelogin-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/onelogin/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile offline_access",
|
||||
"label" => "OIDC Onelogin",
|
||||
"auto_create_users" => false
|
||||
},
|
||||
%{
|
||||
"id" => "keycloak",
|
||||
"discovery_document_uri" => discovery_document_url,
|
||||
"client_id" => "keycloak-client-id",
|
||||
"client_secret" => "keycloak-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/keycloak/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile offline_access",
|
||||
"label" => "OIDC Keycloak",
|
||||
"auto_create_users" => false
|
||||
},
|
||||
%{
|
||||
"id" => "vault",
|
||||
"discovery_document_uri" => discovery_document_url,
|
||||
"client_id" => "vault-client-id",
|
||||
"client_secret" => "vault-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/vault/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile offline_access",
|
||||
"label" => "OIDC Vault",
|
||||
"auto_create_users" => false
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def jwks_attrs do
|
||||
%{
|
||||
"alg" => "RS256",
|
||||
"d" =>
|
||||
"X8TM24Zqbiha9geYYk_vZpANu16IadJLJLJ7ucTc3JaMbK8NCYNcHMoXKnNYPFxmq-UWAEIwh-2" <>
|
||||
"txOiOxuChVrblpfyE4SBJio1T0AUcCwmm8U6G-CsSHMMzWTt2dMTnArHjdyAIgOVRW5SVzhTT" <>
|
||||
"taf4JY-47S-fbcJ7g0hmBbVih5i1sE2fad4I4qFHT-YFU_pnUHbteR6GQuRW4r03Eon8Aje6a" <>
|
||||
"l2AxcYnfF8_cSOIOpkDgGavTtGYhhZPi2jZ7kPm6QGkNW5CyfEq5PGB6JOihw-XIFiiMzYgx0" <>
|
||||
"52rnzoqALoLheXrI0By4kgHSmcqOOmq7aiOff45rlSbpsR",
|
||||
"e" => "AQAB",
|
||||
"kid" => "example@firezone.dev",
|
||||
"kty" => "RSA",
|
||||
"n" =>
|
||||
"qlKll8no4lPYXNSuTTnacpFHiXwPOv_htCYvIXmiR7CWhiiOHQqj7KWXIW7TGxyoLVIyeRM4mwv" <>
|
||||
"kLI-UgsSMYdEKTT0j7Ydjrr0zCunPu5Gxr2yOmcRaszAzGxJL5DwpA0V40RqMlm5OuwdqS4To" <>
|
||||
"_p9LlLxzMF6RZe1OqslV5RZ4Y8FmrWq6BV98eIziEHL0IKdsAIrrOYkkcLDdQeMNuTp_yNB8X" <>
|
||||
"l2TdWSdsbRomrs2dCtCqZcXTsy2EXDceHvYhgAB33nh_w17WLrZQwMM-7kJk36Kk54jZd7i80" <>
|
||||
"AJf_s_plXn1mEh-L5IAL1vg3a9EOMFUl-lPiGqc3td_ykH",
|
||||
"use" => "sig"
|
||||
}
|
||||
end
|
||||
|
||||
def expect_refresh_token(bypass, attrs \\ %{}) do
|
||||
test_pid = self()
|
||||
|
||||
Bypass.expect(bypass, "POST", "/oauth/token", fn conn ->
|
||||
conn = fetch_conn_params(conn)
|
||||
send(test_pid, {:request, conn})
|
||||
Plug.Conn.resp(conn, 200, Jason.encode!(attrs))
|
||||
end)
|
||||
end
|
||||
|
||||
def expect_refresh_token_failure(bypass, attrs \\ %{}) do
|
||||
test_pid = self()
|
||||
|
||||
Bypass.expect(bypass, "POST", "/oauth/token", fn conn ->
|
||||
conn = fetch_conn_params(conn)
|
||||
send(test_pid, {:request, conn})
|
||||
Plug.Conn.resp(conn, 401, Jason.encode!(attrs))
|
||||
end)
|
||||
end
|
||||
|
||||
def expect_userinfo(bypass, attrs \\ %{}) do
|
||||
test_pid = self()
|
||||
|
||||
Bypass.expect(bypass, "GET", "/userinfo", fn conn ->
|
||||
attrs =
|
||||
Map.merge(
|
||||
%{
|
||||
"sub" => "353690423699814251281",
|
||||
"name" => "Ada Lovelace",
|
||||
"given_name" => "Ada",
|
||||
"family_name" => "Lovelace",
|
||||
"picture" =>
|
||||
"https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg",
|
||||
"email" => "ada@example.com",
|
||||
"email_verified" => true,
|
||||
"locale" => "en"
|
||||
},
|
||||
attrs
|
||||
)
|
||||
|
||||
conn = fetch_conn_params(conn)
|
||||
send(test_pid, {:request, conn})
|
||||
Plug.Conn.resp(conn, 200, Jason.encode!(attrs))
|
||||
end)
|
||||
end
|
||||
|
||||
def discovery_document_server do
|
||||
bypass = Bypass.open()
|
||||
endpoint = "http://localhost:#{bypass.port}"
|
||||
test_pid = self()
|
||||
|
||||
Bypass.stub(bypass, "GET", "/.well-known/jwks.json", fn conn ->
|
||||
attrs = %{"keys" => [jwks_attrs()]}
|
||||
Plug.Conn.resp(conn, 200, Jason.encode!(attrs))
|
||||
end)
|
||||
|
||||
Bypass.stub(bypass, "GET", "/.well-known/openid-configuration", fn conn ->
|
||||
conn = fetch_conn_params(conn)
|
||||
send(test_pid, {:request, conn})
|
||||
|
||||
attrs = %{
|
||||
"issuer" => "#{endpoint}/",
|
||||
"authorization_endpoint" => "#{endpoint}/authorize",
|
||||
"token_endpoint" => "#{endpoint}/oauth/token",
|
||||
"device_authorization_endpoint" => "#{endpoint}/oauth/device/code",
|
||||
"userinfo_endpoint" => "#{endpoint}/userinfo",
|
||||
"mfa_challenge_endpoint" => "#{endpoint}/mfa/challenge",
|
||||
"jwks_uri" => "#{endpoint}/.well-known/jwks.json",
|
||||
"registration_endpoint" => "#{endpoint}/oidc/register",
|
||||
"revocation_endpoint" => "#{endpoint}/oauth/revoke",
|
||||
"end_session_endpoint" => "https://example.com",
|
||||
"scopes_supported" => [
|
||||
"openid",
|
||||
"profile",
|
||||
"offline_access",
|
||||
"name",
|
||||
"given_name",
|
||||
"family_name",
|
||||
"nickname",
|
||||
"email",
|
||||
"email_verified",
|
||||
"picture",
|
||||
"created_at",
|
||||
"identities",
|
||||
"phone",
|
||||
"address"
|
||||
],
|
||||
"response_types_supported" => [
|
||||
"code",
|
||||
"token",
|
||||
"id_token",
|
||||
"code token",
|
||||
"code id_token",
|
||||
"token id_token",
|
||||
"code token id_token"
|
||||
],
|
||||
"code_challenge_methods_supported" => [
|
||||
"S256",
|
||||
"plain"
|
||||
],
|
||||
"response_modes_supported" => [
|
||||
"query",
|
||||
"fragment",
|
||||
"form_post"
|
||||
],
|
||||
"subject_types_supported" => [
|
||||
"public"
|
||||
],
|
||||
"id_token_signing_alg_values_supported" => [
|
||||
"HS256",
|
||||
"RS256"
|
||||
],
|
||||
"token_endpoint_auth_methods_supported" => [
|
||||
"client_secret_basic",
|
||||
"client_secret_post"
|
||||
],
|
||||
"claims_supported" => [
|
||||
"aud",
|
||||
"auth_time",
|
||||
"created_at",
|
||||
"email",
|
||||
"email_verified",
|
||||
"exp",
|
||||
"family_name",
|
||||
"given_name",
|
||||
"iat",
|
||||
"identities",
|
||||
"iss",
|
||||
"name",
|
||||
"nickname",
|
||||
"phone_number",
|
||||
"picture",
|
||||
"sub"
|
||||
],
|
||||
"request_uri_parameter_supported" => false,
|
||||
"request_parameter_supported" => false
|
||||
}
|
||||
|
||||
Plug.Conn.resp(conn, 200, Jason.encode!(attrs))
|
||||
end)
|
||||
|
||||
{bypass, "#{endpoint}/.well-known/openid-configuration"}
|
||||
end
|
||||
|
||||
def fetch_conn_params(conn) do
|
||||
opts = Plug.Parsers.init(parsers: [:urlencoded, :json], pass: ["*/*"], json_decoder: Jason)
|
||||
|
||||
conn
|
||||
|> Plug.Conn.fetch_query_params()
|
||||
|> Plug.Parsers.call(opts)
|
||||
end
|
||||
|
||||
def saml_identity_providers_attrs(attrs \\ %{}) do
|
||||
Enum.into(attrs, %{
|
||||
"metadata" => saml_metadata(),
|
||||
"label" => "test",
|
||||
"id" => "test",
|
||||
"auto_create_users" => true
|
||||
})
|
||||
end
|
||||
|
||||
def saml_metadata do
|
||||
"""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<md:EntityDescriptor entityID="http://www.okta.com/exk6ff6p62kFjUR3X5d7"
|
||||
xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata">
|
||||
<md:IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<md:KeyDescriptor use="signing">
|
||||
<ds:KeyInfo
|
||||
xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>MIIDqDCCApCgAwIBAgIGAYMaIfiKMA0GCSqGSIb3DQEBCwUAMIGUMQswCQYDVQQGEwJVUzETMBEG
|
||||
A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU
|
||||
MBIGA1UECwwLU1NPUHJvdmlkZXIxFTATBgNVBAMMDGRldi04Mzg1OTk1NTEcMBoGCSqGSIb3DQEJ
|
||||
ARYNaW5mb0Bva3RhLmNvbTAeFw0yMjA5MDcyMjQ1MTdaFw0zMjA5MDcyMjQ2MTdaMIGUMQswCQYD
|
||||
VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsG
|
||||
A1UECgwET2t0YTEUMBIGA1UECwwLU1NPUHJvdmlkZXIxFTATBgNVBAMMDGRldi04Mzg1OTk1NTEc
|
||||
MBoGCSqGSIb3DQEJARYNaW5mb0Bva3RhLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
|
||||
ggEBAOmj276L3kHm57hNGYTocT6NS4mffPbcvsA2UuKIWfmpV8HLTcmS+NahLtuN841OnRnTn+2p
|
||||
fjlwa1mwJhCODbF3dcVYOkGTPUC4y2nvf1Xas6M7+0O2WIfrzdX/OOUs/ROMnB/O/MpBwMR2SQh6
|
||||
Q3V+9v8g3K9yfMvcifDbl6g9fTliDzqV7I9xF5eJykl+iCAKNaQgp3cO6TaIa5u2ZKtRAdzwnuJC
|
||||
BXMyzaoNs/vfnwzuFtzWP1PSS1Roan+8AMwkYA6BCr1YRIqZ0GSkr/qexFCTZdq0UnSN78fY6CCM
|
||||
RFw5wU0WM9nEpbWzkBBWsYHeTLo5JqR/mZukfjlPDlcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA
|
||||
lUhwzCSnuqt4wlHxJONN4kxUBG8bPnjHxob6jBKK+onFDuSVWZ+7LZw67blz6xdxvlOLaQLi1fK2
|
||||
Fifehbc7KbRLckcgNgg7Y8qfUKdP0/nS0JlyAvlnICQqaHTHwhIzQqTHtTZeeIJHtpWOX/OPRI0S
|
||||
bkygh2qjF8bYn3sX8bGNUQL8iiMxFnvwGrXaErPqlRqFJbWQDBXD+nYDIBw7WN3Jyb0Ydin2zrlh
|
||||
gp3Qooi0TnAir3ncw/UF/+sivCgd+6nX7HkbZtipkMbg7ZByyD9xrOQG2JXrP6PyzGCPwnGMt9pL
|
||||
iiVMepeLNqKZ3UvhrR1uRN0KWu7lduIRhxldLA==</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</md:KeyDescriptor>
|
||||
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
|
||||
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
|
||||
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://dev-83859955.okta.com/app/dev-83859955_firezonesaml_1/exk6ff6p62kFjUR3X5d7/sso/saml"/>
|
||||
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://dev-83859955.okta.com/app/dev-83859955_firezonesaml_1/exk6ff6p62kFjUR3X5d7/sso/saml"/>
|
||||
</md:IDPSSODescriptor>
|
||||
</md:EntityDescriptor>
|
||||
"""
|
||||
def set_config(account, key, value) do
|
||||
upsert_configuration([{:account, account}, {key, value}])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
defmodule Domain.ClientsFixtures do
|
||||
defmodule Domain.DevicesFixtures do
|
||||
alias Domain.Repo
|
||||
alias Domain.Clients
|
||||
alias Domain.Devices
|
||||
alias Domain.{AccountsFixtures, ActorsFixtures, AuthFixtures}
|
||||
|
||||
def client_attrs(attrs \\ %{}) do
|
||||
def device_attrs(attrs \\ %{}) do
|
||||
Enum.into(attrs, %{
|
||||
external_id: Ecto.UUID.generate(),
|
||||
name: "client-#{counter()}",
|
||||
name: "device-#{counter()}",
|
||||
public_key: public_key()
|
||||
})
|
||||
end
|
||||
|
||||
def create_client(attrs \\ %{}) do
|
||||
def create_device(attrs \\ %{}) do
|
||||
attrs = Enum.into(attrs, %{})
|
||||
|
||||
{account, attrs} =
|
||||
@@ -25,7 +25,7 @@ defmodule Domain.ClientsFixtures do
|
||||
|
||||
{actor, attrs} =
|
||||
Map.pop_lazy(attrs, :actor, fn ->
|
||||
ActorsFixtures.create_actor(role: :admin, account: account)
|
||||
ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
end)
|
||||
|
||||
{identity, attrs} =
|
||||
@@ -38,19 +38,19 @@ defmodule Domain.ClientsFixtures do
|
||||
AuthFixtures.create_subject(identity)
|
||||
end)
|
||||
|
||||
attrs = client_attrs(attrs)
|
||||
attrs = device_attrs(attrs)
|
||||
|
||||
{:ok, client} = Clients.upsert_client(attrs, subject)
|
||||
client
|
||||
{:ok, device} = Devices.upsert_device(attrs, subject)
|
||||
device
|
||||
end
|
||||
|
||||
def delete_client(client) do
|
||||
client = Repo.preload(client, :account)
|
||||
actor = ActorsFixtures.create_actor(role: :admin, account: client.account)
|
||||
identity = AuthFixtures.create_identity(account: client.account, actor: actor)
|
||||
def delete_device(device) do
|
||||
device = Repo.preload(device, :account)
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: device.account)
|
||||
identity = AuthFixtures.create_identity(account: device.account, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
{:ok, client} = Clients.delete_client(client, subject)
|
||||
client
|
||||
{:ok, device} = Devices.delete_device(device, subject)
|
||||
device
|
||||
end
|
||||
|
||||
def public_key do
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user