* Found endpoint empty bug

* Fix use_site_ bugs

* Generate private keys client-side instead of on the Firezone server (#451)

* Rename events; add crypto lib

* seemingly working keygen

* Checkpoint

* Remove private key from devices; make tests pass

* Refactor auth to use simplified new router helper

* Fix js bundle

* Refactor event listeners into their own file

* Refactor settings

* Fix JS

* Working live views in unprivileged sections

* Rough draft working

* Checkpoint before fixing tests

* Tests passing

* Max devices per user configuration option (#471)

* Max tunnels per user configuration option

* Clean up remaining tunnel references

* Replace local auth system with Ueberauth / Guardian (#475)

* Checkpoint working authentication

* Working admin and unprivileged auth using Guardian

* Remove Sessions cruft

* More cleanup

* load new secrets

* Remove firezone tmp dirs

* Okta and Google Oauth (#485)

* working oauth!

* Remove keycloak; working google

* Ensure nil to_s

* Passing tests

* Add compile-time prod config

* Fix live_view typo

* Revert key_ttl to vpn_session_duration

* print logs after first configure

* Use get_env/1 for fetching optional config vars

* Disable telemetry from config

* miss the to_s

* Fix sign in page

* add tunnel admin guide

* auth path

* Fix tests

* Device editing no more (#491)
This commit is contained in:
Jamil
2022-03-04 09:32:13 -08:00
committed by GitHub
parent da7dbdf90a
commit a91c2db887
158 changed files with 3651 additions and 3437 deletions

View File

@@ -18,29 +18,40 @@ fi
# Fixes setcap not found on centos 7
PATH=/usr/sbin/:$PATH
sudo -E firezone-ctl reconfigure
sudo -E bash -c "echo \"default['firezone']['connectivity_checks']['enabled'] = false\" >> /etc/firezone/firezone.rb"
sudo -E firezone-ctl reconfigure
# 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
sudo -E firezone-ctl create-or-reset-admin
# Disable telemetry
search="default\['firezone']\['telemetry']\['enabled'] = true"
search="default['firezone']['telemetry']['enabled'] = false"
sudo -E sed -i "s/$search/$replace/" $conf
# XXX: Add more commands here to test
# Bootstrap config
sudo -E firezone-ctl reconfigure
# Wait for app to fully boot
sleep 10
sleep 5
# Helpful for debugging
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
# 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 '<button class="button" type="submit">Sign In</button>'
echo $page | grep '<a class="button" href="/auth/identity">Sign in with email</a>'
echo "Testing telemetry_id survives reconfigures"
tid1=`sudo cat /var/opt/firezone/cache/telemetry_id`

View File

@@ -195,6 +195,7 @@ jobs:
if: always()
run: |
sudo scripts/uninstall.sh
sudo rm -rf /tmp/firezone*
rm -rf omnibus/pkg/*
publish:

View File

@@ -0,0 +1,19 @@
// We need to import the CSS so that webpack will load it.
// The MiniCssExtractPlugin is used to separate it out into
// its own CSS file.
import css from "../css/app.scss"
/* Application fonts */
import "@fontsource/fira-sans"
import "@fontsource/open-sans"
import "@fontsource/fira-mono"
// webpack automatically bundles all modules in your
// entry points. Those entry points can be configured
// in "webpack.config.js".
//
// Import dependencies
//
import "phoenix_html"
import "./live_view.js"
import "./event_listeners.js"

View File

@@ -0,0 +1,14 @@
import { box } from "tweetnacl/nacl-fast"
import { encodeBase64 } from "tweetnacl-util"
let fzCrypto = {
generateKeyPair () {
let kp = box.keyPair()
return {
privateKey: encodeBase64(kp.secretKey),
publicKey: encodeBase64(kp.publicKey)
}
}
}
export { fzCrypto }

View File

@@ -1,14 +0,0 @@
import css from "../css/app.scss"
/* Application fonts */
import "@fontsource/fira-sans"
import "@fontsource/open-sans"
import "@fontsource/fira-mono"
import "phoenix_html"
import {renderQrCode} from "./qrcode.js"
window.addEventListener('DOMContentLoaded', () => {
renderQrCode()
})

View File

@@ -1,15 +1,4 @@
// This is a barebones JS file to use for auth screens. If it gets complicated
// consider just using the full app bundle.
import css from "../css/app.scss"
/* Application fonts */
import "@fontsource/fira-sans"
import "@fontsource/open-sans"
import "@fontsource/fira-mono"
import "phoenix_html"
import { FormatTimestamp } from './util.js'
import {FormatTimestamp} from "./util.js"
// Notification dismiss
document.addEventListener('DOMContentLoaded', () => {

View File

@@ -1,6 +1,7 @@
import hljs from "highlight.js"
import {FormatTimestamp,PasswordStrength} from "./util.js"
import {renderQrCode} from "./qrcode.js"
import {renderConfig} from "./wg_conf.js"
import {fzCrypto} from "./crypto.js"
const highlightCode = function () {
hljs.highlightAll()
@@ -51,6 +52,14 @@ const passwordStrength = function () {
})
}
const generateKeyPair = function () {
let kp = fzCrypto.generateKeyPair()
this.el.value = kp.publicKey
// XXX: Verify
sessionStorage.setItem(kp.publicKey, kp.privateKey)
}
const clipboardCopy = function () {
let button = this.el
let data = button.dataset.clipboard
@@ -69,10 +78,6 @@ Hooks.HighlightCode = {
mounted: highlightCode,
updated: highlightCode
}
Hooks.QrCode = {
mounted: renderQrCode,
updated: renderQrCode
}
Hooks.FormatTimestamp = {
mounted: formatTimestamp,
updated: formatTimestamp
@@ -81,5 +86,12 @@ Hooks.PasswordStrength = {
mounted: passwordStrength,
updated: passwordStrength
}
Hooks.RenderConfig = {
mounted: renderConfig,
updated: renderConfig
}
Hooks.GenerateKeyPair = {
mounted: generateKeyPair
}
export default Hooks

View File

@@ -1,23 +1,7 @@
// We need to import the CSS so that webpack will load it.
// The MiniCssExtractPlugin is used to separate it out into
// its own CSS file.
import css from "../css/app.scss"
/* Application fonts */
import "@fontsource/fira-sans"
import "@fontsource/open-sans"
import "@fontsource/fira-mono"
// webpack automatically bundles all modules in your
// entry points. Those entry points can be configured
// in "webpack.config.js".
//
// Import dependencies
//
import "phoenix_html"
// Encapsulates LiveView initialization
import Hooks from "./hooks.js"
import {Socket, Presence} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import Hooks from "./hooks.js"
import {FormatTimestamp} from "./util.js"
// User Socket
@@ -61,12 +45,14 @@ const liveSocket = new LiveSocket(
const toggleConnectStatus = function (info) {
let success = document.getElementById("web-ui-connect-success")
let error = document.getElementById("web-ui-connect-error")
if (userSocket.isConnected()) {
success.classList.remove("is-hidden")
error.classList.add("is-hidden")
} else {
success.classList.add("is-hidden")
error.classList.remove("is-hidden")
if (success && error) {
if (userSocket.isConnected()) {
success.classList.remove("is-hidden")
error.classList.add("is-hidden")
} else {
success.classList.add("is-hidden")
error.classList.remove("is-hidden")
}
}
}
@@ -115,14 +101,3 @@ notificationChannel.join()
// >> liveSocket.enableLatencySim(1000)
window.liveSocket = liveSocket
// Notification dismiss
document.addEventListener('DOMContentLoaded', () => {
(document.querySelectorAll('.notification .delete') || []).forEach(($delete) => {
const $notification = $delete.parentNode
$delete.addEventListener('click', () => {
$notification.parentNode.removeChild($notification)
})
})
})

View File

@@ -1,20 +0,0 @@
const QRCode = require('qrcode')
const renderQrCode = function () {
let canvas = document.getElementById('qr-canvas')
let conf = document.getElementById('wg-conf')
if (canvas && conf) {
QRCode.toCanvas(canvas, conf.innerHTML, {
errorCorrectionLevel: 'H',
margin: 0,
width: 200,
height: 200
}, function (error) {
if (error) alert('QRCode Encode Error: ' + error)
})
}
}
export { renderQrCode }

View File

@@ -0,0 +1,11 @@
// This is a barebones JS file to use for auth screens.
import css from "../css/app.scss"
/* Application fonts */
import "@fontsource/fira-sans"
import "@fontsource/open-sans"
import "@fontsource/fira-mono"
import "phoenix_html"
import "./event_listeners.js"
import { FormatTimestamp } from './util.js'

View File

@@ -0,0 +1,11 @@
// JS bundle for user layout
import css from "../css/app.scss"
/* Application fonts */
import "@fontsource/fira-sans"
import "@fontsource/open-sans"
import "@fontsource/fira-mono"
import "phoenix_html"
import "./live_view.js"
import "./event_listeners.js"

View File

@@ -0,0 +1,68 @@
const QRCode = require('qrcode')
const alertPrivateKeyError = function () {
}
// 1. Load generated keypair from previous step
// 2. Replace config PrivateKey sentinel with PrivateKey
// 3. Set code el innerHTML to new config
// 4. render QR code
// 5. render download button
const renderConfig = function () {
const publicKey = this.el.dataset.publicKey
if (publicKey) {
const privateKey = sessionStorage.getItem(publicKey)
// XXX: Clear all private keys
sessionStorage.removeItem(publicKey)
const placeholder = document.getElementById("generating-config")
if (privateKey) {
const templateConfig = atob(this.el.dataset.config)
const config = templateConfig.replace("REPLACE_ME", privateKey)
renderDownloadButton(config)
renderQR(config)
renderTunnel(config)
placeholder.classList.add("is-hidden")
} else {
placeholder.innerHTML =
`<p>
Error generating configuration. Could not load private key from
sessionStorage. Close window and try again. If the issue persists,
please contact support@firez.one.
</p>`
}
}
}
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 }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ defmodule FzHttp.VpnSessionScheduler do
"""
use GenServer
alias FzHttpWeb.Events
alias FzHttp.Events
# 1 minute
@interval 60 * 1_000

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
<%= render FzHttpWeb.SharedView, "heading.html", page_title: @page_title %>
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<div class="block">
<%= render FzHttpWeb.SharedView, "devices_table.html", devices: @devices, socket: @socket %>
</div>
<p>
Devices can be added when viewing a User.
<%= link("Go to users ->", to: Routes.user_index_path(@socket, :index)) %>
</p>
</section>

View File

@@ -0,0 +1,23 @@
defmodule FzHttpWeb.DeviceLive.Admin.Index do
@moduledoc """
Handles Device LiveViews.
"""
use FzHttpWeb, :live_view
alias FzHttp.Devices
@impl Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:devices, Devices.list_devices())
|> assign(:page_title, "All Devices")}
end
@doc """
Needed because this view will receive handle_params when modal is closed.
"""
@impl Phoenix.LiveView
def handle_params(_params, _url, socket) do
{:noreply, socket}
end
end

View File

@@ -0,0 +1,2 @@
<%= render FzHttpWeb.SharedView, "heading.html", page_title: "Devices |> #{@page_title}" %>
<%= render FzHttpWeb.SharedView, "show_device.html", assigns %>

View File

@@ -0,0 +1,63 @@
defmodule FzHttpWeb.DeviceLive.Admin.Show do
@moduledoc """
Shows a device for an admin user.
"""
use FzHttpWeb, :live_view
alias FzHttp.{Devices, Users}
@impl Phoenix.LiveView
def mount(%{"id" => device_id} = _params, _session, socket) do
device = Devices.get_device!(device_id)
if device.user_id == socket.assigns.current_user.id || has_role?(socket, :admin) do
{:ok,
socket
|> assign(assigns(device))}
else
{:ok, not_authorized(socket)}
end
end
@doc """
Needed because this view will receive handle_params when modal is closed.
"""
@impl Phoenix.LiveView
def handle_params(_params, _url, socket) do
{:noreply, socket}
end
@impl Phoenix.LiveView
def handle_event("delete_device", _params, socket) do
device = socket.assigns.device
case Devices.delete_device(device) do
{:ok, _deleted_device} ->
{:ok, _deleted_pubkey} = @events_module.delete_device(device.public_key)
{:noreply,
socket
|> redirect(to: Routes.device_admin_index_path(socket, :index))}
# Not likely to ever happen
# {:error, msg} ->
# {:noreply,
# socket
# |> put_flash(:error, "Error deleting device: #{msg}")}
end
end
defp assigns(device) do
[
device: device,
user: Users.get_user!(device.user_id),
page_title: device.name,
allowed_ips: Devices.allowed_ips(device),
dns: Devices.dns(device),
endpoint: Devices.endpoint(device),
port: Application.fetch_env!(:fz_vpn, :wireguard_port),
mtu: Devices.mtu(device),
persistent_keepalive: Devices.persistent_keepalive(device),
config: Devices.as_config(device)
]
end
end

View File

@@ -1,33 +0,0 @@
defmodule FzHttpWeb.DeviceLive.CreateFormComponent do
@moduledoc """
Handles create device form.
"""
use FzHttpWeb, :live_component
alias FzHttp.{Devices, Users}
@impl Phoenix.LiveComponent
def update(assigns, socket) do
{:ok,
socket
|> assign(:options_for_select, Users.as_options_for_select())
|> assign(assigns)}
end
@impl Phoenix.LiveComponent
def handle_event("save", %{"device" => %{"user_id" => user_id}}, socket) do
case Devices.auto_create_device(%{user_id: user_id}) do
{:ok, device} ->
@events_module.update_device(device)
{:noreply,
socket
|> push_redirect(to: Routes.device_show_path(socket, :show, device))}
{:error, changeset} ->
{:noreply,
socket
|> assign(:changeset, changeset)}
end
end
end

View File

@@ -1,27 +0,0 @@
<div>
<.form let={f} for={@changeset} id="add-device" phx-target={@myself} phx-submit="save">
<div class="field">
<%= label f, :user_id, "User", class: "label" %>
<div class="select">
<%= select f, :user_id, @options_for_select, class: "input" %>
</div>
<p class="help is-danger">
<%= error_tag f, :user_id %>
</p>
<p class="help">
Select the user who this device should belong to.
</p>
</div>
<div class="field">
<div class="control">
<div class="level">
<div class="level-left"></div>
<div class="level-right">
<%= submit "Save", phx_disable_with: "Saving...", class: "button is-primary" %>
</div>
</div>
</div>
</div>
</.form>
</div>

View File

@@ -1,57 +0,0 @@
defmodule FzHttpWeb.DeviceLive.FormComponent do
@moduledoc """
Handles device form.
"""
use FzHttpWeb, :live_component
alias FzHttp.{ConnectivityChecks, Devices, Settings}
def update(assigns, socket) do
device = assigns.device
changeset = Devices.change_device(device)
default_device_endpoint = Settings.default_device_endpoint() || ConnectivityChecks.endpoint()
default_device_mtu =
Settings.default_device_mtu() || Application.fetch_env!(:fz_http, :wireguard_mtu)
{:ok,
socket
|> assign(assigns)
|> assign(Devices.defaults(changeset))
|> assign(:default_device_allowed_ips, Settings.default_device_allowed_ips())
|> assign(:default_device_dns, Settings.default_device_dns())
|> assign(:default_device_endpoint, default_device_endpoint)
|> assign(:default_device_mtu, default_device_mtu)
|> assign(
:default_device_persistent_keepalive,
Settings.default_device_persistent_keepalive()
)
|> assign(:changeset, changeset)}
end
def handle_event("change", %{"device" => device_params}, socket) do
changeset = Devices.change_device(socket.assigns.device, device_params)
{:noreply,
socket
|> assign(:changeset, changeset)
|> assign(Devices.defaults(changeset))}
end
def handle_event("save", %{"device" => device_params}, socket) do
device = socket.assigns.device
case Devices.update_device(device, device_params) do
{:ok, device} ->
@events_module.update_device(device)
{:noreply,
socket
|> put_flash(:info, "Device updated successfully.")
|> push_redirect(to: socket.assigns.return_to)}
{:error, changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
end

View File

@@ -1,193 +0,0 @@
<div>
<.form let={f} for={@changeset} id="edit-device" phx-change="change" phx-target={@myself} phx-submit="save">
<div class="field">
<%= label f, :name, class: "label" %>
<div class="control">
<%= text_input f, :name, class: "input #{input_error_class(f, :name)}" %>
</div>
<p class="help is-danger">
<%= error_tag f, :name %>
</p>
</div>
<div class="field">
<%= label f, :use_default_allowed_ips, "Use Default Allowed IPs", class: "label" %>
<div class="control">
<label class="radio">
<%= radio_button f, :use_default_allowed_ips, true %>
Yes
</label>
<label class="radio">
<%= radio_button f, :use_default_allowed_ips, false %>
No
</label>
</div>
<p class="help">
Default: <%= @default_device_allowed_ips %>
</p>
</div>
<div class="field">
<%= label f, :allowed_ips, "Allowed IPs", class: "label" %>
<div class="control">
<%= text_input f, :allowed_ips, class: "input #{input_error_class(f, :allowed_ips)}",
disabled: @use_default_allowed_ips %>
</div>
<p class="help is-danger">
<%= error_tag f, :allowed_ips %>
</p>
</div>
<div class="field">
<%= label f, :use_default_dns, "Use Default DNS Servers", class: "label" %>
<div class="control">
<label class="radio">
<%= radio_button f, :use_default_dns, true %>
Yes
</label>
<label class="radio">
<%= radio_button f, :use_default_dns, false %>
No
</label>
</div>
<p class="help">
Default: <%= @default_device_dns %>
</p>
</div>
<div class="field">
<%= label f, :dns, "DNS Servers", class: "label" %>
<div class="control">
<%= text_input f, :dns, class: "input #{input_error_class(f, :dns)}",
disabled: @use_default_dns %>
</div>
<p class="help is-danger">
<%= error_tag f, :dns %>
</p>
</div>
<div class="field">
<%= label f, :use_default_endpoint, "Use Default Endpoint", class: "label" %>
<div class="control">
<label class="radio">
<%= radio_button f, :use_default_endpoint, true %>
Yes
</label>
<label class="radio">
<%= radio_button f, :use_default_endpoint, false %>
No
</label>
</div>
<p class="help">
Default: <%= @default_device_endpoint %>
</p>
</div>
<div class="field">
<%= label f, :endpoint, "Server Endpoint", class: "label" %>
<p>The IP of the server this device should connect to.</p>
<div class="control">
<%= text_input f, :endpoint, class: "input #{input_error_class(f, :endpoint)}",
disabled: @use_default_endpoint %>
</div>
<p class="help is-danger">
<%= error_tag f, :endpoint %>
</p>
</div>
<div class="field">
<%= label f, :use_default_mtu, "Use Default MTU", class: "label" %>
<div class="control">
<label class="radio">
<%= radio_button f, :use_default_mtu, true %>
Yes
</label>
<label class="radio">
<%= radio_button f, :use_default_mtu, false %>
No
</label>
</div>
<p class="help">
Default: <%= @default_device_mtu %>
</p>
</div>
<div class="field">
<%= label f, :mtu, "Interface MTU", class: "label" %>
<p>The WireGuard interface MTU for this Device.</p>
<div class="contro">
<%= text_input f, :mtu, class: "input #{input_error_class(f, :mtu)}", disabled: @use_default_mtu %>
</div>
<p class="help is-danger">
<%= error_tag f, :mtu %>
</p>
</div>
<div class="field">
<%= label f, :use_default_persistent_keepalive, "Use Default Persistent Keepalive", class: "label" %>
<div class="control">
<label class="radio">
<%= radio_button f, :use_default_persistent_keepalive, true %>
Yes
</label>
<label class="radio">
<%= radio_button f, :use_default_persistent_keepalive, false %>
No
</label>
</div>
<p class="help">
Default: <%= @default_device_persistent_keepalive %>
</p>
</div>
<div class="field">
<%= label f, :persistent_keepalive, "Persistent Keepalive", class: "label" %>
<p>
Interval for WireGuard
<a href="https://www.wireguard.com/quickstart/#nat-and-firewall-traversal-persistence">
persistent keepalive</a>. A value of 0 disables this. Leave this disabled
unless you're experiencing NAT or firewall traversal problems.
</p>
<div class="control">
<%= text_input f, :persistent_keepalive, class: "input #{input_error_class(f, :persistent_keepalive)}",
disabled: @use_default_persistent_keepalive %>
</div>
<p class="help is-danger">
<%= error_tag f, :persistent_keepalive %>
</p>
</div>
<div class="field">
<%= label f, :ipv4, "IPv4 Address", class: "label" %>
<div class="control">
<%= text_input f, :ipv4, class: "input #{input_error_class(f, :ipv4)}" %>
</div>
<p class="help is-danger">
<%= error_tag f, :ipv4 %>
</p>
</div>
<div class="field">
<%= label f, :ipv6, "IPv6 Address", class: "label" %>
<div class="control">
<%= text_input f, :ipv6, class: "input #{input_error_class(f, :ipv6)}" %>
</div>
<p class="help is-danger">
<%= error_tag f, :ipv6 %>
</p>
</div>
<div class="field">
<div class="control">
<div class="level">
<div class="level-left"></div>
<div class="level-right">
<%= submit "Save", phx_disable_with: "Saving...", class: "button is-primary" %>
</div>
</div>
</div>
</div>
</.form>
</div>

View File

@@ -1,23 +0,0 @@
<%= if @live_action == :new do %>
<%= live_modal(
FzHttpWeb.DeviceLive.CreateFormComponent,
return_to: Routes.device_index_path(@socket, :index),
title: "Add Device",
changeset: @changeset,
id: "add-device-modal",
action: @live_action) %>
<% end %>
<%= render FzHttpWeb.SharedView, "heading.html", page_title: @page_title %>
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<div class="block">
<%= render FzHttpWeb.SharedView, "devices_table.html", devices: @devices, socket: @socket %>
</div>
<button class="button" phx-click="create_device">
Add Device
</button>
</section>

View File

@@ -1,64 +0,0 @@
defmodule FzHttpWeb.DeviceLive.Index do
@moduledoc """
Handles Device LiveViews.
"""
use FzHttpWeb, :live_view
alias FzHttp.{Devices, Users}
alias FzHttpWeb.ErrorHelpers
@impl Phoenix.LiveView
def mount(params, session, socket) do
{:ok,
socket
|> assign_defaults(params, session, &load_data/2)
|> assign(:changeset, Devices.new_device())
|> assign(:page_title, "Devices")}
end
@impl Phoenix.LiveView
def handle_event("create_device", _params, socket) do
if Users.count() == 1 do
# Must be the admin user
case Devices.auto_create_device(%{user_id: Users.admin().id}) do
{:ok, device} ->
@events_module.update_device(device)
{:noreply,
socket
|> push_redirect(to: Routes.device_show_path(socket, :show, device))}
{:error, changeset} ->
{:noreply,
socket
|> put_flash(
:error,
"Error creating device: #{ErrorHelpers.aggregated_errors(changeset)}"
)}
end
else
{:noreply,
socket
|> push_patch(to: Routes.device_index_path(socket, :new))}
end
end
@doc """
Needed because this view will receive handle_params when modal is closed.
"""
@impl Phoenix.LiveView
def handle_params(_params, _url, socket) do
{:noreply, socket}
end
defp load_data(_params, socket) do
# XXX: Update this to use new LiveView session auth
user = socket.assigns.current_user
if user.role == :admin do
assign(socket, :devices, Devices.list_devices())
else
not_authorized(socket)
end
end
end

View File

@@ -0,0 +1,91 @@
defmodule FzHttpWeb.DeviceLive.NewFormComponent do
@moduledoc """
Handles device form.
"""
use FzHttpWeb, :live_component
alias FzHttp.Devices
alias FzHttp.Sites
alias FzHttpWeb.ErrorHelpers
@impl Phoenix.LiveComponent
def mount(socket) do
{:ok,
socket
|> assign(:device, nil)
|> assign(:config, nil)}
end
@impl Phoenix.LiveComponent
def update(assigns, socket) do
changeset = new_changeset(socket)
{:ok,
socket
|> assign(assigns)
|> assign(:changeset, changeset)
|> assign(Sites.wireguard_defaults())
|> assign(Devices.defaults(changeset))}
end
@impl Phoenix.LiveComponent
def handle_event("change", %{"device" => device_params}, socket) do
changeset = Devices.new_device(device_params)
{:noreply,
socket
|> assign(:changeset, changeset)
|> assign(Devices.defaults(changeset))}
end
@impl Phoenix.LiveComponent
def handle_event("save", %{"device" => device_params}, socket) do
result =
device_params
|> Map.put("user_id", socket.assigns.target_user_id)
|> create_device(socket)
case result do
{:not_authorized} ->
{:noreply, not_authorized(socket)}
{:ok, device} ->
@events_module.update_device(device)
{:noreply,
socket
|> assign(:device, device)
|> assign(:config, Devices.as_encoded_config(device))}
{:error, changeset} ->
{:noreply,
socket
|> put_flash(:error, ErrorHelpers.aggregated_errors(changeset))
|> assign(:changeset, changeset)}
end
end
defp create_device(params, socket) do
if authorized_to_create?(socket) do
Devices.create_device(params)
else
{:not_authorized}
end
end
defp authorized_to_create?(socket) do
to_string(socket.assigns.current_user.id) == to_string(socket.assigns.target_user_id) ||
has_role?(socket, :admin)
end
# update/2 is called twice: on load and then connect.
# Use blank name the first time to prevent flashing two different names in the form.
# XXX: Clean this up using assign_new/3
defp new_changeset(socket) do
if connected?(socket) do
Devices.new_device()
else
Devices.new_device(%{"name" => nil})
end
end
end

View File

@@ -0,0 +1,260 @@
<div id="new-device-data"
data-public-key={@device && @device.public_key}
data-config={@config}
phx-hook="RenderConfig">
<%= if @device && @config do %>
<%# Device Generated; display config %>
<div class="content">
<p>
<strong>Device added!</strong>
</p>
<p>
Install the
<a target="_blank" href="https://www.wireguard.com/install/">
official WireGuard client
</a>
for your device, then use the below WireGuard configuration to connect.
</p>
<p>
<strong>NOTE:</strong> This configuration <strong>WILL NOT</strong>
be viewable again. Please ensure you've downloaded the
configuration file or copied it somewhere safe
before closing this window.
</p>
<div id="generating-config">
<p>Rendering configuration...</p>
<i class="fa fa-spinner fa-spin"></i>
</div>
<p>
<a class="button is-hidden" id="download-config">
Download WireGuard Configuration
</a>
</p>
<p>
<canvas id="qr-canvas">
Generating QR code...
</canvas>
</p>
<p>
<pre id="wg-conf-container"
class="is-hidden"><code id="wg-conf" class="language-toml"></code></pre>
</p>
</div>
<% else %>
<%# Show form to generate device %>
<div>
<.form let={f} for={@changeset} id="create-device" phx-change="change" phx-target={@myself} phx-submit="save">
<%= hidden_input f, :public_key, id: "device-public-key", phx_hook: "GenerateKeyPair" %>
<%= if @changeset.action do %>
<div class="notification is-danger">
<div class="flash-error">
<%= error_tag f, :base %>
</div>
</div>
<% end %>
<div class="field">
<%= label f, :name, class: "label" %>
<div class="control">
<%= text_input f, :name, class: "input #{input_error_class(f, :name)}" %>
</div>
<p class="help is-danger">
<%= error_tag f, :name %>
</p>
</div>
<div class="field">
<%= label f, :use_site_allowed_ips, "Use Default Allowed IPs", class: "label" %>
<div class="control">
<label class="radio">
<%= radio_button f, :use_site_allowed_ips, true %>
Yes
</label>
<label class="radio">
<%= radio_button f, :use_site_allowed_ips, false %>
No
</label>
</div>
<p class="help">
Default: <%= @allowed_ips %>
</p>
</div>
<div class="field">
<%= label f, :allowed_ips, "Allowed IPs", class: "label" %>
<div class="control">
<%= text_input f, :allowed_ips, class: "input #{input_error_class(f, :allowed_ips)}",
disabled: @use_site_allowed_ips %>
</div>
<p class="help is-danger">
<%= error_tag f, :allowed_ips %>
</p>
</div>
<div class="field">
<%= label f, :use_site_dns, "Use Default DNS Servers", class: "label" %>
<div class="control">
<label class="radio">
<%= radio_button f, :use_site_dns, true %>
Yes
</label>
<label class="radio">
<%= radio_button f, :use_site_dns, false %>
No
</label>
</div>
<p class="help">
Default: <%= @dns %>
</p>
</div>
<div class="field">
<%= label f, :dns, "DNS Servers", class: "label" %>
<div class="control">
<%= text_input f, :dns, class: "input #{input_error_class(f, :dns)}",
disabled: @use_site_dns %>
</div>
<p class="help is-danger">
<%= error_tag f, :dns %>
</p>
</div>
<div class="field">
<%= label f, :use_site_endpoint, "Use Default Endpoint", class: "label" %>
<div class="control">
<label class="radio">
<%= radio_button f, :use_site_endpoint, true %>
Yes
</label>
<label class="radio">
<%= radio_button f, :use_site_endpoint, false %>
No
</label>
</div>
<p class="help">
Default: <%= @endpoint %>
</p>
</div>
<div class="field">
<%= label f, :endpoint, "Server Endpoint", class: "label" %>
<p>The IP of the server this device should connect to.</p>
<div class="control">
<%= text_input f, :endpoint, class: "input #{input_error_class(f, :endpoint)}",
disabled: @use_site_endpoint %>
</div>
<p class="help is-danger">
<%= error_tag f, :endpoint %>
</p>
</div>
<div class="field">
<%= label f, :use_site_mtu, "Use Default MTU", class: "label" %>
<div class="control">
<label class="radio">
<%= radio_button f, :use_site_mtu, true %>
Yes
</label>
<label class="radio">
<%= radio_button f, :use_site_mtu, false %>
No
</label>
</div>
<p class="help">
Default: <%= @mtu %>
</p>
</div>
<div class="field">
<%= label f, :mtu, "Interface MTU", class: "label" %>
<p>The WireGuard interface MTU for this Device.</p>
<div class="contro">
<%= text_input f, :mtu, class: "input #{input_error_class(f, :mtu)}", disabled: @use_site_mtu %>
</div>
<p class="help is-danger">
<%= error_tag f, :mtu %>
</p>
</div>
<div class="field">
<%= label f, :use_site_persistent_keepalive, "Use Default Persistent Keepalive", class: "label" %>
<div class="control">
<label class="radio">
<%= radio_button f, :use_site_persistent_keepalive, true %>
Yes
</label>
<label class="radio">
<%= radio_button f, :use_site_persistent_keepalive, false %>
No
</label>
</div>
<p class="help">
Default: <%= @persistent_keepalive %>
</p>
</div>
<div class="field">
<%= label f, :persistent_keepalive, "Persistent Keepalive", class: "label" %>
<p>
Interval for WireGuard
<a href="https://www.wireguard.com/quickstart/#nat-and-firewall-traversal-persistence">
persistent keepalive</a>. A value of 0 disables this. Leave this disabled
unless you're experiencing NAT or firewall traversal problems.
</p>
<div class="control">
<%= text_input f, :persistent_keepalive, class: "input #{input_error_class(f, :persistent_keepalive)}",
disabled: @use_site_persistent_keepalive %>
</div>
<p class="help is-danger">
<%= error_tag f, :persistent_keepalive %>
</p>
</div>
<div class="field">
<%= label f, :ipv4, "Tunnel IPv4 Address", class: "label" %>
<div class="control">
<%= text_input(f,
:ipv4,
placeholder: "Leave blank to let Firezone assign an IPv4 address",
class: "input #{input_error_class(f, :ipv4)}") %>
</div>
<p class="help is-danger">
<%= error_tag f, :ipv4 %>
</p>
</div>
<div class="field">
<%= label f, :ipv6, "Tunnel IPv6 Address", class: "label" %>
<div class="control">
<%= text_input(f,
:ipv6,
placeholder: "Leave blank to let Firezone assign an IPv6 address",
class: "input #{input_error_class(f, :ipv6)}") %>
</div>
<p class="help is-danger">
<%= error_tag f, :ipv6 %>
</p>
</div>
<%= if @changeset.action do %>
<div class="notification is-danger">
<div class="flash-error">
Error creating device. Scroll up to view fields with errors.
</div>
</div>
<% end %>
<%= Phoenix.View.render(FzHttpWeb.SharedView, "submit_button.html", button_text: "Generate Configuration") %>
</.form>
</div>
<% end %>
</div>

View File

@@ -1,217 +0,0 @@
<%= if @live_action == :edit do %>
<%= live_modal(
FzHttpWeb.DeviceLive.FormComponent,
return_to: Routes.device_show_path(@socket, :show, @device),
title: "Edit #{@device.name}",
id: "device-#{@device.id}",
device: @device,
action: @live_action) %>
<% end %>
<%= render FzHttpWeb.SharedView, "heading.html", page_title: "Devices |> #{@page_title}" %>
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<div class="level">
<div class="level-left">
<h4 class="title is-4">Details</h4>
</div>
<div class="level-right">
<%= live_patch(to: Routes.device_show_path(@socket, :edit, @device), class: "button") do %>
<span class="icon">
<i class="mdi mdi-pencil"></i>
</span>
<span>Edit</span>
<% end %>
</div>
</div>
<table class="table is-bordered is-hoverable is-striped is-fullwidth">
<tbody>
<tr>
<td><strong>User</strong></td>
<td><%= link(@user.email, to: Routes.user_show_path(@socket, :show, @user)) %></td>
</tr>
<tr>
<td><strong>Name</strong></td>
<td><%= @device.name %></td>
</tr>
<%= if Application.fetch_env!(:fz_http, :wireguard_ipv4_enabled) do %>
<tr>
<td><strong>Interface IPv4</strong></td>
<td><%= @device.ipv4 %></td>
</tr>
<% end %>
<%= if Application.fetch_env!(:fz_http, :wireguard_ipv6_enabled) do %>
<tr>
<td><strong>Interface IPv6</strong></td>
<td><%= @device.ipv6 %></td>
</tr>
<% end %>
<tr>
<td><strong>Allowed IPs</strong></td>
<td><%= @allowed_ips %></td>
</tr>
<tr>
<td><strong>DNS Servers</strong></td>
<td><%= @dns || "None" %></td>
</tr>
<tr>
<td><strong>Endpoint</strong></td>
<td><%= @endpoint %></td>
</tr>
<tr>
<td><strong>Persistent Keepalive</strong></td>
<td>
<%= if @persistent_keepalive == 0 do %>
Disabled
<% else %>
Every <%= @persistent_keepalive %> seconds
<% end %>
</td>
</tr>
<tr>
<td><strong>MTU</strong></td>
<td><%= @mtu %></td>
</tr>
<tr>
<td><strong>Public key</strong></td>
<td class="code"><%= @device.public_key %></td>
</tr>
<tr>
<td><strong>Private key</strong></td>
<td class="code"><%= @device.private_key %></td>
</tr>
<tr>
<td><strong>Server public key</strong></td>
<td class="code"><%= @device.server_public_key %></td>
</tr>
</tbody>
</table>
</section>
<section class="section is-main-section">
<div class="level">
<div class="level-left">
<h4 class="title is-4">WireGuard Configuration</h4>
</div>
<div class="level-right">
<div class="field has-addons">
<div
id="shareable-link-trigger"
class={@dropdown_active_class <> " control dropdown is-right"}
phx-capture-click="close_dropdown"
phx-key="escape"
phx-click-away="close_dropdown"
phx-window-keydown="close_dropdown">
<div class="dropdown-trigger">
<button
id="get-shareable-link"
phx-click="create_config_token"
class="button">
<span class="icon is-small">
<i class="mdi mdi-share"></i>
</span>
<span>
Get Shareable Link
</span>
</button>
</div>
<div class="dropdown-menu is-large">
<div class="dropdown-content">
<div class="dropdown-item">
<p>
Anyone with this link can view this device's configuration.
</p>
<p>
Note: Link expires in <strong>10 minutes</strong>.
</p>
</div>
<div class="dropdown-item">
<div class="level">
<div class="level-left">
<code id="shareable-link">
<%= if @device.config_token do %>
<%= shareable_link(@socket, @device) %>
<% end %>
</code>
</div>
<div class="level-right">
<%= if @device.config_token do %>
<button id="copy-shareable-link-button"
data-clipboard={shareable_link(@socket, @device)}
data-target="shareable-link"
class="button"
data-tooltip="Click to copy"
phx-hook="ClipboardCopy">
<span class="icon">
<i class="mdi mdi-content-copy"></i>
</span>
</button>
<% end %>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="control">
<%= link(
to: Routes.device_path(@socket, :download_config, @device),
class: "button") do %>
<span class="icon is-small">
<i class="mdi mdi-download"></i>
</span>
<span>Download Configuration</span>
<% end %>
</div>
</div>
</div>
</div>
<div class="block">
Install the
<a href="https://www.wireguard.com/install/">
official WireGuard client
</a>
for your device, then use the below WireGuard configuration to connect.
</div>
<div class="columns">
<div class="column">
<pre><code id="wg-conf" class="language-toml"><%= @config %></code></pre>
</div>
<div class="column has-text-centered">
<canvas id="qr-canvas" phx-hook="QrCode">
Generating QR code...
</canvas>
</div>
</div>
</section>
<section class="section is-main-section">
<h4 class="title is-4">
Danger Zone
</h4>
<button class="button is-danger"
phx-click="delete_device"
phx-value-device_id={@device.id}
data-confirm="Are you sure? This will immediately disconnect this device and remove all associated data.">
<span class="icon is-small">
<i class="fas fa-trash"></i>
</span>
<span>Delete Device <%= @device.name %></span>
</button>
</section>

View File

@@ -1,102 +0,0 @@
defmodule FzHttpWeb.DeviceLive.Show do
@moduledoc """
Handles Device LiveViews.
"""
use FzHttpWeb, :live_view
alias FzHttp.{Devices, Users}
@impl Phoenix.LiveView
def mount(params, session, socket) do
{:ok,
socket
|> assign(:dropdown_active_class, "")
|> assign_defaults(params, session, &load_data/2)}
end
@doc """
Needed because this view will receive handle_params when modal is closed.
"""
@impl Phoenix.LiveView
def handle_params(_params, _url, socket) do
{:noreply, socket}
end
@impl Phoenix.LiveView
def handle_event("create_config_token", _params, socket) do
device = socket.assigns.device
if authorized?(socket.assigns.current_user, device) do
case Devices.create_config_token(device) do
{:ok, device} ->
{:noreply,
socket
|> assign(:dropdown_active_class, "is-active")
|> assign(:device, device)}
{:error, _changeset} ->
{:noreply,
socket
|> put_flash(:error, "Could not create device config token.")}
end
else
{:noreply, not_authorized(socket)}
end
end
@impl Phoenix.LiveView
def handle_event("close_dropdown", _params, socket) do
{:noreply,
socket
|> assign(:dropdown_active_class, "")}
end
@impl Phoenix.LiveView
def handle_event("delete_device", _params, socket) do
device = socket.assigns.device
if authorized?(socket.assigns.current_user, device) do
case Devices.delete_device(device) do
{:ok, _deleted_device} ->
{:ok, _deleted_pubkey} = @events_module.delete_device(device.public_key)
{:noreply,
socket
|> redirect(to: Routes.device_index_path(socket, :index))}
# Not likely to ever happen
# {:error, msg} ->
# {:noreply,
# socket
# |> put_flash(:error, "Error deleting device: #{msg}")}
end
else
{:noreply, not_authorized(socket)}
end
end
defp load_data(%{"id" => id}, socket) do
device = Devices.get_device!(id)
if authorized?(socket.assigns.current_user, device) do
socket
|> assign(
device: device,
user: Users.get_user!(device.user_id),
page_title: device.name,
allowed_ips: Devices.allowed_ips(device),
dns: Devices.dns(device),
endpoint: Devices.endpoint(device),
mtu: Devices.mtu(device),
persistent_keepalive: Devices.persistent_keepalive(device),
config: Devices.as_config(device)
)
else
not_authorized(socket)
end
end
defp authorized?(user, device) do
device.user_id == user.id || user.role == :admin
end
end

View File

@@ -0,0 +1,70 @@
<%= if @live_action == :new do %>
<%= live_modal(
FzHttpWeb.DeviceLive.NewFormComponent,
return_to: Routes.device_unprivileged_index_path(@socket, :index),
title: "Add Device",
current_user: @current_user,
target_user_id: @current_user.id,
id: "create-device-component") %>
<% end %>
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<h4 class="title is-4"><%= @page_title %></h4>
<div class="block">
Each device corresponds to a WireGuard configuration for connecting to this
Firezone server.
</div>
<div class="block">
<%= if length(@devices) > 0 do %>
<table class="table is-bordered is-hoverable is-striped is-fullwidth">
<thead>
<tr>
<th>Name</th>
<th>Assigned Device IP</th>
<th>Public key</th>
<th>Created</th>
</tr>
</thead>
<tbody>
<%= for device <- @devices do %>
<tr>
<td>
<%= live_patch(device.name, to: Routes.device_unprivileged_show_path(@socket, :show, device)) %>
</td>
<td class="code">
<span><%= device.ipv4 %></span>
<span><%= device.ipv6 %></span>
</td>
<td class="code"><%= device.public_key %></td>
<td id={"device-#{device.id}-inserted-at"} data-timestamp={device.inserted_at} phx-hook="FormatTimestamp">…</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>
<strong>No devices to show.</strong>
</p>
<% end %>
</div>
<div class="level">
<div class="level-left">
<%= live_patch(to: Routes.device_unprivileged_index_path(@socket, :new), class: "button") do %>
Add Device
<% end %>
</div>
<div class="level-right">
Signed in as <%= @current_user.email %>
|
<%= link(to: Routes.auth_path(@socket, :delete), method: :delete) do %>
Sign out
<% end %>
</div>
</div>
</section>

View File

@@ -0,0 +1,30 @@
defmodule FzHttpWeb.DeviceLive.Unprivileged.Index do
@moduledoc """
Handles Device LiveViews.
"""
use FzHttpWeb, :live_view
alias FzHttp.Devices
@impl Phoenix.LiveView
def mount(_params, _session, socket) do
user_id = socket.assigns.current_user.id
{:ok,
socket
|> assign(:devices, Devices.list_devices(user_id))
|> assign(:page_title, "Your Devices")}
end
@doc """
This is called when modal is closed. Conveniently, allows us to reload
devices table.
"""
@impl Phoenix.LiveView
def handle_params(_params, _url, socket) do
user_id = socket.assigns.current_user.id
{:noreply,
socket
|> assign(:devices, Devices.list_devices(user_id))}
end
end

View File

@@ -0,0 +1,4 @@
<div class="block">
<%= live_patch("<- Back to devices", to: Routes.device_unprivileged_index_path(@socket, :index)) %>
</div>
<%= render FzHttpWeb.SharedView, "show_device.html", assigns %>

View File

@@ -0,0 +1,71 @@
defmodule FzHttpWeb.DeviceLive.Unprivileged.Show do
@moduledoc """
Shows a device for an unprivileged user.
"""
use FzHttpWeb, :live_view
alias FzHttp.Devices
alias FzHttp.Users
@impl Phoenix.LiveView
def mount(%{"id" => device_id} = _params, _session, socket) do
device = Devices.get_device!(device_id)
if authorized?(device, socket) do
{:ok,
socket
|> assign(assigns(device))}
else
{:ok, not_authorized(socket)}
end
end
@impl Phoenix.LiveView
def handle_event("delete_device", _params, socket) do
device = socket.assigns.device
case delete_device(device, socket) do
{:ok, _deleted_device} ->
{:ok, _deleted_pubkey} = @events_module.delete_device(device.public_key)
{:noreply,
socket
|> redirect(to: Routes.device_unprivileged_index_path(socket, :index))}
{:not_authorized} ->
{:noreply, not_authorized(socket)}
# Not likely to ever happen
# {:error, msg} ->
# {:noreply,
# socket
# |> put_flash(:error, "Error deleting device: #{msg}")}
end
end
def delete_device(device, socket) do
if socket.assigns.current_user.id == device.user_id do
Devices.delete_device(device)
else
{:not_authorized}
end
end
defp assigns(device) do
[
device: device,
user: Users.get_user!(device.user_id),
page_title: device.name,
allowed_ips: Devices.allowed_ips(device),
port: Application.fetch_env!(:fz_vpn, :wireguard_port),
dns: Devices.dns(device),
endpoint: Devices.endpoint(device),
mtu: Devices.mtu(device),
persistent_keepalive: Devices.persistent_keepalive(device),
config: Devices.as_config(device)
]
end
defp authorized?(device, socket) do
"#{device.user_id}" == "#{socket.assigns.current_user.id}" || has_role?(socket, :admin)
end
end

View File

@@ -4,20 +4,9 @@ defmodule FzHttpWeb.RuleLive.Index do
"""
use FzHttpWeb, :live_view
def mount(params, session, socket) do
def mount(_params, _session, socket) do
{:ok,
socket
|> assign_defaults(params, session, &load_data/2)}
end
defp load_data(_params, socket) do
user = socket.assigns.current_user
if user.role == :admin do
socket
|> assign(:page_title, "Egress Rules")
else
not_authorized(socket)
end
|> assign(:page_title, "Egress Rules")}
end
end

View File

@@ -42,15 +42,6 @@
</p>
</div>
<div class="field">
<div class="control">
<div class="level">
<div class="level-left"></div>
<div class="level-right">
<%= submit "Save", phx_disable_with: "Saving...", class: "button is-primary" %>
</div>
</div>
</div>
</div>
<%= Phoenix.View.render(FzHttpWeb.SharedView, "submit_button.html", []) %>
</.form>
</div>

View File

@@ -7,26 +7,15 @@ defmodule FzHttpWeb.SettingLive.Account do
alias FzHttp.Users
@impl Phoenix.LiveView
def mount(params, session, socket) do
def mount(_params, _session, socket) do
{:ok,
socket
|> assign_defaults(params, session, &load_data/2)}
|> assign(:changeset, Users.change_user(socket.assigns.current_user))
|> assign(:page_title, "Account Settings")}
end
@impl Phoenix.LiveView
def handle_params(_params, _url, socket) do
{:noreply, socket}
end
defp load_data(_params, socket) do
user = socket.assigns.current_user
if user.role == :admin do
socket
|> assign(:changeset, Users.change_user(socket.assigns.current_user))
|> assign(:page_title, "Account Settings")
else
not_authorized(socket)
end
end
end

View File

@@ -1,55 +0,0 @@
<%= render FzHttpWeb.SharedView, "heading.html", page_title: @page_title %>
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<div class="block">
<p>
Configure Firezone-wide default settings.
</p>
</div>
<h4 class="title is-4">Device Defaults</h4>
<div class="block">
<%= live_component(
FzHttpWeb.SettingLive.DefaultFormComponent,
label_text: "Allowed IPs",
placeholder: @allowed_ips_placeholder,
changeset: @changesets["default.device.allowed_ips"],
help_text: @help_texts.allowed_ips,
id: :allowed_ips_form_component) %>
<%= live_component(
FzHttpWeb.SettingLive.DefaultFormComponent,
label_text: "DNS Servers",
placeholder: @dns_placeholder,
changeset: @changesets["default.device.dns"],
help_text: @help_texts.dns,
id: :dns_form_component) %>
<%= live_component(
FzHttpWeb.SettingLive.DefaultFormComponent,
label_text: "Endpoint",
placeholder: @endpoint_placeholder,
changeset: @changesets["default.device.endpoint"],
help_text: @help_texts.endpoint,
id: :endpoint_form_component) %>
<%= live_component(
FzHttpWeb.SettingLive.DefaultFormComponent,
label_text: "Persistent Keepalive",
placeholder: @persistent_keepalive_placeholder,
changeset: @changesets["default.device.persistent_keepalive"],
help_text: @help_texts.persistent_keepalive,
id: :persistent_keepalive_form_component) %>
<%= live_component(
FzHttpWeb.SettingLive.DefaultFormComponent,
label_text: "MTU",
placeholder: @mtu_placeholder,
changeset: @changesets["default.device.mtu"],
help_text: @help_texts.mtu,
id: :mtu_form_component) %>
</div>
</section>

View File

@@ -1,62 +0,0 @@
defmodule FzHttpWeb.SettingLive.DefaultFormComponent do
@moduledoc """
Handles updating setting values, one at a time
"""
use FzHttpWeb, :live_component
alias FzHttp.Settings
@impl Phoenix.LiveComponent
def update(assigns, socket) do
{:ok,
socket
|> assign(:input_class, "input")
|> assign(:form_changed, false)
|> assign(:input_icon, "")
|> assign(assigns)}
end
@impl Phoenix.LiveComponent
def handle_event("save", %{"setting" => %{"value" => value}}, socket) do
key = socket.assigns.changeset.data.key
case Settings.update_setting(key, value) do
{:ok, setting} ->
{:noreply,
socket
|> assign(:input_class, input_class(false))
|> assign(:input_icon, input_icon(false))
|> assign(:changeset, Settings.change_setting(setting, %{}))}
{:error, changeset} ->
{:noreply,
socket
|> assign(:input_class, input_class(true))
|> assign(:input_icon, input_icon(true))
|> assign(:changeset, changeset)}
end
end
@impl Phoenix.LiveComponent
def handle_event("change", _params, socket) do
{:noreply,
socket
|> assign(:form_changed, true)}
end
defp input_icon(false) do
"mdi mdi-check-circle"
end
defp input_icon(true) do
"mdi mdi-alert-circle"
end
defp input_class(false) do
"input is-success"
end
defp input_class(true) do
"input is-danger"
end
end

View File

@@ -1,26 +0,0 @@
<div class="block">
<.form let={f} for={@changeset} id={@id} phx-target={@myself} phx-change="change" phx-submit="save">
<div class="field">
<%= label f, :value, @label_text, class: "label" %>
<div class="field has-addons">
<div class="control has-icons-right is-expanded">
<%= text_input f, :value, placeholder: @placeholder, class: @input_class %>
<span class="icon is-small is-right">
<i class={@input_icon}></i>
</span>
<div class="help is-danger">
<%= error_tag f, :value %>
</div>
<div class="help">
<%= raw @help_text %>
</div>
</div>
<%= if @form_changed do %>
<div class="control">
<%= submit "Save", class: "button is-primary" %>
</div>
<% end %>
</div>
</div>
</.form>
</div>

View File

@@ -1,84 +0,0 @@
defmodule FzHttpWeb.SettingLive.Default do
@moduledoc """
Manages the defaults view.
"""
use FzHttpWeb, :live_view
alias FzHttp.{ConnectivityChecks, Settings}
@help_texts %{
allowed_ips: """
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
<code>0.0.0.0/0, ::/0</code> to route all device traffic through this Firezone server.
""",
dns: """
Comma-separated list of DNS servers to use for devices.
Leaving this blank will omit the <code>DNS</code> section in
generated device configs.
""",
endpoint: """
IPv4 or IPv6 address that devices will be configured to connect
to. Defaults to this server's public IP if not set.
""",
persistent_keepalive: """
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.
""",
mtu: """
WireGuard interface MTU for devices. Defaults to what's set in the configuration file.
Leave this blank if you're unsure what this means.
"""
}
@impl Phoenix.LiveView
def mount(params, session, socket) do
{:ok,
socket
|> assign_defaults(params, session, &load_data/2)}
end
defp endpoint_placeholder do
ConnectivityChecks.endpoint()
end
defp mtu_placeholder do
Application.fetch_env!(:fz_http, :wireguard_mtu)
end
defp dns_placeholder do
Application.fetch_env!(:fz_http, :wireguard_dns)
end
defp allowed_ips_placeholder do
Application.fetch_env!(:fz_http, :wireguard_allowed_ips)
end
defp persistent_keepalive_placeholder do
Application.fetch_env!(:fz_http, :wireguard_persistent_keepalive)
end
defp load_changesets do
Settings.to_list("default.")
|> Map.new(fn setting -> {setting.key, Settings.change_setting(setting)} end)
end
defp load_data(_params, socket) do
user = socket.assigns.current_user
if user.role == :admin do
socket
|> assign(:changesets, load_changesets())
|> assign(:help_texts, @help_texts)
|> assign(:endpoint_placeholder, endpoint_placeholder())
|> assign(:mtu_placeholder, mtu_placeholder())
|> assign(:dns_placeholder, dns_placeholder())
|> assign(:allowed_ips_placeholder, allowed_ips_placeholder())
|> assign(:persistent_keepalive_placeholder, persistent_keepalive_placeholder())
|> assign(:page_title, "Default Settings")
else
not_authorized(socket)
end
end
end

View File

@@ -4,18 +4,19 @@ defmodule FzHttpWeb.SettingLive.Security do
"""
use FzHttpWeb, :live_view
import FzCommon.FzInteger, only: [max_pg_integer: 0]
alias FzHttp.Settings
alias FzHttp.{Sites, Sites.Site}
@hour 3_600
@day 24 * @hour
@impl Phoenix.LiveView
def mount(params, session, socket) do
def mount(_params, _session, socket) do
{:ok,
socket
|> assign_defaults(params, session, &load_data/2)}
|> assign(:form_changed, false)
|> assign(:options, options())
|> assign(:changeset, changeset())
|> assign(:page_title, "Security Settings")}
end
@impl Phoenix.LiveView
@@ -26,15 +27,15 @@ defmodule FzHttpWeb.SettingLive.Security do
end
@impl Phoenix.LiveView
def handle_event("save", %{"setting" => %{"value" => value}}, socket) do
key = socket.assigns.changeset.data.key
def handle_event("save", %{"site" => %{"vpn_session_duration" => vpn_session_duration}}, socket) do
site = Sites.get_site!()
case Settings.update_setting(key, value) do
{:ok, setting} ->
case Sites.update_site(site, %{vpn_session_duration: vpn_session_duration}) do
{:ok, site} ->
{:noreply,
socket
|> assign(:form_changed, false)
|> assign(:changeset, Settings.change_setting(setting, %{}))}
|> assign(:changeset, Sites.change_site(site))}
{:error, changeset} ->
{:noreply,
@@ -43,30 +44,20 @@ defmodule FzHttpWeb.SettingLive.Security do
end
end
defp load_data(_params, socket) do
user = socket.assigns.current_user
defp changeset do
Sites.get_site!()
|> Sites.change_site()
end
if user.role == :admin do
options = [
Never: 0,
Once: max_pg_integer(),
"Every Hour": @hour,
"Every Day": @day,
"Every Week": 7 * @day,
"Every 30 Days": 30 * @day,
"Every 90 Days": 90 * @day
]
setting = Settings.get_setting!(key: "security.require_auth_for_vpn_frequency")
changeset = Settings.change_setting(setting)
socket
|> assign(:form_changed, false)
|> assign(:options, options)
|> assign(:changeset, changeset)
|> assign(:page_title, "Security Settings")
else
not_authorized(socket)
end
defp options do
[
Never: 0,
Once: Site.max_vpn_session_duration(),
"Every Hour": @hour,
"Every Day": @day,
"Every Week": 7 * @day,
"Every 30 Days": 30 * @day,
"Every 90 Days": 90 * @day
]
end
end

View File

@@ -0,0 +1,21 @@
<%= render FzHttpWeb.SharedView, "heading.html", page_title: @page_title %>
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<div class="block">
<p>
Configure default WireGuard settings for this site.
</p>
</div>
<h4 class="title is-4">Site Defaults</h4>
<div class="block">
<%= live_component(
FzHttpWeb.SettingLive.SiteFormComponent,
placeholders: @placeholders,
changeset: @changeset,
id: :site_form_component) %>
</div>
</section>

View File

@@ -0,0 +1,32 @@
defmodule FzHttpWeb.SettingLive.SiteFormComponent do
@moduledoc """
Handles updating site values.
"""
use FzHttpWeb, :live_component
alias FzHttp.Sites
@impl Phoenix.LiveComponent
def update(assigns, socket) do
{:ok,
socket
|> assign(assigns)}
end
@impl Phoenix.LiveComponent
def handle_event("save", %{"site" => site_params}, socket) do
site = Sites.get_site!()
case Sites.update_site(site, site_params) do
{:ok, site} ->
{:noreply,
socket
|> assign(:changeset, Sites.change_site(site))}
{:error, changeset} ->
{:noreply,
socket
|> assign(:changeset, changeset)}
end
end
end

View File

@@ -0,0 +1,100 @@
<div class="block">
<.form let={f} for={@changeset} id={@id} phx-target={@myself} phx-submit="save">
<div class="field">
<%= label f, :allowed_ips, "Allowed IPs", class: "label" %>
<div class="control">
<%= text_input f,
:allowed_ips,
placeholder: @placeholders[:allowed_ips],
class: "input #{input_error_class(f, :allowed_ips)}" %>
</div>
<p class="help is-danger">
<%= error_tag f, :allowed_ips %>
</p>
<p class="help">
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
<code>0.0.0.0/0, ::/0</code> to route all device traffic through this Firezone server.
</p>
</div>
<div class="field">
<%= label f, :dns, "DNS Servers", class: "label" %>
<div class="control">
<%= text_input f,
:dns,
placeholder: @placeholders[:dns],
class: "input #{input_error_class(f, :dns)}" %>
</div>
<p class="help is-danger">
<%= error_tag f, :dns %>
</p>
<p class="help">
Comma-separated list of DNS servers to use for devices.
Leaving this blank will omit the <code>DNS</code> section in
generated device configs.
</p>
</div>
<div class="field">
<%= label f, :endpoint, "Endpoint", class: "label" %>
<div class="control">
<%= text_input f,
:endpoint,
placeholder: @placeholders[:endpoint],
class: "input #{input_error_class(f, :endpoint)}" %>
</div>
<p class="help is-danger">
<%= error_tag f, :endpoint %>
</p>
<p class="help">
IPv4 or IPv6 address that devices will be configured to connect
to. Defaults to this server's public IP if not set.
</p>
</div>
<div class="field">
<%= label f, :persistent_keepalive, "Persistent Keepalive", class: "label" %>
<div class="control">
<%= text_input f,
:persistent_keepalive,
placeholder: @placeholders[:persistent_keepalive],
class: "input #{input_error_class(f, :persistent_keepalive)}" %>
<p class="help is-danger">
<%= error_tag f, :persistent_keepalive %>
</p>
<p class="help">
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.
</p>
</div>
</div>
<div class="field">
<%= label f, :mtu, "MTU", class: "label" %>
<div class="control">
<%= text_input f,
:mtu,
placeholder: @placeholders[:mtu],
class: "input #{input_error_class(f, :mtu)}" %>
</div>
<p class="help is-danger">
<%= error_tag f, :mtu %>
</p>
<p class="help">
WireGuard interface MTU for devices. Defaults to what's set in the configuration file.
Leave this blank if you're unsure what this means.
</p>
</div>
<%= Phoenix.View.render(FzHttpWeb.SharedView, "submit_button.html", []) %>
</.form>
</div>

View File

@@ -0,0 +1,51 @@
defmodule FzHttpWeb.SettingLive.Site do
@moduledoc """
Manages the defaults view.
"""
use FzHttpWeb, :live_view
alias FzHttp.{ConnectivityChecks, Sites}
@impl Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:changeset, changeset())
|> assign(:placeholders, placeholders())
|> assign(:page_title, "Site Settings")}
end
defp endpoint_placeholder do
ConnectivityChecks.endpoint()
end
defp mtu_placeholder do
Application.fetch_env!(:fz_http, :wireguard_mtu)
end
defp dns_placeholder do
Application.fetch_env!(:fz_http, :wireguard_dns)
end
defp allowed_ips_placeholder do
Application.fetch_env!(:fz_http, :wireguard_allowed_ips)
end
defp persistent_keepalive_placeholder do
Application.fetch_env!(:fz_http, :wireguard_persistent_keepalive)
end
defp placeholders do
%{
allowed_ips: allowed_ips_placeholder(),
dns: dns_placeholder(),
persistent_keepalive: persistent_keepalive_placeholder(),
endpoint: endpoint_placeholder(),
mtu: mtu_placeholder()
}
end
defp changeset do
Sites.get_site!() |> Sites.change_site()
end
end

View File

@@ -19,6 +19,7 @@
<th>Email</th>
<th>Devices</th>
<th>Last Signed In</th>
<th>Last Signed In Method</th>
<th>Created</th>
<th>Updated</th>
</tr>
@@ -35,6 +36,7 @@
phx-hook="FormatTimestamp">
</td>
<td><%= user.last_signed_in_method %></td>
<td id={"user-#{user.id}-inserted-at"}
data-timestamp={user.inserted_at}
phx-hook="FormatTimestamp">

View File

@@ -7,10 +7,10 @@ defmodule FzHttpWeb.UserLive.Index do
alias FzHttp.Users
@impl Phoenix.LiveView
def mount(params, session, socket) do
def mount(_params, _session, socket) do
{:ok,
socket
|> assign_defaults(params, session, &load_data/2)
|> assign(:users, Users.list_users(:with_device_counts))
|> assign(:changeset, Users.new_user())
|> assign(:page_title, "Users")}
end
@@ -19,18 +19,4 @@ defmodule FzHttpWeb.UserLive.Index do
def handle_params(_params, _url, socket) do
{:noreply, socket}
end
defp load_data(_params, socket) do
user = socket.assigns.current_user
if user.role == :admin do
assign(
socket,
:users,
Users.list_users(:with_device_counts)
)
else
not_authorized(socket)
end
end
end

View File

@@ -1,11 +1,20 @@
<%= if @live_action == :edit do %>
<%= live_modal(
FzHttpWeb.UserLive.FormComponent,
return_to: Routes.user_show_path(@socket, :show, @user),
title: "Edit #{@user.email}",
id: "user-form-component",
user: @user,
action: @live_action) %>
FzHttpWeb.UserLive.FormComponent,
return_to: Routes.user_show_path(@socket, :show, @user),
title: "Edit #{@user.email}",
id: "user-form-component",
user: @user,
action: @live_action) %>
<% end %>
<%= if @live_action == :new_device do %>
<%= live_modal(
FzHttpWeb.DeviceLive.NewFormComponent,
return_to: Routes.user_show_path(@socket, :show, @user.id),
title: "Add Device",
current_user: @current_user,
target_user_id: @user.id,
id: "create-device-component") %>
<% end %>
<%= render FzHttpWeb.SharedView, "heading.html", page_title: "Users |> #{@user.email}" %>
@@ -41,9 +50,11 @@
<% end %>
</div>
<button class="button" phx-value-user_id={@user.id} phx-click="create_device">
Add Device
</button>
<%= live_patch(
"Add Device",
to: Routes.user_show_path(@socket, :new_device, @user),
id: "add-device-button",
class: "button") %>
</section>
<section class="section is-main-section">

View File

@@ -1,6 +1,7 @@
defmodule FzHttpWeb.UserLive.Show do
@moduledoc """
Handles showing users.
XXX: Admin only
"""
use FzHttpWeb, :live_view
@@ -8,16 +9,29 @@ defmodule FzHttpWeb.UserLive.Show do
alias FzHttpWeb.ErrorHelpers
@impl Phoenix.LiveView
def mount(params, session, socket) do
def mount(%{"id" => user_id} = _params, _session, socket) do
user = Users.get_user!(user_id)
devices = Devices.list_devices(user)
{:ok,
socket
|> assign(:page_title, "Users")
|> assign_defaults(params, session, &load_data/2)}
|> assign(:devices, devices)
|> assign(:device_config, socket.assigns[:device_config])
|> assign(:user, user)
|> assign(:page_title, "Users")}
end
@doc """
Called when a modal is dismissed; reload devices.
"""
@impl Phoenix.LiveView
def handle_params(_params, _url, socket) do
{:noreply, socket}
def handle_params(%{"id" => user_id} = _params, _url, socket) do
user = Users.get_user!(user_id)
devices = Devices.list_devices(user.id)
{:noreply,
socket
|> assign(:devices, devices)}
end
@impl Phoenix.LiveView
@@ -49,47 +63,4 @@ defmodule FzHttpWeb.UserLive.Show do
end
end
end
@impl Phoenix.LiveView
def handle_event("create_device", %{"user_id" => user_id}, socket) do
{:ok, privkey, pubkey, server_pubkey} = @events_module.create_device()
attributes = %{
private_key: privkey,
public_key: pubkey,
server_public_key: server_pubkey,
user_id: user_id,
name: Devices.rand_name()
}
case Devices.create_device(attributes) do
{:ok, device} ->
@events_module.update_device(device)
{:noreply,
socket
|> put_flash(:info, "Device created successfully.")
|> redirect(to: Routes.device_show_path(socket, :show, device))}
{:error, changeset} ->
{:noreply,
socket
|> put_flash(
:error,
"Error creating device: #{ErrorHelpers.aggregated_errors(changeset)}"
)}
end
end
defp load_data(params, socket) do
user = Users.get_user!(params["id"])
if socket.assigns.current_user.role == :admin do
socket
|> assign(:devices, Devices.list_devices(user))
|> assign(:user, user)
else
not_authorized(socket)
end
end
end

View File

@@ -0,0 +1,21 @@
defmodule FzHttpWeb.LiveAuth do
@moduledoc """
Handles loading default assigns and authorizing.
"""
alias FzHttpWeb.Authentication
import Phoenix.LiveView
import FzHttpWeb.AuthorizationHelpers
def on_mount(role, _params, session, socket) do
user = Authentication.get_current_user(session)
if user do
socket
|> assign_new(:current_user, fn -> user end)
|> authorize_role(role)
else
{:halt, not_authorized(socket)}
end
end
end

View File

@@ -4,43 +4,7 @@ defmodule FzHttpWeb.LiveHelpers do
XXX: Consider splitting these up using one of the techniques at
https://bernheisel.com/blog/phoenix-liveview-and-views
"""
import Phoenix.LiveView
import Phoenix.LiveView.Helpers
alias FzHttp.Users
alias FzHttpWeb.Router.Helpers, as: Routes
import FzHttpWeb.ControllerHelpers, only: [root_path_for_role: 1]
@doc """
Load user into socket assigns and call the callback function if provided.
"""
def assign_defaults(socket, params, %{"user_id" => user_id}, callback) do
socket = assign_new(socket, :current_user, fn -> Users.get_user(user_id) end)
if socket.assigns.current_user do
callback.(params, socket)
else
not_authorized(socket)
end
end
def assign_defaults(socket, _params, _session, _decorator) do
not_authorized(socket)
end
def assign_defaults(socket, _params, %{"user_id" => user_id}) do
assign_new(socket, :current_user, fn -> Users.get_user!(user_id) end)
end
def assign_defaults(socket, _params, _session) do
not_authorized(socket)
end
def not_authorized(socket) do
socket
|> put_flash(:error, "Not authorized.")
|> redirect(to: root_path_for_role(socket))
end
def live_modal(component, opts) do
path = Keyword.fetch!(opts, :return_to)
@@ -64,17 +28,6 @@ defmodule FzHttpWeb.LiveHelpers do
end
end
@doc """
URL_HOST is used in releases to set an externally-accessible url. Use that if exists.
"""
def shareable_link(socket, device) do
if url_host = Application.get_env(:fz_http, :url_host) do
"https://" <> url_host <> Routes.device_path(socket, :config, device.config_token)
else
Routes.device_url(socket, :config, device.config_token)
end
end
defp status_digit(response_code) when is_integer(response_code) do
[status_digit | _tail] = Integer.digits(response_code)
status_digit

View File

@@ -7,10 +7,6 @@ defmodule FzHttpWeb.MockEvents do
inside FzHttp and use that for the tests.
"""
def create_device do
{:ok, "privkey", "pubkey", "server_pubkey"}
end
def delete_device(pubkey) do
{:ok, pubkey}
end

View File

@@ -0,0 +1,35 @@
defmodule FzHttpWeb.Plug.Authorization do
@moduledoc """
Plug to ensure user has a specific role.
This should be called after the resource is loaded into
the connection with Guardian.
"""
use FzHttpWeb, :controller
import FzHttpWeb.ControllerHelpers, only: [root_path_for_role: 2]
alias FzHttpWeb.Authentication
@not_authorized "Not authorized."
def init(opts), do: opts
def call(conn, :test) do
conn
end
def call(conn, role), do: require_user_with_role(conn, role)
def require_user_with_role(conn, role) do
user = Authentication.get_current_user(conn)
if user.role == role do
conn
else
conn
|> put_flash(:error, @not_authorized)
|> redirect(to: root_path_for_role(conn, user.role))
|> halt()
end
end
end

View File

@@ -5,6 +5,7 @@ defmodule FzHttpWeb.Router do
use FzHttpWeb, :router
# Limit total requests to 20 per every 10 seconds
@root_rate_limit [rate_limit: {"root", 10_000, 50}, by: :ip]
pipeline :browser do
@@ -15,7 +16,6 @@ defmodule FzHttpWeb.Router do
plug :protect_from_forgery
plug :put_secure_browser_headers
# Limit total requests to 20 per every 10 seconds
# XXX: Make this configurable
plug Hammer.Plug, @root_rate_limit
end
@@ -24,37 +24,114 @@ defmodule FzHttpWeb.Router do
plug :accepts, ["json"]
end
pipeline :require_admin_user do
plug FzHttpWeb.Plug.Authorization, :admin
end
pipeline :require_unprivileged_user do
plug FzHttpWeb.Plug.Authorization, :unprivileged
end
pipeline :require_authenticated do
plug Guardian.Plug.EnsureAuthenticated
end
pipeline :require_unauthenticated do
plug FzHttpWeb.Plug.Authorization, :test
plug Guardian.Plug.EnsureNotAuthenticated
end
pipeline :guardian do
plug FzHttpWeb.Authentication.Pipeline
end
# Ueberauth routes
scope "/auth", FzHttpWeb do
pipe_through [
:browser,
:guardian,
:require_unauthenticated
]
get "/:provider", AuthController, :request
get "/:provider/callback", AuthController, :callback
post "/:provider/callback", AuthController, :callback
end
# Unauthenticated routes
scope "/", FzHttpWeb do
pipe_through :browser
resources "/session", SessionController, only: [:new, :create, :delete], singleton: true
live "/users", UserLive.Index, :index
live "/users/new", UserLive.Index, :new
live "/users/:id", UserLive.Show, :show
live "/users/:id/edit", UserLive.Show, :edit
live "/rules", RuleLive.Index, :index
live "/devices", DeviceLive.Index, :index
live "/devices/new", DeviceLive.Index, :new
live "/devices/:id", DeviceLive.Show, :show
live "/devices/:id/edit", DeviceLive.Show, :edit
get "/devices/:id/dl", DeviceController, :download_config
get "/device_config/:config_token", DeviceController, :config
get "/device_config/:config_token/dl", DeviceController, :download_shared_config
live "/settings/default", SettingLive.Default, :show
live "/settings/security", SettingLive.Security, :show
live "/settings/account", SettingLive.Account, :show
live "/settings/account/edit", SettingLive.Account, :edit
live "/diagnostics/connectivity_checks", ConnectivityCheckLive.Index, :index
get "/sign_in/:token", SessionController, :create
delete "/user", UserController, :delete
get "/user", UserController, :show
pipe_through [
:browser,
:guardian,
:require_unauthenticated
]
get "/", RootController, :index
end
# Authenticated routes
scope "/", FzHttpWeb do
pipe_through [
:browser,
:guardian,
:require_authenticated
]
delete "/sign_out", AuthController, :delete
end
# Authenticated Unprivileged routes
scope "/", FzHttpWeb do
pipe_through [
:browser,
:guardian,
:require_authenticated,
:require_unprivileged_user
]
# Unprivileged Live routes
live_session(
:unprivileged,
on_mount: {FzHttpWeb.LiveAuth, :unprivileged},
root_layout: {FzHttpWeb.LayoutView, :unprivileged}
) do
live "/user_devices", DeviceLive.Unprivileged.Index, :index
live "/user_devices/new", DeviceLive.Unprivileged.Index, :new
live "/user_devices/:id", DeviceLive.Unprivileged.Show, :show
end
end
# Authenticated Admin routes
scope "/", FzHttpWeb do
pipe_through [
:browser,
:guardian,
:require_authenticated,
:require_admin_user
]
# Admins can delete themselves synchronously
delete "/user", UserController, :delete
# Admin Live routes
live_session(
:admin,
on_mount: {FzHttpWeb.LiveAuth, :admin},
root_layout: {FzHttpWeb.LayoutView, :admin}
) do
live "/users", UserLive.Index, :index
live "/users/new", UserLive.Index, :new
live "/users/:id", UserLive.Show, :show
live "/users/:id/edit", UserLive.Show, :edit
live "/users/:id/new_device", UserLive.Show, :new_device
live "/rules", RuleLive.Index, :index
live "/devices", DeviceLive.Admin.Index, :index
live "/devices/:id", DeviceLive.Admin.Show, :show
live "/settings/site", SettingLive.Site, :show
live "/settings/security", SettingLive.Security, :show
live "/settings/account", SettingLive.Account, :show
live "/settings/account/edit", SettingLive.Account, :edit
live "/diagnostics/connectivity_checks", ConnectivityCheckLive.Index, :index
end
end
end

View File

@@ -11,7 +11,8 @@ defmodule FzHttpWeb.Session do
@session_options [
store: :cookie,
key: "_fz_http_key",
same_site: "Strict",
# XXX: Strict doesn't work for SSO auth
# same_site: "Strict",
max_age: @max_cookie_age,
secure: true,
sign: true,

View File

@@ -0,0 +1,31 @@
<h3 class="is-3 title">Sign In</h3>
<hr>
<div class="block">
<%= link("<- Back to sign in methods", to: Routes.root_path(@conn, :index)) %>
</div>
<div class="block">
<%= form_tag @callback_url, method: "post" do %>
<div class="field">
<label for="email" class="label">Email</label>
<div class="control">
<input class="input" type="email" name="email" id="email" required value={@conn.params["email"]} />
</div>
</div>
<div class="field">
<label for="password" class="label">Password</label>
<div class="control">
<input class="input" type="password" name="password" id="password" required />
</div>
</div>
<div class="field">
<div class="control">
<%= submit "Sign In", class: "button" %>
</div>
</div>
<% end %>
</div>

View File

@@ -1,31 +0,0 @@
<div class="level">
<div class="level-left">
<h4 class="title is-4">WireGuard Configuration</h4>
</div>
<div class="level-right">
<%= link(to: Routes.device_path(@conn, :download_shared_config, @device.config_token), class: "button") do %>
<span class="icon is-small">
<i class="mdi mdi-download"></i>
</span>
<span>Download Configuration</span>
<% end %>
</div>
</div>
<div class="content">
Install the
<a href="https://www.wireguard.com/install/">
official WireGuard client
</a>
for your device, then use the below WireGuard configuration to connect.
</div>
<div class="columns">
<div class="column">
<pre><code id="wg-conf" class="language-toml"><%= @config %></code></pre>
</div>
<div class="column has-text-centered">
<canvas id="qr-canvas">
Generating QR code...
</canvas>
</div>
</div>

View File

@@ -0,0 +1,155 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html lang="en" class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<%= live_title_tag assigns[:page_title], prefix: "Firezone • " %>
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/css/app.css")} />
<script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/js/admin.js")}></script>
<!-- Favicon -->
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<meta name="msapplication-config" content="/browserconfig.xml" />
<meta name="msapplication-TileColor" content="331700">
<meta name="theme-color" content="331700">
<%= render(FzHttpWeb.SharedView, "socket_token_headers.html", conn: @conn, current_user: @current_user) %>
</head>
<body>
<div id="app">
<nav id="navbar-main" class="navbar is-fixed-top">
<div class="navbar-brand">
<a class="navbar-item is-hidden-desktop jb-aside-mobile-toggle">
<span class="icon"><i class="mdi mdi-forwardburger mdi-24px"></i></span>
</a>
</div>
<div class="navbar-brand is-right">
<a class="navbar-item is-hidden-desktop jb-navbar-menu-toggle" data-target="navbar-menu">
<span class="icon"><i class="mdi mdi-dots-vertical"></i></span>
</a>
</div>
<div class="navbar-menu fadeIn animated faster" id="navbar-menu">
<div class="navbar-end">
<div class="navbar-item has-dropdown has-dropdown-with-icons has-divider is-hoverable">
<a class="navbar-link is-arrowless">
<div class="is-user-name"><span><%= @current_user.email %></span></div>
<span class="icon"><i class="mdi mdi-chevron-down"></i></span>
</a>
<div class="navbar-dropdown">
<%= link(to: Routes.setting_account_path(@conn, :show), class: "navbar-item") do %>
<span class="icon"><i class="mdi mdi-account"></i></span>
<span>Account Settings</span>
<% end %>
<hr class="navbar-divider">
<%= link(to: Routes.auth_path(@conn, :delete), method: :delete, class: "navbar-item") do %>
<span class="icon"><i class="mdi mdi-logout"></i></span>
<span>Log Out</span>
<% end %>
</div>
</div>
<a href="https://docs.firez.one/?utm_source=docs" title="Documentation" class="navbar-item has-divider is-desktop-icon-only">
<span class="icon"><i class="mdi mdi-help-circle-outline"></i></span>
</a>
<a id="web-ui-connect-success" href="#" title="You are connected to the Firezone Web UI." class="navbar-item has-divider is-desktop-icon-only">
<span class="icon has-text-success"><i class="mdi mdi-wifi"></i></span>
</a>
<a id="web-ui-connect-error" href="#" title="You are disconnected from the Firezone Web UI." class="is-hidden navbar-item has-divider is-desktop-icon-only">
<span class="icon has-text-danger"><i class="mdi mdi-wifi-off"></i></span>
</a>
</div>
</div>
</nav>
<aside class="aside is-placed-left is-expanded">
<div class="aside-tools">
<div class="aside-tools-label">
<span>Firezone</span>
</div>
</div>
<div class="menu is-menu-main">
<p class="menu-label">Configuration</p>
<ul class="menu-list">
<li>
<%= link(to: Routes.user_index_path(@conn, :index), class: nav_class(@conn, ~r"/users")) do %>
<span class="icon"><i class="mdi mdi-account-group"></i></span>
<span class="menu-item-label">Users</span>
<% end %>
</li>
<li>
<%= link(to: Routes.device_admin_index_path(@conn, :index), class: nav_class(@conn, ~r"/devices")) do %>
<span class="icon"><i class="mdi mdi-laptop"></i></span>
<span class="menu-item-label">Devices</span>
<% end %>
</li>
<li>
<%= link(to: Routes.rule_index_path(@conn, :index), class: nav_class(@conn, ~r"/rules")) do %>
<span class="icon"><i class="mdi mdi-traffic-light"></i></span>
<span class="menu-item-label">Rules</span>
<% end %>
</li>
</ul>
<p class="menu-label">Settings</p>
<ul class="menu-list">
<li>
<%= link(to: Routes.setting_site_path(@conn, :show), class: nav_class(@conn, ~r"/settings/site")) do %>
<span class="icon"><i class="mdi mdi-cog"></i></span>
<span class="menu-item-label">Defaults</span>
<% end %>
</li>
<li>
<%= link(to: Routes.setting_account_path(@conn, :show), class: nav_class(@conn, ~r"/settings/account")) do %>
<span class="icon"><i class="mdi mdi-account"></i></span>
<span class="menu-item-label">Account</span>
<% end %>
</li>
<li>
<%= link(to: Routes.setting_security_path(@conn, :show), class: nav_class(@conn, ~r"/settings/security")) do %>
<span class="icon"><i class="mdi mdi-lock"></i></span>
<span class="menu-item-label">Security</span>
<% end %>
</li>
</ul>
<p class="menu-label">Diagnostics</p>
<ul class="menu-list">
<li>
<%= link(to: Routes.connectivity_check_index_path(@conn, :index), class: nav_class(@conn, ~r"/diagnostics/connectivity_checks")) do %>
<span class="icon"><i class="mdi mdi-access-point"></i></span>
<span class="menu-item-label">WAN Connectivity</span>
<% end %>
</li>
</ul>
</div>
</aside>
<%= @inner_content %>
<footer class="footer">
<div class="container-fluid">
<div class="level">
<div class="level-left">
<div class="level-item">
<%= link(to: "mailto:" <> feedback_recipient()) do %>
Leave us feedback!
<% end %>
</div>
</div>
<div class="level-right">
<div class="level-item">
<a href={"https://github.com/firezone/firezone/tree/#{git_sha()}"}>
Version <%= application_version() %>
</a>
</div>
<div class="level-item">
<div class="logo">
<a href="https://firez.one"><img src="/images/logo.svg" alt="firez.one"></a>
</div>
</div>
</div>
</div>
</div>
</footer>
</div>
</body>
</html>

View File

@@ -1,35 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<%= csrf_meta_tag() %>
<title>Firezone</title>
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/css/app.css")} />
<script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/js/device_config.js")}></script>
<!-- Favicon -->
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<meta name="msapplication-config" content="/browserconfig.xml" />
<meta name="msapplication-TileColor" content="331700">
<meta name="theme-color" content="331700">
</head>
<body>
<section class="section hero is-fullheight is-error-section">
<div id="app" class="hero-body">
<div class="container">
<div class="block">
<div class="has-text-centered">
<img src={Routes.static_path(@conn, "/images/logo-text.svg")} alt="firez.one">
</div>
</div>
<%= @inner_content %>
</div>
</div>
</section>
</body>
</html>

View File

@@ -1,8 +0,0 @@
<html>
<head>
<link rel="stylesheet" href="<%= Routes.static_path(FzHttpWeb.Endpoint, "/css/email.css") %>">
</head>
<body>
<%= @inner_content %>
</body>
</html>

View File

@@ -0,0 +1,8 @@
<html>
<head>
<link rel="stylesheet" href={Routes.static_path(FzHttpWeb.Endpoint, "/css/email.css")}>
</head>
<body>
<%= @inner_content %>
</body>
</html>

View File

@@ -1,12 +1,13 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html lang="en" class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded">
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<%= live_title_tag assigns[:page_title], prefix: "Firezone • " %>
<%= csrf_meta_tag() %>
<%= live_title_tag assigns[:page_title] || "Firezone" %>
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/css/app.css")} />
<script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/js/app.js")}></script>
<script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/js/root.js")}></script>
<!-- Favicon -->
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png">
@@ -16,148 +17,23 @@
<meta name="msapplication-config" content="/browserconfig.xml" />
<meta name="msapplication-TileColor" content="331700">
<meta name="theme-color" content="331700">
<!-- User Socket -->
<%= tag :meta, name: "user-token", content: Phoenix.Token.sign(@conn, "user auth", @current_user.id) %>
<!-- Notification Channel -->
<%= tag :meta, name: "channel-token", content: Phoenix.Token.sign(@conn, "channel auth", @current_user.id) %>
<!-- CSRF -->
<%= csrf_meta_tag() %>
</head>
<body>
<div id="app">
<nav id="navbar-main" class="navbar is-fixed-top">
<div class="navbar-brand">
<a class="navbar-item is-hidden-desktop jb-aside-mobile-toggle">
<span class="icon"><i class="mdi mdi-forwardburger mdi-24px"></i></span>
</a>
</div>
<div class="navbar-brand is-right">
<a class="navbar-item is-hidden-desktop jb-navbar-menu-toggle" data-target="navbar-menu">
<span class="icon"><i class="mdi mdi-dots-vertical"></i></span>
</a>
</div>
<div class="navbar-menu fadeIn animated faster" id="navbar-menu">
<div class="navbar-end">
<div class="navbar-item has-dropdown has-dropdown-with-icons has-divider is-hoverable">
<a class="navbar-link is-arrowless">
<div class="is-user-name"><span><%= @current_user.email %></span></div>
<span class="icon"><i class="mdi mdi-chevron-down"></i></span>
</a>
<div class="navbar-dropdown">
<%= link(to: Routes.setting_account_path(@conn, :show), class: "navbar-item") do %>
<span class="icon"><i class="mdi mdi-account"></i></span>
<span>Account Settings</span>
<% end %>
<hr class="navbar-divider">
<%= link(to: Routes.session_path(@conn, :delete), method: :delete, class: "navbar-item") do %>
<span class="icon"><i class="mdi mdi-logout"></i></span>
<span>Log Out</span>
<% end %>
</div>
</div>
<a href="https://docs.firez.one/?utm_source=docs" title="Documentation" class="navbar-item has-divider is-desktop-icon-only">
<span class="icon"><i class="mdi mdi-help-circle-outline"></i></span>
</a>
<a id="web-ui-connect-success" href="#" title="You are connected to the Firezone Web UI." class="navbar-item has-divider is-desktop-icon-only">
<span class="icon has-text-success"><i class="mdi mdi-wifi"></i></span>
</a>
<a id="web-ui-connect-error" href="#" title="You are disconnected from the Firezone Web UI." class="is-hidden navbar-item has-divider is-desktop-icon-only">
<span class="icon has-text-danger"><i class="mdi mdi-wifi-off"></i></span>
</a>
</div>
</div>
</nav>
<aside class="aside is-placed-left is-expanded">
<div class="aside-tools">
<div class="aside-tools-label">
<span>Firezone</span>
</div>
</div>
<div class="menu is-menu-main">
<p class="menu-label">Configuration</p>
<ul class="menu-list">
<li>
<%= link(to: Routes.user_index_path(@conn, :index), class: nav_class(@conn, ~r"/users")) do %>
<span class="icon"><i class="mdi mdi-account-group"></i></span>
<span class="menu-item-label">Users</span>
<% end %>
</li>
<li>
<%= link(to: Routes.device_index_path(@conn, :index), class: nav_class(@conn, ~r"/devices")) do %>
<span class="icon"><i class="mdi mdi-laptop"></i></span>
<span class="menu-item-label">Devices</span>
<% end %>
</li>
<li>
<%= link(to: Routes.rule_index_path(@conn, :index), class: nav_class(@conn, ~r"/rules")) do %>
<span class="icon"><i class="mdi mdi-traffic-light"></i></span>
<span class="menu-item-label">Rules</span>
<% end %>
</li>
</ul>
<p class="menu-label">Settings</p>
<ul class="menu-list">
<li>
<%= link(to: Routes.setting_default_path(@conn, :show), class: nav_class(@conn, ~r"/settings/default")) do %>
<span class="icon"><i class="mdi mdi-cog"></i></span>
<span class="menu-item-label">Defaults</span>
<% end %>
</li>
<li>
<%= link(to: Routes.setting_account_path(@conn, :show), class: nav_class(@conn, ~r"/settings/account")) do %>
<span class="icon"><i class="mdi mdi-account"></i></span>
<span class="menu-item-label">Account</span>
<% end %>
</li>
<li>
<%= link(to: Routes.setting_security_path(@conn, :show), class: nav_class(@conn, ~r"/settings/security")) do %>
<span class="icon"><i class="mdi mdi-lock"></i></span>
<span class="menu-item-label">Security</span>
<% end %>
</li>
</ul>
<p class="menu-label">Diagnostics</p>
<ul class="menu-list">
<li>
<%= link(to: Routes.connectivity_check_index_path(@conn, :index), class: nav_class(@conn, ~r"/diagnostics/connectivity_checks")) do %>
<span class="icon"><i class="mdi mdi-access-point"></i></span>
<span class="menu-item-label">WAN Connectivity</span>
<% end %>
</li>
</ul>
</div>
</aside>
<%= @inner_content %>
<footer class="footer">
<div class="container-fluid">
<div class="level">
<div class="level-left">
<div class="level-item">
<%= link(to: "mailto:" <> feedback_recipient()) do %>
Leave us feedback!
<% end %>
</div>
</div>
<div class="level-right">
<div class="level-item">
<a href={"https://github.com/firezone/firezone/tree/#{git_sha()}"}>
Version <%= application_version() %>
</a>
</div>
<div class="level-item">
<div class="logo">
<a href="https://firez.one"><img src="/images/logo.svg" alt="firez.one"></a>
<body>
<section class="section hero is-fullheight is-error-section">
<div id="app" class="hero-body">
<div class="container">
<div class="columns is-centered">
<div class="column is-two-thirds-tablet is-half-desktop is-one-third-widescreen">
<div class="block">
<div class="has-text-centered">
<img src={Routes.static_path(@conn, "/images/logo-text.svg")} alt="firez.one">
</div>
</div>
<%= @inner_content %>
</div>
</div>
</div>
</footer>
</div>
</div>
</section>
</body>
</html>

View File

@@ -4,10 +4,9 @@
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<%= csrf_meta_tag() %>
<%= live_title_tag assigns[:page_title] || "Firezone" %>
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/css/app.css")} />
<script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/js/auth.js")}></script>
<script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/js/unprivileged.js")}></script>
<!-- Favicon -->
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png">
@@ -17,19 +16,22 @@
<meta name="msapplication-config" content="/browserconfig.xml" />
<meta name="msapplication-TileColor" content="331700">
<meta name="theme-color" content="331700">
<%= render(FzHttpWeb.SharedView, "socket_token_headers.html", current_user: @current_user, conn: @conn) %>
</head>
<body>
<section class="section hero is-fullheight is-error-section">
<div id="app" class="hero-body">
<div class="container">
<div class="columns is-centered">
<div class="column is-two-thirds-tablet is-half-desktop is-one-third-widescreen">
<div class="column">
<div class="block">
<div class="has-text-centered">
<img src={Routes.static_path(@conn, "/images/logo-text.svg")} alt="firez.one">
</div>
</div>
<%= @inner_content %>
</div>
</div>
</div>

View File

@@ -0,0 +1,36 @@
<h3 class="is-3 title">Sign In</h3>
<hr>
<div class="content">
<p>
Please sign in via one of the methods below.
</p>
<%= if @local_enabled do %>
<p>
<%= link(
"Sign in with email",
to: Routes.auth_path(@conn, :request, "identity"),
class: "button") %>
</p>
<% end %>
<%= if @okta_enabled do %>
<p>
<%= link(
"Sign in with Okta",
to: Routes.auth_path(@conn, :request, "okta"),
class: "button") %>
</p>
<% end %>
<%= if @google_enabled do %>
<p>
<%= link(
"Sign in with Google",
to: Routes.auth_path(@conn, :request, "google"),
class: "button") %>
</p>
<% end %>
</div>

View File

@@ -1,37 +0,0 @@
<h3 class="is-3 title">Sign In</h3>
<hr>
<%= form_for @changeset, Routes.session_path(@conn, :create), fn f -> %>
<%= if assigns[:changeset] && @changeset.action do %>
<div class="block">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<div class="field">
<%= label(:session, :email, class: "label") %>
<div class="control">
<%= text_input(f, :email, class: "input #{input_error_class(f, :email)}") %>
</div>
<p class="help is-danger">
<%= error_tag f, :email %>
</p>
</div>
<div class="field">
<%= label(:session, :password, class: "label") %>
<div class="control">
<%= password_input(f, :password, class: "input #{input_error_class(f, :password)}") %>
</div>
<p class="help is-danger">
<%= error_tag f, :password %>
</p>
</div>
<div class="field">
<div class="control">
<%= submit "Sign In", class: "button" %>
</div>
</div>
<% end %>

View File

@@ -0,0 +1,66 @@
<table class="table is-bordered is-hoverable is-striped is-fullwidth">
<tbody>
<%= if has_role?(@current_user, :admin) do %>
<tr>
<td><strong>User</strong></td>
<td><%= link(@user.email, to: Routes.user_show_path(@socket, :show, @user)) %></td>
</tr>
<% end %>
<tr>
<td><strong>Name</strong></td>
<td><%= @device.name %></td>
</tr>
<%= if Application.fetch_env!(:fz_http, :wireguard_ipv4_enabled) do %>
<tr>
<td><strong>Interface IPv4</strong></td>
<td><%= @device.ipv4 %></td>
</tr>
<% end %>
<%= if Application.fetch_env!(:fz_http, :wireguard_ipv6_enabled) do %>
<tr>
<td><strong>Interface IPv6</strong></td>
<td><%= @device.ipv6 %></td>
</tr>
<% end %>
<tr>
<td><strong>Allowed IPs</strong></td>
<td><%= @allowed_ips || "None" %></td>
</tr>
<tr>
<td><strong>DNS Servers</strong></td>
<td><%= @dns || "None" %></td>
</tr>
<tr>
<td><strong>Endpoint</strong></td>
<td><%= @endpoint %>:<%= @port %></td>
</tr>
<tr>
<td><strong>Persistent Keepalive</strong></td>
<td>
<%= if @persistent_keepalive == 0 do %>
Disabled
<% else %>
Every <%= @persistent_keepalive %> seconds
<% end %>
</td>
</tr>
<tr>
<td><strong>MTU</strong></td>
<td><%= @mtu %></td>
</tr>
<tr>
<td><strong>Public key</strong></td>
<td class="code"><%= @device.public_key %></td>
</tr>
</tbody>
</table>

View File

@@ -12,7 +12,7 @@
<%= for device <- @devices do %>
<tr>
<td>
<%= live_patch(device.name, to: Routes.device_show_path(@socket, :show, device)) %>
<%= live_patch(device.name, to: Routes.device_admin_show_path(@socket, :show, device)) %>
</td>
<td class="code">
<span><%= device.ipv4 %></span>

View File

@@ -0,0 +1,23 @@
<section class="section is-main-section">
<%= render FzHttpWeb.SharedView, "flash.html", assigns %>
<h4 class="title is-4">Details</h4>
<%= render(FzHttpWeb.SharedView, "device_details.html", assigns) %>
</section>
<section class="section is-main-section">
<h4 class="title is-4">
Danger Zone
</h4>
<button class="button is-danger"
phx-click="delete_device"
phx-value-device_id={@device.id}
data-confirm="Are you sure? This will immediately disconnect this device and remove all associated data.">
<span class="icon is-small">
<i class="fas fa-trash"></i>
</span>
<span>Delete Device <%= @device.name %></span>
</button>
</section>

View File

@@ -0,0 +1,8 @@
<!-- User Socket -->
<%= tag :meta, name: "user-token", content: Phoenix.Token.sign(@conn, "user auth", @current_user.id) %>
<!-- Notification Channel -->
<%= tag :meta, name: "channel-token", content: Phoenix.Token.sign(@conn, "channel auth", @current_user.id) %>
<!-- CSRF -->
<%= csrf_meta_tag() %>

View File

@@ -0,0 +1,10 @@
<div class="field">
<div class="control">
<div class="level">
<div class="level-left"></div>
<div class="level-right">
<%= submit assigns[:button_text] || "Save", phx_disable_with: "Saving...", class: "button is-primary" %>
</div>
</div>
</div>
</div>

View File

@@ -6,7 +6,7 @@
</p>
<p>
Please
<%= link("reauthenticate", to: Routes.session_path(@conn, :delete), method: :delete) %>
<%= link("reauthenticate", to: Routes.auth_path(@conn, :delete), method: :delete) %>
to renew your VPN session.
</p>
<% else %>
@@ -19,7 +19,7 @@
<span data-timestamp={vpn_expires_at(@user)}>...</span>
</strong>
</p>
<%= link("Reauthenticate", to: Routes.session_path(@conn, :delete), method: :delete) %>
<%= link("Reauthenticate", to: Routes.auth_path(@conn, :delete), method: :delete) %>
to renew your VPN session.
<% else %>
Your VPN session is active indefinitely.
@@ -31,7 +31,7 @@
</div>
<div class="block">
<%= link(to: Routes.session_path(@conn, :delete), method: :delete) do %>
<%= link(to: Routes.auth_path(@conn, :delete), method: :delete) do %>
Sign out
<% end %>
</div>

View File

@@ -0,0 +1,27 @@
defmodule FzHttpWeb.UserFromAuth do
@moduledoc """
Authenticates users.
"""
alias FzHttp.Users
alias FzHttpWeb.Authentication
alias Ueberauth.Auth
def find_or_create(
%Auth{
provider: :identity,
info: %Auth.Info{email: email},
credentials: %Auth.Credentials{other: %{password: password}}
} = _auth
) do
Users.get_by_email(email) |> Authentication.authenticate(password)
end
def find_or_create(%Auth{provider: provider, info: %Auth.Info{email: email}} = _auth)
when provider in [:google, :okta] do
case Users.get_by_email(email) do
nil -> Users.create_unprivileged_user(%{email: email})
user -> {:ok, user}
end
end
end

Some files were not shown because too many files have changed in this diff Show More