diff --git a/.ci/functional_test.sh b/.ci/functional_test.sh deleted file mode 100755 index db37b8256..000000000 --- a/.ci/functional_test.sh +++ /dev/null @@ -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 'Sign in with email' - -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 diff --git a/.dockerignore b/.dockerignore index 14996481d..2c46d2905 100644 --- a/.dockerignore +++ b/.dockerignore @@ -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 diff --git a/.gitignore b/.gitignore index 237681167..c49b1d5ad 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Dockerfile.prod b/Dockerfile.prod index a7f5fc64e..d750d8191 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -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"] diff --git a/apps/api/lib/api/client/views/interface.ex b/apps/api/lib/api/client/views/interface.ex deleted file mode 100644 index ee5725d95..000000000 --- a/apps/api/lib/api/client/views/interface.ex +++ /dev/null @@ -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 diff --git a/apps/api/lib/api/client/channel.ex b/apps/api/lib/api/device/channel.ex similarity index 86% rename from apps/api/lib/api/client/channel.ex rename to apps/api/lib/api/device/channel.ex index bca406934..bda876e27 100644 --- a/apps/api/lib/api/client/channel.ex +++ b/apps/api/lib/api/device/channel.ex @@ -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 }} ) diff --git a/apps/api/lib/api/client/socket.ex b/apps/api/lib/api/device/socket.ex similarity index 68% rename from apps/api/lib/api/client/socket.ex rename to apps/api/lib/api/device/socket.ex index 28fd718e6..dcfadc75a 100644 --- a/apps/api/lib/api/client/socket.ex +++ b/apps/api/lib/api/device/socket.ex @@ -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 diff --git a/apps/api/lib/api/device/views/interface.ex b/apps/api/lib/api/device/views/interface.ex new file mode 100644 index 000000000..2b587a771 --- /dev/null +++ b/apps/api/lib/api/device/views/interface.ex @@ -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 diff --git a/apps/api/lib/api/client/views/relay.ex b/apps/api/lib/api/device/views/relay.ex similarity index 96% rename from apps/api/lib/api/client/views/relay.ex rename to apps/api/lib/api/device/views/relay.ex index dada01721..59133ff40 100644 --- a/apps/api/lib/api/client/views/relay.ex +++ b/apps/api/lib/api/device/views/relay.ex @@ -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 diff --git a/apps/api/lib/api/client/views/resource.ex b/apps/api/lib/api/device/views/resource.ex similarity index 87% rename from apps/api/lib/api/client/views/resource.ex rename to apps/api/lib/api/device/views/resource.ex index 1419f04ad..c6f13ffb9 100644 --- a/apps/api/lib/api/client/views/resource.ex +++ b/apps/api/lib/api/device/views/resource.ex @@ -1,4 +1,4 @@ -defmodule API.Client.Views.Resource do +defmodule API.Device.Views.Resource do alias Domain.Resources def render_many(resources) do diff --git a/apps/api/lib/api/endpoint.ex b/apps/api/lib/api/endpoint.ex index 4b839645c..1c3db72ff 100644 --- a/apps/api/lib/api/endpoint.ex +++ b/apps/api/lib/api/endpoint.ex @@ -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 diff --git a/apps/api/lib/api/gateway/channel.ex b/apps/api/lib/api/gateway/channel.ex index 7f58de876..88c511543 100644 --- a/apps/api/lib/api/gateway/channel.ex +++ b/apps/api/lib/api/gateway/channel.ex @@ -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 diff --git a/apps/api/lib/api/gateway/socket.ex b/apps/api/lib/api/gateway/socket.ex index f93dde965..0eb8ee1eb 100644 --- a/apps/api/lib/api/gateway/socket.ex +++ b/apps/api/lib/api/gateway/socket.ex @@ -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 diff --git a/apps/api/lib/api/gateway/views/client.ex b/apps/api/lib/api/gateway/views/client.ex index 34cf2bd97..cc35fc18c 100644 --- a/apps/api/lib/api/gateway/views/client.ex +++ b/apps/api/lib/api/gateway/views/client.ex @@ -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 diff --git a/apps/api/lib/api/gateway/views/relay.ex b/apps/api/lib/api/gateway/views/relay.ex index 49e4ab650..5e7ef948b 100644 --- a/apps/api/lib/api/gateway/views/relay.ex +++ b/apps/api/lib/api/gateway/views/relay.ex @@ -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 diff --git a/apps/api/lib/api/relay/socket.ex b/apps/api/lib/api/relay/socket.ex index 86473bada..0a0098f8b 100644 --- a/apps/api/lib/api/relay/socket.ex +++ b/apps/api/lib/api/relay/socket.ex @@ -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 diff --git a/apps/api/test/api/client/channel_test.exs b/apps/api/test/api/client/channel_test.exs index 779478b27..e9930d599 100644 --- a/apps/api/test/api/client/channel_test.exs +++ b/apps/api/test/api/client/channel_test.exs @@ -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 diff --git a/apps/api/test/api/client/socket_test.exs b/apps/api/test/api/client/socket_test.exs index 986ce56ba..45d6bcc45 100644 --- a/apps/api/test/api/client/socket_test.exs +++ b/apps/api/test/api/client/socket_test.exs @@ -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) diff --git a/apps/api/test/api/gateway/channel_test.exs b/apps/api/test/api/gateway/channel_test.exs index b0f334d51..0d2bc4e53 100644 --- a/apps/api/test/api/gateway/channel_test.exs +++ b/apps/api/test/api/gateway/channel_test.exs @@ -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 }} ) diff --git a/apps/api/test/api/gateway/socket_test.exs b/apps/api/test/api/gateway/socket_test.exs index 61083fefb..4a37a790f 100644 --- a/apps/api/test/api/gateway/socket_test.exs +++ b/apps/api/test/api/gateway/socket_test.exs @@ -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 diff --git a/apps/api/test/api/relay/socket_test.exs b/apps/api/test/api/relay/socket_test.exs index 29dfd434e..1043b001f 100644 --- a/apps/api/test/api/relay/socket_test.exs +++ b/apps/api/test/api/relay/socket_test.exs @@ -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 diff --git a/apps/api/test/support/channel_case.ex b/apps/api/test/support/channel_case.ex index 40f760830..8b558cf2a 100644 --- a/apps/api/test/support/channel_case.ex +++ b/apps/api/test/support/channel_case.ex @@ -3,7 +3,7 @@ defmodule API.ChannelCase do use Domain.CaseTemplate @presences [ - Domain.Clients.Presence, + Domain.Devices.Presence, Domain.Gateways.Presence, Domain.Relays.Presence ] diff --git a/apps/domain/lib/domain/actors.ex b/apps/domain/lib/domain/actors.ex index 9db327dbb..7482470ac 100644 --- a/apps/domain/lib/domain/actors.ex +++ b/apps/domain/lib/domain/actors.ex @@ -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}) diff --git a/apps/domain/lib/domain/actors/actor.ex b/apps/domain/lib/domain/actors/actor.ex index 3c53c9951..4e009b4b3 100644 --- a/apps/domain/lib/domain/actors/actor.ex +++ b/apps/domain/lib/domain/actors/actor.ex @@ -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() diff --git a/apps/domain/lib/domain/actors/actor/changeset.ex b/apps/domain/lib/domain/actors/actor/changeset.ex index 3aef88d95..3a1d8b3ea 100644 --- a/apps/domain/lib/domain/actors/actor/changeset.ex +++ b/apps/domain/lib/domain/actors/actor/changeset.ex @@ -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 diff --git a/apps/domain/lib/domain/actors/actor/query.ex b/apps/domain/lib/domain/actors/actor/query.ex index f6aba8ace..0e75e34e5 100644 --- a/apps/domain/lib/domain/actors/actor/query.ex +++ b/apps/domain/lib/domain/actors/actor/query.ex @@ -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 diff --git a/apps/domain/lib/domain/actors/authorizer.ex b/apps/domain/lib/domain/actors/authorizer.ex index c4a77864f..9e64434a9 100644 --- a/apps/domain/lib/domain/actors/authorizer.ex +++ b/apps/domain/lib/domain/actors/authorizer.ex @@ -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() ] diff --git a/apps/domain/lib/domain/api_tokens.ex b/apps/domain/lib/domain/api_tokens.ex deleted file mode 100644 index 20f6e0e79..000000000 --- a/apps/domain/lib/domain/api_tokens.ex +++ /dev/null @@ -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 diff --git a/apps/domain/lib/domain/api_tokens/api_token.ex b/apps/domain/lib/domain/api_tokens/api_token.ex deleted file mode 100644 index 794d6a2c9..000000000 --- a/apps/domain/lib/domain/api_tokens/api_token.ex +++ /dev/null @@ -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 diff --git a/apps/domain/lib/domain/api_tokens/api_token/changeset.ex b/apps/domain/lib/domain/api_tokens/api_token/changeset.ex deleted file mode 100644 index e13152b9c..000000000 --- a/apps/domain/lib/domain/api_tokens/api_token/changeset.ex +++ /dev/null @@ -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 diff --git a/apps/domain/lib/domain/api_tokens/api_token/query.ex b/apps/domain/lib/domain/api_tokens/api_token/query.ex deleted file mode 100644 index c7eaa854f..000000000 --- a/apps/domain/lib/domain/api_tokens/api_token/query.ex +++ /dev/null @@ -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 diff --git a/apps/domain/lib/domain/api_tokens/authorizer.ex b/apps/domain/lib/domain/api_tokens/authorizer.ex deleted file mode 100644 index 9ffe489cf..000000000 --- a/apps/domain/lib/domain/api_tokens/authorizer.ex +++ /dev/null @@ -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 diff --git a/apps/domain/lib/domain/application.ex b/apps/domain/lib/domain/application.ex index 6b178ffcc..0c258804c 100644 --- a/apps/domain/lib/domain/application.ex +++ b/apps/domain/lib/domain/application.ex @@ -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 diff --git a/apps/domain/lib/domain/auth.ex b/apps/domain/lib/domain/auth.ex index 2e0cdb727..c15980aed 100644 --- a/apps/domain/lib/domain/auth.ex +++ b/apps/domain/lib/domain/auth.ex @@ -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 diff --git a/apps/domain/lib/domain/auth/adapters.ex b/apps/domain/lib/domain/auth/adapters.ex index c78dceddc..2f9c16cce 100644 --- a/apps/domain/lib/domain/auth/adapters.ex +++ b/apps/domain/lib/domain/auth/adapters.ex @@ -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) diff --git a/apps/domain/lib/domain/auth/adapters/email.ex b/apps/domain/lib/domain/auth/adapters/email.ex index bd80a285a..4026a2ef0 100644 --- a/apps/domain/lib/domain/auth/adapters/email.ex +++ b/apps/domain/lib/domain/auth/adapters/email.ex @@ -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() diff --git a/apps/domain/lib/domain/auth/adapters/token.ex b/apps/domain/lib/domain/auth/adapters/token.ex new file mode 100644 index 000000000..59b5070f8 --- /dev/null +++ b/apps/domain/lib/domain/auth/adapters/token.ex @@ -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 diff --git a/apps/domain/lib/domain/auth/adapters/token/state.ex b/apps/domain/lib/domain/auth/adapters/token/state.ex new file mode 100644 index 000000000..c0e1e3aec --- /dev/null +++ b/apps/domain/lib/domain/auth/adapters/token/state.ex @@ -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 diff --git a/apps/domain/lib/domain/auth/adapters/token/state/changeset.ex b/apps/domain/lib/domain/auth/adapters/token/state/changeset.ex new file mode 100644 index 000000000..a8f659563 --- /dev/null +++ b/apps/domain/lib/domain/auth/adapters/token/state/changeset.ex @@ -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 diff --git a/apps/domain/lib/domain/auth/adapters/userpass.ex b/apps/domain/lib/domain/auth/adapters/userpass.ex index 480754082..87d01004c 100644 --- a/apps/domain/lib/domain/auth/adapters/userpass.ex +++ b/apps/domain/lib/domain/auth/adapters/userpass.ex @@ -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 diff --git a/apps/domain/lib/domain/auth/authorizer.ex b/apps/domain/lib/domain/auth/authorizer.ex index 9da0281ce..99a03fd0a 100644 --- a/apps/domain/lib/domain/auth/authorizer.ex +++ b/apps/domain/lib/domain/auth/authorizer.ex @@ -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 diff --git a/apps/domain/lib/domain/auth/provider.ex b/apps/domain/lib/domain/auth/provider.ex index a322bd4d1..cae9774de 100644 --- a/apps/domain/lib/domain/auth/provider.ex +++ b/apps/domain/lib/domain/auth/provider.ex @@ -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 diff --git a/apps/domain/lib/domain/auth/roles.ex b/apps/domain/lib/domain/auth/roles.ex index 6b51c7be4..3968b0fd0 100644 --- a/apps/domain/lib/domain/auth/roles.ex +++ b/apps/domain/lib/domain/auth/roles.ex @@ -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, diff --git a/apps/domain/lib/domain/changeset.ex b/apps/domain/lib/domain/changeset.ex index f9d1bad3d..89184891b 100644 --- a/apps/domain/lib/domain/changeset.ex +++ b/apps/domain/lib/domain/changeset.ex @@ -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 diff --git a/apps/domain/lib/domain/clients.ex b/apps/domain/lib/domain/clients.ex deleted file mode 100644 index a9d9b0e53..000000000 --- a/apps/domain/lib/domain/clients.ex +++ /dev/null @@ -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 diff --git a/apps/domain/lib/domain/clients/authorizer.ex b/apps/domain/lib/domain/clients/authorizer.ex deleted file mode 100644 index 705244d88..000000000 --- a/apps/domain/lib/domain/clients/authorizer.ex +++ /dev/null @@ -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 diff --git a/apps/domain/lib/domain/clients/client/query.ex b/apps/domain/lib/domain/clients/client/query.ex deleted file mode 100644 index fdeef4eb8..000000000 --- a/apps/domain/lib/domain/clients/client/query.ex +++ /dev/null @@ -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 diff --git a/apps/domain/lib/domain/config.ex b/apps/domain/lib/domain/config.ex index d3523b329..6f732b763 100644 --- a/apps/domain/lib/domain/config.ex +++ b/apps/domain/lib/domain/config.ex @@ -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 diff --git a/apps/domain/lib/domain/config/authorizer.ex b/apps/domain/lib/domain/config/authorizer.ex index da862329c..088a429a9 100644 --- a/apps/domain/lib/domain/config/authorizer.ex +++ b/apps/domain/lib/domain/config/authorizer.ex @@ -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 diff --git a/apps/domain/lib/domain/config/configuration.ex b/apps/domain/lib/domain/config/configuration.ex index bd38b3d36..837415e29 100644 --- a/apps/domain/lib/domain/config/configuration.ex +++ b/apps/domain/lib/domain/config/configuration.ex @@ -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 diff --git a/apps/domain/lib/domain/config/configuration/changeset.ex b/apps/domain/lib/domain/config/configuration/changeset.ex index a7889018c..570a646b3 100644 --- a/apps/domain/lib/domain/config/configuration/changeset.ex +++ b/apps/domain/lib/domain/config/configuration/changeset.ex @@ -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 diff --git a/apps/domain/lib/domain/config/configuration/query.ex b/apps/domain/lib/domain/config/configuration/query.ex new file mode 100644 index 000000000..502f3bcd8 --- /dev/null +++ b/apps/domain/lib/domain/config/configuration/query.ex @@ -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 diff --git a/apps/domain/lib/domain/config/definitions.ex b/apps/domain/lib/domain/config/definitions.ex index e011df669..afbf0d925 100644 --- a/apps/domain/lib/domain/config/definitions.ex +++ b/apps/domain/lib/domain/config/definitions.ex @@ -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 diff --git a/apps/domain/lib/domain/devices.ex b/apps/domain/lib/domain/devices.ex new file mode 100644 index 000000000..17af1c6bb --- /dev/null +++ b/apps/domain/lib/domain/devices.ex @@ -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 diff --git a/apps/domain/lib/domain/devices/authorizer.ex b/apps/domain/lib/domain/devices/authorizer.ex new file mode 100644 index 000000000..d9af0c477 --- /dev/null +++ b/apps/domain/lib/domain/devices/authorizer.ex @@ -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 diff --git a/apps/domain/lib/domain/clients/client.ex b/apps/domain/lib/domain/devices/device.ex similarity index 90% rename from apps/domain/lib/domain/clients/client.ex rename to apps/domain/lib/domain/devices/device.ex index 98102fff4..428d54f7c 100644 --- a/apps/domain/lib/domain/clients/client.ex +++ b/apps/domain/lib/domain/devices/device.ex @@ -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 diff --git a/apps/domain/lib/domain/clients/client/changeset.ex b/apps/domain/lib/domain/devices/device/changeset.ex similarity index 80% rename from apps/domain/lib/domain/clients/client/changeset.ex rename to apps/domain/lib/domain/devices/device/changeset.ex index 0ba4f2abd..8a4891214 100644 --- a/apps/domain/lib/domain/clients/client/changeset.ex +++ b/apps/domain/lib/domain/devices/device/changeset.ex @@ -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 diff --git a/apps/domain/lib/domain/devices/device/query.ex b/apps/domain/lib/domain/devices/device/query.ex new file mode 100644 index 000000000..b3b0c98bb --- /dev/null +++ b/apps/domain/lib/domain/devices/device/query.ex @@ -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 diff --git a/apps/domain/lib/domain/clients/presence.ex b/apps/domain/lib/domain/devices/presence.ex similarity index 69% rename from apps/domain/lib/domain/clients/presence.ex rename to apps/domain/lib/domain/devices/presence.ex index b0bf4f34d..1232022ed 100644 --- a/apps/domain/lib/domain/clients/presence.ex +++ b/apps/domain/lib/domain/devices/presence.ex @@ -1,4 +1,4 @@ -defmodule Domain.Clients.Presence do +defmodule Domain.Devices.Presence do use Phoenix.Presence, otp_app: :domain, pubsub_server: Domain.PubSub diff --git a/apps/domain/lib/domain/gateways.ex b/apps/domain/lib/domain/gateways.ex index 5e55e04ab..1e7a88487 100644 --- a/apps/domain/lib/domain/gateways.ex +++ b/apps/domain/lib/domain/gateways.ex @@ -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 diff --git a/apps/domain/lib/domain/gateways/authorizer.ex b/apps/domain/lib/domain/gateways/authorizer.ex index 224629dae..aa98d0355 100644 --- a/apps/domain/lib/domain/gateways/authorizer.ex +++ b/apps/domain/lib/domain/gateways/authorizer.ex @@ -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() ] diff --git a/apps/domain/lib/domain/relays.ex b/apps/domain/lib/domain/relays.ex index bdd6affa4..9c14c8dc4 100644 --- a/apps/domain/lib/domain/relays.ex +++ b/apps/domain/lib/domain/relays.ex @@ -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 diff --git a/apps/domain/lib/domain/relays/authorizer.ex b/apps/domain/lib/domain/relays/authorizer.ex index f84a155c0..fb757091b 100644 --- a/apps/domain/lib/domain/relays/authorizer.ex +++ b/apps/domain/lib/domain/relays/authorizer.ex @@ -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() ] diff --git a/apps/domain/lib/domain/release.ex b/apps/domain/lib/domain/release.ex index d4951e2d1..6bb6a8705 100644 --- a/apps/domain/lib/domain/release.ex +++ b/apps/domain/lib/domain/release.ex @@ -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 diff --git a/apps/domain/lib/domain/resources.ex b/apps/domain/lib/domain/resources.ex index 8d899763e..f4c28082c 100644 --- a/apps/domain/lib/domain/resources.ex +++ b/apps/domain/lib/domain/resources.ex @@ -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} # ) diff --git a/apps/domain/lib/domain/resources/authorizer.ex b/apps/domain/lib/domain/resources/authorizer.ex index b5f12c74c..83bac9db9 100644 --- a/apps/domain/lib/domain/resources/authorizer.ex +++ b/apps/domain/lib/domain/resources/authorizer.ex @@ -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() ] diff --git a/apps/domain/lib/domain/sandbox.ex b/apps/domain/lib/domain/sandbox.ex index 4f6381932..e0452c4a1 100644 --- a/apps/domain/lib/domain/sandbox.ex +++ b/apps/domain/lib/domain/sandbox.ex @@ -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 diff --git a/apps/domain/lib/domain/telemetry.ex b/apps/domain/lib/domain/telemetry.ex index 754005505..1cdc7c22d 100644 --- a/apps/domain/lib/domain/telemetry.ex +++ b/apps/domain/lib/domain/telemetry.ex @@ -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 diff --git a/apps/domain/lib/domain/telemetry/timer.ex b/apps/domain/lib/domain/telemetry/timer.ex index 159317907..4cf2fe83e 100644 --- a/apps/domain/lib/domain/telemetry/timer.ex +++ b/apps/domain/lib/domain/telemetry/timer.ex @@ -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 diff --git a/apps/domain/lib/domain/validator.ex b/apps/domain/lib/domain/validator.ex index 07d1c17f1..bfcaf22d1 100644 --- a/apps/domain/lib/domain/validator.ex +++ b/apps/domain/lib/domain/validator.ex @@ -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. diff --git a/apps/domain/priv/repo/migrations/20230405181921_create_clients.exs b/apps/domain/priv/repo/migrations/20230405181921_recreate_devices.exs similarity index 72% rename from apps/domain/priv/repo/migrations/20230405181921_create_clients.exs rename to apps/domain/priv/repo/migrations/20230405181921_recreate_devices.exs index bd84a7738..81e6f6182 100644 --- a/apps/domain/priv/repo/migrations/20230405181921_create_clients.exs +++ b/apps/domain/priv/repo/migrations/20230405181921_recreate_devices.exs @@ -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" ) diff --git a/apps/domain/priv/repo/migrations/20230425083010_remove_devices.exs b/apps/domain/priv/repo/migrations/20230425083010_remove_devices.exs deleted file mode 100644 index 450de3afe..000000000 --- a/apps/domain/priv/repo/migrations/20230425083010_remove_devices.exs +++ /dev/null @@ -1,7 +0,0 @@ -defmodule Domain.Repo.Migrations.RemoveDevices do - use Ecto.Migration - - def change do - drop(table(:devices)) - end -end diff --git a/apps/domain/priv/repo/migrations/20230426181111_remove_users.exs b/apps/domain/priv/repo/migrations/20230426181111_remove_users.exs index 5c830ebb2..1930c6a35 100644 --- a/apps/domain/priv/repo/migrations/20230426181111_remove_users.exs +++ b/apps/domain/priv/repo/migrations/20230426181111_remove_users.exs @@ -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" ) diff --git a/apps/domain/priv/repo/migrations/20230519192732_drop_api_tokens.exs b/apps/domain/priv/repo/migrations/20230519192732_drop_api_tokens.exs new file mode 100644 index 000000000..95ffe105a --- /dev/null +++ b/apps/domain/priv/repo/migrations/20230519192732_drop_api_tokens.exs @@ -0,0 +1,7 @@ +defmodule Domain.Repo.Migrations.DropApiTokens do + use Ecto.Migration + + def change do + drop(table(:api_tokens)) + end +end diff --git a/apps/domain/priv/repo/migrations/20230523012008_remove_actors_role.exs b/apps/domain/priv/repo/migrations/20230523012008_remove_actors_role.exs new file mode 100644 index 000000000..d243a2c12 --- /dev/null +++ b/apps/domain/priv/repo/migrations/20230523012008_remove_actors_role.exs @@ -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 diff --git a/apps/domain/priv/repo/migrations/20230523181533_cleanup_configurations.exs b/apps/domain/priv/repo/migrations/20230523181533_cleanup_configurations.exs new file mode 100644 index 000000000..f13fcde3b --- /dev/null +++ b/apps/domain/priv/repo/migrations/20230523181533_cleanup_configurations.exs @@ -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 diff --git a/apps/domain/priv/repo/seeds.exs b/apps/domain/priv/repo/seeds.exs index 4568c730f..3957f3148 100644 --- a/apps/domain/priv/repo/seeds.exs +++ b/apps/domain/priv/repo/seeds.exs @@ -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("") diff --git a/apps/domain/test/domain/actors_test.exs b/apps/domain/test/domain/actors_test.exs index e3a104413..0d0a64e67 100644 --- a/apps/domain/test/domain/actors_test.exs +++ b/apps/domain/test/domain/actors_test.exs @@ -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) diff --git a/apps/domain/test/domain/api_tokens_test.exs b/apps/domain/test/domain/api_tokens_test.exs deleted file mode 100644 index 6dd0698fc..000000000 --- a/apps/domain/test/domain/api_tokens_test.exs +++ /dev/null @@ -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 diff --git a/apps/domain/test/domain/auth/adapters/email_test.exs b/apps/domain/test/domain/auth/adapters/email_test.exs index 60e6d8541..0da90e0ef 100644 --- a/apps/domain/test/domain/auth/adapters/email_test.exs +++ b/apps/domain/test/domain/auth/adapters/email_test.exs @@ -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 %{ diff --git a/apps/domain/test/domain/auth/adapters/openid_connect_test.exs b/apps/domain/test/domain/auth/adapters/openid_connect_test.exs index 3d498d15d..cc8801a75 100644 --- a/apps/domain/test/domain/auth/adapters/openid_connect_test.exs +++ b/apps/domain/test/domain/auth/adapters/openid_connect_test.exs @@ -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( diff --git a/apps/domain/test/domain/auth/adapters/token_test.exs b/apps/domain/test/domain/auth/adapters/token_test.exs new file mode 100644 index 000000000..e016ddc0a --- /dev/null +++ b/apps/domain/test/domain/auth/adapters/token_test.exs @@ -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 diff --git a/apps/domain/test/domain/auth/adapters/userpass_test.exs b/apps/domain/test/domain/auth/adapters/userpass_test.exs index b38516969..5d7060934 100644 --- a/apps/domain/test/domain/auth/adapters/userpass_test.exs +++ b/apps/domain/test/domain/auth/adapters/userpass_test.exs @@ -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 diff --git a/apps/domain/test/domain/auth/oidc/refresher_test.exs b/apps/domain/test/domain/auth/oidc/refresher_test.exs index fd0d0beb1..ede48e383 100644 --- a/apps/domain/test/domain/auth/oidc/refresher_test.exs +++ b/apps/domain/test/domain/auth/oidc/refresher_test.exs @@ -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) diff --git a/apps/domain/test/domain/auth_test.exs b/apps/domain/test/domain/auth_test.exs index b8b50b546..ae156c14e 100644 --- a/apps/domain/test/domain/auth_test.exs +++ b/apps/domain/test/domain/auth_test.exs @@ -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) diff --git a/apps/domain/test/domain/clients_test.exs b/apps/domain/test/domain/clients_test.exs deleted file mode 100644 index 878ab0b51..000000000 --- a/apps/domain/test/domain/clients_test.exs +++ /dev/null @@ -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 diff --git a/apps/domain/test/domain/config/definition_test.exs b/apps/domain/test/domain/config/definition_test.exs index 754a83d3f..776f07e10 100644 --- a/apps/domain/test/domain/config/definition_test.exs +++ b/apps/domain/test/domain/config/definition_test.exs @@ -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} diff --git a/apps/domain/test/domain/config/resolver_test.exs b/apps/domain/test/domain/config/resolver_test.exs index 398972b9d..b24d38567 100644 --- a/apps/domain/test/domain/config/resolver_test.exs +++ b/apps/domain/test/domain/config/resolver_test.exs @@ -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 diff --git a/apps/domain/test/domain/config_test.exs b/apps/domain/test/domain/config_test.exs index 6c456ca9d..99ab6346d 100644 --- a/apps/domain/test/domain/config_test.exs +++ b/apps/domain/test/domain/config_test.exs @@ -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 diff --git a/apps/domain/test/domain/devices_test.exs b/apps/domain/test/domain/devices_test.exs new file mode 100644 index 000000000..a10ee2592 --- /dev/null +++ b/apps/domain/test/domain/devices_test.exs @@ -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 diff --git a/apps/domain/test/domain/gateways_test.exs b/apps/domain/test/domain/gateways_test.exs index 1c631d948..a85749e35 100644 --- a/apps/domain/test/domain/gateways_test.exs +++ b/apps/domain/test/domain/gateways_test.exs @@ -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 diff --git a/apps/domain/test/domain/relays_test.exs b/apps/domain/test/domain/relays_test.exs index d475dd07e..edcbb1e29 100644 --- a/apps/domain/test/domain/relays_test.exs +++ b/apps/domain/test/domain/relays_test.exs @@ -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 diff --git a/apps/domain/test/domain/release_test.exs b/apps/domain/test/domain/release_test.exs deleted file mode 100644 index fa8db9d19..000000000 --- a/apps/domain/test/domain/release_test.exs +++ /dev/null @@ -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 diff --git a/apps/domain/test/domain/resources_test.exs b/apps/domain/test/domain/resources_test.exs index b0ea1045a..61f75fb19 100644 --- a/apps/domain/test/domain/resources_test.exs +++ b/apps/domain/test/domain/resources_test.exs @@ -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) diff --git a/apps/domain/test/domain/telemetry_test.exs b/apps/domain/test/domain/telemetry_test.exs index d038caadf..866cea0cf 100644 --- a/apps/domain/test/domain/telemetry_test.exs +++ b/apps/domain/test/domain/telemetry_test.exs @@ -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 diff --git a/apps/domain/test/support/fixtures/actors_fixtures.ex b/apps/domain/test/support/fixtures/actors_fixtures.ex index c9ac1317d..148be5946 100644 --- a/apps/domain/test/support/fixtures/actors_fixtures.ex +++ b/apps/domain/test/support/fixtures/actors_fixtures.ex @@ -5,8 +5,7 @@ defmodule Domain.ActorsFixtures do def actor_attrs(attrs \\ %{}) do Enum.into(attrs, %{ - type: :user, - role: :unprivileged + type: :account_user }) end diff --git a/apps/domain/test/support/fixtures/api_tokens_fixtures.ex b/apps/domain/test/support/fixtures/api_tokens_fixtures.ex deleted file mode 100644 index c2355c86a..000000000 --- a/apps/domain/test/support/fixtures/api_tokens_fixtures.ex +++ /dev/null @@ -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 diff --git a/apps/domain/test/support/fixtures/auth_fixtures.ex b/apps/domain/test/support/fixtures/auth_fixtures.ex index c702da836..83adf4a7a 100644 --- a/apps/domain/test/support/fixtures/auth_fixtures.ex +++ b/apps/domain/test/support/fixtures/auth_fixtures.ex @@ -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 diff --git a/apps/domain/test/support/fixtures/config_fixtures.ex b/apps/domain/test/support/fixtures/config_fixtures.ex index a47b44a6b..2181ea98c 100644 --- a/apps/domain/test/support/fixtures/config_fixtures.ex +++ b/apps/domain/test/support/fixtures/config_fixtures.ex @@ -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 - """ - - - - - - - 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== - - - - urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified - urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress - - - - - """ + def set_config(account, key, value) do + upsert_configuration([{:account, account}, {key, value}]) end end diff --git a/apps/domain/test/support/fixtures/clients_fixtures.ex b/apps/domain/test/support/fixtures/devices_fixtures.ex similarity index 62% rename from apps/domain/test/support/fixtures/clients_fixtures.ex rename to apps/domain/test/support/fixtures/devices_fixtures.ex index 7d60724e5..42af1fb55 100644 --- a/apps/domain/test/support/fixtures/clients_fixtures.ex +++ b/apps/domain/test/support/fixtures/devices_fixtures.ex @@ -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 diff --git a/apps/domain/test/support/fixtures/gateways_fixtures.ex b/apps/domain/test/support/fixtures/gateways_fixtures.ex index eb0f55a66..f48c7f954 100644 --- a/apps/domain/test/support/fixtures/gateways_fixtures.ex +++ b/apps/domain/test/support/fixtures/gateways_fixtures.ex @@ -22,7 +22,7 @@ defmodule Domain.GatewaysFixtures do {subject, attrs} = Map.pop_lazy(attrs, :subject, fn -> - 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) AuthFixtures.create_subject(identity) end) @@ -35,7 +35,7 @@ defmodule Domain.GatewaysFixtures do def delete_group(group) do group = Repo.preload(group, :account) - actor = ActorsFixtures.create_actor(role: :admin, account: group.account) + actor = ActorsFixtures.create_actor(type: :account_admin_user, account: group.account) identity = AuthFixtures.create_identity(account: group.account, actor: actor) subject = AuthFixtures.create_subject(identity) {:ok, group} = Gateways.delete_group(group, subject) @@ -107,7 +107,7 @@ defmodule Domain.GatewaysFixtures do def delete_gateway(gateway) do gateway = Repo.preload(gateway, :account) - actor = ActorsFixtures.create_actor(role: :admin, account: gateway.account) + actor = ActorsFixtures.create_actor(type: :account_admin_user, account: gateway.account) identity = AuthFixtures.create_identity(account: gateway.account, actor: actor) subject = AuthFixtures.create_subject(identity) {:ok, gateway} = Gateways.delete_gateway(gateway, subject) diff --git a/apps/domain/test/support/fixtures/relays_fixtures.ex b/apps/domain/test/support/fixtures/relays_fixtures.ex index 04c2eec54..04d905245 100644 --- a/apps/domain/test/support/fixtures/relays_fixtures.ex +++ b/apps/domain/test/support/fixtures/relays_fixtures.ex @@ -20,7 +20,7 @@ defmodule Domain.RelaysFixtures do {subject, attrs} = Map.pop_lazy(attrs, :subject, fn -> - 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) AuthFixtures.create_subject(identity) end) @@ -33,7 +33,7 @@ defmodule Domain.RelaysFixtures do def delete_group(group) do group = Repo.preload(group, :account) - actor = ActorsFixtures.create_actor(role: :admin, account: group.account) + actor = ActorsFixtures.create_actor(type: :account_admin_user, account: group.account) identity = AuthFixtures.create_identity(account: group.account, actor: actor) subject = AuthFixtures.create_subject(identity) {:ok, group} = Relays.delete_group(group, subject) @@ -106,7 +106,7 @@ defmodule Domain.RelaysFixtures do def delete_relay(relay) do relay = Repo.preload(relay, :account) - actor = ActorsFixtures.create_actor(role: :admin, account: relay.account) + actor = ActorsFixtures.create_actor(type: :account_admin_user, account: relay.account) identity = AuthFixtures.create_identity(account: relay.account, actor: actor) subject = AuthFixtures.create_subject(identity) {:ok, relay} = Relays.delete_relay(relay, subject) diff --git a/apps/domain/test/support/fixtures/resources_fixtures.ex b/apps/domain/test/support/fixtures/resources_fixtures.ex index 2e4e6a255..9eb3b2c28 100644 --- a/apps/domain/test/support/fixtures/resources_fixtures.ex +++ b/apps/domain/test/support/fixtures/resources_fixtures.ex @@ -33,7 +33,7 @@ defmodule Domain.ResourcesFixtures do {subject, attrs} = Map.pop_lazy(attrs, :subject, fn -> - 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) AuthFixtures.create_subject(identity) end) diff --git a/apps/web/mix.exs b/apps/web/mix.exs index 1f8764f59..52cef96a4 100644 --- a/apps/web/mix.exs +++ b/apps/web/mix.exs @@ -28,7 +28,7 @@ defmodule Web.MixProject do ] end - defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(:test), do: ["test/support", "lib"] defp elixirc_paths(_), do: ["lib"] defp deps do @@ -75,7 +75,11 @@ defmodule Web.MixProject do defp aliases do [ setup: ["deps.get", "assets.setup", "assets.build"], - "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"], + "assets.setup": [ + "cmd cd assets && yarn install", + "tailwind.install --if-missing", + "esbuild.install --if-missing" + ], "assets.build": ["tailwind default", "esbuild default"], "assets.deploy": ["tailwind default --minify", "esbuild default --minify", "phx.digest"], "ecto.seed": ["ecto.create", "ecto.migrate", "run ../domain/priv/repo/seeds.exs"], diff --git a/apps/web/test/support/acceptance_case/vault.ex b/apps/web/test/support/acceptance_case/vault.ex index a93a29ed2..806faa688 100644 --- a/apps/web/test/support/acceptance_case/vault.ex +++ b/apps/web/test/support/acceptance_case/vault.ex @@ -26,7 +26,11 @@ defmodule Web.AcceptanceCase.Vault do :ok end - def setup_oidc_provider(endpoint_url, attrs_overrides \\ %{"auto_create_users" => true}) do + def setup_oidc_provider( + account, + endpoint_url, + attrs_overrides \\ %{"auto_create_users" => true} + ) do :ok = request(:put, "identity/oidc/client/firezone", %{ assignments: "allow_all", @@ -50,7 +54,8 @@ defmodule Web.AcceptanceCase.Vault do {:ok, {200, params}} = request(:get, "identity/oidc/client/firezone") - Domain.Config.put_config!( + Domain.ConfigFixtures.set_config( + account, :openid_connect_providers, [ %{ diff --git a/apps/web/test/web/controllers/browser_controller_test.exs b/apps/web/test/web/controllers/browser_controller_test.exs index 047e78be0..d176563f5 100644 --- a/apps/web/test/web/controllers/browser_controller_test.exs +++ b/apps/web/test/web/controllers/browser_controller_test.exs @@ -3,11 +3,7 @@ defmodule Web.BrowserControllerTest do describe "config/2" do test "returns valid XML browser config", %{conn: conn} do - test_conn = - conn - # |> put_req_header("accept", "application/xml") - |> get(~p"/browser/config.xml") - + test_conn = get(conn, ~p"/browser/config.xml") assert response(test_conn, 200) =~ " ["xml"] -} - -config :web, Web.SAML, - entity_id: "urn:firezone.dev:firezone-app", - certfile_path: Path.expand("../apps/web/priv/cert/saml_selfsigned.pem", __DIR__), - keyfile_path: Path.expand("../apps/web/priv/cert/saml_selfsigned_key.pem", __DIR__) - config :web, cookie_secure: false, cookie_signing_salt: "WjllcThpb2Y=", @@ -144,20 +117,14 @@ config :api, cookie_signing_salt: "WjllcThpb2Y=", cookie_encryption_salt: "M0EzM0R6NEMyaw==" -config :api, API.Gateway.Socket, - key_base: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5SD", - salt: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDejX", - max_age: 30 * 60 - -config :api, API.Relay.Socket, - key_base: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5SD", - salt: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDejX", - max_age: 30 * 60 - ############################### ##### Third-party configs ##### ############################### +config :mime, :types, %{ + "application/xml" => ["xml"] +} + config :logger, :console, level: String.to_atom(System.get_env("LOG_LEVEL", "info")), format: "$time $metadata[$level] $message\n", @@ -171,23 +138,6 @@ config :posthog, api_url: "https://t.firez.one", api_key: "phc_ubuPhiqqjMdedpmbWpG2Ak3axqv5eMVhFDNBaXl9UZK" -# Configures the vault -config :domain, Domain.Vault, - ciphers: [ - default: { - Cloak.Ciphers.AES.GCM, - # In AES.GCM, it is important to specify 12-byte IV length for - # interoperability with other encryption software. See this GitHub - # issue for more details: - # https://github.com/danielberkompas/cloak/issues/93 - # - # In Cloak 2.0, this will be the default iv length for AES.GCM. - tag: "AES.GCM.V1", - key: Base.decode64!("XXJ/NGevpvkG9219RYsz21zZWR7CZ//CqA0ARPIBqys="), - iv_length: 12 - } - ] - config :web, Web.Mailer, adapter: Web.Mailer.NoopAdapter, from_email: "test@firez.one" diff --git a/config/prod.exs b/config/prod.exs index 4559f08e4..70c645de2 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -8,8 +8,6 @@ config :domain, Domain.Repo, pool_size: 10, show_sensitive_data_on_connection_error: false -config :domain, Domain.ConnectivityChecks, url: "https://ping.firez.one/" - ############################### ##### Web ##################### ############################### diff --git a/config/runtime.exs b/config/runtime.exs index 4f930d330..917729c58 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -16,7 +16,8 @@ if config_env() == :prod do pool_size: compile_config!(:database_pool_size), ssl: compile_config!(:database_ssl_enabled), ssl_opts: compile_config!(:database_ssl_opts), - parameters: compile_config!(:database_parameters) + parameters: compile_config!(:database_parameters), + show_sensitive_data_on_connection_error: false external_url = compile_config!(:external_url) @@ -27,29 +28,25 @@ if config_env() == :prod do path: external_url_path } = URI.parse(external_url) - config :domain, - wireguard_ipv4_enabled: compile_config!(:wireguard_ipv4_enabled), - wireguard_ipv4_network: compile_config!(:wireguard_ipv4_network), - wireguard_ipv4_address: compile_config!(:wireguard_ipv4_address), - wireguard_ipv6_enabled: compile_config!(:wireguard_ipv6_enabled), - wireguard_ipv6_network: compile_config!(:wireguard_ipv6_network), - wireguard_ipv6_address: compile_config!(:wireguard_ipv6_address) + config :domain, Domain.Devices, upstream_dns: compile_config!(:devices_upstream_dns) + + config :domain, Domain.Gateways, + gateway_ipv4_masquerade: compile_config!(:gateway_ipv4_masquerade), + gateway_ipv6_masquerade: compile_config!(:gateway_ipv6_masquerade), + key_base: compile_config!(:gateways_auth_token_key_base), + salt: compile_config!(:gateways_auth_token_salt) + + config :domain, Domain.Relays, + key_base: compile_config!(:relays_auth_token_key_base), + salt: compile_config!(:relays_auth_token_salt) config :domain, Domain.Telemetry, enabled: compile_config!(:telemetry_enabled), id: compile_config!(:telemetry_id) - config :domain, Domain.ConnectivityChecks, - http_client_options: compile_config!(:http_client_ssl_opts), - enabled: compile_config!(:connectivity_checks_enabled), - interval: compile_config!(:connectivity_checks_interval) - - config :domain, - admin_email: compile_config!(:default_admin_email), - default_admin_password: compile_config!(:default_admin_password) - - config :domain, - max_devices_per_user: compile_config!(:max_devices_per_user) + config :domain, Domain.Auth, + key_base: compile_config!(:auth_token_key_base), + salt: compile_config!(:auth_token_salt) ############################### ##### Web ##################### @@ -63,7 +60,7 @@ if config_env() == :prod do server: true, http: [ ip: compile_config!(:phoenix_listen_address).address, - port: compile_config!(:phoenix_http_port), + port: compile_config!(:phoenix_http_web_port), protocol_options: compile_config!(:phoenix_http_protocol_options) ], url: [ @@ -87,14 +84,14 @@ if config_env() == :prod do cookie_encryption_salt: compile_config!(:cookie_encryption_salt) ############################### - ##### Web ##################### + ##### API ##################### ############################### - config :web, Web.Endpoint, + config :api, API.Endpoint, server: true, http: [ ip: compile_config!(:phoenix_listen_address).address, - port: compile_config!(:phoenix_http_port), + port: compile_config!(:phoenix_http_api_port), protocol_options: compile_config!(:phoenix_http_protocol_options) ], # TODO: force_ssl: [rewrite_on: [:x_forwarded_proto], hsts: true], @@ -115,38 +112,9 @@ if config_env() == :prod do ##### Third-party configs ##### ############################### - config :web, Web.Auth.HTML.Authentication, secret_key: compile_config!(:guardian_secret_key) - - config :web, Web.Auth.JSON.Authentication, secret_key: compile_config!(:guardian_secret_key) - - config :domain, Domain.Vault, - ciphers: [ - default: { - Cloak.Ciphers.AES.GCM, - # In AES.GCM, it is important to specify 12-byte IV length for - # interoperability with other encryption software. See this GitHub - # issue for more details: - # https://github.com/danielberkompas/cloak/issues/93 - # - # In Cloak 2.0, this will be the default iv length for AES.GCM. - tag: "AES.GCM.V1", - key: Base.decode64!(compile_config!(:database_encryption_key)), - iv_length: 12 - } - ] - config :openid_connect, finch_transport_opts: compile_config!(:http_client_ssl_opts) - config :ueberauth, Ueberauth, - providers: [ - identity: - {Ueberauth.Strategy.Identity, - callback_methods: ["POST"], - callback_url: "#{external_url}/auth/identity/callback", - uid_field: :email} - ] - config :web, Web.Mailer, [ diff --git a/mix.exs b/mix.exs index cd34087b3..5da1f3c3b 100644 --- a/mix.exs +++ b/mix.exs @@ -28,15 +28,13 @@ defmodule Firezone.MixProject do plt_file: {:no_warn, "priv/plts/dialyzer.plt"} ], aliases: aliases(), - default_release: :firezone, + default_release: :web, releases: [ - firezone: [ + web: [ include_executables_for: [:unix], - validate_compile_env: false, + validate_compile_env: true, applications: [ - domain: :permanent, - web: :permanent, - api: :permanent + web: :permanent ], cookie: System.get_env("ERL_COOKIE") ] diff --git a/priv/keycloak-realm.json b/priv/keycloak-realm.json deleted file mode 100644 index 82bf258c1..000000000 --- a/priv/keycloak-realm.json +++ /dev/null @@ -1,2215 +0,0 @@ -{ - "id": "a22cc33d-5ad3-452d-a022-b4f1a1745af3", - "realm": "firezone", - "displayName": "", - "displayNameHtml": "", - "notBefore": 0, - "defaultSignatureAlgorithm": "RS256", - "revokeRefreshToken": false, - "refreshTokenMaxReuse": 0, - "accessTokenLifespan": 300, - "accessTokenLifespanForImplicitFlow": 900, - "ssoSessionIdleTimeout": 1800, - "ssoSessionMaxLifespan": 36000, - "ssoSessionIdleTimeoutRememberMe": 0, - "ssoSessionMaxLifespanRememberMe": 0, - "offlineSessionIdleTimeout": 2592000, - "offlineSessionMaxLifespanEnabled": false, - "offlineSessionMaxLifespan": 5184000, - "clientSessionIdleTimeout": 0, - "clientSessionMaxLifespan": 0, - "clientOfflineSessionIdleTimeout": 0, - "clientOfflineSessionMaxLifespan": 0, - "accessCodeLifespan": 60, - "accessCodeLifespanUserAction": 300, - "accessCodeLifespanLogin": 1800, - "actionTokenGeneratedByAdminLifespan": 43200, - "actionTokenGeneratedByUserLifespan": 300, - "oauth2DeviceCodeLifespan": 600, - "oauth2DevicePollingInterval": 5, - "enabled": true, - "sslRequired": "none", - "registrationAllowed": false, - "registrationEmailAsUsername": false, - "rememberMe": false, - "verifyEmail": false, - "loginWithEmailAllowed": true, - "duplicateEmailsAllowed": false, - "resetPasswordAllowed": false, - "editUsernameAllowed": false, - "bruteForceProtected": false, - "permanentLockout": false, - "maxFailureWaitSeconds": 900, - "minimumQuickLoginWaitSeconds": 60, - "waitIncrementSeconds": 60, - "quickLoginCheckMilliSeconds": 1000, - "maxDeltaTimeSeconds": 43200, - "failureFactor": 30, - "roles": { - "realm": [ - { - "id": "30d85621-c052-4b8e-9dbd-4755dc663ed9", - "name": "uma_authorization", - "description": "${role_uma_authorization}", - "composite": false, - "clientRole": false, - "containerId": "a22cc33d-5ad3-452d-a022-b4f1a1745af3", - "attributes": {} - }, - { - "id": "5f6d403c-a7f6-4621-be85-8adda45b0156", - "name": "offline_access", - "description": "${role_offline-access}", - "composite": false, - "clientRole": false, - "containerId": "a22cc33d-5ad3-452d-a022-b4f1a1745af3", - "attributes": {} - }, - { - "id": "a57d0e46-7bf5-409b-8a25-9ca52ac40a08", - "name": "default-roles-firezone", - "description": "${role_default-roles}", - "composite": true, - "composites": { - "realm": [ - "offline_access", - "uma_authorization" - ] - }, - "clientRole": false, - "containerId": "a22cc33d-5ad3-452d-a022-b4f1a1745af3", - "attributes": {} - } - ], - "client": { - "firezone-saml": [], - "realm-management": [ - { - "id": "3af52d56-dc2b-4acd-90ff-1d5d0c358820", - "name": "manage-events", - "description": "${role_manage-events}", - "composite": false, - "clientRole": true, - "containerId": "a0cd08b0-33cf-4d32-a491-a07913b4b2cc", - "attributes": {} - }, - { - "id": "939ccaaa-d4f9-4ad7-be50-067478608fa4", - "name": "query-realms", - "description": "${role_query-realms}", - "composite": false, - "clientRole": true, - "containerId": "a0cd08b0-33cf-4d32-a491-a07913b4b2cc", - "attributes": {} - }, - { - "id": "98bad0ad-5b0d-471e-b9ed-05fe422f2661", - "name": "realm-admin", - "description": "${role_realm-admin}", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "manage-events", - "query-realms", - "view-authorization", - "manage-realm", - "view-clients", - "view-realm", - "create-client", - "view-users", - "view-identity-providers", - "query-users", - "query-clients", - "impersonation", - "manage-identity-providers", - "view-events", - "query-groups", - "manage-clients", - "manage-users", - "manage-authorization" - ] - } - }, - "clientRole": true, - "containerId": "a0cd08b0-33cf-4d32-a491-a07913b4b2cc", - "attributes": {} - }, - { - "id": "3b92ec03-5be8-4a6b-90d8-0c95dc60687e", - "name": "view-authorization", - "description": "${role_view-authorization}", - "composite": false, - "clientRole": true, - "containerId": "a0cd08b0-33cf-4d32-a491-a07913b4b2cc", - "attributes": {} - }, - { - "id": "1161fb11-c409-4226-a380-1e74959a6422", - "name": "manage-realm", - "description": "${role_manage-realm}", - "composite": false, - "clientRole": true, - "containerId": "a0cd08b0-33cf-4d32-a491-a07913b4b2cc", - "attributes": {} - }, - { - "id": "d136c518-c37b-44d2-ba8f-16298c8338a5", - "name": "view-clients", - "description": "${role_view-clients}", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "query-clients" - ] - } - }, - "clientRole": true, - "containerId": "a0cd08b0-33cf-4d32-a491-a07913b4b2cc", - "attributes": {} - }, - { - "id": "f81a45ad-35af-43a9-9ef9-0908d4a9c9e9", - "name": "view-realm", - "description": "${role_view-realm}", - "composite": false, - "clientRole": true, - "containerId": "a0cd08b0-33cf-4d32-a491-a07913b4b2cc", - "attributes": {} - }, - { - "id": "a3e4e1d8-62fc-40f1-aeb6-524c20f37acb", - "name": "create-client", - "description": "${role_create-client}", - "composite": false, - "clientRole": true, - "containerId": "a0cd08b0-33cf-4d32-a491-a07913b4b2cc", - "attributes": {} - }, - { - "id": "f7e53663-083d-4e33-8dbc-01f4d4106aa2", - "name": "view-users", - "description": "${role_view-users}", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "query-groups", - "query-users" - ] - } - }, - "clientRole": true, - "containerId": "a0cd08b0-33cf-4d32-a491-a07913b4b2cc", - "attributes": {} - }, - { - "id": "f9e42700-8edf-4640-ba27-7756fabc89b8", - "name": "view-identity-providers", - "description": "${role_view-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "a0cd08b0-33cf-4d32-a491-a07913b4b2cc", - "attributes": {} - }, - { - "id": "fe9a489c-86c6-4355-8f8b-3b71bccc8cba", - "name": "query-users", - "description": "${role_query-users}", - "composite": false, - "clientRole": true, - "containerId": "a0cd08b0-33cf-4d32-a491-a07913b4b2cc", - "attributes": {} - }, - { - "id": "fcb723e1-c1a5-4feb-815f-d949683aaf42", - "name": "query-clients", - "description": "${role_query-clients}", - "composite": false, - "clientRole": true, - "containerId": "a0cd08b0-33cf-4d32-a491-a07913b4b2cc", - "attributes": {} - }, - { - "id": "d6626dcc-1866-44ba-b593-aec1e5db6680", - "name": "impersonation", - "description": "${role_impersonation}", - "composite": false, - "clientRole": true, - "containerId": "a0cd08b0-33cf-4d32-a491-a07913b4b2cc", - "attributes": {} - }, - { - "id": "f6e6ef50-8946-42ba-b344-f4c124ecd0b6", - "name": "manage-identity-providers", - "description": "${role_manage-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "a0cd08b0-33cf-4d32-a491-a07913b4b2cc", - "attributes": {} - }, - { - "id": "e20b7ed4-d080-4232-9db5-42a080fc703f", - "name": "query-groups", - "description": "${role_query-groups}", - "composite": false, - "clientRole": true, - "containerId": "a0cd08b0-33cf-4d32-a491-a07913b4b2cc", - "attributes": {} - }, - { - "id": "6b812331-dc99-4803-bb8e-943db354f352", - "name": "view-events", - "description": "${role_view-events}", - "composite": false, - "clientRole": true, - "containerId": "a0cd08b0-33cf-4d32-a491-a07913b4b2cc", - "attributes": {} - }, - { - "id": "522af2d1-55f9-4dd7-baa0-6c387d3bb6ea", - "name": "manage-clients", - "description": "${role_manage-clients}", - "composite": false, - "clientRole": true, - "containerId": "a0cd08b0-33cf-4d32-a491-a07913b4b2cc", - "attributes": {} - }, - { - "id": "e61310e2-13d3-4103-b6e5-5ca5e936f0ac", - "name": "manage-users", - "description": "${role_manage-users}", - "composite": false, - "clientRole": true, - "containerId": "a0cd08b0-33cf-4d32-a491-a07913b4b2cc", - "attributes": {} - }, - { - "id": "e5b19f56-2fee-4565-8398-fa6d0893af02", - "name": "manage-authorization", - "description": "${role_manage-authorization}", - "composite": false, - "clientRole": true, - "containerId": "a0cd08b0-33cf-4d32-a491-a07913b4b2cc", - "attributes": {} - } - ], - "firezone-client": [], - "security-admin-console": [], - "admin-cli": [], - "account-console": [], - "broker": [], - "account": [ - { - "id": "f844f556-fcbd-4156-940e-830f16d084e7", - "name": "delete-account", - "description": "${role_delete-account}", - "composite": false, - "clientRole": true, - "containerId": "836299f4-90e2-4ce0-89c5-a54bdee7a072", - "attributes": {} - }, - { - "id": "910ef7b7-928e-4d2d-8435-67928254aae0", - "name": "manage-account", - "composite": false, - "clientRole": true, - "containerId": "836299f4-90e2-4ce0-89c5-a54bdee7a072", - "attributes": {} - } - ] - } - }, - "groups": [ - { - "id": "e11b6538-2214-474d-8b33-6bc9c19624fd", - "name": "admins", - "path": "/admins", - "attributes": {}, - "realmRoles": [], - "clientRoles": {}, - "subGroups": [] - } - ], - "defaultRole": { - "id": "a57d0e46-7bf5-409b-8a25-9ca52ac40a08", - "name": "default-roles-firezone", - "description": "${role_default-roles}", - "composite": true, - "clientRole": false, - "containerId": "a22cc33d-5ad3-452d-a022-b4f1a1745af3" - }, - "requiredCredentials": [ - "password" - ], - "otpPolicyType": "totp", - "otpPolicyAlgorithm": "HmacSHA1", - "otpPolicyInitialCounter": 0, - "otpPolicyDigits": 6, - "otpPolicyLookAheadWindow": 1, - "otpPolicyPeriod": 30, - "otpSupportedApplications": [ - "FreeOTP", - "Google Authenticator" - ], - "webAuthnPolicyRpEntityName": "keycloak", - "webAuthnPolicySignatureAlgorithms": [ - "ES256" - ], - "webAuthnPolicyRpId": "", - "webAuthnPolicyAttestationConveyancePreference": "not specified", - "webAuthnPolicyAuthenticatorAttachment": "not specified", - "webAuthnPolicyRequireResidentKey": "not specified", - "webAuthnPolicyUserVerificationRequirement": "not specified", - "webAuthnPolicyCreateTimeout": 0, - "webAuthnPolicyAvoidSameAuthenticatorRegister": false, - "webAuthnPolicyAcceptableAaguids": [], - "webAuthnPolicyPasswordlessRpEntityName": "keycloak", - "webAuthnPolicyPasswordlessSignatureAlgorithms": [ - "ES256" - ], - "webAuthnPolicyPasswordlessRpId": "", - "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", - "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", - "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", - "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", - "webAuthnPolicyPasswordlessCreateTimeout": 0, - "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, - "webAuthnPolicyPasswordlessAcceptableAaguids": [], - "scopeMappings": [ - { - "clientScope": "offline_access", - "roles": [ - "offline_access" - ] - } - ], - "clientScopeMappings": { - "account": [ - { - "client": "account-console", - "roles": [ - "manage-account" - ] - } - ] - }, - "clients": [ - { - "id": "836299f4-90e2-4ce0-89c5-a54bdee7a072", - "clientId": "account", - "name": "${client_account}", - "rootUrl": "${authBaseUrl}", - "baseUrl": "/realms/firezone/account/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [ - "/realms/firezone/account/*" - ], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "post.logout.redirect.uris": "+" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "acr", - "roles", - "profile", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "2250e9e5-37c5-497e-8920-7bec6c11e5dd", - "clientId": "account-console", - "name": "${client_account-console}", - "rootUrl": "${authBaseUrl}", - "baseUrl": "/realms/firezone/account/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [ - "/realms/firezone/account/*" - ], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "post.logout.redirect.uris": "+", - "pkce.code.challenge.method": "S256" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "protocolMappers": [ - { - "id": "cabb4fe1-3fc5-44c3-ac2c-b16935a9bc85", - "name": "audience resolve", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-resolve-mapper", - "consentRequired": false, - "config": {} - } - ], - "defaultClientScopes": [ - "web-origins", - "acr", - "roles", - "profile", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "692e7a0c-4d27-45ca-bd42-04400d003048", - "clientId": "admin-cli", - "name": "${client_admin-cli}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": false, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "post.logout.redirect.uris": "+" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "acr", - "roles", - "profile", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "51d6a9d0-c07e-49a4-ba95-f9418c2dafb0", - "clientId": "broker", - "name": "${client_broker}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": true, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "post.logout.redirect.uris": "+" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "acr", - "roles", - "profile", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "bd3e5795-d514-44c1-9ad9-7b50c745b25d", - "clientId": "firezone-client", - "name": "", - "description": "", - "rootUrl": "", - "adminUrl": "", - "baseUrl": "", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "secret": "**********", - "redirectUris": [ - "", - "https://localhost/auth/oidc/keycloak/callback/" - ], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": true, - "protocol": "openid-connect", - "attributes": { - "oidc.ciba.grant.enabled": "false", - "client.secret.creation.time": "1665772021", - "backchannel.logout.session.required": "true", - "post.logout.redirect.uris": "+", - "display.on.consent.screen": "false", - "oauth2.device.authorization.grant.enabled": "false", - "backchannel.logout.revoke.offline.tokens": "false" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, - "nodeReRegistrationTimeout": -1, - "defaultClientScopes": [ - "web-origins", - "acr", - "roles", - "profile", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "325df7cb-baac-4733-b601-b4a0baed099a", - "clientId": "firezone-saml", - "name": "", - "description": "", - "rootUrl": "", - "adminUrl": "", - "baseUrl": "", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [ - "", - "https://localhost/auth/saml/auth/signin/keycloak/" - ], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": true, - "protocol": "saml", - "attributes": { - "saml.assertion.signature": "false", - "saml.force.post.binding": "true", - "saml.server.signature": "true", - "saml.server.signature.keyinfo.ext": "false", - "saml.signing.certificate": "MIICqTCCAZECBgGD19uSLjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1maXJlem9uZS1zYW1sMB4XDTIyMTAxNDE4NTUzNFoXDTMyMTAxNDE4NTcxNFowGDEWMBQGA1UEAwwNZmlyZXpvbmUtc2FtbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMBxaPBC4T59NUxZ5A7Q3l10YXP1+I+RvD6U5nTCUQDDKWjgqX20esV21ZIj/FRhst3FzOjFgOmagm2SOSkR6Kejr+hdgfqHPZIKrHUD3Ue1pwPPHbhUjs1NcEYof3FuH+SiaaqEHl5x/i+Vj1uso9Vh0qHxweHyh0YEScBvHecVL9G3ZbtmFgTN6m47I5W6NIopLrCHzCUUxmeb1ihSprTlTlajmefWwLONlwU/V9esvj341SCtH6TInvlOFBN2+QO0UJZZDZ8hu/F7nKoCNai6J/5eWMk1/tFZrRN0moAf7BkUdrT2zGbhZPJfi5hdvE0RpE4gVH3gnOTo9rzr808CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAJBRMH7OcClY9fGQPKGE9XKSelPWiPimdzWES0Z6PKFTd02opg642+WaEs8fPgl6GdJHYWvZ4Ha98+nrihf5APh2sK62RnsEQrIq/PPL5iZ+oT8jn6UEitg123sRrtjoqV9JRiVRfT536sBGmjGRsPyFumygIO5OIth0JjuD2vFvR1PlhlD2HySmFQjtJEPCiK1ETvQjSuqHh8zD1ODv7xjRqy9hqtpUHL+HRSS8tkOg8bEMOtAKUri8xyEbhu5ysQVXjy6kC3fkOk6atFXj6UIhhPhOqIsSsJpk6zbSVJlcpkrbBTL1lhVpN46eh3NybE8NYMWnZthQSqid26feoEw==", - "saml.artifact.binding.identifier": "lH60OvOihGGt2CFRYdEYO9Kn0p4=", - "saml.artifact.binding": "false", - "saml.signature.algorithm": "RSA_SHA256", - "saml_force_name_id_format": "false", - "saml.client.signature": "true", - "saml.force.name.id.format": "false", - "saml.authnstatement": "true", - "display.on.consent.screen": "false", - "saml.signing.private.key": "MIIEoQIBAAKCAQEAwHFo8ELhPn01TFnkDtDeXXRhc/X4j5G8PpTmdMJRAMMpaOCpfbR6xXbVkiP8VGGy3cXM6MWA6ZqCbZI5KRHop6Ov6F2B+oc9kgqsdQPdR7WnA88duFSOzU1wRih/cW4f5KJpqoQeXnH+L5WPW6yj1WHSofHB4fKHRgRJwG8d5xUv0bdlu2YWBM3qbjsjlbo0iikusIfMJRTGZ5vWKFKmtOVOVqOZ59bAs42XBT9X16y+PfjVIK0fpMie+U4UE3b5A7RQllkNnyG78XucqgI1qLon/l5YyTX+0VmtE3SagB/sGRR2tPbMZuFk8l+LmF28TRGkTiBUfeCc5Oj2vOvzTwIDAQABAoH/ck+GmB7KFx9YQmCCnG6UV1jqetGvIr+b17oMqR0/3FHN2UlrjbgaGMI76W2OBzcD/MTyXgcxiq3695nBB6MB0b5WDgyFBds9er822vXJ5Atw8jCpvdhyJsm2DM/QYpwVx4nTewCA/zk15A4I3Qqw3L3GgIgeVnwNzEyT0BkuXbNhBYLQUm/H0aqpU6SPlc7akXQvcP4tcX5RFaImaRdV7Sc5TW2RkjOEn8yuQnk7wq2SwTb0nqmR9o+x7vjjtd1XAY9hKnCFc0BO2bkJwSfiDBN2z1am+MsVegB610uJb0Y/Xx+Ksrr8fjf5ctOf1HmerwEGFZe1Aaim3OQOkEnVAoGBAPlsoZOjOid88LXQ7V2EsUbdoHd19PLxYO3QWUPsy0rxCVY44319g6DobkiY4wunxSk4XPmoeSGxrCxZfFd4GhM3QTnm/sJk7O6eP9licvYKbpdRkz74gWbDJKNrdS0+swmCZMstHNpzgC+3KUGHhZxAsNWQn5odSVT87m0hgwUbAoGBAMWENfk/q0UsgDAPT5zE2ka3Q89xC94azwYvkC2Bt+02WthFzg0LX83k6UL7Ox2dy3A52zHv/+AXII9p8DOQK8eI+Ptk/1FBEXB4BcxfcIknhFwGOQh5mJqGqVKMcjhF0NSlOvvdZ17J0sdg/eFmj16FTc3aD+uTX05xLcOIyVHdAoGAFY9nVBy67SjmocDCk9/hdw+3TDw9BTcKOuRUowN3+y1ksjhOqc1MsH8G8W2Nyrcg2tRNbSM7/UafbGH71o/CKUEQeVHXWbRhqqqxODhDWbNDJf9eLAZNITXjF+E1Csktrre/wLRQly1+uiEqBskRKpWI850Bvs+jRB5s+a/45FsCgYAgmPFPcXKSuAVv6Rb6iIJ9qUFHuyB6R3JjUvY4jHAOdioIB306KeZl1KGqrEvVV3V70BCc7T8NfMOi55j86Y5QIAr3cnOwtUxrg2Nxl067VApEJcLD402MMy0+sm8nWl5cuehCKQgmXKisvxUnXgXLU7TDhHmcJ3ZTO9qRqsGPAQKBgQCxcUwufFqTD5Th8+g9fgBA9248UbPx8zHfQPHUSNw38V7vj2a3LJlcvbWYBFJMad8JwxY0Kai4+Ul+39e3YeAiyJ2gIlhvQFP4renwOA7oUZ9X+zOgw8QAKy80N+SkHMKkC6rBSM7luRymUWIRmTEDcd+yhLE3MMate6J1XRU0uA==", - "saml_name_id_format": "username", - "saml.allow.ecp.flow": "false", - "saml.onetimeuse.condition": "false", - "saml_signature_canonicalization_method": "http://www.w3.org/2001/10/xml-exc-c14n#" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, - "nodeReRegistrationTimeout": -1, - "defaultClientScopes": [ - "role_list" - ], - "optionalClientScopes": [] - }, - { - "id": "a0cd08b0-33cf-4d32-a491-a07913b4b2cc", - "clientId": "realm-management", - "name": "${client_realm-management}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": true, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "post.logout.redirect.uris": "+" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "acr", - "roles", - "profile", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "c4f8903c-e601-4990-be90-f3cafb8cf094", - "clientId": "security-admin-console", - "name": "${client_security-admin-console}", - "rootUrl": "${authAdminUrl}", - "baseUrl": "/admin/firezone/console/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [ - "/admin/firezone/console/*" - ], - "webOrigins": [ - "+" - ], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "post.logout.redirect.uris": "+", - "pkce.code.challenge.method": "S256" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "protocolMappers": [ - { - "id": "68431c73-23d9-440a-84a8-da997f87bdc4", - "name": "locale", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "locale", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "locale", - "jsonType.label": "String" - } - } - ], - "defaultClientScopes": [ - "web-origins", - "acr", - "roles", - "profile", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - } - ], - "clientScopes": [ - { - "id": "25052823-7eb0-43ed-8dd0-f27cfcb7d0c6", - "name": "roles", - "description": "OpenID Connect scope for add user roles to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "true", - "consent.screen.text": "${rolesScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "736ab3c2-6474-4cc3-825d-50a8f54ecc2d", - "name": "realm roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "user.attribute": "foo", - "access.token.claim": "true", - "claim.name": "realm_access.roles", - "jsonType.label": "String", - "multivalued": "true" - } - }, - { - "id": "e1795708-fbb2-4ceb-9a08-f291c03da8ad", - "name": "audience resolve", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-resolve-mapper", - "consentRequired": false, - "config": {} - }, - { - "id": "7c924754-a88d-459d-b94a-fef37ef3d8e2", - "name": "client roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-client-role-mapper", - "consentRequired": false, - "config": { - "user.attribute": "foo", - "access.token.claim": "true", - "claim.name": "resource_access.${client_id}.roles", - "jsonType.label": "String", - "multivalued": "true" - } - } - ] - }, - { - "id": "5457f36a-e484-4a2b-973e-ac53a3e5c263", - "name": "phone", - "description": "OpenID Connect built-in scope: phone", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${phoneScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "6355b8ff-30a4-4788-8052-2660de265bf5", - "name": "phone number verified", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "phoneNumberVerified", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "phone_number_verified", - "jsonType.label": "boolean" - } - }, - { - "id": "b790aeec-2188-4fc7-9b1f-4be165ca10f8", - "name": "phone number", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "phoneNumber", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "phone_number", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "34cb837f-3790-4127-8ba7-9dd2008e1ff2", - "name": "role_list", - "description": "SAML role list", - "protocol": "saml", - "attributes": { - "consent.screen.text": "${samlRoleListScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "bada63ea-1044-4f44-8d01-78ceae0635b4", - "name": "role list", - "protocol": "saml", - "protocolMapper": "saml-role-list-mapper", - "consentRequired": false, - "config": { - "single": "false", - "attribute.nameformat": "Basic", - "attribute.name": "Role" - } - } - ] - }, - { - "id": "e4bee0f6-1318-4a28-bfe7-1d4fde10aec7", - "name": "acr", - "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "7ee0a034-aa71-40f1-9231-88826c04d236", - "name": "acr loa level", - "protocol": "openid-connect", - "protocolMapper": "oidc-acr-mapper", - "consentRequired": false, - "config": { - "id.token.claim": "true", - "access.token.claim": "true", - "userinfo.token.claim": "true" - } - } - ] - }, - { - "id": "1e02ccb4-2472-4101-8e8b-bd52c636e6f7", - "name": "web-origins", - "description": "OpenID Connect scope for add allowed web origins to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "false", - "consent.screen.text": "" - }, - "protocolMappers": [ - { - "id": "d8c4868b-b5a1-4885-a2d7-745de6055b29", - "name": "allowed web origins", - "protocol": "openid-connect", - "protocolMapper": "oidc-allowed-origins-mapper", - "consentRequired": false, - "config": {} - } - ] - }, - { - "id": "dcd2084c-bdc5-4ee8-b944-c0e8f29b95bf", - "name": "profile", - "description": "OpenID Connect built-in scope: profile", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${profileScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "fb3564b3-efe2-44ca-832f-0743627327f1", - "name": "zoneinfo", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "zoneinfo", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "zoneinfo", - "jsonType.label": "String" - } - }, - { - "id": "00762359-6f62-4147-bbdc-b038ddbd0764", - "name": "given name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "firstName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "given_name", - "jsonType.label": "String" - } - }, - { - "id": "d3b84b3e-8d61-4f45-96df-537a0d4fac0b", - "name": "picture", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "picture", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "picture", - "jsonType.label": "String" - } - }, - { - "id": "dab64ee2-6e2d-4c89-a279-ef64678a4411", - "name": "nickname", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "nickname", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "nickname", - "jsonType.label": "String" - } - }, - { - "id": "cbc013f6-76f5-4de6-9c75-7e7d8e0e5247", - "name": "username", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "preferred_username", - "jsonType.label": "String" - } - }, - { - "id": "93f40b5a-aae2-42a7-b673-a2fb368e1e34", - "name": "full name", - "protocol": "openid-connect", - "protocolMapper": "oidc-full-name-mapper", - "consentRequired": false, - "config": { - "id.token.claim": "true", - "access.token.claim": "true", - "userinfo.token.claim": "true" - } - }, - { - "id": "4e98eb77-64d1-41d4-bf27-8a9bb0c4f8b3", - "name": "family name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "lastName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "family_name", - "jsonType.label": "String" - } - }, - { - "id": "f4e6db9a-4ae6-47ca-96da-db117100cea3", - "name": "profile", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "profile", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "profile", - "jsonType.label": "String" - } - }, - { - "id": "cdde6ba9-c866-42b3-b1f3-1eb3dd30b680", - "name": "locale", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "locale", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "locale", - "jsonType.label": "String" - } - }, - { - "id": "1195e372-8317-4258-9708-c3f34f7277d8", - "name": "website", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "website", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "website", - "jsonType.label": "String" - } - }, - { - "id": "bc35bc42-43c1-4908-89b5-6ff4c39d28f1", - "name": "middle name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "middleName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "middle_name", - "jsonType.label": "String" - } - }, - { - "id": "162be747-d54d-480e-860f-15bfa3103ddd", - "name": "gender", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "gender", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "gender", - "jsonType.label": "String" - } - }, - { - "id": "a5048508-f27d-4f82-8891-ebb5f73ed999", - "name": "updated at", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "updatedAt", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "updated_at", - "jsonType.label": "long" - } - }, - { - "id": "14386740-0c98-4be7-8596-54c4b06afe77", - "name": "birthdate", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "birthdate", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "birthdate", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "f2901b5e-8a99-4d1f-89dc-9a5cf6c02717", - "name": "address", - "description": "OpenID Connect built-in scope: address", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${addressScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "0bcf4a6a-9953-495c-aa80-adf990726893", - "name": "address", - "protocol": "openid-connect", - "protocolMapper": "oidc-address-mapper", - "consentRequired": false, - "config": { - "user.attribute.formatted": "formatted", - "user.attribute.country": "country", - "user.attribute.postal_code": "postal_code", - "userinfo.token.claim": "true", - "user.attribute.street": "street", - "id.token.claim": "true", - "user.attribute.region": "region", - "access.token.claim": "true", - "user.attribute.locality": "locality" - } - } - ] - }, - { - "id": "495e0679-d71f-4b9f-b0c6-a125ea01d5e8", - "name": "email", - "description": "OpenID Connect built-in scope: email", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${emailScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "233fd2cd-7398-41f4-9866-7044490617f0", - "name": "email verified", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "emailVerified", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "email_verified", - "jsonType.label": "boolean" - } - }, - { - "id": "a717da78-c9a8-4db8-9a0f-28033aacb345", - "name": "email", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "email", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "email", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "5e014983-7c19-47e8-877c-0eb45231c6e3", - "name": "microprofile-jwt", - "description": "Microprofile - JWT built-in scope", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "6848d162-c7da-427d-a8cb-1b740b6c5d25", - "name": "upn", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "upn", - "jsonType.label": "String" - } - }, - { - "id": "e57c29b9-2506-4a37-bd39-307ec0faa1f0", - "name": "groups", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "multivalued": "true", - "userinfo.token.claim": "true", - "user.attribute": "foo", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "groups", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "0c6e1add-2c19-4f0e-8ad6-03f4c2fd9086", - "name": "offline_access", - "description": "OpenID Connect built-in scope: offline_access", - "protocol": "openid-connect", - "attributes": { - "consent.screen.text": "${offlineAccessScopeConsentText}", - "display.on.consent.screen": "true" - } - } - ], - "defaultDefaultClientScopes": [ - "web-origins", - "roles", - "role_list", - "email", - "profile", - "acr" - ], - "defaultOptionalClientScopes": [ - "offline_access", - "phone", - "microprofile-jwt", - "address" - ], - "browserSecurityHeaders": { - "contentSecurityPolicyReportOnly": "", - "xContentTypeOptions": "nosniff", - "xRobotsTag": "none", - "xFrameOptions": "SAMEORIGIN", - "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", - "xXSSProtection": "1; mode=block", - "strictTransportSecurity": "max-age=31536000; includeSubDomains" - }, - "smtpServer": {}, - "eventsEnabled": false, - "eventsListeners": [ - "jboss-logging" - ], - "enabledEventTypes": [], - "adminEventsEnabled": false, - "adminEventsDetailsEnabled": false, - "identityProviders": [], - "identityProviderMappers": [], - "components": { - "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ - { - "id": "644a1f9f-59d4-4b9c-93b7-b7891b8326d8", - "name": "Trusted Hosts", - "providerId": "trusted-hosts", - "subType": "anonymous", - "subComponents": {}, - "config": { - "host-sending-registration-request-must-match": [ - "true" - ], - "client-uris-must-match": [ - "true" - ] - } - }, - { - "id": "676bb8f2-76aa-445c-a6ea-a65eebc3f891", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "authenticated", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "saml-user-attribute-mapper", - "saml-role-list-mapper", - "oidc-address-mapper", - "oidc-full-name-mapper", - "oidc-usermodel-attribute-mapper", - "oidc-usermodel-property-mapper", - "oidc-sha256-pairwise-sub-mapper", - "saml-user-property-mapper" - ] - } - }, - { - "id": "909a97d0-5ae1-4032-826e-f141e94fd70b", - "name": "Max Clients Limit", - "providerId": "max-clients", - "subType": "anonymous", - "subComponents": {}, - "config": { - "max-clients": [ - "200" - ] - } - }, - { - "id": "5c56d234-38d9-48d2-90c2-3a30fcd66420", - "name": "Consent Required", - "providerId": "consent-required", - "subType": "anonymous", - "subComponents": {}, - "config": {} - }, - { - "id": "57d64f65-738e-4e85-b7a6-2ea8d67d66e5", - "name": "Full Scope Disabled", - "providerId": "scope", - "subType": "anonymous", - "subComponents": {}, - "config": {} - }, - { - "id": "ca30784b-25f5-4b4a-af95-5e363b8781dc", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "oidc-sha256-pairwise-sub-mapper", - "oidc-usermodel-property-mapper", - "saml-role-list-mapper", - "oidc-full-name-mapper", - "oidc-usermodel-attribute-mapper", - "saml-user-attribute-mapper", - "saml-user-property-mapper", - "oidc-address-mapper" - ] - } - }, - { - "id": "0fc8702b-2718-469c-8e83-eb26f806cf4d", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "authenticated", - "subComponents": {}, - "config": { - "allow-default-scopes": [ - "true" - ] - } - }, - { - "id": "1c68f5ad-2a9e-4394-936e-2d436e46f715", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allow-default-scopes": [ - "true" - ] - } - } - ], - "org.keycloak.userprofile.UserProfileProvider": [ - { - "id": "fe6c92b2-942e-4f85-ad9f-76aaf6878f8e", - "providerId": "declarative-user-profile", - "subComponents": {}, - "config": {} - } - ], - "org.keycloak.keys.KeyProvider": [ - { - "id": "e37b3ccb-ec33-4063-b67e-5e9c9ac1eb1a", - "name": "rsa-generated", - "providerId": "rsa-generated", - "subComponents": {}, - "config": { - "priority": [ - "100" - ] - } - }, - { - "id": "bb77c545-a449-44ec-997c-7efd1647ca5c", - "name": "hmac-generated", - "providerId": "hmac-generated", - "subComponents": {}, - "config": { - "priority": [ - "100" - ], - "algorithm": [ - "HS256" - ] - } - }, - { - "id": "ceaedfda-cd03-45fb-a617-59aeb0f7bf62", - "name": "rsa-enc-generated", - "providerId": "rsa-enc-generated", - "subComponents": {}, - "config": { - "priority": [ - "100" - ], - "algorithm": [ - "RSA-OAEP" - ] - } - }, - { - "id": "f98ffdb9-eb0d-4dd0-baac-4746556595e5", - "name": "aes-generated", - "providerId": "aes-generated", - "subComponents": {}, - "config": { - "priority": [ - "100" - ] - } - } - ] - }, - "internationalizationEnabled": false, - "supportedLocales": [], - "authenticationFlows": [ - { - "id": "648c54b2-368d-4fd8-9870-4305fbe36ac6", - "alias": "Account verification options", - "description": "Method with which to verity the existing account", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-email-verification", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Verify Existing Account by Re-authentication", - "userSetupAllowed": false - } - ] - }, - { - "id": "4ed8d1d5-5e7d-46f7-9987-8a176b8f781c", - "alias": "Authentication Options", - "description": "Authentication options.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "basic-auth", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "basic-auth-otp", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-spnego", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 30, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "efc6df06-3810-442f-8ccb-3c0c38fcbf10", - "alias": "Browser - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-otp-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "2bb67a19-0ee5-4a6e-b9f6-a4093dd52e7d", - "alias": "Direct Grant - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "direct-grant-validate-otp", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "c8acb54c-be4f-4dde-8a25-4d82c3705613", - "alias": "First broker login - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-otp-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "4211e5e4-990a-4f9b-8709-183c0aa31960", - "alias": "Handle Existing Account", - "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-confirm-link", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Account verification options", - "userSetupAllowed": false - } - ] - }, - { - "id": "782b0454-2aeb-42a0-981b-a986866137fb", - "alias": "Reset - Conditional OTP", - "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-otp", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "106e526a-3f6a-42d3-990f-8f14fb49d8ef", - "alias": "User creation or linking", - "description": "Flow for the existing/non-existing user alternatives", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticatorConfig": "create unique user config", - "authenticator": "idp-create-user-if-unique", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Handle Existing Account", - "userSetupAllowed": false - } - ] - }, - { - "id": "4e5df2eb-5fc5-44ef-97fa-35f1e7c01aba", - "alias": "Verify Existing Account by Re-authentication", - "description": "Reauthentication of existing account", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-username-password-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "First broker login - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "ba589416-fbba-4bed-9539-7270b2872371", - "alias": "browser", - "description": "browser based authentication", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-cookie", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-spnego", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "identity-provider-redirector", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 25, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 30, - "autheticatorFlow": true, - "flowAlias": "forms", - "userSetupAllowed": false - } - ] - }, - { - "id": "06728976-310c-4c35-b352-a80dbb690749", - "alias": "clients", - "description": "Base authentication for clients", - "providerId": "client-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "client-secret", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-jwt", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-secret-jwt", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 30, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-x509", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 40, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "12b29bbd-fb22-4d49-9dcc-c3ff17af1e3a", - "alias": "direct grant", - "description": "OpenID Connect Resource Owner Grant", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "direct-grant-validate-username", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "direct-grant-validate-password", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 30, - "autheticatorFlow": true, - "flowAlias": "Direct Grant - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "8c46aa41-e58c-4cf2-a092-0147fa7d640a", - "alias": "docker auth", - "description": "Used by Docker clients to authenticate against the IDP", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "docker-http-basic-authenticator", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "109a3a37-28ad-4b85-9b69-adbfa9aef87d", - "alias": "first broker login", - "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticatorConfig": "review profile config", - "authenticator": "idp-review-profile", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "User creation or linking", - "userSetupAllowed": false - } - ] - }, - { - "id": "56ce6e93-74d8-421f-ad0d-8bade834ef98", - "alias": "forms", - "description": "Username, password, otp and other auth forms.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-username-password-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Browser - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "7adbab75-4795-46ed-b26d-6434f9c32363", - "alias": "http challenge", - "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "no-cookie-redirect", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Authentication Options", - "userSetupAllowed": false - } - ] - }, - { - "id": "4b3c8b6b-d98c-42be-90b3-5846873ec82a", - "alias": "registration", - "description": "registration flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-page-form", - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": true, - "flowAlias": "registration form", - "userSetupAllowed": false - } - ] - }, - { - "id": "fc64bd4e-7e99-4765-892d-93e5195b3cdf", - "alias": "registration form", - "description": "registration form", - "providerId": "form-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-user-creation", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-profile-action", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 40, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-password-action", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 50, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-recaptcha-action", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 60, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "c2821b19-4cb5-4090-b18b-0764f6ae4d92", - "alias": "reset credentials", - "description": "Reset credentials for a user if they forgot their password or something", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "reset-credentials-choose-user", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-credential-email", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-password", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 30, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 40, - "autheticatorFlow": true, - "flowAlias": "Reset - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "4970610f-5cdf-45d6-9c4c-d17c6d8356a1", - "alias": "saml ecp", - "description": "SAML ECP Profile Authentication Flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "http-basic-authenticator", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - } - ], - "authenticatorConfig": [ - { - "id": "54a6bee1-851b-466c-b3ae-02ee129bb4ac", - "alias": "create unique user config", - "config": { - "require.password.update.after.registration": "false" - } - }, - { - "id": "52e91497-d0b9-4aa3-af42-6f0539ef48b3", - "alias": "review profile config", - "config": { - "update.profile.on.first.login": "missing" - } - } - ], - "requiredActions": [ - { - "alias": "CONFIGURE_TOTP", - "name": "Configure OTP", - "providerId": "CONFIGURE_TOTP", - "enabled": true, - "defaultAction": false, - "priority": 10, - "config": {} - }, - { - "alias": "terms_and_conditions", - "name": "Terms and Conditions", - "providerId": "terms_and_conditions", - "enabled": false, - "defaultAction": false, - "priority": 20, - "config": {} - }, - { - "alias": "UPDATE_PASSWORD", - "name": "Update Password", - "providerId": "UPDATE_PASSWORD", - "enabled": true, - "defaultAction": false, - "priority": 30, - "config": {} - }, - { - "alias": "UPDATE_PROFILE", - "name": "Update Profile", - "providerId": "UPDATE_PROFILE", - "enabled": true, - "defaultAction": false, - "priority": 40, - "config": {} - }, - { - "alias": "VERIFY_EMAIL", - "name": "Verify Email", - "providerId": "VERIFY_EMAIL", - "enabled": true, - "defaultAction": false, - "priority": 50, - "config": {} - }, - { - "alias": "delete_account", - "name": "Delete Account", - "providerId": "delete_account", - "enabled": false, - "defaultAction": false, - "priority": 60, - "config": {} - }, - { - "alias": "webauthn-register", - "name": "Webauthn Register", - "providerId": "webauthn-register", - "enabled": true, - "defaultAction": false, - "priority": 70, - "config": {} - }, - { - "alias": "webauthn-register-passwordless", - "name": "Webauthn Register Passwordless", - "providerId": "webauthn-register-passwordless", - "enabled": true, - "defaultAction": false, - "priority": 80, - "config": {} - }, - { - "alias": "update_user_locale", - "name": "Update User Locale", - "providerId": "update_user_locale", - "enabled": true, - "defaultAction": false, - "priority": 1000, - "config": {} - } - ], - "browserFlow": "browser", - "registrationFlow": "registration", - "directGrantFlow": "direct grant", - "resetCredentialsFlow": "reset credentials", - "clientAuthenticationFlow": "clients", - "dockerAuthenticationFlow": "docker auth", - "attributes": { - "cibaBackchannelTokenDeliveryMode": "poll", - "cibaAuthRequestedUserHint": "login_hint", - "clientOfflineSessionMaxLifespan": "0", - "oauth2DevicePollingInterval": "5", - "clientSessionIdleTimeout": "0", - "clientOfflineSessionIdleTimeout": "0", - "cibaInterval": "5", - "cibaExpiresIn": "120", - "oauth2DeviceCodeLifespan": "600", - "parRequestUriLifespan": "60", - "clientSessionMaxLifespan": "0", - "frontendUrl": "", - "acr.loa.map": "[]" - }, - "keycloakVersion": "19.0.3", - "userManagedAccessAllowed": false, - "clientProfiles": { - "profiles": [] - }, - "clientPolicies": { - "policies": [] - } -} diff --git a/priv/wg0.client.conf b/priv/wg0.client.conf deleted file mode 100644 index f5b469368..000000000 --- a/priv/wg0.client.conf +++ /dev/null @@ -1,14 +0,0 @@ -# This config corresponds to the wireguard-client device -# created when the DB is bootstrapped from apps/fz_http/priv/repo/seeds.exs. -[Interface] -Address = 100.64.100.1/32,fd00::6/128 -DNS = 127.0.0.11 -PrivateKey = UJ3WN7k8mnRTj33BAoiA1lw0ag24oB7NsTg01MaeNUI= -MTU = 1280 - -[Peer] -PublicKey = is+0ov0/SZ9I+qyDD+adVoH9LreWHa85QQgpt6RUtA4= -PresharedKey = C+Tte1echarIObr6rq+nFeYQ1QO5xo5N29ygDjMlpS8= -PersistentKeepalive = 25 -AllowedIPs = 0.0.0.0/0,::/0 -Endpoint = firezone:51820 diff --git a/priv/wg_dev_private_key b/priv/wg_dev_private_key deleted file mode 100644 index 3721eb35c..000000000 --- a/priv/wg_dev_private_key +++ /dev/null @@ -1 +0,0 @@ -wIq2WRxCctZAt9WB642jTTl/HFys9Ntht033s/0YKXI=