mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 18:18:55 +00:00
0.3.0 (#465)
* 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:
@@ -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`
|
||||
|
||||
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -195,6 +195,7 @@ jobs:
|
||||
if: always()
|
||||
run: |
|
||||
sudo scripts/uninstall.sh
|
||||
sudo rm -rf /tmp/firezone*
|
||||
rm -rf omnibus/pkg/*
|
||||
|
||||
publish:
|
||||
|
||||
19
apps/fz_http/assets/js/admin.js
Normal file
19
apps/fz_http/assets/js/admin.js
Normal 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"
|
||||
14
apps/fz_http/assets/js/crypto.js
Normal file
14
apps/fz_http/assets/js/crypto.js
Normal 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 }
|
||||
@@ -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()
|
||||
})
|
||||
@@ -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', () => {
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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 }
|
||||
11
apps/fz_http/assets/js/root.js
Normal file
11
apps/fz_http/assets/js/root.js
Normal 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'
|
||||
11
apps/fz_http/assets/js/unprivileged.js
Normal file
11
apps/fz_http/assets/js/unprivileged.js
Normal 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"
|
||||
68
apps/fz_http/assets/js/wg_conf.js
Normal file
68
apps/fz_http/assets/js/wg_conf.js
Normal 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 }
|
||||
56
apps/fz_http/assets/package-lock.json
generated
56
apps/fz_http/assets/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
72
apps/fz_http/lib/fz_http/sites.ex
Normal file
72
apps/fz_http/lib/fz_http/sites.ex
Normal 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
|
||||
73
apps/fz_http/lib/fz_http/sites/site.ex
Normal file
73
apps/fz_http/lib/fz_http/sites/site.ex
Normal 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
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,7 @@ defmodule FzHttp.VpnSessionScheduler do
|
||||
"""
|
||||
use GenServer
|
||||
|
||||
alias FzHttpWeb.Events
|
||||
alias FzHttp.Events
|
||||
|
||||
# 1 minute
|
||||
@interval 60 * 1_000
|
||||
|
||||
@@ -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
|
||||
|
||||
75
apps/fz_http/lib/fz_http_web/authentication.ex
Normal file
75
apps/fz_http/lib/fz_http_web/authentication.ex
Normal 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
|
||||
33
apps/fz_http/lib/fz_http_web/authentication/error_handler.ex
Normal file
33
apps/fz_http/lib/fz_http_web/authentication/error_handler.ex
Normal 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
|
||||
15
apps/fz_http/lib/fz_http_web/authentication/pipeline.ex
Normal file
15
apps/fz_http/lib/fz_http_web/authentication/pipeline.ex
Normal 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
|
||||
34
apps/fz_http/lib/fz_http_web/authorization_helpers.ex
Normal file
34
apps/fz_http/lib/fz_http_web/authorization_helpers.ex
Normal 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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
51
apps/fz_http/lib/fz_http_web/controllers/auth_controller.ex
Normal file
51
apps/fz_http/lib/fz_http_web/controllers/auth_controller.ex
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
<%= render FzHttpWeb.SharedView, "heading.html", page_title: "Devices |> #{@page_title}" %>
|
||||
<%= render FzHttpWeb.SharedView, "show_device.html", assigns %>
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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 %>
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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>
|
||||
51
apps/fz_http/lib/fz_http_web/live/setting_live/site_live.ex
Normal file
51
apps/fz_http/lib/fz_http_web/live/setting_live/site_live.ex
Normal 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
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
21
apps/fz_http/lib/fz_http_web/live_auth.ex
Normal file
21
apps/fz_http/lib/fz_http_web/live_auth.ex
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
35
apps/fz_http/lib/fz_http_web/plug/authorization.ex
Normal file
35
apps/fz_http/lib/fz_http_web/plug/authorization.ex
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
155
apps/fz_http/lib/fz_http_web/templates/layout/admin.html.heex
Normal file
155
apps/fz_http/lib/fz_http_web/templates/layout/admin.html.heex
Normal 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>
|
||||
@@ -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>
|
||||
@@ -1,8 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="<%= Routes.static_path(FzHttpWeb.Endpoint, "/css/email.css") %>">
|
||||
</head>
|
||||
<body>
|
||||
<%= @inner_content %>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href={Routes.static_path(FzHttpWeb.Endpoint, "/css/email.css")}>
|
||||
</head>
|
||||
<body>
|
||||
<%= @inner_content %>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
36
apps/fz_http/lib/fz_http_web/templates/root/auth.html.heex
Normal file
36
apps/fz_http/lib/fz_http_web/templates/root/auth.html.heex
Normal 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>
|
||||
@@ -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 %>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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() %>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
27
apps/fz_http/lib/fz_http_web/user_from_auth.ex
Normal file
27
apps/fz_http/lib/fz_http_web/user_from_auth.ex
Normal 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
Reference in New Issue
Block a user