Fix some of TODOs left from IAM PR (#1627)

This commit is contained in:
Andrew Dryga
2023-05-24 12:18:52 -06:00
committed by GitHub
parent c30f571d73
commit 9e1669c333
114 changed files with 2933 additions and 6166 deletions

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View 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

View 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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +0,0 @@
defmodule Domain.Repo.Migrations.RemoveDevices do
use Ecto.Migration
def change do
drop(table(:devices))
end
end

View File

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

View File

@@ -0,0 +1,7 @@
defmodule Domain.Repo.Migrations.DropApiTokens do
use Ecto.Migration
def change do
drop(table(:api_tokens))
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,8 +5,7 @@ defmodule Domain.ActorsFixtures do
def actor_attrs(attrs \\ %{}) do
Enum.into(attrs, %{
type: :user,
role: :unprivileged
type: :account_user
})
end

View File

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

View File

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

View File

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

View File

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