+ Error generating configuration. Could not load private key from
+ sessionStorage. Close window and try again. If the issue persists,
+ please contact support@firez.one.
+
`
+ }
+ }
+}
+
+const renderDownloadButton = function (config) {
+ let button = document.getElementById("download-config")
+ button.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(config))
+ button.setAttribute("download", window.location.hostname + ".conf")
+ button.classList.remove("is-hidden")
+}
+
+const renderTunnel = function (config) {
+ let code = document.getElementById("wg-conf")
+ let container = document.getElementById("wg-conf-container")
+ code.innerHTML = config
+ container.classList.remove("is-hidden")
+}
+
+const renderQR = function (config) {
+ let canvas = document.getElementById("qr-canvas")
+ if (canvas) {
+ QRCode.toCanvas(canvas, config, {
+ errorCorrectionLevel: "H",
+ margin: 0,
+ width: 200,
+ height: 200
+ }, function (error) {
+ if (error) alert("QRCode Encode Error: " + error)
+ })
+ }
+}
+
+export { renderConfig }
diff --git a/apps/fz_http/assets/package-lock.json b/apps/fz_http/assets/package-lock.json
index 03c984d41..e31f1e725 100644
--- a/apps/fz_http/assets/package-lock.json
+++ b/apps/fz_http/assets/package-lock.json
@@ -17,7 +17,9 @@
"phoenix": "file:../../../deps/phoenix",
"phoenix_html": "file:../../../deps/phoenix_html",
"phoenix_live_view": "file:../../../deps/phoenix_live_view",
- "qrcode": "^1.3.3"
+ "qrcode": "^1.3.3",
+ "tweetnacl": "^1.0.3",
+ "tweetnacl-util": "^0.15.1"
},
"devDependencies": {
"@babel/core": "^7.16.12",
@@ -2447,6 +2449,12 @@
"tweetnacl": "^0.14.3"
}
},
+ "node_modules/bcrypt-pbkdf/node_modules/tweetnacl": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
+ "dev": true
+ },
"node_modules/big.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@@ -7174,6 +7182,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/sshpk/node_modules/tweetnacl": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
+ "dev": true
+ },
"node_modules/ssri": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
@@ -7558,10 +7572,14 @@
}
},
"node_modules/tweetnacl": {
- "version": "0.14.5",
- "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
- "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
- "dev": true
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
+ "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
+ },
+ "node_modules/tweetnacl-util": {
+ "version": "0.15.1",
+ "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz",
+ "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw=="
},
"node_modules/type-fest": {
"version": "0.18.1",
@@ -9821,6 +9839,14 @@
"dev": true,
"requires": {
"tweetnacl": "^0.14.3"
+ },
+ "dependencies": {
+ "tweetnacl": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
+ "dev": true
+ }
}
},
"big.js": {
@@ -13265,6 +13291,14 @@
"jsbn": "~0.1.0",
"safer-buffer": "^2.0.2",
"tweetnacl": "~0.14.0"
+ },
+ "dependencies": {
+ "tweetnacl": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
+ "dev": true
+ }
}
},
"ssri": {
@@ -13543,10 +13577,14 @@
}
},
"tweetnacl": {
- "version": "0.14.5",
- "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
- "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
- "dev": true
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
+ "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
+ },
+ "tweetnacl-util": {
+ "version": "0.15.1",
+ "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz",
+ "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw=="
},
"type-fest": {
"version": "0.18.1",
diff --git a/apps/fz_http/assets/package.json b/apps/fz_http/assets/package.json
index 56cd5cf5e..3d18d8faf 100644
--- a/apps/fz_http/assets/package.json
+++ b/apps/fz_http/assets/package.json
@@ -22,7 +22,9 @@
"phoenix": "file:../../../deps/phoenix",
"phoenix_html": "file:../../../deps/phoenix_html",
"phoenix_live_view": "file:../../../deps/phoenix_live_view",
- "qrcode": "^1.3.3"
+ "qrcode": "^1.3.3",
+ "tweetnacl": "^1.0.3",
+ "tweetnacl-util": "^0.15.1"
},
"devDependencies": {
"@babel/core": "^7.16.12",
diff --git a/apps/fz_http/assets/webpack.config.js b/apps/fz_http/assets/webpack.config.js
index 7002b6f7d..190b57172 100644
--- a/apps/fz_http/assets/webpack.config.js
+++ b/apps/fz_http/assets/webpack.config.js
@@ -12,14 +12,13 @@ module.exports = (env, options) => ({
]
},
entry: {
- 'app': glob.sync('./vendor/**/*.js').concat([
+ 'admin': glob.sync('./vendor/**/*.js').concat([
// Local JS files to include in the bundle
- './js/hooks.js',
- './js/app.js',
+ './js/admin.js',
'./node_modules/admin-one-bulma-dashboard/src/js/main.js'
]),
- 'auth': ['./js/auth.js'],
- 'device_config': ['./js/device_config.js']
+ 'root': ['./js/root.js'],
+ 'unprivileged': ['./js/unprivileged.js']
},
output: {
path: path.resolve(__dirname, '../priv/static/js'),
diff --git a/apps/fz_http/lib/fz_http/devices.ex b/apps/fz_http/lib/fz_http/devices.ex
index b775a945e..5322d62c8 100644
--- a/apps/fz_http/lib/fz_http/devices.ex
+++ b/apps/fz_http/lib/fz_http/devices.ex
@@ -4,13 +4,8 @@ defmodule FzHttp.Devices do
"""
import Ecto.Query, warn: false
- alias FzCommon.{FzCrypto, NameGenerator}
- alias FzHttp.{ConnectivityChecks, Devices.Device, Repo, Settings, Telemetry, Users, Users.User}
-
- # Device configs can be viewable for 10 minutes
- @config_token_expires_in_sec 600
-
- @events_module Application.compile_env!(:fz_http, :events_module)
+ alias FzCommon.NameGenerator
+ alias FzHttp.{Devices.Device, Repo, Sites, Telemetry, Users, Users.User}
def list_devices do
Repo.all(Device)
@@ -26,15 +21,6 @@ defmodule FzHttp.Devices do
Repo.one(from d in Device, where: d.user_id == ^user_id, select: count())
end
- def get_device!(config_token: config_token) do
- now = DateTime.utc_now()
-
- Repo.one!(
- from d in Device,
- where: d.config_token == ^config_token and d.config_token_expires_at > ^now
- )
- end
-
def get_device!(id), do: Repo.get!(Device, id)
def create_device(attrs \\ %{}) do
@@ -58,26 +44,6 @@ defmodule FzHttp.Devices do
result
end
- @doc """
- Creates device with fields populated from the VPN process.
- """
- def auto_create_device(attrs \\ %{}) do
- {:ok, privkey, pubkey, server_pubkey} = @events_module.create_device()
-
- attributes =
- Map.merge(
- %{
- private_key: privkey,
- public_key: pubkey,
- server_public_key: server_pubkey,
- name: rand_name()
- },
- attrs
- )
-
- create_device(attributes)
- end
-
def update_device(%Device{} = device, attrs) do
device
|> Device.update_changeset(attrs)
@@ -93,10 +59,6 @@ defmodule FzHttp.Devices do
Device.update_changeset(device, attrs)
end
- def rand_name do
- NameGenerator.generate()
- end
-
@doc """
Builds ipv4 / ipv6 config string for a device.
"""
@@ -119,7 +81,7 @@ defmodule FzHttp.Devices do
end
def to_peer_list do
- vpn_duration = Settings.vpn_duration()
+ vpn_duration = Sites.vpn_duration()
Repo.all(
from d in Device,
@@ -136,97 +98,57 @@ defmodule FzHttp.Devices do
end)
end
- def new_device do
- change_device(%Device{})
+ def new_device(attrs \\ %{}) do
+ change_device(%Device{}, Map.merge(%{"name" => NameGenerator.generate()}, attrs))
end
- def endpoint(device) do
- if device.use_default_endpoint do
- Settings.default_device_endpoint() ||
- Application.fetch_env!(:fz_http, :wireguard_endpoint) ||
- ConnectivityChecks.endpoint()
- else
- device.endpoint
- end
- end
+ def allowed_ips(device), do: config(device, :allowed_ips)
+ def endpoint(device), do: config(device, :endpoint)
+ def dns(device), do: config(device, :dns)
+ def mtu(device), do: config(device, :mtu)
+ def persistent_keepalive(device), do: config(device, :persistent_keepalive)
- def allowed_ips(device) do
- if device.use_default_allowed_ips do
- Settings.default_device_allowed_ips() ||
- Application.fetch_env!(:fz_http, :wireguard_allowed_ips)
+ defp config(device, key) do
+ if Map.get(device, String.to_atom("use_site_#{key}")) do
+ Map.get(Sites.wireguard_defaults(), key)
else
- device.allowed_ips
- end
- end
-
- def dns(device) do
- if device.use_default_dns do
- Settings.default_device_dns() ||
- Application.fetch_env!(:fz_http, :wireguard_dns)
- else
- device.dns
- end
- end
-
- def mtu(device) do
- if device.use_default_mtu do
- Settings.default_device_mtu() ||
- Application.fetch_env!(:fz_http, :wireguard_mtu)
- else
- device.mtu
- end
- end
-
- def persistent_keepalive(device) do
- if device.use_default_persistent_keepalive do
- Settings.default_device_persistent_keepalive() ||
- Application.fetch_env!(:fz_http, :wireguard_persistent_keepalive)
- else
- device.persistent_keepalive
+ Map.get(device, key)
end
end
def defaults(changeset) do
~w(
- use_default_allowed_ips
- use_default_dns
- use_default_endpoint
- use_default_mtu
- use_default_persistent_keepalive
+ use_site_allowed_ips
+ use_site_dns
+ use_site_endpoint
+ use_site_mtu
+ use_site_persistent_keepalive
)a
|> Enum.map(fn field -> {field, Device.field(changeset, field)} end)
|> Map.new()
end
+ def as_encoded_config(device), do: Base.encode64(as_config(device))
+
def as_config(device) do
wireguard_port = Application.fetch_env!(:fz_vpn, :wireguard_port)
+ server_public_key = Application.fetch_env!(:fz_vpn, :wireguard_public_key)
"""
[Interface]
- PrivateKey = #{device.private_key}
+ PrivateKey = REPLACE_ME
Address = #{inet(device)}
#{mtu_config(device)}
#{dns_config(device)}
[Peer]
- PublicKey = #{device.server_public_key}
+ PublicKey = #{server_public_key}
#{allowed_ips_config(device)}
Endpoint = #{endpoint(device)}:#{wireguard_port}
#{persistent_keepalive_config(device)}
"""
end
- def create_config_token(device) do
- expires_at = DateTime.add(DateTime.utc_now(), @config_token_expires_in_sec, :second)
-
- config_token_attrs = %{
- config_token: FzCrypto.rand_token(6),
- config_token_expires_at: expires_at
- }
-
- update_device(device, config_token_attrs)
- end
-
defp mtu_config(device) do
m = mtu(device)
diff --git a/apps/fz_http/lib/fz_http/devices/device.ex b/apps/fz_http/lib/fz_http/devices/device.ex
index 75b7e1b34..5136f70c8 100644
--- a/apps/fz_http/lib/fz_http/devices/device.ex
+++ b/apps/fz_http/lib/fz_http/devices/device.ex
@@ -18,30 +18,27 @@ defmodule FzHttp.Devices.Device do
import FzHttp.Queries.INET
- alias FzHttp.Users.User
+ alias FzHttp.{Devices, Users.User}
schema "devices" do
field :uuid, Ecto.UUID, autogenerate: true
field :name, :string
field :public_key, :string
- field :use_default_allowed_ips, :boolean, read_after_writes: true, default: true
- field :use_default_dns, :boolean, read_after_writes: true, default: true
- field :use_default_endpoint, :boolean, read_after_writes: true, default: true
- field :use_default_mtu, :boolean, read_after_writes: true, default: true
- field :use_default_persistent_keepalive, :boolean, read_after_writes: true, default: true
+ field :use_site_allowed_ips, :boolean, read_after_writes: true, default: true
+ field :use_site_dns, :boolean, read_after_writes: true, default: true
+ field :use_site_endpoint, :boolean, read_after_writes: true, default: true
+ field :use_site_mtu, :boolean, read_after_writes: true, default: true
+ field :use_site_persistent_keepalive, :boolean, read_after_writes: true, default: true
field :endpoint, :string
field :mtu, :integer
field :persistent_keepalive, :integer
field :allowed_ips, :string
field :dns, :string
- field :private_key, FzHttp.Encrypted.Binary
- field :server_public_key, :string
field :remote_ip, EctoNetwork.INET
field :ipv4, EctoNetwork.INET, read_after_writes: true
field :ipv6, EctoNetwork.INET, read_after_writes: true
field :last_seen_at, :utc_datetime_usec
- field :config_token, :string
- field :config_token_expires_at, :utc_datetime_usec
+ field :key_regenerated_at, :utc_datetime_usec, read_after_writes: true
belongs_to :user, User
@@ -54,6 +51,7 @@ defmodule FzHttp.Devices.Device do
|> put_next_ip(:ipv4)
|> put_next_ip(:ipv6)
|> shared_changeset()
+ |> validate_max_devices()
end
def update_changeset(device, attrs) do
@@ -69,11 +67,11 @@ defmodule FzHttp.Devices.Device do
defp shared_cast(device, attrs) do
device
|> cast(attrs, [
- :use_default_allowed_ips,
- :use_default_dns,
- :use_default_endpoint,
- :use_default_mtu,
- :use_default_persistent_keepalive,
+ :use_site_allowed_ips,
+ :use_site_dns,
+ :use_site_endpoint,
+ :use_site_mtu,
+ :use_site_persistent_keepalive,
:allowed_ips,
:dns,
:endpoint,
@@ -82,13 +80,10 @@ defmodule FzHttp.Devices.Device do
:remote_ip,
:ipv4,
:ipv6,
- :server_public_key,
- :private_key,
:user_id,
:name,
:public_key,
- :config_token,
- :config_token_expires_at
+ :key_regenerated_at
])
end
@@ -97,18 +92,10 @@ defmodule FzHttp.Devices.Device do
|> validate_required([
:user_id,
:name,
- :public_key,
- :server_public_key,
- :private_key
+ :public_key
])
- |> validate_required_unless_default([
- :allowed_ips,
- :dns,
- :endpoint,
- :mtu,
- :persistent_keepalive
- ])
- |> validate_omitted_if_default([
+ |> validate_required_unless_site([:endpoint])
+ |> validate_omitted_if_site([
:allowed_ips,
:dns,
:endpoint,
@@ -136,30 +123,43 @@ defmodule FzHttp.Devices.Device do
|> validate_in_network(:ipv4)
|> validate_in_network(:ipv6)
|> unique_constraint(:public_key)
- |> unique_constraint(:private_key)
|> unique_constraint([:user_id, :name])
end
- defp validate_omitted_if_default(changeset, fields) when is_list(fields) do
- fields_to_validate =
- defaulted_fields(changeset, fields)
- |> Enum.map(fn field ->
- String.trim(Atom.to_string(field), "use_default_") |> String.to_atom()
- end)
+ defp validate_max_devices(changeset) do
+ user_id = changeset.changes.user_id || changeset.data.user_id
+ count = Devices.count(user_id)
+ max_devices = Application.fetch_env!(:fz_http, :max_devices_per_user)
- validate_omitted(changeset, fields_to_validate)
+ if count >= max_devices do
+ add_error(
+ changeset,
+ :base,
+ "Maximum device limit reached. Remove an existing device before creating a new one."
+ )
+ else
+ changeset
+ end
end
- defp validate_required_unless_default(changeset, fields) when is_list(fields) do
- fields_as_atoms = Enum.map(fields, fn field -> String.to_atom("use_default_#{field}") end)
- fields_to_validate = fields_as_atoms -- defaulted_fields(changeset, fields)
- validate_required(changeset, fields_to_validate)
+ defp validate_omitted_if_site(changeset, fields) when is_list(fields) do
+ validate_omitted(changeset, filter_site_fields(changeset, fields, use_site: true))
end
- defp defaulted_fields(changeset, fields) do
+ defp validate_required_unless_site(changeset, fields) when is_list(fields) do
+ validate_required(changeset, filter_site_fields(changeset, fields, use_site: false))
+ end
+
+ defp filter_site_fields(changeset, fields, use_site: use_site) when is_boolean(use_site) do
fields
- |> Enum.map(fn field -> String.to_atom("use_default_#{field}") end)
- |> Enum.filter(fn field -> get_field(changeset, field) end)
+ |> Enum.map(fn field -> String.to_atom("use_site_#{field}") end)
+ |> Enum.filter(fn site_field -> get_field(changeset, site_field) == use_site end)
+ |> Enum.map(fn field ->
+ field
+ |> Atom.to_string()
+ |> String.trim("use_site_")
+ |> String.to_atom()
+ end)
end
defp validate_ipv4_required(changeset) do
@@ -180,19 +180,19 @@ defmodule FzHttp.Devices.Device do
defp validate_in_network(%Ecto.Changeset{changes: %{ipv4: ip}} = changeset, :ipv4) do
net = Application.fetch_env!(:fz_http, :wireguard_ipv4_network)
- maybe_add_net_error(changeset, net, ip, :ipv4)
+ add_net_error_if_outside_bounds(changeset, net, ip, :ipv4)
end
defp validate_in_network(changeset, :ipv4), do: changeset
defp validate_in_network(%Ecto.Changeset{changes: %{ipv6: ip}} = changeset, :ipv6) do
net = Application.fetch_env!(:fz_http, :wireguard_ipv6_network)
- maybe_add_net_error(changeset, net, ip, :ipv6)
+ add_net_error_if_outside_bounds(changeset, net, ip, :ipv6)
end
defp validate_in_network(changeset, :ipv6), do: changeset
- def maybe_add_net_error(changeset, net, ip, ip_type) do
+ defp add_net_error_if_outside_bounds(changeset, net, ip, ip_type) do
%{address: address} = ip
cidr = CIDR.parse(net)
@@ -205,6 +205,7 @@ defmodule FzHttp.Devices.Device do
defp put_next_ip(changeset, ip_type) when ip_type in [:ipv4, :ipv6] do
case changeset do
+ # Don't put a new ip if the user is trying to assign one manually
%Ecto.Changeset{changes: %{^ip_type => _ip}} ->
changeset
diff --git a/apps/fz_http/lib/fz_http_web/events.ex b/apps/fz_http/lib/fz_http/events.ex
similarity index 81%
rename from apps/fz_http/lib/fz_http_web/events.ex
rename to apps/fz_http/lib/fz_http/events.ex
index 35600d2e1..6eecd7a2b 100644
--- a/apps/fz_http/lib/fz_http_web/events.ex
+++ b/apps/fz_http/lib/fz_http/events.ex
@@ -1,14 +1,10 @@
-defmodule FzHttpWeb.Events do
+defmodule FzHttp.Events do
@moduledoc """
Handles interfacing with other processes in the system.
"""
alias FzHttp.{Devices, Rules}
- def create_device do
- GenServer.call(vpn_pid(), :create_device)
- end
-
# set_config is used because devices need to be re-evaluated in case a
# device is added to a User that's not active.
def update_device(_device) do
@@ -16,11 +12,11 @@ defmodule FzHttpWeb.Events do
end
def delete_device(device_pubkey) when is_binary(device_pubkey) do
- GenServer.call(vpn_pid(), {:delete_device, device_pubkey})
+ GenServer.call(vpn_pid(), {:remove_peer, device_pubkey})
end
def delete_device(device) when is_struct(device) do
- GenServer.call(vpn_pid(), {:delete_device, device.public_key})
+ GenServer.call(vpn_pid(), {:remove_peer, device.public_key})
end
def add_rule(rule) do
diff --git a/apps/fz_http/lib/fz_http/macros.ex b/apps/fz_http/lib/fz_http/macros.ex
deleted file mode 100644
index 91c90da3a..000000000
--- a/apps/fz_http/lib/fz_http/macros.ex
+++ /dev/null
@@ -1,20 +0,0 @@
-defmodule FzHttp.Macros do
- @moduledoc """
- Metaprogramming macros
- """
-
- @doc """
- Defines getters for all Setting keys as functions on the Settings module.
- """
- defmacro def_settings(keys) do
- quote bind_quoted: [keys: keys] do
- Enum.each(keys, fn key ->
- fun_name = key |> String.replace(".", "_") |> String.to_atom() |> Macro.var(__MODULE__)
-
- def unquote(fun_name) do
- get_setting!(key: unquote(key)).value
- end
- end)
- end
- end
-end
diff --git a/apps/fz_http/lib/fz_http/sessions.ex b/apps/fz_http/lib/fz_http/sessions.ex
deleted file mode 100644
index d770683e1..000000000
--- a/apps/fz_http/lib/fz_http/sessions.ex
+++ /dev/null
@@ -1,51 +0,0 @@
-defmodule FzHttp.Sessions do
- @moduledoc """
- The Sessions context.
- """
-
- import Ecto.Query, warn: false
- alias FzHttp.Repo
-
- alias FzHttp.Users.Session
-
- @doc """
- Gets a single session.
-
- Raises `Ecto.NoResultsError` if the Session does not exist.
-
- ## Examples
-
- iex> get_session!(123)
- %Session{}
-
- iex> get_session!(456)
- ** (Ecto.NoResultsError)
-
- """
- def get_session!(email: email), do: Repo.get_by!(Session, email: email)
- def get_session!(id), do: Repo.get!(Session, id)
- def get_session(email: email), do: Repo.get_by(Session, email: email)
- def get_session(id), do: Repo.get(Session, id)
-
- def new_session do
- Session.changeset(%Session{}, %{})
- end
-
- @doc """
- Creates a session.
-
- ## Examples
-
- iex> create_session(%{field: value})
- {:ok, %Session{}}
-
- iex> create_session(%{field: bad_value})
- {:error, %Ecto.Changeset{}}
-
- """
- def create_session(%Session{} = session, %{} = attrs) do
- session
- |> Session.create_changeset(attrs)
- |> Repo.update()
- end
-end
diff --git a/apps/fz_http/lib/fz_http/settings.ex b/apps/fz_http/lib/fz_http/settings.ex
deleted file mode 100644
index 22472ae2b..000000000
--- a/apps/fz_http/lib/fz_http/settings.ex
+++ /dev/null
@@ -1,107 +0,0 @@
-defmodule FzHttp.Settings do
- @moduledoc """
- The Settings context.
- """
-
- import FzHttp.Macros
- import Ecto.Query, warn: false
- import FzCommon.FzInteger, only: [max_pg_integer: 0]
- alias FzHttp.Repo
-
- alias FzHttp.Settings.Setting
-
- def_settings(~w(
- default.device.allowed_ips
- default.device.dns
- default.device.endpoint
- default.device.mtu
- default.device.persistent_keepalive
- security.require_auth_for_vpn_frequency
- ))
-
- @doc """
- Returns the list of settings.
-
- ## Examples
-
- iex> list_settings()
- [%Setting{}, ...]
-
- """
- def list_settings do
- Repo.all(Setting)
- end
-
- @doc """
- Gets a single setting by its ID.
-
- Raises `Ecto.NoResultsError` if the Setting does not exist.
-
- ## Examples
-
- iex> get_setting!(123)
- %Setting{}
-
- iex> get_setting!(456)
- ** (Ecto.NoResultsError)
-
- """
- def get_setting!(key: key) do
- Repo.one!(from s in Setting, where: s.key == ^key)
- end
-
- def get_setting!(id), do: Repo.get!(Setting, id)
-
- @doc """
- Updates a setting.
-
- ## Examples
-
- iex> update_setting(setting, %{field: new_value})
- {:ok, %Setting{}}
-
- iex> update_setting(setting, %{field: bad_value})
- {:error, %Ecto.Changeset{}}
-
- """
- def update_setting(%Setting{} = setting, attrs) do
- setting
- |> Setting.changeset(attrs)
- |> Repo.update()
- end
-
- def update_setting(key, value) when is_binary(key) do
- get_setting!(key: key)
- |> update_setting(%{value: value})
- end
-
- @doc """
- Returns an `%Ecto.Changeset{}` for tracking setting changes.
-
- ## Examples
-
- iex> change_setting(setting)
- %Ecto.Changeset{data: %Setting{}}
-
- """
- def change_setting(%Setting{} = setting, attrs \\ %{}) do
- Setting.changeset(setting, attrs)
- end
-
- @doc """
- Returns a list of all the settings beginning with the specified key prefix.
- """
- def to_list(prefix \\ "") do
- starts_with = prefix <> "%"
- Repo.all(from s in Setting, where: ilike(s.key, ^starts_with))
- end
-
- def vpn_sessions_expire? do
- freq = vpn_duration()
- freq > 0 && freq < max_pg_integer()
- end
-
- def vpn_duration do
- String.to_integer(security_require_auth_for_vpn_frequency())
- end
-end
diff --git a/apps/fz_http/lib/fz_http/settings/setting.ex b/apps/fz_http/lib/fz_http/settings/setting.ex
deleted file mode 100644
index c67ffb646..000000000
--- a/apps/fz_http/lib/fz_http/settings/setting.ex
+++ /dev/null
@@ -1,104 +0,0 @@
-defmodule FzHttp.Settings.Setting do
- @moduledoc """
- Represents Firezone runtime configuration settings.
-
- Each record in the table has a unique key corresponding to a configuration setting.
-
- Settings values can be changed at application runtime on the fly.
- Settings cannot be created or destroyed by the running application.
-
- Settings are created / destroyed in migrations.
- """
-
- use Ecto.Schema
- import Ecto.Changeset
- import FzCommon.FzInteger, only: [max_pg_integer: 0]
-
- import FzHttp.SharedValidators,
- only: [
- validate_fqdn_or_ip: 2,
- validate_list_of_ips: 2,
- validate_list_of_ips_or_cidrs: 2,
- validate_no_duplicates: 2
- ]
-
- @mtu_range 576..1500
- @persistent_keepalive_range 0..120
-
- schema "settings" do
- field :key, :string
- field :value, :string
-
- timestamps(type: :utc_datetime_usec)
- end
-
- @doc false
- def changeset(setting, attrs) do
- setting
- |> cast(attrs, [:key, :value])
- |> validate_required([:key])
- |> validate_setting()
- end
-
- defp validate_setting(%{data: %{key: key}, changes: %{value: _value}} = changeset) do
- changeset
- |> validate_kv_pair(key)
- end
-
- defp validate_setting(changeset), do: changeset
-
- defp validate_kv_pair(changeset, "default.device.dns") do
- changeset
- |> validate_list_of_ips(:value)
- |> validate_no_duplicates(:value)
- end
-
- defp validate_kv_pair(changeset, "default.device.allowed_ips") do
- changeset
- |> validate_list_of_ips_or_cidrs(:value)
- |> validate_no_duplicates(:value)
- end
-
- defp validate_kv_pair(changeset, "default.device.endpoint") do
- changeset
- |> validate_fqdn_or_ip(:value)
- end
-
- defp validate_kv_pair(changeset, "default.device.mtu") do
- validate_range(changeset, @mtu_range)
- end
-
- defp validate_kv_pair(changeset, "default.device.persistent_keepalive") do
- validate_range(changeset, @persistent_keepalive_range)
- end
-
- defp validate_kv_pair(changeset, "security.require_auth_for_vpn_frequency") do
- validate_range(changeset, 0..max_pg_integer())
- end
-
- defp validate_kv_pair(changeset, unknown_key) do
- validate_change(changeset, :key, fn _current_field, _value ->
- [{:key, "is invalid: #{unknown_key} is not a valid setting"}]
- end)
- end
-
- defp validate_range(changeset, range) do
- validate_change(changeset, :value, fn _current_field, value ->
- case Integer.parse(value) do
- :error ->
- [{:value, "must be an integer"}]
-
- {val, _str} ->
- add_error_for_range(val, range)
- end
- end)
- end
-
- defp add_error_for_range(val, start..finish) do
- if val < start || val > finish do
- [{:value, "is invalid: must be between #{start} and #{finish}"}]
- else
- []
- end
- end
-end
diff --git a/apps/fz_http/lib/fz_http/sites.ex b/apps/fz_http/lib/fz_http/sites.ex
new file mode 100644
index 000000000..872e5880e
--- /dev/null
+++ b/apps/fz_http/lib/fz_http/sites.ex
@@ -0,0 +1,72 @@
+defmodule FzHttp.Sites do
+ @moduledoc """
+ The Sites context.
+ """
+
+ import Ecto.Query, warn: false
+ alias FzHttp.{ConnectivityChecks, Repo, Sites.Site}
+
+ @wg_settings [:allowed_ips, :dns, :endpoint, :persistent_keepalive, :mtu]
+
+ def get_site! do
+ get_site!(name: "default")
+ end
+
+ def get_site!(name: name) do
+ Repo.one!(
+ from s in Site,
+ where: s.name == ^name
+ )
+ end
+
+ def get_site!(id) do
+ Repo.get!(Site, id)
+ end
+
+ def change_site(%Site{} = site) do
+ Site.changeset(site, %{})
+ end
+
+ def update_site(%Site{} = site, attrs) do
+ site
+ |> Site.changeset(attrs)
+ |> Repo.update()
+ end
+
+ def vpn_sessions_expire? do
+ freq = vpn_duration()
+ freq > 0 && freq < Site.max_vpn_session_duration()
+ end
+
+ def vpn_duration do
+ get_site!().vpn_session_duration
+ end
+
+ def wireguard_defaults do
+ site = get_site!()
+
+ @wg_settings
+ |> Enum.map(fn s ->
+ site_val = Map.get(site, s)
+
+ if is_nil(site_val) do
+ {s, default(s)}
+ else
+ {s, site_val}
+ end
+ end)
+ |> Map.new()
+ end
+
+ defp default(:endpoint) do
+ app_env(:wireguard_endpoint) || ConnectivityChecks.endpoint()
+ end
+
+ defp default(key) do
+ app_env(String.to_atom("wireguard_#{key}"))
+ end
+
+ defp app_env(key) do
+ Application.fetch_env!(:fz_http, key)
+ end
+end
diff --git a/apps/fz_http/lib/fz_http/sites/site.ex b/apps/fz_http/lib/fz_http/sites/site.ex
new file mode 100644
index 000000000..dca925d9b
--- /dev/null
+++ b/apps/fz_http/lib/fz_http/sites/site.ex
@@ -0,0 +1,73 @@
+defmodule FzHttp.Sites.Site do
+ @moduledoc """
+ Represents a VPN / Firewall site and its config.
+ """
+
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ import FzHttp.SharedValidators,
+ only: [
+ validate_fqdn_or_ip: 2,
+ validate_list_of_ips: 2,
+ validate_list_of_ips_or_cidrs: 2,
+ validate_no_duplicates: 2
+ ]
+
+ @primary_key {:id, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+
+ @minute 60
+ @hour 60 * @minute
+ @day 24 * @hour
+ @min_mtu 576
+ @max_mtu 1500
+ @min_persistent_keepalive 0
+ @max_persistent_keepalive 1 * @hour
+ @min_vpn_session_duration 0
+ @max_vpn_session_duration 30 * @day
+
+ schema "sites" do
+ field :name, :string
+ field :dns, :string
+ field :allowed_ips, :string
+ field :endpoint, :string
+ field :persistent_keepalive, :integer
+ field :mtu, :integer
+ field :vpn_session_duration, :integer
+
+ timestamps(type: :utc_datetime_usec)
+ end
+
+ def changeset(site, attrs) do
+ site
+ |> cast(attrs, [
+ :name,
+ :dns,
+ :allowed_ips,
+ :endpoint,
+ :persistent_keepalive,
+ :mtu,
+ :vpn_session_duration
+ ])
+ |> validate_required(:name)
+ |> validate_list_of_ips(:dns)
+ |> validate_no_duplicates(:dns)
+ |> validate_list_of_ips_or_cidrs(:allowed_ips)
+ |> validate_no_duplicates(:allowed_ips)
+ |> validate_fqdn_or_ip(:endpoint)
+ |> validate_number(:mtu, greater_than_or_equal_to: @min_mtu, less_than_or_equal_to: @max_mtu)
+ |> validate_number(:persistent_keepalive,
+ greater_than_or_equal_to: @min_persistent_keepalive,
+ less_than_or_equal_to: @max_persistent_keepalive
+ )
+ |> validate_number(:vpn_session_duration,
+ greater_than_or_equal_to: @min_vpn_session_duration,
+ less_than_or_equal_to: @max_vpn_session_duration
+ )
+ end
+
+ def max_vpn_session_duration do
+ @max_vpn_session_duration
+ end
+end
diff --git a/apps/fz_http/lib/fz_http/users.ex b/apps/fz_http/lib/fz_http/users.ex
index 15268e6a0..3f1350e65 100644
--- a/apps/fz_http/lib/fz_http/users.ex
+++ b/apps/fz_http/lib/fz_http/users.ex
@@ -4,10 +4,9 @@ defmodule FzHttp.Users do
"""
import Ecto.Query, warn: false
- import FzCommon.FzInteger, only: [max_pg_integer: 0]
alias FzCommon.{FzCrypto, FzMap}
- alias FzHttp.{Devices.Device, Repo, Telemetry, Users.User}
+ alias FzHttp.{Devices.Device, Repo, Sites.Site, Telemetry, Users.User}
# one hour
@sign_in_token_validity_secs 3600
@@ -39,6 +38,10 @@ defmodule FzHttp.Users do
def get_user(id), do: Repo.get(User, id)
+ def get_by_email(email) do
+ Repo.get_by(User, email: email)
+ end
+
def create_admin_user(attrs) do
create_user_with_role(attrs, :admin)
end
@@ -47,20 +50,20 @@ defmodule FzHttp.Users do
create_user_with_role(attrs, :unprivileged)
end
- defp create_user_with_role(attrs, role) when is_map(attrs) do
+ def create_user_with_role(attrs, role) when is_map(attrs) do
attrs
|> Map.put(:role, role)
|> create_user()
end
- defp create_user_with_role(attrs, role) when is_list(attrs) do
+ def create_user_with_role(attrs, role) when is_list(attrs) do
attrs
|> Enum.into(%{})
|> Map.put(:role, role)
|> create_user()
end
- defp create_user(attrs) when is_map(attrs) do
+ def create_user(attrs) when is_map(attrs) do
attrs = FzMap.stringify_keys(attrs)
result =
@@ -94,7 +97,7 @@ defmodule FzHttp.Users do
Repo.delete(user)
end
- def change_user(%User{} = user) do
+ def change_user(%User{} = user \\ struct(User)) do
User.changeset(user, %{})
end
@@ -126,13 +129,14 @@ defmodule FzHttp.Users do
Repo.all(query)
end
- # XXX: Assume only one admin
- def admin do
- Repo.one(
- from u in User,
- where: u.role == :admin,
- limit: 1
- )
+ def update_last_signed_in(user, %{provider: provider} = _auth) do
+ method =
+ case provider do
+ :identity -> "email"
+ m -> to_string(m)
+ end
+
+ update_user(user, %{last_signed_in_at: DateTime.utc_now(), last_signed_in_method: method})
end
@doc """
@@ -144,7 +148,7 @@ defmodule FzHttp.Users do
end
def vpn_session_expired?(user, duration) do
- max = max_pg_integer()
+ max = Site.max_vpn_session_duration()
case duration do
0 ->
diff --git a/apps/fz_http/lib/fz_http/users/session.ex b/apps/fz_http/lib/fz_http/users/session.ex
deleted file mode 100644
index d2def18c3..000000000
--- a/apps/fz_http/lib/fz_http/users/session.ex
+++ /dev/null
@@ -1,49 +0,0 @@
-defmodule FzHttp.Users.Session do
- @moduledoc """
- Represents a Session
- """
-
- use Ecto.Schema
- import Ecto.Changeset
- alias FzHttp.{Users, Users.User}
-
- schema "users" do
- field :role, Ecto.Enum, values: [:unprivileged, :admin], default: :unprivileged
- field :email, :string
- field :password, :string, virtual: true
- field :last_signed_in_at, :utc_datetime_usec
- end
-
- def create_changeset(session, attrs \\ %{}) do
- session
- |> cast(attrs, [:email, :password, :last_signed_in_at])
- |> authenticate_user()
- |> set_last_signed_in_at()
- end
-
- def changeset(session, attrs) do
- session
- |> cast(attrs, [:email, :password, :last_signed_in_at])
- end
-
- defp set_last_signed_in_at(%Ecto.Changeset{valid?: true} = changeset) do
- last_signed_in_at = DateTime.utc_now()
- change(changeset, last_signed_in_at: last_signed_in_at)
- end
-
- defp set_last_signed_in_at(changeset), do: changeset
-
- defp authenticate_user(%Ecto.Changeset{valid?: true} = changeset) do
- email = changeset.data.email
- password = changeset.changes[:password]
- user = Users.get_user!(email: email)
-
- case User.authenticate_user(user, password) do
- {:ok, _} ->
- changeset
-
- {:error, error_msg} ->
- add_error(changeset, :password, "invalid: #{error_msg}")
- end
- end
-end
diff --git a/apps/fz_http/lib/fz_http/users/user.ex b/apps/fz_http/lib/fz_http/users/user.ex
index c930cc6c1..1debb5897 100644
--- a/apps/fz_http/lib/fz_http/users/user.ex
+++ b/apps/fz_http/lib/fz_http/users/user.ex
@@ -17,6 +17,7 @@ defmodule FzHttp.Users.User do
field :role, Ecto.Enum, values: [:unprivileged, :admin], default: :unprivileged
field :email, :string
field :last_signed_in_at, :utc_datetime_usec
+ field :last_signed_in_method, :string
field :password_hash, :string
field :sign_in_token, :string
field :sign_in_token_created_at, :utc_datetime_usec
@@ -43,13 +44,12 @@ defmodule FzHttp.Users.User do
:password,
:password_confirmation
])
- |> validate_required([:email, :password, :password_confirmation])
+ |> validate_required([:email])
|> validate_password_equality()
|> validate_length(:password, min: @min_password_length, max: @max_password_length)
|> validate_format(:email, ~r/@/)
|> unique_constraint(:email)
|> put_password_hash()
- |> validate_required([:password_hash])
end
# Sign in token
@@ -155,7 +155,7 @@ defmodule FzHttp.Users.User do
def changeset(user, attrs) do
user
- |> cast(attrs, [:email, :last_signed_in_at])
+ |> cast(attrs, [:email, :last_signed_in_method, :last_signed_in_at])
end
def authenticate_user(user, password_candidate) do
diff --git a/apps/fz_http/lib/fz_http/vpn_session_scheduler.ex b/apps/fz_http/lib/fz_http/vpn_session_scheduler.ex
index f86afb185..323292e67 100644
--- a/apps/fz_http/lib/fz_http/vpn_session_scheduler.ex
+++ b/apps/fz_http/lib/fz_http/vpn_session_scheduler.ex
@@ -4,7 +4,7 @@ defmodule FzHttp.VpnSessionScheduler do
"""
use GenServer
- alias FzHttpWeb.Events
+ alias FzHttp.Events
# 1 minute
@interval 60 * 1_000
diff --git a/apps/fz_http/lib/fz_http_web.ex b/apps/fz_http/lib/fz_http_web.ex
index 14585f768..0832dfe80 100644
--- a/apps/fz_http/lib/fz_http_web.ex
+++ b/apps/fz_http/lib/fz_http_web.ex
@@ -42,6 +42,7 @@ defmodule FzHttpWeb do
use Phoenix.HTML
import FzHttpWeb.ErrorHelpers
+ import FzHttpWeb.AuthorizationHelpers
import FzHttpWeb.Gettext
import Phoenix.LiveView.Helpers
alias FzHttpWeb.Router.Helpers, as: Routes
@@ -101,6 +102,9 @@ defmodule FzHttpWeb do
# Import basic rendering functionality (render, render_layout, etc)
import Phoenix.View
+ # Authorization Helpers
+ import FzHttpWeb.AuthorizationHelpers
+
import FzHttpWeb.ErrorHelpers
import FzHttpWeb.Gettext
alias FzHttpWeb.Router.Helpers, as: Routes
diff --git a/apps/fz_http/lib/fz_http_web/authentication.ex b/apps/fz_http/lib/fz_http_web/authentication.ex
new file mode 100644
index 000000000..2fd3246c8
--- /dev/null
+++ b/apps/fz_http/lib/fz_http_web/authentication.ex
@@ -0,0 +1,75 @@
+defmodule FzHttpWeb.Authentication do
+ @moduledoc """
+ Authentication helpers.
+ """
+ use Guardian, otp_app: :fz_http
+
+ alias FzHttp.Telemetry
+ alias FzHttp.Users
+ alias FzHttp.Users.User
+
+ @guardian_token_name "guardian_default_token"
+
+ def subject_for_token(resource, _claims) do
+ {:ok, to_string(resource.id)}
+ end
+
+ def resource_from_claims(%{"sub" => id}) do
+ case Users.get_user(id) do
+ nil -> {:error, :resource_not_found}
+ user -> {:ok, user}
+ end
+ end
+
+ @doc """
+ Authenticates a user against a password hash. Only makes sense
+ for local auth.
+ """
+ def authenticate(%User{} = user, password) when is_binary(password) do
+ if user.password_hash do
+ authenticate(
+ user,
+ password,
+ Argon2.verify_pass(password, user.password_hash)
+ )
+ else
+ {:error, :invalid_credentials}
+ end
+ end
+
+ def authenticate(_user, _password) do
+ authenticate(nil, nil, Argon2.no_user_verify())
+ end
+
+ defp authenticate(user, _password, true) do
+ {:ok, user}
+ end
+
+ defp authenticate(_user, _password, false) do
+ {:error, :invalid_credentials}
+ end
+
+ def sign_in(conn, user, auth) do
+ Telemetry.login(user)
+ Users.update_last_signed_in(user, auth)
+ __MODULE__.Plug.sign_in(conn, user)
+ end
+
+ def sign_out(conn) do
+ __MODULE__.Plug.sign_out(conn)
+ end
+
+ def get_current_user(%Plug.Conn{} = conn) do
+ __MODULE__.Plug.current_resource(conn)
+ end
+
+ def get_current_user(%{@guardian_token_name => token} = _session) do
+ case Guardian.resource_from_token(__MODULE__, token) do
+ {:ok, resource, _claims} ->
+ resource
+
+ {:error, _reason} ->
+ nil
+ end
+ end
+end
diff --git a/apps/fz_http/lib/fz_http_web/authentication/error_handler.ex b/apps/fz_http/lib/fz_http_web/authentication/error_handler.ex
new file mode 100644
index 000000000..7cff25bb2
--- /dev/null
+++ b/apps/fz_http/lib/fz_http_web/authentication/error_handler.ex
@@ -0,0 +1,33 @@
+defmodule FzHttpWeb.Authentication.ErrorHandler do
+ @moduledoc """
+ Error Handler module implementation for Guardian.
+ """
+
+ use FzHttpWeb, :controller
+ alias FzHttpWeb.Authentication
+ alias FzHttpWeb.Router.Helpers, as: Routes
+ import FzHttpWeb.ControllerHelpers, only: [root_path_for_role: 2]
+
+ @behaviour Guardian.Plug.ErrorHandler
+
+ @impl Guardian.Plug.ErrorHandler
+ def auth_error(conn, {:already_authenticated, _reason}, _opts) do
+ user = Authentication.get_current_user(conn)
+
+ conn
+ |> redirect(to: root_path_for_role(conn, user.role))
+ end
+
+ @impl Guardian.Plug.ErrorHandler
+ def auth_error(conn, {:unauthenticated, _reason}, _opts) do
+ conn
+ |> redirect(to: Routes.root_path(conn, :index))
+ end
+
+ @impl Guardian.Plug.ErrorHandler
+ def auth_error(conn, {type, _reason}, _opts) do
+ conn
+ |> put_resp_content_type("text/plain")
+ |> send_resp(401, to_string(type))
+ end
+end
diff --git a/apps/fz_http/lib/fz_http_web/authentication/pipeline.ex b/apps/fz_http/lib/fz_http_web/authentication/pipeline.ex
new file mode 100644
index 000000000..8dfa41485
--- /dev/null
+++ b/apps/fz_http/lib/fz_http_web/authentication/pipeline.ex
@@ -0,0 +1,15 @@
+defmodule FzHttpWeb.Authentication.Pipeline do
+ @moduledoc """
+ Plug implementation module for Guardian.
+ """
+
+ use Guardian.Plug.Pipeline,
+ otp_app: :fz_http,
+ error_handler: FzHttpWeb.Authentication.ErrorHandler,
+ module: FzHttpWeb.Authentication
+
+ @claims %{"typ" => "access"}
+
+ plug Guardian.Plug.VerifySession, claims: @claims, refresh_from_cookie: true
+ plug Guardian.Plug.LoadResource, allow_blank: true
+end
diff --git a/apps/fz_http/lib/fz_http_web/authorization_helpers.ex b/apps/fz_http/lib/fz_http_web/authorization_helpers.ex
new file mode 100644
index 000000000..7e6f69202
--- /dev/null
+++ b/apps/fz_http/lib/fz_http_web/authorization_helpers.ex
@@ -0,0 +1,34 @@
+defmodule FzHttpWeb.AuthorizationHelpers do
+ @moduledoc """
+ Authorization-related helpers
+ """
+
+ import Phoenix.LiveView
+ alias FzHttpWeb.Router.Helpers, as: Routes
+
+ def not_authorized(socket) do
+ socket
+ |> put_flash(:error, "Not authorized.")
+ |> redirect(to: Routes.root_path(socket, :index))
+ end
+
+ def has_role?(%Phoenix.LiveView.Socket{} = socket, role) do
+ socket.assigns.current_user && socket.assigns.current_user.role == role
+ end
+
+ def has_role?(%FzHttp.Users.User{} = user, role) do
+ user.role == role
+ end
+
+ def has_role?(_, _) do
+ false
+ end
+
+ def authorize_role(socket, role) do
+ if has_role?(socket, role) do
+ {:cont, socket}
+ else
+ {:halt, not_authorized(socket)}
+ end
+ end
+end
diff --git a/apps/fz_http/lib/fz_http_web/channels/notification_channel.ex b/apps/fz_http/lib/fz_http_web/channels/notification_channel.ex
index 98b6fb757..d45076258 100644
--- a/apps/fz_http/lib/fz_http_web/channels/notification_channel.ex
+++ b/apps/fz_http/lib/fz_http_web/channels/notification_channel.ex
@@ -39,6 +39,7 @@ defmodule FzHttpWeb.NotificationChannel do
email: user.email,
online_at: DateTime.utc_now(),
last_signed_in_at: user.last_signed_in_at,
+ last_signed_in_method: user.last_signed_in_method,
remote_ip: socket.assigns.remote_ip,
user_agent: socket.assigns.user_agent
}
diff --git a/apps/fz_http/lib/fz_http_web/controller_helpers.ex b/apps/fz_http/lib/fz_http_web/controller_helpers.ex
index d4a80eca7..877277556 100644
--- a/apps/fz_http/lib/fz_http_web/controller_helpers.ex
+++ b/apps/fz_http/lib/fz_http_web/controller_helpers.ex
@@ -3,94 +3,13 @@ defmodule FzHttpWeb.ControllerHelpers do
Useful helpers for controllers
"""
- import Plug.Conn,
- only: [
- get_session: 2,
- put_resp_content_type: 2,
- send_resp: 3,
- halt: 1
- ]
-
- import Phoenix.Controller,
- only: [
- put_flash: 3,
- redirect: 2
- ]
-
- alias FzHttp.Users
alias FzHttpWeb.Router.Helpers, as: Routes
- def redirect_unauthenticated(conn, _options) do
- case get_session(conn, :user_id) do
- nil ->
- conn
- |> redirect(to: Routes.session_path(conn, :new))
- |> halt()
-
- _ ->
- conn
- end
+ def root_path_for_role(conn, :admin) do
+ Routes.user_index_path(conn, :index)
end
- def authorize_authenticated(conn, _options) do
- user = Users.get_user!(get_session(conn, :user_id))
-
- case user.role do
- :unprivileged ->
- conn
- |> put_flash(:error, "Not authorized.")
- |> redirect(to: root_path_for_role(conn, user))
- |> halt()
-
- :admin ->
- conn
- end
- end
-
- def root_path_for_role(%Plug.Conn{} = conn) do
- user_id = get_session(conn, :user_id)
-
- if is_nil(user_id) do
- Routes.session_path(conn, :new)
- else
- user = Users.get_user!(user_id)
- root_path_for_role(conn, user)
- end
- end
-
- def root_path_for_role(socket) do
- user = Map.get(socket.assigns, :current_user)
-
- if is_nil(user) do
- Routes.session_path(socket, :new)
- else
- root_path_for_role(socket, user)
- end
- end
-
- def root_path_for_role(conn_or_sock, user) do
- case user.role do
- :unprivileged ->
- Routes.user_path(conn_or_sock, :show)
-
- :admin ->
- Routes.device_index_path(conn_or_sock, :index)
-
- _ ->
- Routes.session_path(conn_or_sock, :new)
- end
- end
-
- def require_authenticated(conn, _options) do
- case get_session(conn, :user_id) do
- nil ->
- conn
- |> put_resp_content_type("text/plain")
- |> send_resp(403, "Forbidden")
- |> halt()
-
- _ ->
- conn
- end
+ def root_path_for_role(conn, :unprivileged) do
+ Routes.device_unprivileged_index_path(conn, :index)
end
end
diff --git a/apps/fz_http/lib/fz_http_web/controllers/auth_controller.ex b/apps/fz_http/lib/fz_http_web/controllers/auth_controller.ex
new file mode 100644
index 000000000..d3927ad03
--- /dev/null
+++ b/apps/fz_http/lib/fz_http_web/controllers/auth_controller.ex
@@ -0,0 +1,51 @@
+defmodule FzHttpWeb.AuthController do
+ @moduledoc """
+ Implements the CRUD for a Session
+ """
+ use FzHttpWeb, :controller
+
+ alias FzHttpWeb.Authentication
+ alias FzHttpWeb.UserFromAuth
+ alias Ueberauth.Strategy.Helpers
+
+ plug Ueberauth
+
+ def request(conn, _params) do
+ conn
+ |> render("request.html", callback_url: Helpers.callback_url(conn))
+ end
+
+ def callback(%{assigns: %{ueberauth_failure: %{errors: errors}}} = conn, _params) do
+ msg =
+ errors
+ |> Enum.map_join(". ", fn error -> error.message end)
+
+ conn
+ |> put_flash(:error, msg)
+ |> redirect(to: Routes.root_path(conn, :index))
+ end
+
+ def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
+ case UserFromAuth.find_or_create(auth) do
+ {:ok, user} ->
+ conn
+ |> put_session(:live_socket_id, "users_socket:#{user.id}")
+ |> Authentication.sign_in(user, auth)
+ |> configure_session(renew: true)
+ |> redirect(to: root_path_for_role(conn, user.role))
+
+ {:error, reason} ->
+ conn
+ |> put_flash(:error, "Error signing in: #{reason}")
+ |> request(%{})
+ end
+ end
+
+ def delete(conn, _params) do
+ conn
+ |> put_flash(:info, "You are now signed out.")
+ |> Authentication.sign_out()
+ |> clear_session()
+ |> redirect(to: Routes.root_path(conn, :index))
+ end
+end
diff --git a/apps/fz_http/lib/fz_http_web/controllers/device_controller.ex b/apps/fz_http/lib/fz_http_web/controllers/device_controller.ex
deleted file mode 100644
index 4c9402978..000000000
--- a/apps/fz_http/lib/fz_http_web/controllers/device_controller.ex
+++ /dev/null
@@ -1,42 +0,0 @@
-defmodule FzHttpWeb.DeviceController do
- @moduledoc """
- Entrypoint for Device LiveView
- """
- use FzHttpWeb, :controller
-
- import FzCommon.FzString, only: [sanitize_filename: 1]
- alias FzHttp.Devices
-
- plug :redirect_unauthenticated when action not in [:config, :download_shared_config]
- plug :authorize_authenticated when action not in [:config, :download_shared_config]
-
- def download_config(conn, %{"id" => device_id}) do
- device = Devices.get_device!(device_id)
- render_download(conn, device)
- end
-
- def download_shared_config(conn, %{"config_token" => config_token}) do
- device = Devices.get_device!(config_token: config_token)
- render_download(conn, device)
- end
-
- def config(conn, %{"config_token" => config_token}) do
- device = Devices.get_device!(config_token: config_token)
-
- conn
- |> put_root_layout({FzHttpWeb.LayoutView, "device_config.html"})
- |> render("config.html", config: Devices.as_config(device), device: device)
- end
-
- defp render_download(conn, device) do
- filename = "#{sanitize_filename(FzHttpWeb.Endpoint.host())}.conf"
- content_type = "text/plain"
-
- conn
- |> send_download(
- {:binary, Devices.as_config(device)},
- filename: filename,
- content_type: content_type
- )
- end
-end
diff --git a/apps/fz_http/lib/fz_http_web/controllers/root_controller.ex b/apps/fz_http/lib/fz_http_web/controllers/root_controller.ex
index d81a8b251..df349191b 100644
--- a/apps/fz_http/lib/fz_http_web/controllers/root_controller.ex
+++ b/apps/fz_http/lib/fz_http_web/controllers/root_controller.ex
@@ -1,13 +1,20 @@
defmodule FzHttpWeb.RootController do
@moduledoc """
- Handles redirecting from /
+ Firezone landing page -- show auth methods.
"""
use FzHttpWeb, :controller
- plug :redirect_unauthenticated
-
def index(conn, _params) do
conn
- |> redirect(to: root_path_for_role(conn))
+ |> render(
+ "auth.html",
+ okta_enabled: conf(:okta_auth_enabled),
+ google_enabled: conf(:google_auth_enabled),
+ local_enabled: conf(:local_auth_enabled)
+ )
+ end
+
+ defp conf(key) do
+ Application.fetch_env!(:fz_http, key)
end
end
diff --git a/apps/fz_http/lib/fz_http_web/controllers/session_controller.ex b/apps/fz_http/lib/fz_http_web/controllers/session_controller.ex
deleted file mode 100644
index becd52c36..000000000
--- a/apps/fz_http/lib/fz_http_web/controllers/session_controller.ex
+++ /dev/null
@@ -1,83 +0,0 @@
-defmodule FzHttpWeb.SessionController do
- @moduledoc """
- Implements the CRUD for a Session
- """
-
- alias FzHttp.{Sessions, Telemetry, Users}
- use FzHttpWeb, :controller
-
- plug :put_root_layout, "auth.html"
-
- # GET /session/new
- def new(conn, _params) do
- if redirect_authenticated?(conn) do
- conn
- |> redirect(to: root_path_for_role(conn))
- |> halt()
- else
- changeset = Sessions.new_session()
- render(conn, "new.html", changeset: changeset)
- end
- end
-
- # POST /session
- def create(conn, %{"session" => %{"email" => email, "password" => password}}) do
- case Sessions.get_session(email: email) do
- nil ->
- conn
- |> put_flash(:error, "Email not found.")
- |> assign(:changeset, Sessions.new_session())
- |> redirect(to: Routes.session_path(conn, :new))
-
- record ->
- case Sessions.create_session(record, %{email: email, password: password}) do
- {:ok, session} ->
- conn
- |> clear_session()
- |> assign(:current_session, session)
- |> put_session(:user_id, session.id)
- |> put_session(:live_socket_id, "users_socket:#{session.id}")
- |> redirect(to: Routes.root_path(conn, :index))
-
- {:error, _changeset} ->
- conn
- |> put_flash(:error, "Error signing in. Ensure email and password are correct.")
- |> assign(:changeset, Sessions.new_session())
- |> redirect(to: Routes.session_path(conn, :new))
- end
- end
- end
-
- # GET /sign_in/:token
- def create(conn, %{"token" => token}) do
- case Users.consume_sign_in_token(token) do
- {:ok, user} ->
- Telemetry.login(user)
-
- conn
- |> clear_session()
- |> put_session(:user_id, user.id)
- |> put_session(:live_socket_id, "users_socket:#{user.id}")
- |> redirect(to: Routes.device_index_path(conn, :index))
-
- {:error, error_msg} ->
- conn
- |> put_flash(:error, error_msg)
- |> redirect(to: Routes.session_path(conn, :new))
- end
- end
-
- # DELETE /sign_out
- def delete(conn, _params) do
- # XXX: Disconnect all WebSockets.
- conn
- |> clear_session()
- |> put_flash(:info, "Signed out successfully.")
- |> redirect(to: Routes.session_path(conn, :new))
- end
-
- defp redirect_authenticated?(conn) do
- user_id = get_session(conn, :user_id)
- Users.exists?(user_id)
- end
-end
diff --git a/apps/fz_http/lib/fz_http_web/controllers/user_controller.ex b/apps/fz_http/lib/fz_http_web/controllers/user_controller.ex
index 367122090..f689c13c6 100644
--- a/apps/fz_http/lib/fz_http_web/controllers/user_controller.ex
+++ b/apps/fz_http/lib/fz_http_web/controllers/user_controller.ex
@@ -4,29 +4,11 @@ defmodule FzHttpWeb.UserController do
"""
alias FzHttp.Users
+ alias FzHttpWeb.Authentication
use FzHttpWeb, :controller
- plug :require_authenticated
- plug :redirect_unauthenticated when action in [:index]
-
- def index(conn, _params) do
- conn
- |> redirect(to: Routes.user_index_path(conn, :index))
- end
-
- def show(conn, _params) do
- user_id = get_session(conn, :user_id)
- user = Users.get_user!(user_id)
-
- conn
- |> put_root_layout({FzHttpWeb.LayoutView, "auth.html"})
- |> put_layout({FzHttpWeb.LayoutView, "app.html"})
- |> render("show.html", user: user)
- end
-
def delete(conn, _params) do
- user_id = get_session(conn, :user_id)
- user = Users.get_user!(user_id)
+ user = Authentication.get_current_user(conn)
case Users.delete_user(user) do
{:ok, _user} ->
@@ -35,13 +17,13 @@ defmodule FzHttpWeb.UserController do
conn
|> clear_session()
|> put_flash(:info, "Account deleted successfully.")
- |> redirect(to: Routes.session_path(conn, :new))
+ |> redirect(to: Routes.root_path(conn, :index))
{:error, msg} ->
conn
|> clear_session()
|> put_flash(:error, "Error deleting account: #{msg}")
- |> redirect(to: Routes.session_path(conn, :new))
+ |> redirect(to: Routes.root_path(conn, :index))
end
end
end
diff --git a/apps/fz_http/lib/fz_http_web/endpoint.ex b/apps/fz_http/lib/fz_http_web/endpoint.ex
index fb11ce9f2..1188b90ff 100644
--- a/apps/fz_http/lib/fz_http_web/endpoint.ex
+++ b/apps/fz_http/lib/fz_http_web/endpoint.ex
@@ -8,7 +8,7 @@ defmodule FzHttpWeb.Endpoint do
socket "/socket", FzHttpWeb.UserSocket,
websocket: [
- connect_info: [:peer_data, :x_headers],
+ connect_info: [:peer_data, :x_headers, :uri],
# XXX: channel token should prevent CSWH but double check
check_origin: false
],
diff --git a/apps/fz_http/lib/fz_http_web/live/connectivity_check_live/index_live.ex b/apps/fz_http/lib/fz_http_web/live/connectivity_check_live/index_live.ex
index 83af77abe..95e44cd84 100644
--- a/apps/fz_http/lib/fz_http_web/live/connectivity_check_live/index_live.ex
+++ b/apps/fz_http/lib/fz_http_web/live/connectivity_check_live/index_live.ex
@@ -7,21 +7,12 @@ defmodule FzHttpWeb.ConnectivityCheckLive.Index do
alias FzHttp.ConnectivityChecks
@impl Phoenix.LiveView
- def mount(params, session, socket) do
+ def mount(_params, _session, socket) do
+ connectivity_checks = ConnectivityChecks.list_connectivity_checks(limit: 20)
+
{:ok,
socket
- |> assign_defaults(params, session, &load_data/2)
+ |> assign(:connectivity_checks, connectivity_checks)
|> assign(:page_title, "WAN Connectivity Checks")}
end
-
- defp load_data(_params, socket) do
- user = socket.assigns.current_user
-
- if user.role == :admin do
- socket
- |> assign(:connectivity_checks, ConnectivityChecks.list_connectivity_checks(limit: 20))
- else
- not_authorized(socket)
- end
- end
end
diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/admin/index.html.heex b/apps/fz_http/lib/fz_http_web/live/device_live/admin/index.html.heex
new file mode 100644
index 000000000..d9d8c44a1
--- /dev/null
+++ b/apps/fz_http/lib/fz_http_web/live/device_live/admin/index.html.heex
@@ -0,0 +1,14 @@
+<%= render FzHttpWeb.SharedView, "heading.html", page_title: @page_title %>
+
+
+ <%= render FzHttpWeb.SharedView, "flash.html", assigns %>
+
+
- Interval for WireGuard
-
- persistent keepalive. A value of 0 disables this. Leave this disabled
- unless you're experiencing NAT or firewall traversal problems.
-
+ NOTE: This configuration WILL NOT
+ be viewable again. Please ensure you've downloaded the
+ configuration file or copied it somewhere safe
+ before closing this window.
+
+ Interval for WireGuard
+
+ persistent keepalive. A value of 0 disables this. Leave this disabled
+ unless you're experiencing NAT or firewall traversal problems.
+
+ Configures the default AllowedIPs setting for devices.
+ 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.
+
+ Interval in seconds to send persistent keepalive packets. Most users won't need to change
+ this. Set to 0 or leave blank to disable. Leave this blank if you're unsure what this means.
+