From a91c2db887000fa1bc17b2a20d494542fe00ce5e Mon Sep 17 00:00:00 2001 From: Jamil Date: Fri, 4 Mar 2022 09:32:13 -0800 Subject: [PATCH] 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) --- .ci/functional_test.sh | 27 +- .github/workflows/ci.yml | 1 + apps/fz_http/assets/js/admin.js | 19 + apps/fz_http/assets/js/crypto.js | 14 + apps/fz_http/assets/js/device_config.js | 14 - .../assets/js/{auth.js => event_listeners.js} | 13 +- apps/fz_http/assets/js/hooks.js | 22 +- .../assets/js/{app.js => live_view.js} | 45 +- apps/fz_http/assets/js/qrcode.js | 20 - apps/fz_http/assets/js/root.js | 11 + apps/fz_http/assets/js/unprivileged.js | 11 + apps/fz_http/assets/js/wg_conf.js | 68 +++ apps/fz_http/assets/package-lock.json | 56 +- apps/fz_http/assets/package.json | 4 +- apps/fz_http/assets/webpack.config.js | 9 +- apps/fz_http/lib/fz_http/devices.ex | 126 +---- apps/fz_http/lib/fz_http/devices/device.ex | 97 ++-- .../lib/{fz_http_web => fz_http}/events.ex | 10 +- apps/fz_http/lib/fz_http/macros.ex | 20 - apps/fz_http/lib/fz_http/sessions.ex | 51 -- apps/fz_http/lib/fz_http/settings.ex | 107 ---- apps/fz_http/lib/fz_http/settings/setting.ex | 104 ---- apps/fz_http/lib/fz_http/sites.ex | 72 +++ apps/fz_http/lib/fz_http/sites/site.ex | 73 +++ apps/fz_http/lib/fz_http/users.ex | 32 +- apps/fz_http/lib/fz_http/users/session.ex | 49 -- apps/fz_http/lib/fz_http/users/user.ex | 6 +- .../lib/fz_http/vpn_session_scheduler.ex | 2 +- apps/fz_http/lib/fz_http_web.ex | 4 + .../fz_http/lib/fz_http_web/authentication.ex | 75 +++ .../authentication/error_handler.ex | 33 ++ .../fz_http_web/authentication/pipeline.ex | 15 + .../lib/fz_http_web/authorization_helpers.ex | 34 ++ .../channels/notification_channel.ex | 1 + .../lib/fz_http_web/controller_helpers.ex | 89 +--- .../controllers/auth_controller.ex | 51 ++ .../controllers/device_controller.ex | 42 -- .../controllers/root_controller.ex | 15 +- .../controllers/session_controller.ex | 83 --- .../controllers/user_controller.ex | 26 +- apps/fz_http/lib/fz_http_web/endpoint.ex | 2 +- .../connectivity_check_live/index_live.ex | 17 +- .../live/device_live/admin/index.html.heex | 14 + .../live/device_live/admin/index_live.ex | 23 + .../live/device_live/admin/show.html.heex | 2 + .../live/device_live/admin/show_live.ex | 63 +++ .../live/device_live/create_form_component.ex | 33 -- .../create_form_component.html.heex | 27 - .../live/device_live/form_component.ex | 57 --- .../live/device_live/form_component.html.heex | 193 ------- .../live/device_live/index.html.heex | 23 - .../live/device_live/index_live.ex | 64 --- .../live/device_live/new_form_component.ex | 91 ++++ .../device_live/new_form_component.html.heex | 260 ++++++++++ .../live/device_live/show.html.heex | 217 -------- .../fz_http_web/live/device_live/show_live.ex | 102 ---- .../device_live/unprivileged/index.html.heex | 70 +++ .../device_live/unprivileged/index_live.ex | 30 ++ .../device_live/unprivileged/show.html.heex | 4 + .../device_live/unprivileged/show_live.ex | 71 +++ .../fz_http_web/live/rule_live/index_live.ex | 15 +- .../account_form_component.html.heex | 11 +- .../live/setting_live/account_live.ex | 17 +- .../live/setting_live/default.html.heex | 55 -- .../setting_live/default_form_component.ex | 62 --- .../default_form_component.html.heex | 26 - .../live/setting_live/default_live.ex | 84 --- .../live/setting_live/security_live.ex | 59 +-- .../live/setting_live/site.html.heex | 21 + .../live/setting_live/site_form_component.ex | 32 ++ .../site_form_component.html.heex | 100 ++++ .../live/setting_live/site_live.ex | 51 ++ .../live/user_live/index.html.heex | 2 + .../fz_http_web/live/user_live/index_live.ex | 18 +- .../fz_http_web/live/user_live/show.html.heex | 29 +- .../fz_http_web/live/user_live/show_live.ex | 67 +-- apps/fz_http/lib/fz_http_web/live_auth.ex | 21 + apps/fz_http/lib/fz_http_web/live_helpers.ex | 47 -- apps/fz_http/lib/fz_http_web/mock_events.ex | 4 - .../lib/fz_http_web/plug/authorization.ex | 35 ++ apps/fz_http/lib/fz_http_web/router.ex | 137 +++-- apps/fz_http/lib/fz_http_web/session.ex | 3 +- .../templates/auth/request.html.heex | 31 ++ .../templates/device/config.html.heex | 31 -- .../templates/layout/admin.html.heex | 155 ++++++ .../layout/{app.html.eex => app.html.heex} | 0 .../templates/layout/device_config.html.heex | 35 -- .../templates/layout/email.html.eex | 8 - .../templates/layout/email.html.heex | 8 + .../templates/layout/root.html.heex | 158 +----- ...{auth.html.heex => unprivileged.html.heex} | 8 +- .../fz_http_web/templates/root/auth.html.heex | 36 ++ .../templates/session/new.html.heex | 37 -- .../templates/shared/device_details.html.heex | 66 +++ .../templates/shared/devices_table.html.heex | 2 +- .../templates/shared/show_device.html.heex | 23 + .../shared/socket_token_headers.html.heex | 8 + .../templates/shared/submit_button.html.heex | 10 + .../fz_http_web/templates/user/show.html.heex | 6 +- .../fz_http/lib/fz_http_web/user_from_auth.ex | 27 + .../lib/fz_http_web/views/auth_view.ex | 3 + .../lib/fz_http_web/views/root_view.ex | 3 + .../lib/fz_http_web/views/session_view.ex | 3 - .../lib/fz_http_web/views/user_view.ex | 10 +- apps/fz_http/mix.exs | 13 +- .../20220208184257_settings_to_sites.exs | 89 ++++ ...9005201_rename_use_default_to_use_site.exs | 11 + .../20220211201727_remove_private_keys.exs | 12 + .../20220219165023_add_key_regenerated_at.exs | 9 + ...5313_add_last_signed_in_method_to_user.exs | 9 + apps/fz_http/priv/repo/seeds.exs | 6 +- apps/fz_http/test/fz_http/devices_test.exs | 89 +++- .../{fz_http_web => fz_http}/events_test.exs | 11 +- apps/fz_http/test/fz_http/sessions_test.exs | 56 -- apps/fz_http/test/fz_http/settings_test.exs | 91 ---- apps/fz_http/test/fz_http/sites_test.exs | 70 +++ .../test/fz_http_web/authentication_test.exs | 28 + .../controllers/device_controller_test.exs | 44 -- .../controllers/session_controller_test.exs | 84 ++- .../controllers/user_controller_test.exs | 15 +- .../connectivity_check_live/index_test.exs | 4 +- .../live/device_live/admin/index_test.exs | 33 ++ .../live/device_live/admin/show_test.exs | 29 ++ .../live/device_live/index_test.exs | 53 -- .../live/device_live/show_test.exs | 365 -------------- .../device_live/unprivileged/index_test.exs | 66 +++ .../fz_http_web/live/rule_live/index_test.exs | 12 +- .../live/setting_live/account_test.exs | 14 +- .../live/setting_live/default_test.exs | 177 ------- .../live/setting_live/security_test.exs | 4 +- .../live/setting_live/site_test.exs | 202 ++++++++ .../fz_http_web/live/user_live/index_test.exs | 14 +- .../fz_http_web/live/user_live/show_test.exs | 477 ++++++++++++++++-- apps/fz_http/test/support/conn_case.ex | 40 +- .../test/support/fixtures/devices_fixtures.ex | 12 +- .../support/fixtures/sessions_fixtures.ex | 15 - .../support/fixtures/settings_fixtures.ex | 15 - .../test/support/fixtures/sites_fixtures.ex | 15 + .../test/support/fixtures/users_fixtures.ex | 9 +- apps/fz_http/test/support/test_helpers.ex | 21 +- apps/fz_vpn/lib/fz_vpn/cli/live.ex | 23 +- apps/fz_vpn/lib/fz_vpn/cli/sandbox.ex | 7 +- apps/fz_vpn/lib/fz_vpn/server.ex | 38 +- apps/fz_vpn/test/fz_vpn/cli/sandbox_test.exs | 15 - apps/fz_vpn/test/fz_vpn/server_test.exs | 11 +- config/config.exs | 9 +- config/dev.exs | 44 +- config/prod.exs | 10 + config/releases.exs | 62 +++ config/test.exs | 10 + .../administer/security-considerations.md | 48 +- docs/docs/deploy/resource-requirements.md | 3 - docs/docs/reference/configuration-file.md | 1 + docs/docs/user-guides/split-tunnel.md | 4 +- mix.lock | 25 +- .../cookbooks/firezone/attributes/default.rb | 34 ++ .../cookbooks/firezone/libraries/config.rb | 43 +- scripts/generate_keypairs.sh | 9 - 158 files changed, 3651 insertions(+), 3437 deletions(-) create mode 100644 apps/fz_http/assets/js/admin.js create mode 100644 apps/fz_http/assets/js/crypto.js delete mode 100644 apps/fz_http/assets/js/device_config.js rename apps/fz_http/assets/js/{auth.js => event_listeners.js} (60%) rename apps/fz_http/assets/js/{app.js => live_view.js} (70%) delete mode 100644 apps/fz_http/assets/js/qrcode.js create mode 100644 apps/fz_http/assets/js/root.js create mode 100644 apps/fz_http/assets/js/unprivileged.js create mode 100644 apps/fz_http/assets/js/wg_conf.js rename apps/fz_http/lib/{fz_http_web => fz_http}/events.ex (81%) delete mode 100644 apps/fz_http/lib/fz_http/macros.ex delete mode 100644 apps/fz_http/lib/fz_http/sessions.ex delete mode 100644 apps/fz_http/lib/fz_http/settings.ex delete mode 100644 apps/fz_http/lib/fz_http/settings/setting.ex create mode 100644 apps/fz_http/lib/fz_http/sites.ex create mode 100644 apps/fz_http/lib/fz_http/sites/site.ex delete mode 100644 apps/fz_http/lib/fz_http/users/session.ex create mode 100644 apps/fz_http/lib/fz_http_web/authentication.ex create mode 100644 apps/fz_http/lib/fz_http_web/authentication/error_handler.ex create mode 100644 apps/fz_http/lib/fz_http_web/authentication/pipeline.ex create mode 100644 apps/fz_http/lib/fz_http_web/authorization_helpers.ex create mode 100644 apps/fz_http/lib/fz_http_web/controllers/auth_controller.ex delete mode 100644 apps/fz_http/lib/fz_http_web/controllers/device_controller.ex delete mode 100644 apps/fz_http/lib/fz_http_web/controllers/session_controller.ex create mode 100644 apps/fz_http/lib/fz_http_web/live/device_live/admin/index.html.heex create mode 100644 apps/fz_http/lib/fz_http_web/live/device_live/admin/index_live.ex create mode 100644 apps/fz_http/lib/fz_http_web/live/device_live/admin/show.html.heex create mode 100644 apps/fz_http/lib/fz_http_web/live/device_live/admin/show_live.ex delete mode 100644 apps/fz_http/lib/fz_http_web/live/device_live/create_form_component.ex delete mode 100644 apps/fz_http/lib/fz_http_web/live/device_live/create_form_component.html.heex delete mode 100644 apps/fz_http/lib/fz_http_web/live/device_live/form_component.ex delete mode 100644 apps/fz_http/lib/fz_http_web/live/device_live/form_component.html.heex delete mode 100644 apps/fz_http/lib/fz_http_web/live/device_live/index.html.heex delete mode 100644 apps/fz_http/lib/fz_http_web/live/device_live/index_live.ex create mode 100644 apps/fz_http/lib/fz_http_web/live/device_live/new_form_component.ex create mode 100644 apps/fz_http/lib/fz_http_web/live/device_live/new_form_component.html.heex delete mode 100644 apps/fz_http/lib/fz_http_web/live/device_live/show.html.heex delete mode 100644 apps/fz_http/lib/fz_http_web/live/device_live/show_live.ex create mode 100644 apps/fz_http/lib/fz_http_web/live/device_live/unprivileged/index.html.heex create mode 100644 apps/fz_http/lib/fz_http_web/live/device_live/unprivileged/index_live.ex create mode 100644 apps/fz_http/lib/fz_http_web/live/device_live/unprivileged/show.html.heex create mode 100644 apps/fz_http/lib/fz_http_web/live/device_live/unprivileged/show_live.ex delete mode 100644 apps/fz_http/lib/fz_http_web/live/setting_live/default.html.heex delete mode 100644 apps/fz_http/lib/fz_http_web/live/setting_live/default_form_component.ex delete mode 100644 apps/fz_http/lib/fz_http_web/live/setting_live/default_form_component.html.heex delete mode 100644 apps/fz_http/lib/fz_http_web/live/setting_live/default_live.ex create mode 100644 apps/fz_http/lib/fz_http_web/live/setting_live/site.html.heex create mode 100644 apps/fz_http/lib/fz_http_web/live/setting_live/site_form_component.ex create mode 100644 apps/fz_http/lib/fz_http_web/live/setting_live/site_form_component.html.heex create mode 100644 apps/fz_http/lib/fz_http_web/live/setting_live/site_live.ex create mode 100644 apps/fz_http/lib/fz_http_web/live_auth.ex create mode 100644 apps/fz_http/lib/fz_http_web/plug/authorization.ex create mode 100644 apps/fz_http/lib/fz_http_web/templates/auth/request.html.heex delete mode 100644 apps/fz_http/lib/fz_http_web/templates/device/config.html.heex create mode 100644 apps/fz_http/lib/fz_http_web/templates/layout/admin.html.heex rename apps/fz_http/lib/fz_http_web/templates/layout/{app.html.eex => app.html.heex} (100%) delete mode 100644 apps/fz_http/lib/fz_http_web/templates/layout/device_config.html.heex delete mode 100644 apps/fz_http/lib/fz_http_web/templates/layout/email.html.eex create mode 100644 apps/fz_http/lib/fz_http_web/templates/layout/email.html.heex rename apps/fz_http/lib/fz_http_web/templates/layout/{auth.html.heex => unprivileged.html.heex} (87%) create mode 100644 apps/fz_http/lib/fz_http_web/templates/root/auth.html.heex delete mode 100644 apps/fz_http/lib/fz_http_web/templates/session/new.html.heex create mode 100644 apps/fz_http/lib/fz_http_web/templates/shared/device_details.html.heex create mode 100644 apps/fz_http/lib/fz_http_web/templates/shared/show_device.html.heex create mode 100644 apps/fz_http/lib/fz_http_web/templates/shared/socket_token_headers.html.heex create mode 100644 apps/fz_http/lib/fz_http_web/templates/shared/submit_button.html.heex create mode 100644 apps/fz_http/lib/fz_http_web/user_from_auth.ex create mode 100644 apps/fz_http/lib/fz_http_web/views/auth_view.ex create mode 100644 apps/fz_http/lib/fz_http_web/views/root_view.ex delete mode 100644 apps/fz_http/lib/fz_http_web/views/session_view.ex create mode 100644 apps/fz_http/priv/repo/migrations/20220208184257_settings_to_sites.exs create mode 100644 apps/fz_http/priv/repo/migrations/20220209005201_rename_use_default_to_use_site.exs create mode 100644 apps/fz_http/priv/repo/migrations/20220211201727_remove_private_keys.exs create mode 100644 apps/fz_http/priv/repo/migrations/20220219165023_add_key_regenerated_at.exs create mode 100644 apps/fz_http/priv/repo/migrations/20220227215313_add_last_signed_in_method_to_user.exs rename apps/fz_http/test/{fz_http_web => fz_http}/events_test.exs (91%) delete mode 100644 apps/fz_http/test/fz_http/sessions_test.exs delete mode 100644 apps/fz_http/test/fz_http/settings_test.exs create mode 100644 apps/fz_http/test/fz_http/sites_test.exs create mode 100644 apps/fz_http/test/fz_http_web/authentication_test.exs delete mode 100644 apps/fz_http/test/fz_http_web/controllers/device_controller_test.exs create mode 100644 apps/fz_http/test/fz_http_web/live/device_live/admin/index_test.exs create mode 100644 apps/fz_http/test/fz_http_web/live/device_live/admin/show_test.exs delete mode 100644 apps/fz_http/test/fz_http_web/live/device_live/index_test.exs delete mode 100644 apps/fz_http/test/fz_http_web/live/device_live/show_test.exs create mode 100644 apps/fz_http/test/fz_http_web/live/device_live/unprivileged/index_test.exs delete mode 100644 apps/fz_http/test/fz_http_web/live/setting_live/default_test.exs create mode 100644 apps/fz_http/test/fz_http_web/live/setting_live/site_test.exs delete mode 100644 apps/fz_http/test/support/fixtures/sessions_fixtures.ex delete mode 100644 apps/fz_http/test/support/fixtures/settings_fixtures.ex create mode 100644 apps/fz_http/test/support/fixtures/sites_fixtures.ex delete mode 100755 scripts/generate_keypairs.sh diff --git a/.ci/functional_test.sh b/.ci/functional_test.sh index a6fc752ce..f626bdf58 100755 --- a/.ci/functional_test.sh +++ b/.ci/functional_test.sh @@ -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 '' - +echo $page | grep 'Sign in with email' echo "Testing telemetry_id survives reconfigures" tid1=`sudo cat /var/opt/firezone/cache/telemetry_id` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9e376f9e..067f2dc99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -195,6 +195,7 @@ jobs: if: always() run: | sudo scripts/uninstall.sh + sudo rm -rf /tmp/firezone* rm -rf omnibus/pkg/* publish: diff --git a/apps/fz_http/assets/js/admin.js b/apps/fz_http/assets/js/admin.js new file mode 100644 index 000000000..254ee9bdc --- /dev/null +++ b/apps/fz_http/assets/js/admin.js @@ -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" diff --git a/apps/fz_http/assets/js/crypto.js b/apps/fz_http/assets/js/crypto.js new file mode 100644 index 000000000..298bcb81e --- /dev/null +++ b/apps/fz_http/assets/js/crypto.js @@ -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 } diff --git a/apps/fz_http/assets/js/device_config.js b/apps/fz_http/assets/js/device_config.js deleted file mode 100644 index d1c7beaf5..000000000 --- a/apps/fz_http/assets/js/device_config.js +++ /dev/null @@ -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() -}) diff --git a/apps/fz_http/assets/js/auth.js b/apps/fz_http/assets/js/event_listeners.js similarity index 60% rename from apps/fz_http/assets/js/auth.js rename to apps/fz_http/assets/js/event_listeners.js index 85d04d301..3d572f166 100644 --- a/apps/fz_http/assets/js/auth.js +++ b/apps/fz_http/assets/js/event_listeners.js @@ -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', () => { diff --git a/apps/fz_http/assets/js/hooks.js b/apps/fz_http/assets/js/hooks.js index d4d3a0018..0f4e5a0ca 100644 --- a/apps/fz_http/assets/js/hooks.js +++ b/apps/fz_http/assets/js/hooks.js @@ -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 diff --git a/apps/fz_http/assets/js/app.js b/apps/fz_http/assets/js/live_view.js similarity index 70% rename from apps/fz_http/assets/js/app.js rename to apps/fz_http/assets/js/live_view.js index cf1bb9d23..ca36a9a99 100644 --- a/apps/fz_http/assets/js/app.js +++ b/apps/fz_http/assets/js/live_view.js @@ -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) - }) - }) -}) diff --git a/apps/fz_http/assets/js/qrcode.js b/apps/fz_http/assets/js/qrcode.js deleted file mode 100644 index 54b0f729b..000000000 --- a/apps/fz_http/assets/js/qrcode.js +++ /dev/null @@ -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 } diff --git a/apps/fz_http/assets/js/root.js b/apps/fz_http/assets/js/root.js new file mode 100644 index 000000000..ba14e1a6b --- /dev/null +++ b/apps/fz_http/assets/js/root.js @@ -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' diff --git a/apps/fz_http/assets/js/unprivileged.js b/apps/fz_http/assets/js/unprivileged.js new file mode 100644 index 000000000..bb60e6d35 --- /dev/null +++ b/apps/fz_http/assets/js/unprivileged.js @@ -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" diff --git a/apps/fz_http/assets/js/wg_conf.js b/apps/fz_http/assets/js/wg_conf.js new file mode 100644 index 000000000..89a674c91 --- /dev/null +++ b/apps/fz_http/assets/js/wg_conf.js @@ -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 = + `

+ Error generating configuration. Could not load private key from + sessionStorage. Close window and try again. If the issue persists, + please contact support@firez.one. +

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

+ Devices can be added when viewing a User. + <%= link("Go to users ->", to: Routes.user_index_path(@socket, :index)) %> +

+
diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/admin/index_live.ex b/apps/fz_http/lib/fz_http_web/live/device_live/admin/index_live.ex new file mode 100644 index 000000000..9c2339371 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/live/device_live/admin/index_live.ex @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/admin/show.html.heex b/apps/fz_http/lib/fz_http_web/live/device_live/admin/show.html.heex new file mode 100644 index 000000000..1dccf34ca --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/live/device_live/admin/show.html.heex @@ -0,0 +1,2 @@ +<%= render FzHttpWeb.SharedView, "heading.html", page_title: "Devices |> #{@page_title}" %> +<%= render FzHttpWeb.SharedView, "show_device.html", assigns %> diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/admin/show_live.ex b/apps/fz_http/lib/fz_http_web/live/device_live/admin/show_live.ex new file mode 100644 index 000000000..3c3aa40af --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/live/device_live/admin/show_live.ex @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/create_form_component.ex b/apps/fz_http/lib/fz_http_web/live/device_live/create_form_component.ex deleted file mode 100644 index 7f8738313..000000000 --- a/apps/fz_http/lib/fz_http_web/live/device_live/create_form_component.ex +++ /dev/null @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/create_form_component.html.heex b/apps/fz_http/lib/fz_http_web/live/device_live/create_form_component.html.heex deleted file mode 100644 index a624ad92f..000000000 --- a/apps/fz_http/lib/fz_http_web/live/device_live/create_form_component.html.heex +++ /dev/null @@ -1,27 +0,0 @@ -
- <.form let={f} for={@changeset} id="add-device" phx-target={@myself} phx-submit="save"> -
- <%= label f, :user_id, "User", class: "label" %> -
- <%= select f, :user_id, @options_for_select, class: "input" %> -
-

- <%= error_tag f, :user_id %> -

-

- Select the user who this device should belong to. -

-
- -
-
-
-
-
- <%= submit "Save", phx_disable_with: "Saving...", class: "button is-primary" %> -
-
-
-
- -
diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/form_component.ex b/apps/fz_http/lib/fz_http_web/live/device_live/form_component.ex deleted file mode 100644 index 22de9d797..000000000 --- a/apps/fz_http/lib/fz_http_web/live/device_live/form_component.ex +++ /dev/null @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/form_component.html.heex b/apps/fz_http/lib/fz_http_web/live/device_live/form_component.html.heex deleted file mode 100644 index 64e031ad2..000000000 --- a/apps/fz_http/lib/fz_http_web/live/device_live/form_component.html.heex +++ /dev/null @@ -1,193 +0,0 @@ -
- <.form let={f} for={@changeset} id="edit-device" phx-change="change" phx-target={@myself} phx-submit="save"> -
- <%= label f, :name, class: "label" %> -
- <%= text_input f, :name, class: "input #{input_error_class(f, :name)}" %> -
-

- <%= error_tag f, :name %> -

-
- -
- <%= label f, :use_default_allowed_ips, "Use Default Allowed IPs", class: "label" %> -
- - -
-

- Default: <%= @default_device_allowed_ips %> -

-
- -
- <%= label f, :allowed_ips, "Allowed IPs", class: "label" %> -
- <%= text_input f, :allowed_ips, class: "input #{input_error_class(f, :allowed_ips)}", - disabled: @use_default_allowed_ips %> -
-

- <%= error_tag f, :allowed_ips %> -

-
- -
- <%= label f, :use_default_dns, "Use Default DNS Servers", class: "label" %> -
- - -
-

- Default: <%= @default_device_dns %> -

-
- -
- <%= label f, :dns, "DNS Servers", class: "label" %> -
- <%= text_input f, :dns, class: "input #{input_error_class(f, :dns)}", - disabled: @use_default_dns %> -
-

- <%= error_tag f, :dns %> -

-
- -
- <%= label f, :use_default_endpoint, "Use Default Endpoint", class: "label" %> -
- - -
-

- Default: <%= @default_device_endpoint %> -

-
- -
- <%= label f, :endpoint, "Server Endpoint", class: "label" %> -

The IP of the server this device should connect to.

-
- <%= text_input f, :endpoint, class: "input #{input_error_class(f, :endpoint)}", - disabled: @use_default_endpoint %> -
-

- <%= error_tag f, :endpoint %> -

-
- -
- <%= label f, :use_default_mtu, "Use Default MTU", class: "label" %> -
- - -
-

- Default: <%= @default_device_mtu %> -

-
- -
- <%= label f, :mtu, "Interface MTU", class: "label" %> -

The WireGuard interface MTU for this Device.

-
- <%= text_input f, :mtu, class: "input #{input_error_class(f, :mtu)}", disabled: @use_default_mtu %> -
-

- <%= error_tag f, :mtu %> -

-
- -
- <%= label f, :use_default_persistent_keepalive, "Use Default Persistent Keepalive", class: "label" %> -
- - -
-

- Default: <%= @default_device_persistent_keepalive %> -

-
- -
- <%= label f, :persistent_keepalive, "Persistent Keepalive", class: "label" %> -

- Interval for WireGuard - - persistent keepalive. A value of 0 disables this. Leave this disabled - unless you're experiencing NAT or firewall traversal problems. -

-
- <%= text_input f, :persistent_keepalive, class: "input #{input_error_class(f, :persistent_keepalive)}", - disabled: @use_default_persistent_keepalive %> -
-

- <%= error_tag f, :persistent_keepalive %> -

-
- -
- <%= label f, :ipv4, "IPv4 Address", class: "label" %> - -
- <%= text_input f, :ipv4, class: "input #{input_error_class(f, :ipv4)}" %> -
-

- <%= error_tag f, :ipv4 %> -

-
- -
- <%= label f, :ipv6, "IPv6 Address", class: "label" %> - -
- <%= text_input f, :ipv6, class: "input #{input_error_class(f, :ipv6)}" %> -
-

- <%= error_tag f, :ipv6 %> -

-
- -
-
-
-
-
- <%= submit "Save", phx_disable_with: "Saving...", class: "button is-primary" %> -
-
-
-
- -
diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/index.html.heex b/apps/fz_http/lib/fz_http_web/live/device_live/index.html.heex deleted file mode 100644 index 847467b8f..000000000 --- a/apps/fz_http/lib/fz_http_web/live/device_live/index.html.heex +++ /dev/null @@ -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 %> - -
- <%= render FzHttpWeb.SharedView, "flash.html", assigns %> - -
- <%= render FzHttpWeb.SharedView, "devices_table.html", devices: @devices, socket: @socket %> -
- - -
diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/index_live.ex b/apps/fz_http/lib/fz_http_web/live/device_live/index_live.ex deleted file mode 100644 index 1764bfa52..000000000 --- a/apps/fz_http/lib/fz_http_web/live/device_live/index_live.ex +++ /dev/null @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/new_form_component.ex b/apps/fz_http/lib/fz_http_web/live/device_live/new_form_component.ex new file mode 100644 index 000000000..c9d081d73 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/live/device_live/new_form_component.ex @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/new_form_component.html.heex b/apps/fz_http/lib/fz_http_web/live/device_live/new_form_component.html.heex new file mode 100644 index 000000000..eaf7076bf --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/live/device_live/new_form_component.html.heex @@ -0,0 +1,260 @@ +
+ + <%= if @device && @config do %> + <%# Device Generated; display config %> + +
+

+ Device added! +

+ +

+ Install the + + official WireGuard client + + for your device, then use the below WireGuard configuration to connect. +

+ +

+ NOTE: This configuration WILL NOT + be viewable again. Please ensure you've downloaded the + configuration file or copied it somewhere safe + before closing this window. +

+ +
+

Rendering configuration...

+ +
+ +

+ +

+

+ + Generating QR code... + +

+

+

+

+
+ <% else %> + <%# Show form to generate device %> +
+ <.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 %> +
+
+ <%= error_tag f, :base %> +
+
+ <% end %> + +
+ <%= label f, :name, class: "label" %> +
+ <%= text_input f, :name, class: "input #{input_error_class(f, :name)}" %> +
+

+ <%= error_tag f, :name %> +

+
+ +
+ <%= label f, :use_site_allowed_ips, "Use Default Allowed IPs", class: "label" %> +
+ + +
+

+ Default: <%= @allowed_ips %> +

+
+ +
+ <%= label f, :allowed_ips, "Allowed IPs", class: "label" %> +
+ <%= text_input f, :allowed_ips, class: "input #{input_error_class(f, :allowed_ips)}", + disabled: @use_site_allowed_ips %> +
+

+ <%= error_tag f, :allowed_ips %> +

+
+ +
+ <%= label f, :use_site_dns, "Use Default DNS Servers", class: "label" %> +
+ + +
+

+ Default: <%= @dns %> +

+
+ +
+ <%= label f, :dns, "DNS Servers", class: "label" %> +
+ <%= text_input f, :dns, class: "input #{input_error_class(f, :dns)}", + disabled: @use_site_dns %> +
+

+ <%= error_tag f, :dns %> +

+
+ +
+ <%= label f, :use_site_endpoint, "Use Default Endpoint", class: "label" %> +
+ + +
+

+ Default: <%= @endpoint %> +

+
+ +
+ <%= label f, :endpoint, "Server Endpoint", class: "label" %> +

The IP of the server this device should connect to.

+
+ <%= text_input f, :endpoint, class: "input #{input_error_class(f, :endpoint)}", + disabled: @use_site_endpoint %> +
+

+ <%= error_tag f, :endpoint %> +

+
+ +
+ <%= label f, :use_site_mtu, "Use Default MTU", class: "label" %> +
+ + +
+

+ Default: <%= @mtu %> +

+
+ +
+ <%= label f, :mtu, "Interface MTU", class: "label" %> +

The WireGuard interface MTU for this Device.

+
+ <%= text_input f, :mtu, class: "input #{input_error_class(f, :mtu)}", disabled: @use_site_mtu %> +
+

+ <%= error_tag f, :mtu %> +

+
+ +
+ <%= label f, :use_site_persistent_keepalive, "Use Default Persistent Keepalive", class: "label" %> +
+ + +
+

+ Default: <%= @persistent_keepalive %> +

+
+ +
+ <%= label f, :persistent_keepalive, "Persistent Keepalive", class: "label" %> +

+ Interval for WireGuard + + persistent keepalive. A value of 0 disables this. Leave this disabled + unless you're experiencing NAT or firewall traversal problems. +

+
+ <%= text_input f, :persistent_keepalive, class: "input #{input_error_class(f, :persistent_keepalive)}", + disabled: @use_site_persistent_keepalive %> +
+

+ <%= error_tag f, :persistent_keepalive %> +

+
+ +
+ <%= label f, :ipv4, "Tunnel IPv4 Address", class: "label" %> + +
+ <%= text_input(f, + :ipv4, + placeholder: "Leave blank to let Firezone assign an IPv4 address", + class: "input #{input_error_class(f, :ipv4)}") %> +
+

+ <%= error_tag f, :ipv4 %> +

+
+ +
+ <%= label f, :ipv6, "Tunnel IPv6 Address", class: "label" %> + +
+ <%= text_input(f, + :ipv6, + placeholder: "Leave blank to let Firezone assign an IPv6 address", + class: "input #{input_error_class(f, :ipv6)}") %> +
+

+ <%= error_tag f, :ipv6 %> +

+
+ + <%= if @changeset.action do %> +
+
+ Error creating device. Scroll up to view fields with errors. +
+
+ <% end %> + + <%= Phoenix.View.render(FzHttpWeb.SharedView, "submit_button.html", button_text: "Generate Configuration") %> + +
+ <% end %> +
diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/show.html.heex b/apps/fz_http/lib/fz_http_web/live/device_live/show.html.heex deleted file mode 100644 index 3e53d0abd..000000000 --- a/apps/fz_http/lib/fz_http_web/live/device_live/show.html.heex +++ /dev/null @@ -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}" %> - -
- <%= render FzHttpWeb.SharedView, "flash.html", assigns %> - -
-
-

Details

-
-
- <%= live_patch(to: Routes.device_show_path(@socket, :edit, @device), class: "button") do %> - - - - Edit - <% end %> -
-
- - - - - - - - - - - - - - <%= if Application.fetch_env!(:fz_http, :wireguard_ipv4_enabled) do %> - - - - - <% end %> - - <%= if Application.fetch_env!(:fz_http, :wireguard_ipv6_enabled) do %> - - - - - <% end %> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
User<%= link(@user.email, to: Routes.user_show_path(@socket, :show, @user)) %>
Name<%= @device.name %>
Interface IPv4<%= @device.ipv4 %>
Interface IPv6<%= @device.ipv6 %>
Allowed IPs<%= @allowed_ips %>
DNS Servers<%= @dns || "None" %>
Endpoint<%= @endpoint %>
Persistent Keepalive - <%= if @persistent_keepalive == 0 do %> - Disabled - <% else %> - Every <%= @persistent_keepalive %> seconds - <% end %> -
MTU<%= @mtu %>
Public key<%= @device.public_key %>
Private key<%= @device.private_key %>
Server public key<%= @device.server_public_key %>
-
- -
-
-
-

WireGuard Configuration

-
-
-
- - -
- <%= link( - to: Routes.device_path(@socket, :download_config, @device), - class: "button") do %> - - - - Download Configuration - <% end %> -
-
-
-
-
- Install the - - official WireGuard client - - for your device, then use the below WireGuard configuration to connect. -
-
-
-
<%= @config %>
-
-
- - Generating QR code... - -
-
-
- -
-

- Danger Zone -

- - -
diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/show_live.ex b/apps/fz_http/lib/fz_http_web/live/device_live/show_live.ex deleted file mode 100644 index 008c26596..000000000 --- a/apps/fz_http/lib/fz_http_web/live/device_live/show_live.ex +++ /dev/null @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/unprivileged/index.html.heex b/apps/fz_http/lib/fz_http_web/live/device_live/unprivileged/index.html.heex new file mode 100644 index 000000000..7309866cf --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/live/device_live/unprivileged/index.html.heex @@ -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 %> + +
+ <%= render FzHttpWeb.SharedView, "flash.html", assigns %> + +

<%= @page_title %>

+ +
+ Each device corresponds to a WireGuard configuration for connecting to this + Firezone server. +
+ + +
+ <%= if length(@devices) > 0 do %> + + + + + + + + + + + <%= for device <- @devices do %> + + + + + + + <% end %> + +
NameAssigned Device IPPublic keyCreated
+ <%= live_patch(device.name, to: Routes.device_unprivileged_show_path(@socket, :show, device)) %> + + <%= device.ipv4 %> + <%= device.ipv6 %> + <%= device.public_key %>…
+ <% else %> +

+ No devices to show. +

+ <% end %> +
+ +
+
+ <%= live_patch(to: Routes.device_unprivileged_index_path(@socket, :new), class: "button") do %> + Add Device + <% end %> +
+
+ Signed in as <%= @current_user.email %> + | + <%= link(to: Routes.auth_path(@socket, :delete), method: :delete) do %> + Sign out + <% end %> +
+
+
diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/unprivileged/index_live.ex b/apps/fz_http/lib/fz_http_web/live/device_live/unprivileged/index_live.ex new file mode 100644 index 000000000..a452225dd --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/live/device_live/unprivileged/index_live.ex @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/unprivileged/show.html.heex b/apps/fz_http/lib/fz_http_web/live/device_live/unprivileged/show.html.heex new file mode 100644 index 000000000..798f6adfd --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/live/device_live/unprivileged/show.html.heex @@ -0,0 +1,4 @@ +
+ <%= live_patch("<- Back to devices", to: Routes.device_unprivileged_index_path(@socket, :index)) %> +
+<%= render FzHttpWeb.SharedView, "show_device.html", assigns %> diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/unprivileged/show_live.ex b/apps/fz_http/lib/fz_http_web/live/device_live/unprivileged/show_live.ex new file mode 100644 index 000000000..325be440d --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/live/device_live/unprivileged/show_live.ex @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/live/rule_live/index_live.ex b/apps/fz_http/lib/fz_http_web/live/rule_live/index_live.ex index ccab8c3ac..c30086432 100644 --- a/apps/fz_http/lib/fz_http_web/live/rule_live/index_live.ex +++ b/apps/fz_http/lib/fz_http_web/live/rule_live/index_live.ex @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/account_form_component.html.heex b/apps/fz_http/lib/fz_http_web/live/setting_live/account_form_component.html.heex index 58f15b8db..f4033e09c 100644 --- a/apps/fz_http/lib/fz_http_web/live/setting_live/account_form_component.html.heex +++ b/apps/fz_http/lib/fz_http_web/live/setting_live/account_form_component.html.heex @@ -42,15 +42,6 @@

-
-
-
-
-
- <%= submit "Save", phx_disable_with: "Saving...", class: "button is-primary" %> -
-
-
-
+ <%= Phoenix.View.render(FzHttpWeb.SharedView, "submit_button.html", []) %> diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/account_live.ex b/apps/fz_http/lib/fz_http_web/live/setting_live/account_live.ex index f1cceb322..68126cd7c 100644 --- a/apps/fz_http/lib/fz_http_web/live/setting_live/account_live.ex +++ b/apps/fz_http/lib/fz_http_web/live/setting_live/account_live.ex @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/default.html.heex b/apps/fz_http/lib/fz_http_web/live/setting_live/default.html.heex deleted file mode 100644 index a0500da56..000000000 --- a/apps/fz_http/lib/fz_http_web/live/setting_live/default.html.heex +++ /dev/null @@ -1,55 +0,0 @@ -<%= render FzHttpWeb.SharedView, "heading.html", page_title: @page_title %> - -
- <%= render FzHttpWeb.SharedView, "flash.html", assigns %> - -
-

- Configure Firezone-wide default settings. -

-
- -

Device Defaults

- -
- <%= 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) %> -
-
diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/default_form_component.ex b/apps/fz_http/lib/fz_http_web/live/setting_live/default_form_component.ex deleted file mode 100644 index 3965747b2..000000000 --- a/apps/fz_http/lib/fz_http_web/live/setting_live/default_form_component.ex +++ /dev/null @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/default_form_component.html.heex b/apps/fz_http/lib/fz_http_web/live/setting_live/default_form_component.html.heex deleted file mode 100644 index abd7a372c..000000000 --- a/apps/fz_http/lib/fz_http_web/live/setting_live/default_form_component.html.heex +++ /dev/null @@ -1,26 +0,0 @@ -
- <.form let={f} for={@changeset} id={@id} phx-target={@myself} phx-change="change" phx-submit="save"> -
- <%= label f, :value, @label_text, class: "label" %> -
-
- <%= text_input f, :value, placeholder: @placeholder, class: @input_class %> - - - -
- <%= error_tag f, :value %> -
-
- <%= raw @help_text %> -
-
- <%= if @form_changed do %> -
- <%= submit "Save", class: "button is-primary" %> -
- <% end %> -
-
- -
diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/default_live.ex b/apps/fz_http/lib/fz_http_web/live/setting_live/default_live.ex deleted file mode 100644 index 38fcf8f91..000000000 --- a/apps/fz_http/lib/fz_http_web/live/setting_live/default_live.ex +++ /dev/null @@ -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 - 0.0.0.0/0, ::/0 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 DNS 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 diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/security_live.ex b/apps/fz_http/lib/fz_http_web/live/setting_live/security_live.ex index 30d6dc170..dfa1102bc 100644 --- a/apps/fz_http/lib/fz_http_web/live/setting_live/security_live.ex +++ b/apps/fz_http/lib/fz_http_web/live/setting_live/security_live.ex @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/site.html.heex b/apps/fz_http/lib/fz_http_web/live/setting_live/site.html.heex new file mode 100644 index 000000000..d4fc0acf1 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/live/setting_live/site.html.heex @@ -0,0 +1,21 @@ +<%= render FzHttpWeb.SharedView, "heading.html", page_title: @page_title %> + +
+ <%= render FzHttpWeb.SharedView, "flash.html", assigns %> + +
+

+ Configure default WireGuard settings for this site. +

+
+ +

Site Defaults

+ +
+ <%= live_component( + FzHttpWeb.SettingLive.SiteFormComponent, + placeholders: @placeholders, + changeset: @changeset, + id: :site_form_component) %> +
+
diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/site_form_component.ex b/apps/fz_http/lib/fz_http_web/live/setting_live/site_form_component.ex new file mode 100644 index 000000000..0c438b10b --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/live/setting_live/site_form_component.ex @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/site_form_component.html.heex b/apps/fz_http/lib/fz_http_web/live/setting_live/site_form_component.html.heex new file mode 100644 index 000000000..0c05e8a69 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/live/setting_live/site_form_component.html.heex @@ -0,0 +1,100 @@ +
+ <.form let={f} for={@changeset} id={@id} phx-target={@myself} phx-submit="save"> +
+ <%= label f, :allowed_ips, "Allowed IPs", class: "label" %> + +
+ <%= text_input f, + :allowed_ips, + placeholder: @placeholders[:allowed_ips], + class: "input #{input_error_class(f, :allowed_ips)}" %> +
+ +

+ <%= error_tag f, :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 + 0.0.0.0/0, ::/0 to route all device traffic through this Firezone server. +

+
+ +
+ <%= label f, :dns, "DNS Servers", class: "label" %> + +
+ <%= text_input f, + :dns, + placeholder: @placeholders[:dns], + class: "input #{input_error_class(f, :dns)}" %> +
+ +

+ <%= error_tag f, :dns %> +

+

+ Comma-separated list of DNS servers to use for devices. + Leaving this blank will omit the DNS section in + generated device configs. +

+
+ +
+ <%= label f, :endpoint, "Endpoint", class: "label" %> + +
+ <%= text_input f, + :endpoint, + placeholder: @placeholders[:endpoint], + class: "input #{input_error_class(f, :endpoint)}" %> +
+

+ <%= error_tag f, :endpoint %> +

+

+ IPv4 or IPv6 address that devices will be configured to connect + to. Defaults to this server's public IP if not set. +

+
+ +
+ <%= label f, :persistent_keepalive, "Persistent Keepalive", class: "label" %> + +
+ <%= text_input f, + :persistent_keepalive, + placeholder: @placeholders[:persistent_keepalive], + class: "input #{input_error_class(f, :persistent_keepalive)}" %> +

+ <%= error_tag f, :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. +

+
+
+ +
+ <%= label f, :mtu, "MTU", class: "label" %> + +
+ <%= text_input f, + :mtu, + placeholder: @placeholders[:mtu], + class: "input #{input_error_class(f, :mtu)}" %> +
+

+ <%= error_tag f, :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. +

+
+ + <%= Phoenix.View.render(FzHttpWeb.SharedView, "submit_button.html", []) %> + +
diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/site_live.ex b/apps/fz_http/lib/fz_http_web/live/setting_live/site_live.ex new file mode 100644 index 000000000..de2e94ea3 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/live/setting_live/site_live.ex @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/live/user_live/index.html.heex b/apps/fz_http/lib/fz_http_web/live/user_live/index.html.heex index f5d0a6b95..6d3868072 100644 --- a/apps/fz_http/lib/fz_http_web/live/user_live/index.html.heex +++ b/apps/fz_http/lib/fz_http_web/live/user_live/index.html.heex @@ -19,6 +19,7 @@ Email Devices Last Signed In + Last Signed In Method Created Updated @@ -35,6 +36,7 @@ phx-hook="FormatTimestamp"> … + <%= user.last_signed_in_method %> diff --git a/apps/fz_http/lib/fz_http_web/live/user_live/index_live.ex b/apps/fz_http/lib/fz_http_web/live/user_live/index_live.ex index 5d4234e26..5d65d9587 100644 --- a/apps/fz_http/lib/fz_http_web/live/user_live/index_live.ex +++ b/apps/fz_http/lib/fz_http_web/live/user_live/index_live.ex @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/live/user_live/show.html.heex b/apps/fz_http/lib/fz_http_web/live/user_live/show.html.heex index 79620953a..d9a526501 100644 --- a/apps/fz_http/lib/fz_http_web/live/user_live/show.html.heex +++ b/apps/fz_http/lib/fz_http_web/live/user_live/show.html.heex @@ -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 %> - + <%= live_patch( + "Add Device", + to: Routes.user_show_path(@socket, :new_device, @user), + id: "add-device-button", + class: "button") %>
diff --git a/apps/fz_http/lib/fz_http_web/live/user_live/show_live.ex b/apps/fz_http/lib/fz_http_web/live/user_live/show_live.ex index 291429848..15fa587a3 100644 --- a/apps/fz_http/lib/fz_http_web/live/user_live/show_live.ex +++ b/apps/fz_http/lib/fz_http_web/live/user_live/show_live.ex @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/live_auth.ex b/apps/fz_http/lib/fz_http_web/live_auth.ex new file mode 100644 index 000000000..63aac17de --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/live_auth.ex @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/live_helpers.ex b/apps/fz_http/lib/fz_http_web/live_helpers.ex index 0ca11b19d..06d07201e 100644 --- a/apps/fz_http/lib/fz_http_web/live_helpers.ex +++ b/apps/fz_http/lib/fz_http_web/live_helpers.ex @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/mock_events.ex b/apps/fz_http/lib/fz_http_web/mock_events.ex index e9a4ad44f..3295bebf2 100644 --- a/apps/fz_http/lib/fz_http_web/mock_events.ex +++ b/apps/fz_http/lib/fz_http_web/mock_events.ex @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/plug/authorization.ex b/apps/fz_http/lib/fz_http_web/plug/authorization.ex new file mode 100644 index 000000000..949fccabc --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/plug/authorization.ex @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/router.ex b/apps/fz_http/lib/fz_http_web/router.ex index 7ab8c6dcc..547c7b068 100644 --- a/apps/fz_http/lib/fz_http_web/router.ex +++ b/apps/fz_http/lib/fz_http_web/router.ex @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/session.ex b/apps/fz_http/lib/fz_http_web/session.ex index bab02ff17..71b6d1389 100644 --- a/apps/fz_http/lib/fz_http_web/session.ex +++ b/apps/fz_http/lib/fz_http_web/session.ex @@ -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, diff --git a/apps/fz_http/lib/fz_http_web/templates/auth/request.html.heex b/apps/fz_http/lib/fz_http_web/templates/auth/request.html.heex new file mode 100644 index 000000000..84ec8358a --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/templates/auth/request.html.heex @@ -0,0 +1,31 @@ +

Sign In

+ +
+ +
+ <%= link("<- Back to sign in methods", to: Routes.root_path(@conn, :index)) %> +
+ +
+ <%= form_tag @callback_url, method: "post" do %> +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ <%= submit "Sign In", class: "button" %> +
+
+ <% end %> +
diff --git a/apps/fz_http/lib/fz_http_web/templates/device/config.html.heex b/apps/fz_http/lib/fz_http_web/templates/device/config.html.heex deleted file mode 100644 index d36c3b476..000000000 --- a/apps/fz_http/lib/fz_http_web/templates/device/config.html.heex +++ /dev/null @@ -1,31 +0,0 @@ -
-
-

WireGuard Configuration

-
-
- <%= link(to: Routes.device_path(@conn, :download_shared_config, @device.config_token), class: "button") do %> - - - - Download Configuration - <% end %> -
-
- -
- Install the - - official WireGuard client - - for your device, then use the below WireGuard configuration to connect. -
-
-
-
<%= @config %>
-
-
- - Generating QR code... - -
-
diff --git a/apps/fz_http/lib/fz_http_web/templates/layout/admin.html.heex b/apps/fz_http/lib/fz_http_web/templates/layout/admin.html.heex new file mode 100644 index 000000000..433d2a202 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/templates/layout/admin.html.heex @@ -0,0 +1,155 @@ + + + + + + + <%= live_title_tag assigns[:page_title], prefix: "Firezone • " %> + + + + + + + + + + + + <%= render(FzHttpWeb.SharedView, "socket_token_headers.html", conn: @conn, current_user: @current_user) %> + + +
+ + + + <%= @inner_content %> + + +
+ + diff --git a/apps/fz_http/lib/fz_http_web/templates/layout/app.html.eex b/apps/fz_http/lib/fz_http_web/templates/layout/app.html.heex similarity index 100% rename from apps/fz_http/lib/fz_http_web/templates/layout/app.html.eex rename to apps/fz_http/lib/fz_http_web/templates/layout/app.html.heex diff --git a/apps/fz_http/lib/fz_http_web/templates/layout/device_config.html.heex b/apps/fz_http/lib/fz_http_web/templates/layout/device_config.html.heex deleted file mode 100644 index 72804a0da..000000000 --- a/apps/fz_http/lib/fz_http_web/templates/layout/device_config.html.heex +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - <%= csrf_meta_tag() %> - Firezone - - - - - - - - - - - - - -
-
-
-
-
- firez.one -
-
- <%= @inner_content %> -
-
-
- - diff --git a/apps/fz_http/lib/fz_http_web/templates/layout/email.html.eex b/apps/fz_http/lib/fz_http_web/templates/layout/email.html.eex deleted file mode 100644 index 0c3ddf32f..000000000 --- a/apps/fz_http/lib/fz_http_web/templates/layout/email.html.eex +++ /dev/null @@ -1,8 +0,0 @@ - - - "> - - - <%= @inner_content %> - - diff --git a/apps/fz_http/lib/fz_http_web/templates/layout/email.html.heex b/apps/fz_http/lib/fz_http_web/templates/layout/email.html.heex new file mode 100644 index 000000000..873d06587 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/templates/layout/email.html.heex @@ -0,0 +1,8 @@ + + + + + + <%= @inner_content %> + + diff --git a/apps/fz_http/lib/fz_http_web/templates/layout/root.html.heex b/apps/fz_http/lib/fz_http_web/templates/layout/root.html.heex index b175946be..48687ed38 100644 --- a/apps/fz_http/lib/fz_http_web/templates/layout/root.html.heex +++ b/apps/fz_http/lib/fz_http_web/templates/layout/root.html.heex @@ -1,12 +1,13 @@ - - + + - <%= live_title_tag assigns[:page_title], prefix: "Firezone • " %> + <%= csrf_meta_tag() %> + <%= live_title_tag assigns[:page_title] || "Firezone" %> - + @@ -16,148 +17,23 @@ - - - <%= tag :meta, name: "user-token", content: Phoenix.Token.sign(@conn, "user auth", @current_user.id) %> - - - <%= tag :meta, name: "channel-token", content: Phoenix.Token.sign(@conn, "channel auth", @current_user.id) %> - - - <%= csrf_meta_tag() %> - -
- - - - <%= @inner_content %> - - -
+ +
diff --git a/apps/fz_http/lib/fz_http_web/templates/layout/auth.html.heex b/apps/fz_http/lib/fz_http_web/templates/layout/unprivileged.html.heex similarity index 87% rename from apps/fz_http/lib/fz_http_web/templates/layout/auth.html.heex rename to apps/fz_http/lib/fz_http_web/templates/layout/unprivileged.html.heex index c5df478db..fa92e30fc 100644 --- a/apps/fz_http/lib/fz_http_web/templates/layout/auth.html.heex +++ b/apps/fz_http/lib/fz_http_web/templates/layout/unprivileged.html.heex @@ -4,10 +4,9 @@ - <%= csrf_meta_tag() %> <%= live_title_tag assigns[:page_title] || "Firezone" %> - + @@ -17,19 +16,22 @@ + <%= render(FzHttpWeb.SharedView, "socket_token_headers.html", current_user: @current_user, conn: @conn) %>
-
+
firez.one
+ <%= @inner_content %> +
diff --git a/apps/fz_http/lib/fz_http_web/templates/root/auth.html.heex b/apps/fz_http/lib/fz_http_web/templates/root/auth.html.heex new file mode 100644 index 000000000..51b274c5d --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/templates/root/auth.html.heex @@ -0,0 +1,36 @@ +

Sign In

+ +
+ +
+

+ Please sign in via one of the methods below. +

+ + <%= if @local_enabled do %> +

+ <%= link( + "Sign in with email", + to: Routes.auth_path(@conn, :request, "identity"), + class: "button") %> +

+ <% end %> + + <%= if @okta_enabled do %> +

+ <%= link( + "Sign in with Okta", + to: Routes.auth_path(@conn, :request, "okta"), + class: "button") %> +

+ <% end %> + + <%= if @google_enabled do %> +

+ <%= link( + "Sign in with Google", + to: Routes.auth_path(@conn, :request, "google"), + class: "button") %> +

+ <% end %> +
diff --git a/apps/fz_http/lib/fz_http_web/templates/session/new.html.heex b/apps/fz_http/lib/fz_http_web/templates/session/new.html.heex deleted file mode 100644 index ab3a9183f..000000000 --- a/apps/fz_http/lib/fz_http_web/templates/session/new.html.heex +++ /dev/null @@ -1,37 +0,0 @@ -

Sign In

- -
- -<%= form_for @changeset, Routes.session_path(@conn, :create), fn f -> %> - <%= if assigns[:changeset] && @changeset.action do %> -
-

Oops, something went wrong! Please check the errors below.

-
- <% end %> - -
- <%= label(:session, :email, class: "label") %> -
- <%= text_input(f, :email, class: "input #{input_error_class(f, :email)}") %> -
-

- <%= error_tag f, :email %> -

-
- -
- <%= label(:session, :password, class: "label") %> -
- <%= password_input(f, :password, class: "input #{input_error_class(f, :password)}") %> -
-

- <%= error_tag f, :password %> -

-
- -
-
- <%= submit "Sign In", class: "button" %> -
-
-<% end %> diff --git a/apps/fz_http/lib/fz_http_web/templates/shared/device_details.html.heex b/apps/fz_http/lib/fz_http_web/templates/shared/device_details.html.heex new file mode 100644 index 000000000..e5610d18f --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/templates/shared/device_details.html.heex @@ -0,0 +1,66 @@ + + + + <%= if has_role?(@current_user, :admin) do %> + + + + + <% end %> + + + + + + + <%= if Application.fetch_env!(:fz_http, :wireguard_ipv4_enabled) do %> + + + + + <% end %> + + <%= if Application.fetch_env!(:fz_http, :wireguard_ipv6_enabled) do %> + + + + + <% end %> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
User<%= link(@user.email, to: Routes.user_show_path(@socket, :show, @user)) %>
Name<%= @device.name %>
Interface IPv4<%= @device.ipv4 %>
Interface IPv6<%= @device.ipv6 %>
Allowed IPs<%= @allowed_ips || "None" %>
DNS Servers<%= @dns || "None" %>
Endpoint<%= @endpoint %>:<%= @port %>
Persistent Keepalive + <%= if @persistent_keepalive == 0 do %> + Disabled + <% else %> + Every <%= @persistent_keepalive %> seconds + <% end %> +
MTU<%= @mtu %>
Public key<%= @device.public_key %>
diff --git a/apps/fz_http/lib/fz_http_web/templates/shared/devices_table.html.heex b/apps/fz_http/lib/fz_http_web/templates/shared/devices_table.html.heex index 55fe43fa5..1d4e16c3f 100644 --- a/apps/fz_http/lib/fz_http_web/templates/shared/devices_table.html.heex +++ b/apps/fz_http/lib/fz_http_web/templates/shared/devices_table.html.heex @@ -12,7 +12,7 @@ <%= for device <- @devices do %> - <%= 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)) %> <%= device.ipv4 %> diff --git a/apps/fz_http/lib/fz_http_web/templates/shared/show_device.html.heex b/apps/fz_http/lib/fz_http_web/templates/shared/show_device.html.heex new file mode 100644 index 000000000..17ace8ad9 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/templates/shared/show_device.html.heex @@ -0,0 +1,23 @@ +
+ <%= render FzHttpWeb.SharedView, "flash.html", assigns %> + +

Details

+ + <%= render(FzHttpWeb.SharedView, "device_details.html", assigns) %> +
+ +
+

+ Danger Zone +

+ + +
diff --git a/apps/fz_http/lib/fz_http_web/templates/shared/socket_token_headers.html.heex b/apps/fz_http/lib/fz_http_web/templates/shared/socket_token_headers.html.heex new file mode 100644 index 000000000..f1e111031 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/templates/shared/socket_token_headers.html.heex @@ -0,0 +1,8 @@ + +<%= tag :meta, name: "user-token", content: Phoenix.Token.sign(@conn, "user auth", @current_user.id) %> + + +<%= tag :meta, name: "channel-token", content: Phoenix.Token.sign(@conn, "channel auth", @current_user.id) %> + + +<%= csrf_meta_tag() %> diff --git a/apps/fz_http/lib/fz_http_web/templates/shared/submit_button.html.heex b/apps/fz_http/lib/fz_http_web/templates/shared/submit_button.html.heex new file mode 100644 index 000000000..b68ef68f4 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/templates/shared/submit_button.html.heex @@ -0,0 +1,10 @@ +
+
+
+
+
+ <%= submit assigns[:button_text] || "Save", phx_disable_with: "Saving...", class: "button is-primary" %> +
+
+
+
diff --git a/apps/fz_http/lib/fz_http_web/templates/user/show.html.heex b/apps/fz_http/lib/fz_http_web/templates/user/show.html.heex index e2b76917e..74cc70bb1 100644 --- a/apps/fz_http/lib/fz_http_web/templates/user/show.html.heex +++ b/apps/fz_http/lib/fz_http_web/templates/user/show.html.heex @@ -6,7 +6,7 @@

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.

<% else %> @@ -19,7 +19,7 @@ ...

- <%= 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 @@
- <%= link(to: Routes.session_path(@conn, :delete), method: :delete) do %> + <%= link(to: Routes.auth_path(@conn, :delete), method: :delete) do %> Sign out <% end %>
diff --git a/apps/fz_http/lib/fz_http_web/user_from_auth.ex b/apps/fz_http/lib/fz_http_web/user_from_auth.ex new file mode 100644 index 000000000..7a804a4f3 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/user_from_auth.ex @@ -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 diff --git a/apps/fz_http/lib/fz_http_web/views/auth_view.ex b/apps/fz_http/lib/fz_http_web/views/auth_view.ex new file mode 100644 index 000000000..14d919245 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/views/auth_view.ex @@ -0,0 +1,3 @@ +defmodule FzHttpWeb.AuthView do + use FzHttpWeb, :view +end diff --git a/apps/fz_http/lib/fz_http_web/views/root_view.ex b/apps/fz_http/lib/fz_http_web/views/root_view.ex new file mode 100644 index 000000000..4d7a3751a --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/views/root_view.ex @@ -0,0 +1,3 @@ +defmodule FzHttpWeb.RootView do + use FzHttpWeb, :view +end diff --git a/apps/fz_http/lib/fz_http_web/views/session_view.ex b/apps/fz_http/lib/fz_http_web/views/session_view.ex deleted file mode 100644 index 9b0ca2a63..000000000 --- a/apps/fz_http/lib/fz_http_web/views/session_view.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule FzHttpWeb.SessionView do - use FzHttpWeb, :view -end diff --git a/apps/fz_http/lib/fz_http_web/views/user_view.ex b/apps/fz_http/lib/fz_http_web/views/user_view.ex index 93b7a2d32..93bef7259 100644 --- a/apps/fz_http/lib/fz_http_web/views/user_view.ex +++ b/apps/fz_http/lib/fz_http_web/views/user_view.ex @@ -4,21 +4,21 @@ defmodule FzHttpWeb.UserView do """ use FzHttpWeb, :view - alias FzHttp.{Settings, Users} + alias FzHttp.{Sites, Users} def admin_email do - Users.admin().email + Application.fetch_env!(:fz_http, :admin_email) end def vpn_sessions_expire? do - Settings.vpn_sessions_expire?() + Sites.vpn_sessions_expire?() end def vpn_expires_at(user) do - Users.vpn_session_expires_at(user, Settings.vpn_duration()) + Users.vpn_session_expires_at(user, Sites.vpn_duration()) end def vpn_expired?(user) do - Users.vpn_session_expired?(user, Settings.vpn_duration()) + Users.vpn_session_expired?(user, Sites.vpn_duration()) end end diff --git a/apps/fz_http/mix.exs b/apps/fz_http/mix.exs index 9e9f13ed6..60e656473 100644 --- a/apps/fz_http/mix.exs +++ b/apps/fz_http/mix.exs @@ -38,7 +38,12 @@ defmodule FzHttp.MixProject do def application do [ mod: {FzHttp.Application, []}, - extra_applications: [:logger, :runtime_tools] + extra_applications: [ + :logger, + :runtime_tools, + :ueberauth_okta, + :ueberauth_identity + ] ] end @@ -58,6 +63,12 @@ defmodule FzHttp.MixProject do {:cloak_ecto, "~> 1.2"}, {:excoveralls, "~> 0.14", only: :test}, {:floki, ">= 0.0.0", only: :test}, + {:guardian, "~> 2.0"}, + {:guardian_db, "~> 2.0"}, + {:ueberauth, "~> 0.6"}, + {:ueberauth_google, "~> 0.10"}, + {:ueberauth_okta, "~> 0.2"}, + {:ueberauth_identity, "~> 0.3"}, {:httpoison, "~> 1.8"}, {:argon2_elixir, "~> 2.0"}, {:phoenix_pubsub, "~> 2.0"}, diff --git a/apps/fz_http/priv/repo/migrations/20220208184257_settings_to_sites.exs b/apps/fz_http/priv/repo/migrations/20220208184257_settings_to_sites.exs new file mode 100644 index 000000000..440c77e5c --- /dev/null +++ b/apps/fz_http/priv/repo/migrations/20220208184257_settings_to_sites.exs @@ -0,0 +1,89 @@ +defmodule FzHttp.Repo.Migrations.SettingsToSites do + use Ecto.Migration + + def change do + create table(:sites, primary_key: false) do + add :id, :uuid, primary_key: true + add :name, :string + add :dns, :string + add :allowed_ips, :string + add :endpoint, :string + add :persistent_keepalive, :integer + add :mtu, :integer + add :vpn_session_duration, :integer + + timestamps(type: :utc_datetime_usec) + end + + now = DateTime.utc_now() + + execute(""" + INSERT INTO sites (id, name, inserted_at, updated_at) + VALUES (gen_random_uuid(), 'default', '#{now}', '#{now}') + """) + + execute(""" + UPDATE sites + SET dns = ( + SELECT value + FROM settings + WHERE key = 'default.device.dns' + ) + WHERE sites.name = 'default' + """) + + execute(""" + UPDATE sites + SET allowed_ips = ( + SELECT value + FROM settings + WHERE key = 'default.device.allowed_ips' + ) + WHERE sites.name = 'default' + """) + + execute(""" + UPDATE sites + SET endpoint = ( + SELECT value + FROM settings + WHERE key = 'default.device.endpoint' + ) + WHERE sites.name = 'default' + """) + + execute(""" + UPDATE sites + SET persistent_keepalive = ( + SELECT value::INTEGER + FROM settings + WHERE key = 'default.device.persistent_keepalive' + ) + WHERE sites.name = 'default' + """) + + execute(""" + UPDATE sites + SET mtu = ( + SELECT value::INTEGER + FROM settings + WHERE key = 'default.device.mtu' + ) + WHERE sites.name = 'default' + """) + + execute(""" + UPDATE sites + SET vpn_session_duration = ( + SELECT value::INTEGER + FROM settings + WHERE key = 'security.require_auth_for_vpn_frequency' + ) + WHERE sites.name = 'default' + """) + + drop table(:settings) + + create unique_index(:sites, :name) + end +end diff --git a/apps/fz_http/priv/repo/migrations/20220209005201_rename_use_default_to_use_site.exs b/apps/fz_http/priv/repo/migrations/20220209005201_rename_use_default_to_use_site.exs new file mode 100644 index 000000000..dcdb096dd --- /dev/null +++ b/apps/fz_http/priv/repo/migrations/20220209005201_rename_use_default_to_use_site.exs @@ -0,0 +1,11 @@ +defmodule FzHttp.Repo.Migrations.RenameUseDefaultToUseSite do + use Ecto.Migration + + def change do + rename table(:devices), :use_default_allowed_ips, to: :use_site_allowed_ips + rename table(:devices), :use_default_dns, to: :use_site_dns + rename table(:devices), :use_default_endpoint, to: :use_site_endpoint + rename table(:devices), :use_default_persistent_keepalive, to: :use_site_persistent_keepalive + rename table(:devices), :use_default_mtu, to: :use_site_mtu + end +end diff --git a/apps/fz_http/priv/repo/migrations/20220211201727_remove_private_keys.exs b/apps/fz_http/priv/repo/migrations/20220211201727_remove_private_keys.exs new file mode 100644 index 000000000..d96be04ef --- /dev/null +++ b/apps/fz_http/priv/repo/migrations/20220211201727_remove_private_keys.exs @@ -0,0 +1,12 @@ +defmodule FzHttp.Repo.Migrations.RemovePrivateKeys do + use Ecto.Migration + + def change do + alter table(:devices) do + remove :private_key + remove :server_public_key + remove :config_token + remove :config_token_expires_at + end + end +end diff --git a/apps/fz_http/priv/repo/migrations/20220219165023_add_key_regenerated_at.exs b/apps/fz_http/priv/repo/migrations/20220219165023_add_key_regenerated_at.exs new file mode 100644 index 000000000..8d78a9b25 --- /dev/null +++ b/apps/fz_http/priv/repo/migrations/20220219165023_add_key_regenerated_at.exs @@ -0,0 +1,9 @@ +defmodule FzHttp.Repo.Migrations.AddKeyRegeneratedAt do + use Ecto.Migration + + def change do + alter table(:devices) do + add :key_regenerated_at, :utc_datetime_usec, null: false, default: fragment("now()") + end + end +end diff --git a/apps/fz_http/priv/repo/migrations/20220227215313_add_last_signed_in_method_to_user.exs b/apps/fz_http/priv/repo/migrations/20220227215313_add_last_signed_in_method_to_user.exs new file mode 100644 index 000000000..0e43c25f9 --- /dev/null +++ b/apps/fz_http/priv/repo/migrations/20220227215313_add_last_signed_in_method_to_user.exs @@ -0,0 +1,9 @@ +defmodule FzHttp.Repo.Migrations.AddLastSignedInMethodToUser do + use Ecto.Migration + + def change do + alter table(:users) do + add :last_signed_in_method, :string + end + end +end diff --git a/apps/fz_http/priv/repo/seeds.exs b/apps/fz_http/priv/repo/seeds.exs index 12368cf11..8058ae73c 100644 --- a/apps/fz_http/priv/repo/seeds.exs +++ b/apps/fz_http/priv/repo/seeds.exs @@ -15,8 +15,8 @@ alias FzHttp.{Devices, ConnectivityChecks, Rules, Users} {:ok, user} = Users.create_admin_user(%{ email: "firezone@localhost", - password: "firezone", - password_confirmation: "firezone" + password: "firezone1234", + password_confirmation: "firezone1234" }) {:ok, device} = @@ -24,8 +24,6 @@ alias FzHttp.{Devices, ConnectivityChecks, Rules, Users} user_id: user.id, name: "Factory Device", public_key: "3Fo+SNnDJ6hi8qzPt3nWLwgjCVwvpjHL35qJeatKwEc=", - server_public_key: "QFvMfHTjlJN9cfUiK1w4XmxOomH6KRTCMrVC6z3TWFM=", - private_key: "2JSZtpSHM+69Hm7L3BSGIymbq0byw39iWLevKESd1EM=", remote_ip: %Postgrex.INET{address: {127, 0, 0, 1}} }) diff --git a/apps/fz_http/test/fz_http/devices_test.exs b/apps/fz_http/test/fz_http/devices_test.exs index 37ff91b82..619220c42 100644 --- a/apps/fz_http/test/fz_http/devices_test.exs +++ b/apps/fz_http/test/fz_http/devices_test.exs @@ -30,14 +30,34 @@ defmodule FzHttp.DevicesTest do end end + setup context do + if max_devices = context[:max_devices] do + restore_env(:max_devices_per_user, max_devices, &on_exit/1) + else + context + end + end + @device_attrs %{ name: "dummy", public_key: "dummy", - private_key: "dummy", - server_public_key: "dummy", user_id: nil } + @tag max_devices: 1 + test "prevents creating more than max_devices_per_user", %{device: device} do + assert {:error, + %Ecto.Changeset{ + errors: [ + base: + {"Maximum device limit reached. Remove an existing device before creating a new one.", + []} + ] + }} = Devices.create_device(%{@device_attrs | user_id: device.user_id}) + + assert [device] == Devices.list_devices(device.user_id) + end + test "creates device with empty attributes", %{user: user} do assert {:ok, _device} = Devices.create_device(%{@device_attrs | user_id: user.id}) end @@ -107,11 +127,11 @@ defmodule FzHttp.DevicesTest do @attrs %{ name: "Go hard or go home.", allowed_ips: "0.0.0.0", - use_default_allowed_ips: false + use_site_allowed_ips: false } @valid_dns_attrs %{ - use_default_dns: false, + use_site_dns: false, dns: "1.1.1.1, 1.0.0.1, 2606:4700:4700::1111, 2606:4700:4700::1001" } @@ -124,44 +144,57 @@ defmodule FzHttp.DevicesTest do } @valid_allowed_ips_attrs %{ - use_default_allowed_ips: false, + use_site_allowed_ips: false, allowed_ips: "0.0.0.0/0, ::/0, ::0/0, 192.168.1.0/24" } @valid_endpoint_ipv4_attrs %{ - use_default_endpoint: false, + use_site_endpoint: false, endpoint: "5.5.5.5" } @valid_endpoint_ipv6_attrs %{ - use_default_endpoint: false, + use_site_endpoint: false, endpoint: "fd00::1" } @valid_endpoint_host_attrs %{ - use_default_endpoint: false, + use_site_endpoint: false, endpoint: "valid-endpoint.example.com" } @invalid_endpoint_ipv4_attrs %{ - use_default_endpoint: false, + use_site_endpoint: false, endpoint: "265.1.1.1" } @invalid_endpoint_ipv6_attrs %{ - use_default_endpoint: false, + use_site_endpoint: false, endpoint: "deadbeef::1" } @invalid_endpoint_host_attrs %{ - use_default_endpoint: false, + use_site_endpoint: false, endpoint: "can't have this" } + @empty_endpoint_attrs %{ + use_site_endpoint: false, + endpoint: "" + } + @invalid_allowed_ips_attrs %{ allowed_ips: "1.1.1.1, 11, foobar" } + @fields_use_site [ + %{use_site_allowed_ips: true, allowed_ips: "1.1.1.1"}, + %{use_site_dns: true, dns: "1.1.1.1"}, + %{use_site_endpoint: true, endpoint: "1.1.1.1"}, + %{use_site_persistent_keepalive: true, persistent_keepalive: 1}, + %{use_site_mtu: true, mtu: 1000} + ] + test "updates device", %{device: device} do {:ok, test_device} = Devices.update_device(device, @attrs) assert @attrs = test_device @@ -196,6 +229,22 @@ defmodule FzHttp.DevicesTest do } end + test "prevents updating fields if use_site_", %{device: device} do + for attrs <- @fields_use_site do + field = + Map.keys(attrs) + |> Enum.filter(fn attr -> !String.starts_with?(Atom.to_string(attr), "use_site_") end) + |> List.first() + + assert {:error, changeset} = Devices.update_device(device, attrs) + + assert changeset.errors[field] == { + "must not be present", + [] + } + end + end + test "prevents updating device with invalid ipv6 endpoint", %{device: device} do {:error, changeset} = Devices.update_device(device, @invalid_endpoint_ipv6_attrs) @@ -214,6 +263,15 @@ defmodule FzHttp.DevicesTest do } end + test "prevents updating device with empty endpoint", %{device: device} do + {:error, changeset} = Devices.update_device(device, @empty_endpoint_attrs) + + assert changeset.errors[:endpoint] == { + "can't be blank", + [{:validation, :required}] + } + end + test "prevents updating device with invalid dns", %{device: device} do {:error, changeset} = Devices.update_device(device, @invalid_dns_attrs) @@ -317,15 +375,6 @@ defmodule FzHttp.DevicesTest do end end - describe "rand_name/0" do - test "generates a random name" do - name1 = Devices.rand_name() - name2 = Devices.rand_name() - - assert name1 != name2 - end - end - describe "to_peer_list/0" do setup [:create_device] diff --git a/apps/fz_http/test/fz_http_web/events_test.exs b/apps/fz_http/test/fz_http/events_test.exs similarity index 91% rename from apps/fz_http/test/fz_http_web/events_test.exs rename to apps/fz_http/test/fz_http/events_test.exs index 6c9aecc60..a12f38f9e 100644 --- a/apps/fz_http/test/fz_http_web/events_test.exs +++ b/apps/fz_http/test/fz_http/events_test.exs @@ -1,11 +1,10 @@ -defmodule FzHttpWeb.EventsTest do +defmodule FzHttp.EventsTest do @moduledoc """ XXX: Use start_supervised! somehow here to allow async tests. """ use FzHttp.DataCase, async: false - alias FzHttp.Devices - alias FzHttpWeb.Events + alias FzHttp.{Devices, Events} # XXX: Not needed with start_supervised! setup do @@ -15,12 +14,6 @@ defmodule FzHttpWeb.EventsTest do end) end - describe "create_device/0" do - test "receives info to create device" do - assert {:ok, _privkey, _pubkey, _server_pubkey} = Events.create_device() - end - end - describe "update_device/1" do setup [:create_device] diff --git a/apps/fz_http/test/fz_http/sessions_test.exs b/apps/fz_http/test/fz_http/sessions_test.exs deleted file mode 100644 index 6b2aee38f..000000000 --- a/apps/fz_http/test/fz_http/sessions_test.exs +++ /dev/null @@ -1,56 +0,0 @@ -defmodule FzHttp.SessionsTest do - use FzHttp.DataCase, async: true - - alias FzHttp.Sessions - - describe "get_session!/1" do - setup [:create_session] - - test "gets session by id", %{session: session} do - assert session.id == Sessions.get_session!(session.id).id - end - - test "gets session by email", %{session: session} do - assert session.id == Sessions.get_session!(email: session.email).id - end - end - - describe "get_session/1" do - setup [:create_session] - - test "gets session by id", %{session: session} do - assert session.id == Sessions.get_session(session.id).id - end - - test "gets session by email", %{session: session} do - assert session.id == Sessions.get_session(email: session.email).id - end - end - - describe "new_session/0" do - test "returns changeset" do - assert %Ecto.Changeset{} = Sessions.new_session() - end - end - - describe "create_session/2" do - setup [:create_user] - - @password_params %{password: "password1234"} - @invalid_params %{password: "invalid"} - - test "creates session (updates existing record)", %{user: user} do - session = Sessions.get_session!(email: user.email) - assert is_nil(session.last_signed_in_at) - - {:ok, test_session} = Sessions.create_session(session, @password_params) - assert !is_nil(test_session.last_signed_in_at) - end - - test "doesn't create session with invalid password", %{user: user} do - session = Sessions.get_session!(email: user.email) - assert {:error, changeset} = Sessions.create_session(session, @invalid_params) - assert [password: _] = changeset.errors - end - end -end diff --git a/apps/fz_http/test/fz_http/settings_test.exs b/apps/fz_http/test/fz_http/settings_test.exs deleted file mode 100644 index 4e86a99fe..000000000 --- a/apps/fz_http/test/fz_http/settings_test.exs +++ /dev/null @@ -1,91 +0,0 @@ -defmodule FzHttp.SettingsTest do - use FzHttp.DataCase - - alias FzHttp.Settings - - @setting_keys ~w( - default.device.dns - default.device.allowed_ips - default.device.endpoint - default.device.mtu - ) - - describe "settings" do - alias FzHttp.Settings.Setting - - import FzHttp.SettingsFixtures - - @valid_settings [ - %{ - "default.device.dns" => "8.8.8.8", - "default.device.allowed_ips" => "::/0", - "default.device.endpoint" => "172.10.10.10", - "default.device.persistent_keepalive" => "20", - "default.device.mtu" => "1280" - }, - %{ - "default.device.dns" => "8.8.8.8", - "default.device.allowed_ips" => "::/0", - "default.device.endpoint" => "foobar.example.com", - "default.device.persistent_keepalive" => "15", - "default.device.mtu" => "1420" - } - ] - @invalid_settings %{ - "default.device.dns" => "foobar", - "default.device.allowed_ips" => "foobar", - "default.device.endpoint" => "foobar", - "default.device.persistent_keepalive" => "-120", - "default.device.mtu" => "1501" - } - - test "get_setting!/1 returns the setting with given id" do - setting = setting_fixture() - assert Settings.get_setting!(setting.id) == setting - end - - test "get_setting!/1 returns the setting with the given key" do - for key <- @setting_keys do - setting = Settings.get_setting!(key: key) - assert setting.key == key - end - end - - test "update_setting/2 with valid data updates the setting via provided setting" do - for key <- @setting_keys do - for valid_setting <- @valid_settings do - value = valid_setting[key] - setting = Settings.get_setting!(key: key) - assert {:ok, %Setting{} = setting} = Settings.update_setting(setting, %{value: value}) - assert setting.key == key - assert setting.value == value - end - end - end - - test "update_setting/2 with valid data updates the setting via key, value" do - for key <- @setting_keys do - for valid_setting <- @valid_settings do - value = valid_setting[key] - assert {:ok, %Setting{} = setting} = Settings.update_setting(key, value) - assert setting.key == key - assert setting.value == value - end - end - end - - test "update_setting/2 with invalid data returns error changeset" do - for key <- @setting_keys do - value = @invalid_settings[key] - assert {:error, %Ecto.Changeset{}} = Settings.update_setting(key, value) - setting = Settings.get_setting!(key: key) - refute setting.value == value - end - end - - test "change_setting/1 returns a setting changeset" do - setting = setting_fixture() - assert %Ecto.Changeset{} = Settings.change_setting(setting) - end - end -end diff --git a/apps/fz_http/test/fz_http/sites_test.exs b/apps/fz_http/test/fz_http/sites_test.exs new file mode 100644 index 000000000..b466a7c15 --- /dev/null +++ b/apps/fz_http/test/fz_http/sites_test.exs @@ -0,0 +1,70 @@ +defmodule FzHttp.SitesTest do + use FzHttp.DataCase + + alias FzHttp.Sites + + describe "sites" do + alias FzHttp.Sites.Site + + import FzHttp.SitesFixtures + + @valid_sites [ + %{ + "dns" => "8.8.8.8", + "allowed_ips" => "::/0", + "endpoint" => "172.10.10.10", + "persistent_keepalive" => "20", + "mtu" => "1280" + }, + %{ + "dns" => "8.8.8.8", + "allowed_ips" => "::/0", + "endpoint" => "foobar.example.com", + "persistent_keepalive" => "15", + "mtu" => "1420" + } + ] + @invalid_site %{ + "dns" => "foobar", + "allowed_ips" => "foobar", + "endpoint" => "foobar", + "persistent_keepalive" => "-120", + "mtu" => "1501" + } + + test "get_site/1 returns the site with given id" do + site = site_fixture() + assert Sites.get_site!(site.id) == site + end + + test "get_site!/1 returns the site with the given name" do + site = Sites.get_site!(name: "default") + assert site.name == "default" + end + + test "update_site/2 with valid data updates the site via provided site" do + site = Sites.get_site!(name: "default") + + for attrs <- @valid_sites do + assert {:ok, %Site{}} = Sites.update_site(site, attrs) + end + end + + test "update_site/2 with invalid data returns error changeset" do + site = Sites.get_site!(name: "default") + assert {:error, %Ecto.Changeset{}} = Sites.update_site(site, @invalid_site) + site = Sites.get_site!(name: "default") + + refute site.dns == "foobar" + refute site.allowed_ips == "foobar" + refute site.endpoint == "foobar" + refute site.persistent_keepalive == -120 + refute site.mtu == 1501 + end + + test "change_site/1 returns a site changeset" do + site = site_fixture() + assert %Ecto.Changeset{} = Sites.change_site(site) + end + end +end diff --git a/apps/fz_http/test/fz_http_web/authentication_test.exs b/apps/fz_http/test/fz_http_web/authentication_test.exs new file mode 100644 index 000000000..76949d4f9 --- /dev/null +++ b/apps/fz_http/test/fz_http_web/authentication_test.exs @@ -0,0 +1,28 @@ +defmodule FzHttpWeb.AuthenticationTest do + use FzHttpWeb.ConnCase, async: true + + alias FzHttpWeb.Authentication + + describe "authenticate/2" do + setup :create_user + + @success {:ok, %{}} + @error {:error, :invalid_credentials} + + test "authenticates user with valid credentials", %{user: user} do + assert @success = Authentication.authenticate(user, "password1234") + end + + test "returns error for missing user" do + assert @error = Authentication.authenticate(nil, "password1234") + end + + test "returns error for missing password", %{user: user} do + assert @error = Authentication.authenticate(user, nil) + end + + test "returns error for incorrect password", %{user: user} do + assert @error = Authentication.authenticate(user, "incorrect password") + end + end +end diff --git a/apps/fz_http/test/fz_http_web/controllers/device_controller_test.exs b/apps/fz_http/test/fz_http_web/controllers/device_controller_test.exs deleted file mode 100644 index aa7100854..000000000 --- a/apps/fz_http/test/fz_http_web/controllers/device_controller_test.exs +++ /dev/null @@ -1,44 +0,0 @@ -defmodule FzHttpWeb.DeviceControllerTest do - use FzHttpWeb.ConnCase, async: true - - describe "index" do - setup [:create_user] - - test "authenticated loads device live view", %{authed_conn: conn, user: _user} do - test_conn = get(conn, Routes.root_path(conn, :index)) - - assert redirected_to(test_conn) == Routes.device_index_path(test_conn, :index) - end - - test "unauthenticated redirects to sign in", %{unauthed_conn: conn, user: _user} do - test_conn = get(conn, Routes.root_path(conn, :index)) - - assert redirected_to(test_conn) == Routes.session_path(conn, :new) - end - end - - describe "config" do - setup :create_device_with_config_token - - test "config can be shown", %{device: device, unauthed_conn: conn} do - test_conn = get(conn, Routes.device_path(conn, :config, device.config_token)) - - assert html_response(test_conn, 200) =~ "Download Configuration" - assert html_response(test_conn, 200) =~ device.config_token - end - end - - describe "download_shared_config" do - setup :create_device_with_config_token - - test "config can be downloaded", %{device: device, unauthed_conn: conn} do - test_conn = - get( - conn, - Routes.device_path(conn, :download_shared_config, device.config_token) - ) - - assert test_conn.resp_body == FzHttp.Devices.as_config(device) - end - end -end diff --git a/apps/fz_http/test/fz_http_web/controllers/session_controller_test.exs b/apps/fz_http/test/fz_http_web/controllers/session_controller_test.exs index 290863fbb..3a8042d60 100644 --- a/apps/fz_http/test/fz_http_web/controllers/session_controller_test.exs +++ b/apps/fz_http/test/fz_http_web/controllers/session_controller_test.exs @@ -1,95 +1,79 @@ -defmodule FzHttpWeb.SessionControllerTest do +defmodule FzHttpWeb.AuthControllerTest do use FzHttpWeb.ConnCase, async: true describe "new" do setup [:create_user] - test "unauthed: loads the sign in form", %{unauthed_conn: conn, user: _user} do - test_conn = get(conn, Routes.session_path(conn, :new)) + test "unauthed: loads the sign in form", %{unauthed_conn: conn} do + test_conn = get(conn, Routes.root_path(conn, :index)) assert html_response(test_conn, 200) =~ "Sign In" end - test "authed: redirects to devices page", %{authed_conn: conn, user: _user} do - test_conn = get(conn, Routes.session_path(conn, :new)) + test "authed as admin: redirects to users page", %{admin_conn: conn} do + test_conn = get(conn, Routes.root_path(conn, :index)) - assert redirected_to(test_conn) == Routes.device_index_path(test_conn, :index) + assert redirected_to(test_conn) == Routes.user_index_path(test_conn, :index) + end + + test "authed as unprivileged: redirects to user_devices page", %{unprivileged_conn: conn} do + test_conn = get(conn, Routes.root_path(conn, :index)) + + assert redirected_to(test_conn) == Routes.device_unprivileged_index_path(test_conn, :index) end end describe "create session" do setup [:create_user] - test "invalid email", %{unauthed_conn: conn, user: _user} do + test "invalid email", %{unauthed_conn: conn} do params = %{ - "session" => %{ - "email" => "invalid@test", - "password" => "test" - } + "email" => "invalid@test", + "password" => "test" } - test_conn = post(conn, Routes.session_path(conn, :create), params) + test_conn = post(conn, Routes.auth_path(conn, :callback, :identity), params) - assert redirected_to(test_conn) == Routes.session_path(test_conn, :new) - assert get_flash(test_conn, :error) =~ "Email not found." + assert test_conn.request_path == Routes.auth_path(test_conn, :callback, :identity) + assert get_flash(test_conn, :error) == "Error signing in: invalid_credentials" end test "invalid password", %{unauthed_conn: conn, user: user} do params = %{ - "session" => %{ - "email" => user.email, - "password" => "invalid" - } + "email" => user.email, + "password" => "invalid" } - test_conn = post(conn, Routes.session_path(conn, :create), params) + test_conn = post(conn, Routes.auth_path(conn, :callback, :identity), params) - assert redirected_to(test_conn) == Routes.session_path(test_conn, :new) - - assert get_flash(test_conn, :error) =~ - "Error signing in. Ensure email and password are correct." + assert test_conn.request_path == Routes.auth_path(test_conn, :callback, :identity) + assert get_flash(test_conn, :error) == "Error signing in: invalid_credentials" end test "valid params", %{unauthed_conn: conn, user: user} do params = %{ - "session" => %{ - "email" => user.email, - "password" => "password1234" - } + "email" => user.email, + "password" => "password1234" } - test_conn = post(conn, Routes.session_path(conn, :create), params) + test_conn = post(conn, Routes.auth_path(conn, :callback, :identity), params) - assert redirected_to(test_conn) == Routes.root_path(test_conn, :index) - assert get_session(test_conn, :user_id) == user.id - end - - test "token invalid; session not set", %{unauthed_conn: conn, user: _user} do - test_conn = get(conn, Routes.session_path(conn, :create, "invalid")) - - assert redirected_to(test_conn) == Routes.session_path(test_conn, :new) - assert get_flash(test_conn, :error) == "Token invalid." - end - - test "token valid; sets session", %{unauthed_conn: conn, user: user} do - test_conn = get(conn, Routes.session_path(conn, :create, user.sign_in_token)) - - assert redirected_to(test_conn) == Routes.device_index_path(test_conn, :index) - assert get_session(test_conn, :user_id) == user.id + assert redirected_to(test_conn) == Routes.user_index_path(test_conn, :index) + assert current_user(test_conn).id == user.id end end describe "when deleting a session" do setup :create_user - test "user signed in", %{authed_conn: conn, user: _user} do - test_conn = delete(conn, Routes.session_path(conn, :delete)) - assert redirected_to(test_conn) == Routes.session_path(test_conn, :new) + test "user signed in", %{admin_conn: conn} do + test_conn = delete(conn, Routes.auth_path(conn, :delete)) + assert redirected_to(test_conn) == Routes.root_path(test_conn, :index) end - test "user not signed in", %{unauthed_conn: conn, user: _user} do - test_conn = delete(conn, Routes.session_path(conn, :delete)) - assert redirected_to(test_conn) == Routes.session_path(test_conn, :new) + test "user not signed in", %{unauthed_conn: conn} do + test_conn = delete(conn, Routes.auth_path(conn, :delete)) + assert redirected_to(test_conn) == Routes.root_path(test_conn, :index) end end end diff --git a/apps/fz_http/test/fz_http_web/controllers/user_controller_test.exs b/apps/fz_http/test/fz_http_web/controllers/user_controller_test.exs index e671cafa8..bbfcc8555 100644 --- a/apps/fz_http/test/fz_http_web/controllers/user_controller_test.exs +++ b/apps/fz_http/test/fz_http_web/controllers/user_controller_test.exs @@ -4,31 +4,30 @@ defmodule FzHttpWeb.UserControllerTest do alias FzHttp.Users describe "when user signed in" do - test "deletes the user", %{authed_conn: conn} do + test "deletes the user", %{admin_conn: conn} do test_conn = delete(conn, Routes.user_path(conn, :delete)) - assert redirected_to(test_conn) == Routes.session_path(test_conn, :new) + assert redirected_to(test_conn) == Routes.root_path(test_conn, :index) end end describe "when user is already deleted" do - test "returns 404", %{authed_conn: conn} do - conn - |> get_session(:user_id) + test "returns 404", %{admin_user: user, admin_conn: conn} do + user.id |> Users.get_user!() |> Users.delete_user() - assert_raise(Ecto.NoResultsError, fn -> + assert_raise(Ecto.StaleEntryError, fn -> delete(conn, Routes.user_path(conn, :delete)) end) end end describe "when user not signed in" do - test "redirects to 403", %{unauthed_conn: conn} do + test "delete redirects to sign in", %{unauthed_conn: conn} do test_conn = delete(conn, Routes.user_path(conn, :delete)) - assert text_response(test_conn, 403) + assert redirected_to(test_conn) == Routes.root_path(test_conn, :index) end end end diff --git a/apps/fz_http/test/fz_http_web/live/connectivity_check_live/index_test.exs b/apps/fz_http/test/fz_http_web/live/connectivity_check_live/index_test.exs index f9955b981..b82d4999c 100644 --- a/apps/fz_http/test/fz_http_web/live/connectivity_check_live/index_test.exs +++ b/apps/fz_http/test/fz_http_web/live/connectivity_check_live/index_test.exs @@ -5,7 +5,7 @@ defmodule FzHttpWeb.ConnectivityCheckLive.IndexTest do setup :create_connectivity_checks test "show connectivity checks", %{ - authed_conn: conn, + admin_conn: conn, connectivity_checks: connectivity_checks } do path = Routes.connectivity_check_index_path(conn, :index) @@ -20,7 +20,7 @@ defmodule FzHttpWeb.ConnectivityCheckLive.IndexTest do describe "unauthenticated/connectivity_checks list" do test "mount redirects to session path", %{unauthed_conn: conn} do path = Routes.connectivity_check_index_path(conn, :index) - expected_path = Routes.session_path(conn, :new) + expected_path = Routes.root_path(conn, :index) assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path) end end diff --git a/apps/fz_http/test/fz_http_web/live/device_live/admin/index_test.exs b/apps/fz_http/test/fz_http_web/live/device_live/admin/index_test.exs new file mode 100644 index 000000000..22252cca7 --- /dev/null +++ b/apps/fz_http/test/fz_http_web/live/device_live/admin/index_test.exs @@ -0,0 +1,33 @@ +defmodule FzHttpWeb.DeviceLive.Admin.IndexTest do + use FzHttpWeb.ConnCase, async: false + + describe "authenticated/device list" do + setup :create_devices + + test "includes the device name in the list", %{admin_conn: conn, devices: devices} do + path = Routes.device_admin_index_path(conn, :index) + {:ok, _view, html} = live(conn, path) + + for device <- devices do + assert html =~ device.name + end + end + end + + describe "authenticated but user deleted" do + test "redirects to not authorized", %{admin_conn: conn} do + path = Routes.device_admin_index_path(conn, :index) + clear_users() + expected_path = Routes.root_path(conn, :index) + assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path) + end + end + + describe "unauthenticated" do + test "mount redirects to session path", %{unauthed_conn: conn} do + path = Routes.device_admin_index_path(conn, :index) + expected_path = Routes.root_path(conn, :index) + assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path) + end + end +end diff --git a/apps/fz_http/test/fz_http_web/live/device_live/admin/show_test.exs b/apps/fz_http/test/fz_http_web/live/device_live/admin/show_test.exs new file mode 100644 index 000000000..013974aa5 --- /dev/null +++ b/apps/fz_http/test/fz_http_web/live/device_live/admin/show_test.exs @@ -0,0 +1,29 @@ +defmodule FzHttpWeb.DeviceLive.Admin.ShowTest do + use FzHttpWeb.ConnCase, async: true + + describe "unauthenticated" do + setup :create_device + + @tag :unauthed + test "mount redirects to session path", %{unauthed_conn: conn, device: device} do + path = Routes.device_admin_show_path(conn, :show, device) + expected_path = Routes.root_path(conn, :index) + assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path) + end + end + + # XXX: Revisit this when RBAC is more fleshed out. Admins can now view other admins' devices. + # describe "authenticated as other user" do + # setup [:create_device, :create_other_user_device] + # + # test "mount redirects to session path", %{ + # admin_conn: conn, + # device: _device, + # other_device: other_device + # } do + # path = Routes.device_admin_show_path(conn, :show, other_device) + # expected_path = Routes.auth_path(conn, :request) + # assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path) + # end + # end +end diff --git a/apps/fz_http/test/fz_http_web/live/device_live/index_test.exs b/apps/fz_http/test/fz_http_web/live/device_live/index_test.exs deleted file mode 100644 index 29e19c361..000000000 --- a/apps/fz_http/test/fz_http_web/live/device_live/index_test.exs +++ /dev/null @@ -1,53 +0,0 @@ -defmodule FzHttpWeb.DeviceLive.IndexTest do - use FzHttpWeb.ConnCase, async: true - - alias FzHttp.{Devices, Devices.Device} - - describe "authenticated/device list" do - setup :create_devices - - test "includes the device name in the list", %{authed_conn: conn, devices: devices} do - path = Routes.device_index_path(conn, :index) - {:ok, _view, html} = live(conn, path) - - for device <- devices do - assert html =~ device.name - end - end - end - - describe "authenticated but user deleted" do - setup [:create_user] - - test "redirects to not authorized", %{authed_conn: conn} do - path = Routes.device_index_path(conn, :index) - clear_users() - expected_path = Routes.session_path(conn, :new) - assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path) - end - end - - describe "authenticated/creates device" do - test "creates device", %{authed_conn: conn} do - path = Routes.device_index_path(conn, :index) - {:ok, view, _html} = live(conn, path) - - view - |> element("button", "Add Device") - |> render_click() - - device = Devices.list_devices() |> List.first() - - assert %Device{} = device - assert_redirected(view, Routes.device_show_path(conn, :show, device)) - end - end - - describe "unauthenticated" do - test "mount redirects to session path", %{unauthed_conn: conn} do - path = Routes.device_index_path(conn, :index) - expected_path = Routes.session_path(conn, :new) - assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path) - end - end -end diff --git a/apps/fz_http/test/fz_http_web/live/device_live/show_test.exs b/apps/fz_http/test/fz_http_web/live/device_live/show_test.exs deleted file mode 100644 index ae60045d3..000000000 --- a/apps/fz_http/test/fz_http_web/live/device_live/show_test.exs +++ /dev/null @@ -1,365 +0,0 @@ -defmodule FzHttpWeb.DeviceLive.ShowTest do - use FzHttpWeb.ConnCase, async: true - - describe "authenticated" do - setup :create_device - - @valid_params %{"device" => %{"name" => "new_name"}} - @invalid_params %{"device" => %{"name" => ""}} - @allowed_ips "2.2.2.2" - @allowed_ips_change %{ - "device" => %{"use_default_allowed_ips" => "false", "allowed_ips" => @allowed_ips} - } - @allowed_ips_unchanged %{ - "device" => %{"use_default_allowed_ips" => "true", "allowed_ips" => @allowed_ips} - } - @dns "8.8.8.8, 8.8.4.4" - @dns_change %{ - "device" => %{"use_default_dns" => "false", "dns" => @dns} - } - @dns_unchanged %{ - "device" => %{"use_default_dns" => "true", "dns" => @dns} - } - @wireguard_endpoint "6.6.6.6" - @endpoint_change %{ - "device" => %{"use_default_endpoint" => "false", "endpoint" => @wireguard_endpoint} - } - @endpoint_unchanged %{ - "device" => %{"use_default_endpoint" => "true", "endpoint" => @wireguard_endpoint} - } - @mtu_change %{ - "device" => %{"use_default_mtu" => "false", "mtu" => "1280"} - } - @mtu_unchanged %{ - "device" => %{"use_default_mtu" => "true", "mtu" => "1280"} - } - @persistent_keepalive_change %{ - "device" => %{ - "use_default_persistent_keepalive" => "false", - "persistent_keepalive" => "120" - } - } - @persistent_keepalive_unchanged %{ - "device" => %{"use_default_persistent_keepalive" => "true", "persistent_keepalive" => "5"} - } - @default_allowed_ips_change %{ - "device" => %{"use_default_allowed_ips" => "false"} - } - @default_dns_change %{ - "device" => %{"use_default_dns" => "false"} - } - @default_endpoint_change %{ - "device" => %{"use_default_endpoint" => "false"} - } - @default_mtu_change %{ - "device" => %{"use_default_mtu" => "false"} - } - @default_persistent_keepalive_change %{ - "device" => %{"use_default_persistent_keepalive" => "false"} - } - - test "shows device details", %{authed_conn: conn, device: device} do - path = Routes.device_show_path(conn, :show, device) - {:ok, _view, html} = live(conn, path) - assert html =~ "#{device.name}" - assert html =~ "

Details

" - end - - test "opens modal", %{authed_conn: conn, device: device} do - path = Routes.device_show_path(conn, :show, device) - {:ok, view, _html} = live(conn, path) - - view - |> element("a", "Edit") - |> render_click() - - assert_patched(view, Routes.device_show_path(conn, :edit, device)) - end - - test "allows name changes", %{authed_conn: conn, device: device} do - path = Routes.device_show_path(conn, :edit, device) - {:ok, view, _html} = live(conn, path) - - view - |> form("#edit-device") - |> render_submit(@valid_params) - - flash = assert_redirected(view, Routes.device_show_path(conn, :show, device)) - assert flash["info"] == "Device updated successfully." - end - - test "prevents allowed_ips changes when use_default_allowed_ips is true ", %{ - authed_conn: conn, - device: device - } do - path = Routes.device_show_path(conn, :edit, device) - {:ok, view, _html} = live(conn, path) - - test_view = - view - |> form("#edit-device") - |> render_submit(@allowed_ips_unchanged) - - assert test_view =~ "must not be present" - end - - test "prevents dns changes when use_default_dns is true", %{ - authed_conn: conn, - device: device - } do - path = Routes.device_show_path(conn, :edit, device) - {:ok, view, _html} = live(conn, path) - - test_view = - view - |> form("#edit-device") - |> render_submit(@dns_unchanged) - - assert test_view =~ "must not be present" - end - - test "prevents endpoint changes when use_default_endpoint is true", %{ - authed_conn: conn, - device: device - } do - path = Routes.device_show_path(conn, :edit, device) - {:ok, view, _html} = live(conn, path) - - test_view = - view - |> form("#edit-device") - |> render_submit(@endpoint_unchanged) - - assert test_view =~ "must not be present" - end - - test "prevents mtu changes when use_default_mtu is true", %{ - authed_conn: conn, - device: device - } do - path = Routes.device_show_path(conn, :edit, device) - {:ok, view, _html} = live(conn, path) - - test_view = - view - |> form("#edit-device") - |> render_submit(@mtu_unchanged) - - assert test_view =~ "must not be present" - end - - test "prevents persistent_keepalive changes when use_default_persistent_keepalive is true", - %{ - authed_conn: conn, - device: device - } do - path = Routes.device_show_path(conn, :edit, device) - {:ok, view, _html} = live(conn, path) - - test_view = - view - |> form("#edit-device") - |> render_submit(@persistent_keepalive_unchanged) - - assert test_view =~ "must not be present" - end - - test "allows allowed_ips changes", %{authed_conn: conn, device: device} do - path = Routes.device_show_path(conn, :edit, device) - {:ok, view, _html} = live(conn, path) - - view - |> form("#edit-device") - |> render_submit(@allowed_ips_change) - - flash = assert_redirected(view, Routes.device_show_path(conn, :show, device)) - assert flash["info"] == "Device updated successfully." - - {:ok, _view, html} = live(conn, path) - assert html =~ "AllowedIPs = #{@allowed_ips}" - end - - test "allows dns changes", %{authed_conn: conn, device: device} do - path = Routes.device_show_path(conn, :edit, device) - {:ok, view, _html} = live(conn, path) - - view - |> form("#edit-device") - |> render_submit(@dns_change) - - flash = assert_redirected(view, Routes.device_show_path(conn, :show, device)) - assert flash["info"] == "Device updated successfully." - - {:ok, _view, html} = live(conn, path) - assert html =~ "DNS = #{@dns}" - end - - test "allows endpoint changes", %{authed_conn: conn, device: device} do - path = Routes.device_show_path(conn, :edit, device) - {:ok, view, _html} = live(conn, path) - - view - |> form("#edit-device") - |> render_submit(@endpoint_change) - - flash = assert_redirected(view, Routes.device_show_path(conn, :show, device)) - assert flash["info"] == "Device updated successfully." - - {:ok, _view, html} = live(conn, path) - assert html =~ "Endpoint = #{@wireguard_endpoint}:51820" - end - - test "allows mtu changes", %{authed_conn: conn, device: device} do - path = Routes.device_show_path(conn, :edit, device) - {:ok, view, _html} = live(conn, path) - - view - |> form("#edit-device") - |> render_submit(@mtu_change) - - flash = assert_redirected(view, Routes.device_show_path(conn, :show, device)) - assert flash["info"] == "Device updated successfully." - - {:ok, _view, html} = live(conn, path) - assert html =~ "MTU = 1280" - end - - test "allows persistent_keepalive changes", %{authed_conn: conn, device: device} do - path = Routes.device_show_path(conn, :edit, device) - {:ok, view, _html} = live(conn, path) - - view - |> form("#edit-device") - |> render_submit(@persistent_keepalive_change) - - flash = assert_redirected(view, Routes.device_show_path(conn, :show, device)) - assert flash["info"] == "Device updated successfully." - - {:ok, _view, html} = live(conn, path) - assert html =~ "PersistentKeepalive = 120" - end - - test "prevents empty names", %{authed_conn: conn, device: device} do - path = Routes.device_show_path(conn, :edit, device) - {:ok, view, _html} = live(conn, path) - - test_view = - view - |> form("#edit-device") - |> render_submit(@invalid_params) - - assert test_view =~ "can't be blank" - end - - test "on use_default_allowed_ips change", %{authed_conn: conn, device: device} do - path = Routes.device_show_path(conn, :edit, device) - {:ok, view, _html} = live(conn, path) - - test_view = - view - |> form("#edit-device") - |> render_change(@default_allowed_ips_change) - - assert test_view =~ """ - \ - """ - end - - test "on use_default_dns change", %{authed_conn: conn, device: device} do - path = Routes.device_show_path(conn, :edit, device) - {:ok, view, _html} = live(conn, path) - - test_view = - view - |> form("#edit-device") - |> render_change(@default_dns_change) - - assert test_view =~ """ - \ - """ - end - - test "on use_default_endpoint change", %{authed_conn: conn, device: device} do - path = Routes.device_show_path(conn, :edit, device) - {:ok, view, _html} = live(conn, path) - - test_view = - view - |> form("#edit-device") - |> render_change(@default_endpoint_change) - - assert test_view =~ """ - \ - """ - end - - test "on use_default_mtu change", %{authed_conn: conn, device: device} do - path = Routes.device_show_path(conn, :edit, device) - {:ok, view, _html} = live(conn, path) - - test_view = - view - |> form("#edit-device") - |> render_change(@default_mtu_change) - - assert test_view =~ """ - \ - """ - end - - test "on use_default_persistent_keepalive change", %{authed_conn: conn, device: device} do - path = Routes.device_show_path(conn, :edit, device) - {:ok, view, _html} = live(conn, path) - - test_view = - view - |> form("#edit-device") - |> render_change(@default_persistent_keepalive_change) - - assert test_view =~ """ - \ - """ - end - end - - describe "delete own device" do - setup :create_device - - test "successful", %{authed_conn: conn, device: device} do - path = Routes.device_show_path(conn, :show, device) - {:ok, view, _html} = live(conn, path) - - view - |> element("button", "Delete Device #{device.name}") - |> render_click() - - _flash = assert_redirected(view, Routes.device_index_path(conn, :index)) - end - end - - describe "unauthenticated" do - setup :create_device - - @tag :unauthed - test "mount redirects to session path", %{unauthed_conn: conn, device: device} do - path = Routes.device_show_path(conn, :show, device) - expected_path = Routes.session_path(conn, :new) - assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path) - end - end - - # XXX: Revisit this when RBAC is more fleshed out. Admins can now view other admins' devices. - # describe "authenticated as other user" do - # setup [:create_device, :create_other_user_device] - # - # test "mount redirects to session path", %{ - # authed_conn: conn, - # device: _device, - # other_device: other_device - # } do - # path = Routes.device_show_path(conn, :show, other_device) - # expected_path = Routes.session_path(conn, :new) - # assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path) - # end - # end -end diff --git a/apps/fz_http/test/fz_http_web/live/device_live/unprivileged/index_test.exs b/apps/fz_http/test/fz_http_web/live/device_live/unprivileged/index_test.exs new file mode 100644 index 000000000..b39ec73ee --- /dev/null +++ b/apps/fz_http/test/fz_http_web/live/device_live/unprivileged/index_test.exs @@ -0,0 +1,66 @@ +defmodule FzHttpWeb.DeviceLive.Unprivileged.IndexTest do + use FzHttpWeb.ConnCase, async: false + + # alias FzHttp.{Devices, Devices.Device} + + describe "authenticated/device list" do + setup :create_devices + + test "includes the device name in the list", %{admin_conn: conn, devices: devices} do + path = Routes.device_admin_index_path(conn, :index) + {:ok, _view, html} = live(conn, path) + + for device <- devices do + assert html =~ device.name + end + end + end + + describe "authenticated but user deleted" do + test "redirects to not authorized", %{admin_conn: conn} do + path = Routes.device_admin_index_path(conn, :index) + clear_users() + expected_path = Routes.root_path(conn, :index) + assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path) + end + end + + describe "authenticated/creates device" do + test "shows new form", %{unprivileged_conn: conn} do + path = Routes.device_unprivileged_index_path(conn, :index) + {:ok, view, _html} = live(conn, path) + + new_view = + view + |> element("a", "Add Device") + |> render_click() + + assert_patched(view, Routes.device_unprivileged_index_path(conn, :new)) + assert new_view =~ "Add Device" + + assert new_view =~ """ +
element("#create-device") + |> render_submit(%{"device" => %{"public_key" => "test-pubkey", "name" => "test-tunnel"}}) + + assert new_view =~ "Device added!" + end + end + + describe "unauthenticated" do + test "mount redirects to session path", %{unauthed_conn: conn} do + path = Routes.device_admin_index_path(conn, :index) + expected_path = Routes.root_path(conn, :index) + assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path) + end + end +end diff --git a/apps/fz_http/test/fz_http_web/live/rule_live/index_test.exs b/apps/fz_http/test/fz_http_web/live/rule_live/index_test.exs index ab38138b0..2fcb628c7 100644 --- a/apps/fz_http/test/fz_http_web/live/rule_live/index_test.exs +++ b/apps/fz_http/test/fz_http_web/live/rule_live/index_test.exs @@ -7,7 +7,7 @@ defmodule FzHttpWeb.RuleLive.IndexTest do @destination "1.2.3.4" @allow_params %{"rule" => %{"action" => "accept", "destination" => @destination}} - test "adds to allowlist", %{authed_conn: conn} do + test "adds to allowlist", %{admin_conn: conn} do path = Routes.rule_index_path(conn, :index) {:ok, view, _html} = live(conn, path) @@ -19,7 +19,7 @@ defmodule FzHttpWeb.RuleLive.IndexTest do assert test_view =~ @destination end - test "validation fails", %{authed_conn: conn, rule: _rule} do + test "validation fails", %{admin_conn: conn, rule: _rule} do path = Routes.rule_index_path(conn, :index) {:ok, view, _html} = live(conn, path) @@ -48,7 +48,7 @@ defmodule FzHttpWeb.RuleLive.IndexTest do refute valid_view =~ "is invalid" end - test "removes from allowlist", %{authed_conn: conn, rule: rule} do + test "removes from allowlist", %{admin_conn: conn, rule: rule} do path = Routes.rule_index_path(conn, :index) {:ok, view, _html} = live(conn, path) @@ -67,7 +67,7 @@ defmodule FzHttpWeb.RuleLive.IndexTest do @destination "1.2.3.4" @deny_params %{"rule" => %{"action" => "drop", "destination" => @destination}} - test "adds to denylist", %{authed_conn: conn, rule: _rule} do + test "adds to denylist", %{admin_conn: conn, rule: _rule} do path = Routes.rule_index_path(conn, :index) {:ok, view, _html} = live(conn, path) @@ -79,7 +79,7 @@ defmodule FzHttpWeb.RuleLive.IndexTest do assert test_view =~ @destination end - test "validation fails", %{authed_conn: conn, rule: _rule} do + test "validation fails", %{admin_conn: conn, rule: _rule} do path = Routes.rule_index_path(conn, :index) {:ok, view, _html} = live(conn, path) @@ -96,7 +96,7 @@ defmodule FzHttpWeb.RuleLive.IndexTest do assert test_view =~ "is invalid" end - test "removes from denylist", %{authed_conn: conn, rule: rule} do + test "removes from denylist", %{admin_conn: conn, rule: rule} do path = Routes.rule_index_path(conn, :index) {:ok, view, _html} = live(conn, path) diff --git a/apps/fz_http/test/fz_http_web/live/setting_live/account_test.exs b/apps/fz_http/test/fz_http_web/live/setting_live/account_test.exs index a49511f41..a36dd16ef 100644 --- a/apps/fz_http/test/fz_http_web/live/setting_live/account_test.exs +++ b/apps/fz_http/test/fz_http_web/live/setting_live/account_test.exs @@ -7,17 +7,17 @@ defmodule FzHttpWeb.SettingLive.AccountTest do describe "when unauthenticated" do test "mount redirects to session path", %{unauthed_conn: conn} do path = Routes.setting_account_path(conn, :show) - expected_path = Routes.session_path(conn, :new) + expected_path = Routes.root_path(conn, :index) assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path) end end describe "when live_action is show" do - test "shows account details", %{authed_conn: conn} do + test "shows account details", %{admin_user: user, admin_conn: conn} do path = Routes.setting_account_path(conn, :show) {:ok, _view, html} = live(conn, path) - user = Users.get_user!(get_session(conn, :user_id)) + user = Users.get_user!(user.id) assert html =~ "Delete Your Account" assert html =~ user.email @@ -33,7 +33,7 @@ defmodule FzHttpWeb.SettingLive.AccountTest do "Change email or enter new password below" end - test "saves email when submitted", %{authed_conn: conn} do + test "saves email when submitted", %{admin_conn: conn} do path = Routes.setting_account_path(conn, :edit) {:ok, view, _html} = live(conn, path) @@ -45,7 +45,7 @@ defmodule FzHttpWeb.SettingLive.AccountTest do assert flash["info"] == "Account updated successfully." end - test "doesn't allow empty email", %{authed_conn: conn} do + test "doesn't allow empty email", %{admin_conn: conn} do path = Routes.setting_account_path(conn, :edit) {:ok, view, _html} = live(conn, path) @@ -65,7 +65,7 @@ defmodule FzHttpWeb.SettingLive.AccountTest do assert test_view =~ "can't be blank" end - test "renders validation errors", %{authed_conn: conn} do + test "renders validation errors", %{admin_conn: conn} do path = Routes.setting_account_path(conn, :edit) {:ok, view, _html} = live(conn, path) @@ -77,7 +77,7 @@ defmodule FzHttpWeb.SettingLive.AccountTest do assert test_view =~ "has invalid format" end - test "closes modal", %{authed_conn: conn} do + test "closes modal", %{admin_conn: conn} do path = Routes.setting_account_path(conn, :edit) {:ok, view, _html} = live(conn, path) diff --git a/apps/fz_http/test/fz_http_web/live/setting_live/default_test.exs b/apps/fz_http/test/fz_http_web/live/setting_live/default_test.exs deleted file mode 100644 index 6acbcd7ec..000000000 --- a/apps/fz_http/test/fz_http_web/live/setting_live/default_test.exs +++ /dev/null @@ -1,177 +0,0 @@ -defmodule FzHttpWeb.SettingLive.DefaultTest do - use FzHttpWeb.ConnCase, async: true - - alias FzHttp.Settings - - describe "authenticated/settings default" do - @valid_allowed_ips %{ - "setting" => %{"value" => "1.1.1.1"} - } - @valid_dns %{ - "setting" => %{"value" => "1.1.1.1"} - } - @valid_endpoint %{ - "setting" => %{"value" => "1.1.1.1"} - } - - @invalid_allowed_ips %{ - "setting" => %{"value" => "foobar"} - } - @invalid_dns %{ - "setting" => %{"value" => "foobar"} - } - @invalid_endpoint %{ - "setting" => %{"value" => "foobar"} - } - - setup %{authed_conn: conn} do - path = Routes.setting_default_path(conn, :show) - {:ok, view, html} = live(conn, path) - - %{html: html, view: view} - end - - test "renders current settings", %{html: html} do - assert html =~ - (Settings.default_device_allowed_ips() || - Application.fetch_env!(:fz_http, :wireguard_allowed_ips)) - - assert html =~ - (Settings.default_device_dns() || Application.fetch_env!(:fz_http, :wireguard_dns)) - - assert html =~ """ - id="endpoint_form_component"\ - """ - - assert html =~ """ - id="persistent_keepalive_form_component"\ - """ - end - - test "hides Save button by default", %{html: html} do - refute html =~ """ - \ - """ - end - - test "shows Save button after allowed_ips form is changed", %{view: view} do - test_view = - view - |> element("#allowed_ips_form_component") - |> render_change(@valid_allowed_ips) - - assert test_view =~ """ - \ - """ - end - - test "shows Save button after dns form is changed", %{view: view} do - test_view = - view - |> element("#dns_form_component") - |> render_change(@valid_dns) - - assert test_view =~ """ - \ - """ - end - - test "shows Save button after endpoint form is changed", %{view: view} do - test_view = - view - |> element("#endpoint_form_component") - |> render_change(@valid_endpoint) - - assert test_view =~ """ - \ - """ - end - - test "updates default allowed_ips", %{view: view} do - test_view = - view - |> element("#allowed_ips_form_component") - |> render_submit(@valid_allowed_ips) - - refute test_view =~ "is invalid" - - assert test_view =~ """ - \ - """ - end - - test "updates default dns", %{view: view} do - test_view = - view - |> element("#dns_form_component") - |> render_submit(@valid_dns) - - refute test_view =~ "is invalid" - - assert test_view =~ """ - \ - """ - end - - test "updates default endpoint", %{view: view} do - test_view = - view - |> element("#endpoint_form_component") - |> render_submit(@valid_endpoint) - - refute test_view =~ "is invalid" - - assert test_view =~ """ - \ - """ - end - - test "prevents invalid allowed_ips", %{view: view} do - test_view = - view - |> element("#allowed_ips_form_component") - |> render_submit(@invalid_allowed_ips) - - assert test_view =~ "is invalid" - - refute test_view =~ """ - element("#dns_form_component") - |> render_submit(@invalid_dns) - - assert test_view =~ "is invalid" - - refute test_view =~ """ - element("#endpoint_form_component") - |> render_submit(@invalid_endpoint) - - assert test_view =~ "is invalid" - - refute test_view =~ """ - %{"allowed_ips" => "1.1.1.1"} + } + @valid_dns %{ + "site" => %{"dns" => "1.1.1.1"} + } + @valid_endpoint %{ + "site" => %{"endpoint" => "1.1.1.1"} + } + @valid_persistent_keepalive %{ + "site" => %{"persistent_keepalive" => "1"} + } + @valid_mtu %{ + "site" => %{"mtu" => "1000"} + } + + @invalid_allowed_ips %{ + "site" => %{"allowed_ips" => "foobar"} + } + @invalid_dns %{ + "site" => %{"dns" => "foobar"} + } + @invalid_endpoint %{ + "site" => %{"endpoint" => "foobar"} + } + @invalid_persistent_keepalive %{ + "site" => %{"persistent_keepalive" => "-1"} + } + @invalid_mtu %{ + "site" => %{"mtu" => "0"} + } + + setup %{admin_conn: conn} do + path = Routes.setting_site_path(conn, :show) + {:ok, view, html} = live(conn, path) + + %{html: html, view: view} + end + + test "renders current sites", %{html: html} do + assert html =~ + (Sites.get_site!().allowed_ips || + Application.fetch_env!(:fz_http, :wireguard_allowed_ips)) + + assert html =~ + (Sites.get_site!().dns || Application.fetch_env!(:fz_http, :wireguard_dns)) + + assert html =~ """ + id="site_form_component_endpoint"\ + """ + + assert html =~ """ + id="site_form_component_persistent_keepalive"\ + """ + end + + test "updates site allowed_ips", %{view: view} do + test_view = + view + |> element("#site_form_component") + |> render_submit(@valid_allowed_ips) + + refute test_view =~ "is invalid" + + assert test_view =~ """ + \ + """ + end + + test "updates site dns", %{view: view} do + test_view = + view + |> element("#site_form_component") + |> render_submit(@valid_dns) + + refute test_view =~ "is invalid" + + assert test_view =~ """ + \ + """ + end + + test "updates site endpoint", %{view: view} do + test_view = + view + |> element("#site_form_component") + |> render_submit(@valid_endpoint) + + refute test_view =~ "is invalid" + + assert test_view =~ """ + \ + """ + end + + test "updates site persistent_keepalive", %{view: view} do + test_view = + view + |> element("#site_form_component") + |> render_submit(@valid_persistent_keepalive) + + refute test_view =~ "is invalid" + + assert test_view =~ """ + \ + """ + end + + test "updates site mtu", %{view: view} do + test_view = + view + |> element("#site_form_component") + |> render_submit(@valid_mtu) + + refute test_view =~ "is invalid" + + assert test_view =~ """ + \ + """ + end + + test "prevents invalid allowed_ips", %{view: view} do + test_view = + view + |> element("#site_form_component") + |> render_submit(@invalid_allowed_ips) + + assert test_view =~ "is invalid" + + assert test_view =~ """ + \ + """ + end + + test "prevents invalid dns", %{view: view} do + test_view = + view + |> element("#site_form_component") + |> render_submit(@invalid_dns) + + assert test_view =~ "is invalid" + + assert test_view =~ """ + \ + """ + end + + test "prevents invalid endpoint", %{view: view} do + test_view = + view + |> element("#site_form_component") + |> render_submit(@invalid_endpoint) + + assert test_view =~ "is invalid" + + assert test_view =~ """ + \ + """ + end + + test "prevents invalid persistent_keepalive", %{view: view} do + test_view = + view + |> element("#site_form_component") + |> render_submit(@invalid_persistent_keepalive) + + assert test_view =~ "must be greater than or equal to 0" + + assert test_view =~ """ + \ + """ + end + + test "prevents invalid mtu", %{view: view} do + test_view = + view + |> element("#site_form_component") + |> render_submit(@invalid_mtu) + + assert test_view =~ "must be greater than or equal to 576" + + assert test_view =~ """ + \ + """ + end + end + + describe "unauthenticated/settings default" do + @tag :unauthed + test "mount redirects to session path", %{unauthed_conn: conn} do + path = Routes.setting_site_path(conn, :show) + expected_path = Routes.root_path(conn, :index) + assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path) + end + end +end diff --git a/apps/fz_http/test/fz_http_web/live/user_live/index_test.exs b/apps/fz_http/test/fz_http_web/live/user_live/index_test.exs index 6822f44e5..132988b78 100644 --- a/apps/fz_http/test/fz_http_web/live/user_live/index_test.exs +++ b/apps/fz_http/test/fz_http_web/live/user_live/index_test.exs @@ -7,7 +7,7 @@ defmodule FzHttpWeb.UserLive.IndexTest do setup [:create_devices, :create_users] test "includes the created user email in the list", %{ - authed_conn: conn, + admin_conn: conn, devices: _devices, users: users } do @@ -20,7 +20,7 @@ defmodule FzHttpWeb.UserLive.IndexTest do end test "includes device_counts in the list", %{ - authed_conn: conn, + admin_conn: conn, devices: _devices, users: _users } do @@ -32,7 +32,7 @@ defmodule FzHttpWeb.UserLive.IndexTest do end end - test "navigates to user show", %{authed_conn: conn, users: users} do + test "navigates to user show", %{admin_conn: conn, users: users} do path = Routes.user_index_path(conn, :index) {:ok, view, _html} = live(conn, path) user = List.first(users) @@ -50,7 +50,7 @@ defmodule FzHttpWeb.UserLive.IndexTest do test "redirects to sign in", %{unauthed_conn: conn} do path = Routes.user_index_path(conn, :index) - expected_path = Routes.session_path(conn, :new) + expected_path = Routes.root_path(conn, :index) assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path) end end @@ -74,7 +74,7 @@ defmodule FzHttpWeb.UserLive.IndexTest do } } - test "successfully creates user", %{authed_conn: conn} do + test "successfully creates user", %{admin_conn: conn} do path = Routes.user_index_path(conn, :new) {:ok, view, _html} = live(conn, path) @@ -88,7 +88,7 @@ defmodule FzHttpWeb.UserLive.IndexTest do assert new_path == Routes.user_show_path(conn, :show, user) end - test "renders errors", %{authed_conn: conn} do + test "renders errors", %{admin_conn: conn} do path = Routes.user_index_path(conn, :new) {:ok, view, _html} = live(conn, path) @@ -105,7 +105,7 @@ defmodule FzHttpWeb.UserLive.IndexTest do describe "add user modal" do setup :create_users - test "shows the modal", %{authed_conn: conn} do + test "shows the modal", %{admin_conn: conn} do path = Routes.user_index_path(conn, :index) {:ok, view, _html} = live(conn, path) diff --git a/apps/fz_http/test/fz_http_web/live/user_live/show_test.exs b/apps/fz_http/test/fz_http_web/live/user_live/show_test.exs index e2545e6c3..725af6310 100644 --- a/apps/fz_http/test/fz_http_web/live/user_live/show_test.exs +++ b/apps/fz_http/test/fz_http_web/live/user_live/show_test.exs @@ -5,23 +5,432 @@ defmodule FzHttpWeb.UserLive.ShowTest do describe "authenticated show" do setup :create_device - test "includes the device name", %{authed_conn: conn, device: device} do + test "includes the device name", %{admin_conn: conn, device: device} do path = Routes.user_show_path(conn, :show, device.user_id) {:ok, _view, html} = live(conn, path) assert html =~ device.name end + end - test "opens the edit modal", %{authed_conn: conn, device: device} do - path = Routes.user_show_path(conn, :show, device.user_id) + describe "authenticated show device" do + setup :create_device + + test "shows device details", %{admin_conn: conn, device: device} do + path = Routes.device_admin_show_path(conn, :show, device) + {:ok, _view, html} = live(conn, path) + assert html =~ "#{device.name}" + assert html =~ "

Details

" + end + end + + describe "authenticated new device" do + @device_id_regex ~r/device-(?.*)-inserted-at/ + @valid_params %{ + "device" => %{ + "public_key" => "test-pubkey", + "name" => "new_name" + } + } + @invalid_params %{ + "device" => %{ + "public_key" => "test-pubkey", + "name" => "" + } + } + @allowed_ips "2.2.2.2" + @allowed_ips_change %{ + "device" => %{ + "public_key" => "test-pubkey", + "use_site_allowed_ips" => "false", + "allowed_ips" => @allowed_ips + } + } + @allowed_ips_unchanged %{ + "device" => %{ + "public_key" => "test-pubkey", + "use_site_allowed_ips" => "true", + "allowed_ips" => @allowed_ips + } + } + @dns "8.8.8.8, 8.8.4.4" + @dns_change %{ + "device" => %{ + "public_key" => "test-pubkey", + "use_site_dns" => "false", + "dns" => @dns + } + } + @dns_unchanged %{ + "device" => %{ + "public_key" => "test-pubkey", + "use_site_dns" => "true", + "dns" => @dns + } + } + @wireguard_endpoint "6.6.6.6" + @endpoint_change %{ + "device" => %{ + "public_key" => "test-pubkey", + "use_site_endpoint" => "false", + "endpoint" => @wireguard_endpoint + } + } + @endpoint_unchanged %{ + "device" => %{ + "public_key" => "test-pubkey", + "use_site_endpoint" => "true", + "endpoint" => @wireguard_endpoint + } + } + @mtu_change %{ + "device" => %{ + "public_key" => "test-pubkey", + "use_site_mtu" => "false", + "mtu" => "1280" + } + } + @mtu_unchanged %{ + "device" => %{ + "public_key" => "test-pubkey", + "use_site_mtu" => "true", + "mtu" => "1280" + } + } + @persistent_keepalive_change %{ + "device" => %{ + "public_key" => "test-pubkey", + "use_site_persistent_keepalive" => "false", + "persistent_keepalive" => "120" + } + } + @persistent_keepalive_unchanged %{ + "device" => %{ + "public_key" => "test-pubkey", + "use_site_persistent_keepalive" => "true", + "persistent_keepalive" => "5" + } + } + @default_allowed_ips_change %{ + "device" => %{ + "public_key" => "test-pubkey", + "use_site_allowed_ips" => "false" + } + } + @default_dns_change %{ + "device" => %{ + "public_key" => "test-pubkey", + "use_site_dns" => "false" + } + } + @default_endpoint_change %{ + "device" => %{ + "public_key" => "test-pubkey", + "use_site_endpoint" => "false" + } + } + @default_mtu_change %{ + "device" => %{ + "public_key" => "test-pubkey", + "use_site_mtu" => "false" + } + } + @default_persistent_keepalive_change %{ + "device" => %{ + "public_key" => "test-pubkey", + "use_site_persistent_keepalive" => "false" + } + } + + def device_id(view) do + %{"device_id" => device_id} = Regex.named_captures(@device_id_regex, view) + device_id + end + + test "opens modal", %{admin_conn: conn, admin_user: user} do + path = Routes.user_show_path(conn, :new_device, user.id) {:ok, view, _html} = live(conn, path) view - |> element("a", "Change Email or Password") + |> element("#add-device-button") |> render_click() - new_path = assert_patch(view) - assert new_path == Routes.user_show_path(conn, :edit, device.user_id) + assert_patched(view, Routes.user_show_path(conn, :new_device, user.id)) + end + + test "allows name changes", %{admin_conn: conn, admin_user: user} do + path = Routes.user_show_path(conn, :new_device, user.id) + {:ok, view, _html} = live(conn, path) + + test_view = + view + |> form("#create-device") + |> render_submit(@valid_params) + + assert test_view =~ "Device added!" + end + + test "prevents allowed_ips changes when use_site_allowed_ips is true", %{ + admin_conn: conn, + admin_user: user + } do + path = Routes.user_show_path(conn, :new_device, user.id) + {:ok, view, _html} = live(conn, path) + + test_view = + view + |> form("#create-device") + |> render_submit(@allowed_ips_unchanged) + + assert test_view =~ "must not be present" + end + + test "prevents dns changes when use_site_dns is true", %{ + admin_conn: conn, + admin_user: user + } do + path = Routes.user_show_path(conn, :new_device, user.id) + {:ok, view, _html} = live(conn, path) + + test_view = + view + |> form("#create-device") + |> render_submit(@dns_unchanged) + + assert test_view =~ "must not be present" + end + + test "prevents endpoint changes when use_site_endpoint is true", %{ + admin_conn: conn, + admin_user: user + } do + path = Routes.user_show_path(conn, :new_device, user.id) + {:ok, view, _html} = live(conn, path) + + test_view = + view + |> form("#create-device") + |> render_submit(@endpoint_unchanged) + + assert test_view =~ "must not be present" + end + + test "prevents mtu changes when use_site_mtu is true", %{ + admin_conn: conn, + admin_user: user + } do + path = Routes.user_show_path(conn, :new_device, user.id) + {:ok, view, _html} = live(conn, path) + + test_view = + view + |> form("#create-device") + |> render_submit(@mtu_unchanged) + + assert test_view =~ "must not be present" + end + + test "prevents persistent_keepalive changes when use_site_persistent_keepalive is true", + %{ + admin_conn: conn, + admin_user: user + } do + path = Routes.user_show_path(conn, :new_device, user.id) + {:ok, view, _html} = live(conn, path) + + test_view = + view + |> form("#create-device") + |> render_submit(@persistent_keepalive_unchanged) + + assert test_view =~ "must not be present" + end + + test "allows allowed_ips changes", %{admin_conn: conn, admin_user: user} do + path = Routes.user_show_path(conn, :new_device, user.id) + {:ok, view, _html} = live(conn, path) + + test_view = + view + |> form("#create-device") + |> render_submit(@allowed_ips_change) + + assert test_view =~ "Device added!" + + path = Routes.user_show_path(conn, :show, user.id) + {:ok, _view, html} = live(conn, path) + path = Routes.device_admin_show_path(conn, :show, device_id(html)) + {:ok, _view, html} = live(conn, path) + assert html =~ @allowed_ips + end + + test "allows dns changes", %{admin_conn: conn, admin_user: user} do + path = Routes.user_show_path(conn, :new_device, user.id) + {:ok, view, _html} = live(conn, path) + + test_view = + view + |> form("#create-device") + |> render_submit(@dns_change) + + assert test_view =~ "Device added!" + + path = Routes.user_show_path(conn, :show, user.id) + {:ok, _view, html} = live(conn, path) + path = Routes.device_admin_show_path(conn, :show, device_id(html)) + {:ok, _view, html} = live(conn, path) + assert html =~ @dns + end + + test "allows endpoint changes", %{admin_conn: conn, admin_user: user} do + path = Routes.user_show_path(conn, :new_device, user.id) + {:ok, view, _html} = live(conn, path) + + test_view = + view + |> form("#create-device") + |> render_submit(@endpoint_change) + + assert test_view =~ "Device added!" + + path = Routes.user_show_path(conn, :show, user.id) + {:ok, _view, html} = live(conn, path) + path = Routes.device_admin_show_path(conn, :show, device_id(html)) + {:ok, _view, html} = live(conn, path) + assert html =~ @wireguard_endpoint + end + + test "allows mtu changes", %{admin_conn: conn, admin_user: user} do + path = Routes.user_show_path(conn, :new_device, user.id) + {:ok, view, _html} = live(conn, path) + + test_view = + view + |> form("#create-device") + |> render_submit(@mtu_change) + + assert test_view =~ "Device added!" + + path = Routes.user_show_path(conn, :show, user.id) + {:ok, _view, html} = live(conn, path) + path = Routes.device_admin_show_path(conn, :show, device_id(html)) + {:ok, _view, html} = live(conn, path) + assert html =~ "1280" + end + + test "allows persistent_keepalive changes", %{admin_conn: conn, admin_user: user} do + path = Routes.user_show_path(conn, :new_device, user.id) + {:ok, view, _html} = live(conn, path) + + test_view = + view + |> form("#create-device") + |> render_submit(@persistent_keepalive_change) + + assert test_view =~ "Device added!" + + path = Routes.user_show_path(conn, :show, user.id) + {:ok, _view, html} = live(conn, path) + path = Routes.device_admin_show_path(conn, :show, device_id(html)) + {:ok, _view, html} = live(conn, path) + assert html =~ "120" + end + + test "prevents empty names", %{admin_conn: conn, admin_user: user} do + path = Routes.user_show_path(conn, :new_device, user.id) + {:ok, view, _html} = live(conn, path) + + test_view = + view + |> form("#create-device") + |> render_submit(@invalid_params) + + assert test_view =~ "can't be blank" + end + + test "on use_site_allowed_ips change", %{admin_conn: conn, admin_user: user} do + path = Routes.user_show_path(conn, :new_device, user.id) + {:ok, view, _html} = live(conn, path) + + test_view = + view + |> form("#create-device") + |> render_change(@default_allowed_ips_change) + + assert test_view =~ """ + \ + """ + end + + test "on use_site_dns change", %{admin_conn: conn, admin_user: user} do + path = Routes.user_show_path(conn, :new_device, user.id) + {:ok, view, _html} = live(conn, path) + + test_view = + view + |> form("#create-device") + |> render_change(@default_dns_change) + + assert test_view =~ """ + \ + """ + end + + test "on use_site_endpoint change", %{admin_conn: conn, admin_user: user} do + path = Routes.user_show_path(conn, :new_device, user.id) + {:ok, view, _html} = live(conn, path) + + test_view = + view + |> form("#create-device") + |> render_change(@default_endpoint_change) + + assert test_view =~ """ + \ + """ + end + + test "on use_site_mtu change", %{admin_conn: conn, admin_user: user} do + path = Routes.user_show_path(conn, :new_device, user.id) + {:ok, view, _html} = live(conn, path) + + test_view = + view + |> form("#create-device") + |> render_change(@default_mtu_change) + + assert test_view =~ """ + \ + """ + end + + test "on use_site_persistent_keepalive change", %{admin_conn: conn, admin_user: user} do + path = Routes.user_show_path(conn, :new_device, user.id) + {:ok, view, _html} = live(conn, path) + + test_view = + view + |> form("#create-device") + |> render_change(@default_persistent_keepalive_change) + + assert test_view =~ """ + \ + """ + end + end + + describe "delete own device" do + setup :create_device + + test "successful", %{admin_conn: conn, device: device} do + path = Routes.device_admin_show_path(conn, :show, device) + {:ok, view, _html} = live(conn, path) + + view + |> element("button", "Delete Device #{device.name}") + |> render_click() + + _flash = assert_redirected(view, Routes.device_admin_index_path(conn, :index)) end end @@ -30,29 +439,29 @@ defmodule FzHttpWeb.UserLive.ShowTest do test "redirects to sign in", %{unauthed_conn: conn, device: device} do path = Routes.user_show_path(conn, :show, device.user_id) - expected_path = Routes.session_path(conn, :new) + expected_path = Routes.root_path(conn, :index) assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path) end end describe "delete self" do - test "displays flash message with error", %{user_id: user_id, authed_conn: conn} do - path = Routes.user_show_path(conn, :show, user_id) + test "displays flash message with error", %{admin_user: user, admin_conn: conn} do + path = Routes.user_show_path(conn, :show, user.id) {:ok, view, _html} = live(conn, path) - new_view = + test_view = view |> element("button", "Delete User") |> render_click() - assert new_view =~ "Use the account section to delete your account." + assert test_view =~ "Use the account section to delete your account." end end describe "delete_user" do setup :create_users - test "deletes the user", %{authed_conn: conn, users: users} do + test "deletes the user", %{admin_conn: conn, users: users} do user = List.last(users) path = Routes.user_show_path(conn, :show, user.id) {:ok, view, _html} = live(conn, path) @@ -70,7 +479,7 @@ defmodule FzHttpWeb.UserLive.ShowTest do describe "edit user" do setup :create_users - setup %{users: users, authed_conn: conn} do + setup %{users: users, admin_conn: conn} do user = List.last(users) path = Routes.user_show_path(conn, :edit, user.id) {:ok, view, _html} = live(conn, path) @@ -81,7 +490,7 @@ defmodule FzHttpWeb.UserLive.ShowTest do assert new_path == Routes.user_show_path(conn, :show, user) end - %{success: success, view: view, conn: conn, user: user} + %{success: success, view: view, admin_conn: conn, user: user} end @new_email_attrs %{"user" => %{"email" => "newemail@localhost"}} @@ -103,7 +512,12 @@ defmodule FzHttpWeb.UserLive.ShowTest do } } - test "successfully changes email", %{success: success, view: view, user: user, conn: conn} do + test "successfully changes email", %{ + success: success, + view: view, + user: user, + admin_conn: conn + } do view |> element("form#user-form") |> render_submit(@new_email_attrs) @@ -111,7 +525,12 @@ defmodule FzHttpWeb.UserLive.ShowTest do success.(conn, view, user) end - test "successfully changes password", %{success: success, view: view, conn: conn, user: user} do + test "successfully changes password", %{ + success: success, + view: view, + admin_conn: conn, + user: user + } do view |> element("form#user-form") |> render_submit(@new_password_attrs) @@ -122,7 +541,7 @@ defmodule FzHttpWeb.UserLive.ShowTest do test "successfully changes email and password", %{ success: success, view: view, - conn: conn, + admin_conn: conn, user: user } do view @@ -133,31 +552,13 @@ defmodule FzHttpWeb.UserLive.ShowTest do end test "displays errors for invalid changes", %{view: view} do - new_view = + test_view = view |> element("form#user-form") |> render_submit(@invalid_attrs) - assert new_view =~ "has invalid format" - assert new_view =~ "should be at least 12 character(s)" - end - end - - describe "create_device" do - setup :create_users - - test "creates a new device for user", %{authed_conn: conn, users: users} do - user = List.last(users) - path = Routes.user_show_path(conn, :show, user.id) - {:ok, view, _html} = live(conn, path) - - view - |> element("button", "Add Device") - |> render_click() - - {new_path, flash} = assert_redirect(view) - assert flash["info"] == "Device created successfully." - assert new_path =~ ~r/\/devices\/\d+/ + assert test_view =~ "has invalid format" + assert test_view =~ "should be at least 12 character(s)" end end end diff --git a/apps/fz_http/test/support/conn_case.ex b/apps/fz_http/test/support/conn_case.ex index 63d1911be..dfbaa8736 100644 --- a/apps/fz_http/test/support/conn_case.ex +++ b/apps/fz_http/test/support/conn_case.ex @@ -19,7 +19,7 @@ defmodule FzHttpWeb.ConnCase do alias Ecto.Adapters.SQL.Sandbox - alias FzHttp.SessionsFixtures + alias FzHttp.UsersFixtures using do quote do @@ -32,6 +32,11 @@ defmodule FzHttpWeb.ConnCase do # The default endpoint for testing @endpoint FzHttpWeb.Endpoint + + def current_user(test_conn) do + get_session(test_conn) + |> FzHttpWeb.Authentication.get_current_user() + end end end @@ -39,12 +44,24 @@ defmodule FzHttpWeb.ConnCase do Phoenix.ConnTest.build_conn() end - def authed_conn do - session = SessionsFixtures.session() + def admin_conn do + authed_conn(:admin) + end - {session.id, - new_conn() - |> Plug.Test.init_test_session(%{user_id: session.id})} + def unprivileged_conn do + authed_conn(:unprivileged) + end + + defp authed_conn(role) do + user = UsersFixtures.user(%{role: role}) + + conn = new_conn() |> FzHttpWeb.Authentication.sign_in(user, %{provider: :identity}) + + {user, + conn + |> Plug.Test.init_test_session(%{ + "guardian_default_token" => conn.private.guardian_default_token + })} end setup tags do @@ -54,7 +71,14 @@ defmodule FzHttpWeb.ConnCase do Sandbox.mode(FzHttp.Repo, {:shared, self()}) end - {user_id, authed_conn} = authed_conn() - {:ok, user_id: user_id, unauthed_conn: new_conn(), authed_conn: authed_conn} + {unprivileged_user, unprivileged_conn} = unprivileged_conn() + {admin_user, admin_conn} = admin_conn() + + {:ok, + unauthed_conn: new_conn(), + admin_user: admin_user, + unprivileged_user: unprivileged_user, + admin_conn: admin_conn, + unprivileged_conn: unprivileged_conn} end end diff --git a/apps/fz_http/test/support/fixtures/devices_fixtures.ex b/apps/fz_http/test/support/fixtures/devices_fixtures.ex index cbac814d0..f7a5e98df 100644 --- a/apps/fz_http/test/support/fixtures/devices_fixtures.ex +++ b/apps/fz_http/test/support/fixtures/devices_fixtures.ex @@ -6,14 +6,6 @@ defmodule FzHttp.DevicesFixtures do alias FzHttp.{Devices, UsersFixtures} - @doc """ - Generate a device with config token - """ - def device_with_config_token(attrs \\ %{}) do - {:ok, device} = Devices.create_config_token(device(attrs)) - device - end - @doc """ Generate a device. """ @@ -24,9 +16,7 @@ defmodule FzHttp.DevicesFixtures do default_attrs = %{ user_id: user_id, public_key: "test-pubkey", - name: "factory", - private_key: "test-privkey", - server_public_key: "test-server-pubkey" + name: "factory" } {:ok, device} = Devices.create_device(Map.merge(default_attrs, attrs)) diff --git a/apps/fz_http/test/support/fixtures/sessions_fixtures.ex b/apps/fz_http/test/support/fixtures/sessions_fixtures.ex deleted file mode 100644 index 1f5086a46..000000000 --- a/apps/fz_http/test/support/fixtures/sessions_fixtures.ex +++ /dev/null @@ -1,15 +0,0 @@ -defmodule FzHttp.SessionsFixtures do - @moduledoc """ - This module defines test helpers for creating - entities via the `FzHttp.Sessions` context. - """ - alias FzHttp.{Sessions, UsersFixtures} - - def session(_attrs \\ %{}) do - email = UsersFixtures.user().email - record = Sessions.get_session!(email: email) - create_params = %{email: email, password: "password1234"} - {:ok, session} = Sessions.create_session(record, create_params) - session - end -end diff --git a/apps/fz_http/test/support/fixtures/settings_fixtures.ex b/apps/fz_http/test/support/fixtures/settings_fixtures.ex deleted file mode 100644 index 68127a0a6..000000000 --- a/apps/fz_http/test/support/fixtures/settings_fixtures.ex +++ /dev/null @@ -1,15 +0,0 @@ -defmodule FzHttp.SettingsFixtures do - @moduledoc """ - This module defines test helpers for creating - entities via the `FzHttp.Settings` context. - """ - - alias FzHttp.Settings - - @doc """ - Generate a setting. - """ - def setting_fixture(key \\ "default.device.dns") do - Settings.get_setting!(key: key) - end -end diff --git a/apps/fz_http/test/support/fixtures/sites_fixtures.ex b/apps/fz_http/test/support/fixtures/sites_fixtures.ex new file mode 100644 index 000000000..6aeba667f --- /dev/null +++ b/apps/fz_http/test/support/fixtures/sites_fixtures.ex @@ -0,0 +1,15 @@ +defmodule FzHttp.SitesFixtures do + @moduledoc """ + This module defines test helpers for creating + entities via the `FzHttp.Sites` context. + """ + + alias FzHttp.Sites + + @doc """ + Get a site by name (or the default one) + """ + def site_fixture(name \\ "default") do + Sites.get_site!(name: name) + end +end diff --git a/apps/fz_http/test/support/fixtures/users_fixtures.ex b/apps/fz_http/test/support/fixtures/users_fixtures.ex index f1f0bbaaa..8b0f72b87 100644 --- a/apps/fz_http/test/support/fixtures/users_fixtures.ex +++ b/apps/fz_http/test/support/fixtures/users_fixtures.ex @@ -15,9 +15,14 @@ defmodule FzHttp.UsersFixtures do case Repo.get_by(User, email: email) do nil -> {:ok, user} = - %{email: email, password: "password1234", password_confirmation: "password1234"} + %{ + email: email, + role: :admin, + password: "password1234", + password_confirmation: "password1234" + } |> Map.merge(attrs) - |> Users.create_admin_user() + |> Users.create_user() user diff --git a/apps/fz_http/test/support/test_helpers.ex b/apps/fz_http/test/support/test_helpers.ex index 790ff98e5..149f372a6 100644 --- a/apps/fz_http/test/support/test_helpers.ex +++ b/apps/fz_http/test/support/test_helpers.ex @@ -8,7 +8,6 @@ defmodule FzHttp.TestHelpers do DevicesFixtures, Repo, RulesFixtures, - SessionsFixtures, Users, Users.User, UsersFixtures @@ -24,17 +23,6 @@ defmodule FzHttp.TestHelpers do Repo.delete_all(User) end - def create_device_with_config_token(tags) do - device = - if tags[:unauthed] || is_nil(tags[:user_id]) do - DevicesFixtures.device_with_config_token() - else - DevicesFixtures.device_with_config_token(%{user_id: tags[:user_id]}) - end - - {:ok, device: device} - end - def create_device(tags) do device = if tags[:unauthed] || is_nil(tags[:user_id]) do @@ -53,8 +41,7 @@ defmodule FzHttp.TestHelpers do DevicesFixtures.device(%{ user_id: user_id, name: "other device", - public_key: "other-pubkey", - private_key: "other-privkey" + public_key: "other-pubkey" }) {:ok, other_device: device} @@ -82,7 +69,6 @@ defmodule FzHttp.TestHelpers do DevicesFixtures.device(%{ name: "device #{num}", public_key: "#{num}", - private_key: "#{num}", user_id: user_id }) end) @@ -95,11 +81,6 @@ defmodule FzHttp.TestHelpers do {:ok, user: user} end - def create_session(_) do - session = SessionsFixtures.session() - {:ok, session: session} - end - def create_accept_rule(_) do rule = RulesFixtures.rule(%{action: :accept}) {:ok, rule: rule} diff --git a/apps/fz_vpn/lib/fz_vpn/cli/live.ex b/apps/fz_vpn/lib/fz_vpn/cli/live.ex index 82b67a92b..acbd7cd19 100644 --- a/apps/fz_vpn/lib/fz_vpn/cli/live.ex +++ b/apps/fz_vpn/lib/fz_vpn/cli/live.ex @@ -8,9 +8,6 @@ defmodule FzVpn.CLI.Live do See FzVpn.Server for higher-level functionality. """ - # Outputs the privkey - @genkey_cmd "wg genkey" - import FzCommon.CLI require Logger @@ -22,32 +19,14 @@ defmodule FzVpn.CLI.Live do :ok = GenServer.call(:global.whereis_name(:fz_wall_server), :teardown) end - @doc """ - Calls wg genkey - """ - def genkey do - privkey = - exec!(@genkey_cmd) - |> String.trim() - - {privkey, pubkey(privkey)} - end - def set_peer(pubkey, inet) do set("peer #{pubkey} allowed-ips #{inet}") end - def delete_peer(pubkey) do + def remove_peer(pubkey) do set("peer #{pubkey} remove") end - def pubkey(privkey) when is_nil(privkey), do: nil - - def pubkey(privkey) when is_binary(privkey) do - exec!("echo #{privkey} | wg pubkey") - |> String.trim() - end - def set(config_str) do # Empty config string results in invalid command if String.length(config_str) > 0 do diff --git a/apps/fz_vpn/lib/fz_vpn/cli/sandbox.ex b/apps/fz_vpn/lib/fz_vpn/cli/sandbox.ex index 2312e04c7..17161594e 100644 --- a/apps/fz_vpn/lib/fz_vpn/cli/sandbox.ex +++ b/apps/fz_vpn/lib/fz_vpn/cli/sandbox.ex @@ -25,7 +25,6 @@ defmodule FzVpn.CLI.Sandbox do def interface_address, do: "eth0" def setup, do: @default_returned def teardown, do: @default_returned - def genkey, do: {rand_key(), rand_key()} def pubkey(_privkey), do: rand_key() def exec!(_cmd) do @@ -36,7 +35,7 @@ defmodule FzVpn.CLI.Sandbox do @default_returned end - def delete_peers do + def remove_peers do @wg_show |> String.split("\n") |> Enum.filter(fn line -> @@ -46,11 +45,11 @@ defmodule FzVpn.CLI.Sandbox do String.replace_leading(line, "peer: ", "") end) |> Enum.each(fn pubkey -> - delete_peer(pubkey) + remove_peer(pubkey) end) end - def delete_peer(_pubkey) do + def remove_peer(_pubkey) do @default_returned end diff --git a/apps/fz_vpn/lib/fz_vpn/server.ex b/apps/fz_vpn/lib/fz_vpn/server.ex index eefa2b852..e7fbd09a9 100644 --- a/apps/fz_vpn/lib/fz_vpn/server.ex +++ b/apps/fz_vpn/lib/fz_vpn/server.ex @@ -1,23 +1,6 @@ defmodule FzVpn.Server do @moduledoc """ - Functions for reading / writing the WireGuard config - - Startup: - Set empty config - - Received events: - - start: set config and apply it - - new_peer: gen peer pubkey, return it, but don't apply config - - commit_peer: commit pending peer to config - - remove_peer: remove peer - - Config is a data structure that looks like this: - - %{ - pubkey1 => {device1_ipv4, device1_ipv6}, - pubkey2 => {device2_ipv4, device2_ipv6}, - ... - } + Functions for reading / writing the WireGuard config. """ alias FzVpn.Config @@ -41,15 +24,8 @@ defmodule FzVpn.Server do end @impl GenServer - def handle_call(:create_device, _from, config) do - server_pubkey = Application.get_env(:fz_vpn, :wireguard_public_key) - {privkey, pubkey} = cli().genkey() - {:reply, {:ok, privkey, pubkey, server_pubkey}, config} - end - - @impl GenServer - def handle_call({:delete_device, pubkey}, _from, config) do - cli().delete_peer(pubkey) + def handle_call({:remove_peer, pubkey}, _from, config) do + cli().remove_peer(pubkey) new_config = Map.delete(config, pubkey) {:reply, {:ok, pubkey}, new_config} end @@ -61,12 +37,6 @@ defmodule FzVpn.Server do {:reply, :ok, new_config} end - @impl GenServer - def handle_call({:update_device, pubkey, inet}, _from, config) do - cli().set_peer(pubkey, inet) - {:reply, :ok, Map.put(config, pubkey, inet)} - end - @doc """ Determines which peers to remove, add, and change and sets them on the WireGuard interface. """ @@ -78,7 +48,7 @@ defmodule FzVpn.Server do defp delete_old_peers(old_config, new_config) do for pubkey <- Map.keys(old_config) -- Map.keys(new_config) do - cli().delete_peer(pubkey) + cli().remove_peer(pubkey) end end diff --git a/apps/fz_vpn/test/fz_vpn/cli/sandbox_test.exs b/apps/fz_vpn/test/fz_vpn/cli/sandbox_test.exs index d50fd91c9..15057e0fb 100644 --- a/apps/fz_vpn/test/fz_vpn/cli/sandbox_test.exs +++ b/apps/fz_vpn/test/fz_vpn/cli/sandbox_test.exs @@ -13,21 +13,6 @@ defmodule FzVpn.CLI.SandboxTest do assert cli().teardown() == @expected_returned end - test "genkey" do - {privkey, pubkey} = cli().genkey() - - assert is_binary(privkey) - assert is_binary(pubkey) - end - - test "pubkey" do - {privkey, _pubkey} = cli().genkey() - pubkey = cli().pubkey(privkey) - - assert is_binary(pubkey) - assert String.length(pubkey) == 44 - end - test "exec!" do assert cli().exec!("dummy") == @expected_returned end diff --git a/apps/fz_vpn/test/fz_vpn/server_test.exs b/apps/fz_vpn/test/fz_vpn/server_test.exs index 7f86a3b56..b48b23cba 100644 --- a/apps/fz_vpn/test/fz_vpn/server_test.exs +++ b/apps/fz_vpn/test/fz_vpn/server_test.exs @@ -2,7 +2,6 @@ defmodule FzVpn.ServerTest do use ExUnit.Case, async: true import FzVpn.CLI - @empty [] @single_peer [ %{public_key: "test-pubkey", inet: "127.0.0.1/32,::1/128"} ] @@ -23,17 +22,9 @@ defmodule FzVpn.ServerTest do %{test_pid: test_pid} end - @tag stubbed_config: @empty - test "generates new peer when requested", %{test_pid: test_pid} do - assert {:ok, _, _, _} = GenServer.call(test_pid, :create_device) - # Peers aren't added to config until device is successfully created - - assert :sys.get_state(test_pid) == %{} - end - @tag stubbed_config: @single_peer test "removes peers from config when removed", %{test_pid: test_pid} do - GenServer.call(test_pid, {:delete_device, "test-pubkey"}) + GenServer.call(test_pid, {:remove_peer, "test-pubkey"}) assert :sys.get_state(test_pid) == %{} end diff --git a/config/config.exs b/config/config.exs index 0290f3720..224f6e062 100644 --- a/config/config.exs +++ b/config/config.exs @@ -41,6 +41,12 @@ config :posthog, api_url: "https://telemetry.firez.one", api_key: "phc_ubuPhiqqjMdedpmbWpG2Ak3axqv5eMVhFDNBaXl9UZK" +# Guardian configuration +config :fz_http, FzHttpWeb.Authentication, + issuer: "fz_http", + # Generate with mix guardian.gen.secret + secret_key: "GApJ4c4a/KJLrBePgTDUk0n67AbjCvI9qdypKZEaJFXl6s9H3uRcIhTt49Fij5UO" + config :fz_http, telemetry_id: "543aae08-5a2b-428d-b704-2956dd3f5a57", url_host: "firezone.dev", @@ -55,6 +61,7 @@ config :fz_http, wireguard_ipv6_network: "fd00::3:2:0/120", wireguard_ipv6_address: "fd00::3:2:1", wireguard_mtu: "1420", + max_devices_per_user: 10, telemetry_module: FzCommon.Telemetry, supervision_tree_mode: :full, http_client: HTTPoison, @@ -66,7 +73,7 @@ config :fz_http, ecto_repos: [FzHttp.Repo], admin_email: "firezone@localhost", default_admin_password: "firezone1234", - events_module: FzHttpWeb.Events, + events_module: FzHttp.Events, server_process_opts: [name: {:global, :fz_http_server}] config :fz_wall, diff --git a/config/dev.exs b/config/dev.exs index e8ba8bea6..5325b4c1a 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1,5 +1,7 @@ import Config +alias FzCommon.ConfigHelpers + # Configure your database if url = System.get_env("DATABASE_URL") do config :fz_http, FzHttp.Repo, @@ -58,6 +60,43 @@ config :fz_wall, egress_interface: egress_interface, cli: FzWall.CLI.Sandbox +# Auth +local_auth_enabled = (System.get_env("LOCAL_AUTH_ENABLED") && true) || false +okta_auth_enabled = (System.get_env("OKTA_AUTH_ENABLED") && true) || false +google_auth_enabled = (System.get_env("GOOGLE_AUTH_ENABLED") && true) || false + +# Configure strategies +identity_strategy = + {:identity, {Ueberauth.Strategy.Identity, [callback_methods: ["POST"], uid_field: :email]}} + +okta_strategy = {:okta, {Ueberauth.Strategy.Okta, []}} +google_strategy = {:google, {Ueberauth.Strategy.Google, []}} + +providers = + [ + {local_auth_enabled, identity_strategy}, + {google_auth_enabled, google_strategy}, + {okta_auth_enabled, okta_strategy} + ] + |> Enum.filter(fn {key, _val} -> key end) + |> Enum.map(fn {_key, val} -> val end) + +config :ueberauth, Ueberauth, providers: providers + +if okta_auth_enabled do + config :ueberauth, Ueberauth.Strategy.Okta.OAuth, + client_id: System.get_env("OKTA_CLIENT_ID"), + client_secret: System.get_env("OKTA_CLIENT_SECRET"), + site: System.get_env("OKTA_SITE") +end + +if google_auth_enabled do + config :ueberauth, Ueberauth.Strategy.Google.OAuth, + client_id: System.get_env("GOOGLE_CLIENT_ID"), + client_secret: System.get_env("GOOGLE_CLIENT_SECRET"), + redirect_uri: System.get_env("GOOGLE_REDIRECT_URI") +end + # ## SSL Support # # In order to use HTTPS in development, a self-signed @@ -108,4 +147,7 @@ config :phoenix, :stacktrace_depth, 20 config :phoenix, :plug_init_mode, :runtime config :fz_http, - telemetry_module: FzCommon.MockTelemetry + telemetry_module: FzCommon.MockTelemetry, + local_auth_enabled: local_auth_enabled, + okta_auth_enabled: google_auth_enabled, + google_auth_enabled: okta_auth_enabled diff --git a/config/prod.exs b/config/prod.exs index 1598255d5..aa225477b 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -43,4 +43,14 @@ end config :logger, level: :info config :fz_http, + local_auth_enabled: true, + google_auth_enabled: true, + okta_auth_enabled: true, connectivity_checks_url: "https://ping.firez.one/" + +config :ueberauth, Ueberauth, + providers: [ + {:identity, {Ueberauth.Strategy.Identity, [callback_methods: ["POST"], uid_field: :email]}}, + {:okta, {Ueberauth.Strategy.Okta, []}}, + {:google, {Ueberauth.Strategy.Google, []}} + ] diff --git a/config/releases.exs b/config/releases.exs index 4248af5de..1d8c034f7 100644 --- a/config/releases.exs +++ b/config/releases.exs @@ -37,6 +37,27 @@ wireguard_mtu = System.fetch_env!("WIREGUARD_MTU") wireguard_endpoint = System.fetch_env!("WIREGUARD_ENDPOINT") telemetry_enabled = FzString.to_boolean(System.fetch_env!("TELEMETRY_ENABLED")) telemetry_id = System.fetch_env!("TELEMETRY_ID") +guardian_secret_key = System.fetch_env!("GUARDIAN_SECRET_KEY") + +# Local auth +local_auth_enabled = FzString.to_boolean(System.fetch_env!("LOCAL_AUTH_ENABLED")) + +# Okta auth +okta_auth_enabled = FzString.to_boolean(System.fetch_env!("OKTA_AUTH_ENABLED")) +okta_client_id = System.get_env("OKTA_CLIENT_ID") +okta_client_secret = System.get_env("OKTA_CLIENT_SECRET") +okta_site = System.get_env("OKTA_SITE") + +# Google auth +google_auth_enabled = FzString.to_boolean(System.fetch_env!("GOOGLE_AUTH_ENABLED")) +google_client_id = System.get_env("GOOGLE_CLIENT_ID") +google_client_secret = System.get_env("GOOGLE_CLIENT_SECRET") +google_redirect_uri = System.get_env("GOOGLE_REDIRECT_URI") + +max_devices_per_user = + System.fetch_env!("MAX_DEVICES_PER_USER") + |> String.to_integer() + |> FzInteger.clamp(0, 100) telemetry_module = if telemetry_enabled do @@ -123,7 +144,15 @@ config :fz_vpn, wireguard_port: wireguard_port, cli: FzVpn.CLI.Live +# Guardian configuration +config :fz_http, FzHttpWeb.Authentication, + issuer: "fz_http", + secret_key: guardian_secret_key + config :fz_http, + local_auth_enabled: local_auth_enabled, + okta_auth_enabled: okta_auth_enabled, + google_auth_enabled: google_auth_enabled, wireguard_dns: wireguard_dns, wireguard_allowed_ips: wireguard_allowed_ips, wireguard_persistent_keepalive: wireguard_persistent_keepalive, @@ -141,3 +170,36 @@ config :fz_http, connectivity_checks_interval: connectivity_checks_interval, admin_email: admin_email, default_admin_password: default_admin_password + +# Configure strategies +identity_strategy = + {:identity, {Ueberauth.Strategy.Identity, [callback_methods: ["POST"], uid_field: :email]}} + +okta_strategy = {:okta, {Ueberauth.Strategy.Okta, []}} +google_strategy = {:google, {Ueberauth.Strategy.Google, []}} + +providers = + [ + {local_auth_enabled, identity_strategy}, + {google_auth_enabled, google_strategy}, + {okta_auth_enabled, okta_strategy} + ] + |> Enum.filter(fn {key, _val} -> key end) + |> Enum.map(fn {_key, val} -> val end) + +config :ueberauth, Ueberauth, providers: providers + +# Configure OAuth portion of enabled strategies +if okta_auth_enabled do + config :ueberauth, Ueberauth.Strategy.Okta.OAuth, + client_id: okta_client_id, + client_secret: okta_client_secret, + site: okta_site +end + +if google_auth_enabled do + config :ueberauth, Ueberauth.Strategy.Google.OAuth, + client_id: google_client_id, + client_secret: google_client_secret, + redirect_uri: google_redirect_uri +end diff --git a/config/test.exs b/config/test.exs index a53c463c2..cf9ec3d97 100644 --- a/config/test.exs +++ b/config/test.exs @@ -38,6 +38,9 @@ config :fz_http, FzHttpWeb.Endpoint, server: true config :fz_http, + local_auth_enabled: true, + google_auth_enabled: true, + okta_auth_enabled: true, telemetry_module: FzCommon.MockTelemetry, supervision_tree_mode: :test, connectivity_checks_interval: 86_400, @@ -47,3 +50,10 @@ config :fz_http, # Print only warnings and errors during test config :logger, level: :warn + +config :ueberauth, Ueberauth, + providers: [ + {:identity, {Ueberauth.Strategy.Identity, [callback_methods: ["POST"], uid_field: :email]}}, + {:okta, {Ueberauth.Strategy.Okta, []}}, + {:google, {Ueberauth.Strategy.Google, []}} + ] diff --git a/docs/docs/administer/security-considerations.md b/docs/docs/administer/security-considerations.md index 845c6e493..c764ac584 100644 --- a/docs/docs/administer/security-considerations.md +++ b/docs/docs/administer/security-considerations.md @@ -4,10 +4,17 @@ title: Security Considerations nav_order: 6 parent: Administer description: > - Firezone services uses the following list of ports. + Security considerations in production deployments. --- --- +**Disclaimer**: Firezone is still beta software. The codebase has not yet +received a formal security audit. For highly sensitive and mission-critical +production deployments, we recommend limiting access to the web interface, as +detailed [below](#production-deployments). + +## List of services and ports + Shown below is a table of ports used by Firezone services. @@ -15,12 +22,47 @@ Shown below is a table of ports used by Firezone services. | Service | Default port | Listen address | Description | | Nginx | `80` `443` | `all` | Public HTTP(S) port for administering Firezone and facilitating authentication. | | WireGuard | `51820` | `all` | Public WireGuard port used for VPN connections. | -| Postgresql | `15432` | `127.0.0.1` | Local-only port used for bundled Postgresql server | +| Postgresql | `15432` | `127.0.0.1` | Local-only port used for bundled Postgresql server. | | Phoenix | `13000` | `127.0.0.1` | Local-only port used by upstream elixir app server. | +## Production deployments + +For production and public-facing deployments where a single administrator +will be responsible for generating and distributing device configurations to +end users, we advise you to consider limiting access to Firezone's publicly +exposed web UI (by default ports `443/tcp` and `80/tcp`) +and instead use the WireGuard tunnel itself to manage Firezone. + +For example, assuming an administrator has generated a device configuration and +established a tunnel with local WireGuard address `10.3.2.2`, the following `ufw` +configuration would allow the administrator the ability to reach the Firezone web +UI on the default `10.3.2.1` tunnel address for the server's `wg-firezone` interface: + +```text +root@demo:~# ufw status verbose +Status: active +Logging: on (low) +Default: deny (incoming), allow (outgoing), allow (routed) +New profiles: skip + +To Action From +-- ------ ---- +22/tcp ALLOW IN Anywhere +51820/udp ALLOW IN Anywhere +Anywhere ALLOW IN 10.3.2.2 +22/tcp (v6) ALLOW IN Anywhere (v6) +51820/udp (v6) ALLOW IN Anywhere (v6) +``` + +This would leave only `22/tcp` exposed for SSH access to manage the server (optional), +and `51820/udp` exposed in order to establish WireGuard tunnels. + +**Note**: This type of configuration has not been fully tested with SSO +authentication and may it to break or behave unexpectedly. + ## Reporting Security Issues -To report any security-related bugs, see [our security reporting policy +To report any security-related bugs, see [our security bug reporting policy ](https://github.com/firezone/firezone/blob/master/SECURITY.md). diff --git a/docs/docs/deploy/resource-requirements.md b/docs/docs/deploy/resource-requirements.md index 0b6b1faa1..efbff509e 100644 --- a/docs/docs/deploy/resource-requirements.md +++ b/docs/docs/deploy/resource-requirements.md @@ -14,9 +14,6 @@ users and bandwidth requirements grow. In general, more CPU cores translate to higher bandwidth capacity per tunnel while more RAM will help with higher counts of users and tunnels. -In the vast majority of cases, with WireGuard, your network link speed is going -to be the bottleneck before your CPU will. - \ [Previous: Supported Platforms]({%link docs/deploy/supported-platforms.md%}){:.btn.mr-2} [Next: Prerequisites]({%link docs/deploy/prerequisites.md%}){:.btn.btn-purple} diff --git a/docs/docs/reference/configuration-file.md b/docs/docs/reference/configuration-file.md index e8603935b..54305005a 100644 --- a/docs/docs/reference/configuration-file.md +++ b/docs/docs/reference/configuration-file.md @@ -132,6 +132,7 @@ Shown below is a complete listing of the configuration options available in | `default['firezone']['wireguard']['ipv6']['enabled']` | Enable or disable IPv6 for WireGuard network. | `true` | | `default['firezone']['wireguard']['ipv6']['network']` | WireGuard network IPv6 address pool. | `'fd00::3:2:0/120'` | | `default['firezone']['wireguard']['ipv6']['address']` | WireGuard interface IPv6 address. Must be within IPv6 address pool. | `'fd00::3:2:1'` | +| `default['firezone']['wireguard']['max_devices_per_user']` | Maximum number of devices a user can have. | `10` | | `default['firezone']['runit']['svlogd_bin']` | Runit svlogd bin location. | `"#{node['firezone']['install_directory']}/embedded/bin/svlogd"` | | `default['firezone']['ssl']['directory']` | SSL directory for storing generated certs. | `'/var/opt/firezone/ssl'` | | `default['firezone']['ssl']['enabled']` | Enable or disable SSL for nginx. | `true` | diff --git a/docs/docs/user-guides/split-tunnel.md b/docs/docs/user-guides/split-tunnel.md index 77cf638c2..0e8aa9680 100644 --- a/docs/docs/user-guides/split-tunnel.md +++ b/docs/docs/user-guides/split-tunnel.md @@ -34,9 +34,9 @@ In this example, the CIDR range for the `ap-northeast-2` AWS region was used. Note: When deciding where to route a packet, Firezone chooses the egress interface corresponding to the most specific route first. -## Step 2 - Regenerate WireGuard tunnel configurations +## Step 2 - Regenerate WireGuard configurations -To update existing WireGuard tunnels with the new split tunnel configuration, +To update existing user devices with the new split tunnel configuration, users will need to regenerate the configuration files and add them to their native WireGuard client. diff --git a/mix.lock b/mix.lock index bbabb795b..34fe05286 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,7 @@ %{ "argon2_elixir": {:hex, :argon2_elixir, "2.4.1", "edb27bdd326bc738f3e4614eddc2f73507be6fedc9533c6bcc6f15bbac9c85cc", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "0e21f52a373739d00bdfd5fe6da2f04eea623cb4f66899f7526dd9db03903d9f"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, - "certifi": {:hex, :certifi, "2.8.0", "d4fb0a6bb20b7c9c3643e22507e42f356ac090a1dcea9ab99e27e0376d695eba", [:rebar3], [], "hexpm", "6ac7efc1c6f8600b08d625292d4bbf584e14847ce1b6b5c44d983d273e1097ea"}, + "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, "cidr": {:git, "https://github.com/firezone/cidr-elixir.git", "9072aaab069bca38ef55fd901a37448861596532", []}, "cloak": {:hex, :cloak, "1.1.1", "6f8f6674cacd3c504daf2aaeba8f9cde3ae8009ce01ff854dd3e92fbb7954c69", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "d440c4ea3a5a31baeaea4592b534dfdccc4ded0ee098b92955a5658cbe7be625"}, "cloak_ecto": {:hex, :cloak_ecto, "1.2.0", "e86a3df3bf0dc8980f70406bcb0af2858bac247d55494d40bc58a152590bd402", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "8bcc677185c813fe64b786618bd6689b1707b35cd95acaae0834557b15a0c62f"}, @@ -10,22 +10,24 @@ "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"}, "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, - "credo": {:hex, :credo, "1.6.2", "2f82b29a47c0bb7b72f023bf3a34d151624f1cbe1e6c4e52303b05a11166a701", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "ae9dc112bc368e7b145c547bec2ed257ef88955851c15057c7835251a17211c6"}, + "credo": {:hex, :credo, "1.6.4", "ddd474afb6e8c240313f3a7b0d025cc3213f0d171879429bf8535d7021d9ad78", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "c28f910b61e1ff829bffa056ef7293a8db50e87f2c57a9b5c3f57eee124536b7"}, "db_connection": {:hex, :db_connection, "2.4.1", "6411f6e23f1a8b68a82fa3a36366d4881f21f47fc79a9efb8c615e62050219da", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ea36d226ec5999781a9a8ad64e5d8c4454ecedc7a4d643e4832bf08efca01f00"}, "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.19", "de0d033d5ff9fc396a24eadc2fcf2afa3d120841eb3f1004d138cbf9273210e8", [:mix], [], "hexpm", "527ab6630b5c75c3a3960b75844c314ec305c76d9899bb30f71cb85952a9dc45"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.20", "89970db71b11b6b89759ce16807e857df154f8df3e807b2920a8c39834a9e5cf", [:mix], [], "hexpm", "1eb0d2dabeeeff200e0d17dc3048a6045aab271f73ebb82e416464832eb57bdd"}, "ecto": {:hex, :ecto, "3.7.1", "a20598862351b29f80f285b21ec5297da1181c0442687f9b8329f0445d228892", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d36e5b39fc479e654cffd4dbe1865d9716e4a9b6311faff799b6f90ab81b8638"}, "ecto_network": {:hex, :ecto_network, "1.3.0", "1e77fa37c20e0f6a426d3862732f3317b0fa4c18f123d325f81752a491d7304e", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 0.0.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.14.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "053a5e46ef2837e8ea5ea97c82fa0f5494699209eddd764e663c85f11b2865bd"}, "ecto_sql": {:hex, :ecto_sql, "3.7.2", "55c60aa3a06168912abf145c6df38b0295c34118c3624cf7a6977cd6ce043081", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.4.0 or ~> 0.5.0 or ~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c218ea62f305dcaef0b915fb56583195e7b91c91dcfb006ba1f669bfacbff2a"}, "elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.28.0", "7eaf526dd8c80ae8c04d52ac8801594426ae322b52a6156cd038f30bafa8226f", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "e55cdadf69a5d1f4cfd8477122ebac5e1fadd433a8c1022dafc5025e48db0131"}, + "ex_doc": {:hex, :ex_doc, "0.28.2", "e031c7d1a9fc40959da7bf89e2dc269ddc5de631f9bd0e326cbddf7d8085a9da", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "51ee866993ffbd0e41c084a7677c570d0fc50cb85c6b5e76f8d936d9587fa719"}, "excoveralls": {:hex, :excoveralls, "0.14.4", "295498f1ae47bdc6dce59af9a585c381e1aefc63298d48172efaaa90c3d251db", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e3ab02f2df4c1c7a519728a6f0a747e71d7d6e846020aae338173619217931c1"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "floki": {:hex, :floki, "0.32.0", "f915dc15258bc997d49be1f5ef7d3992f8834d6f5695270acad17b41f5bcc8e2", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "1c5a91cae1fd8931c26a4826b5e2372c284813904c8bacb468b5de39c7ececbd"}, "gettext": {:hex, :gettext, "0.19.1", "564953fd21f29358e68b91634799d9d26989f8d039d7512622efb3c3b1c97892", [:mix], [], "hexpm", "10c656c0912b8299adba9b061c06947511e3f109ab0d18b44a866a4498e77222"}, - "hackney": {:hex, :hackney, "1.18.0", "c4443d960bb9fba6d01161d01cd81173089686717d9490e5d3606644c48d121f", [:rebar3], [{:certifi, "~>2.8.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "9afcda620704d720db8c6a3123e9848d09c87586dc1c10479c42627b905b5c5e"}, + "guardian": {:hex, :guardian, "2.2.1", "5a4a949fd46eac79ef37f074ada7d1ef82e274bc99e335c286e042f5383f4f80", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "09b5c4d08f18524bd33ffe49617003cbca9f617237e23b5f42223cda61c5f052"}, + "guardian_db": {:hex, :guardian_db, "2.1.0", "ec95a9d99cdd1e550555d09a7bb4a340d8887aad0697f594590c2fd74be02426", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:guardian, "~> 1.0 or ~> 2.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "f8e7d543ac92c395f3a7fd5acbe6829faeade57d688f7562e2f0fca8f94a0d70"}, + "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, "hammer": {:hex, :hammer, "6.0.0", "72ec6fff10e9d63856968988a22ee04c4d6d5248071ddccfbda50aa6c455c1d7", [:mix], [{:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "d8e1ec2e534c4aae508b906759e077c3c1eb3e2b9425235d4b7bbab0b016210a"}, "hammer_plug": {:hex, :hammer_plug, "2.1.1", "eb5390380eff6600e24e93edfe6a34d39f35280cbdd1caa0995b58bb8489f00d", [:make, :mix], [{:hammer, "~> 6.0", [hex: :hammer, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "0fbc3e8b1aacecb7affea65c85c349fdbd00ff28a74bbe6ca30c9f4c76d71e4b"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, @@ -34,20 +36,22 @@ "inet_cidr": {:hex, :inet_cidr, "1.0.4", "a05744ab7c221ca8e395c926c3919a821eb512e8f36547c062f62c4ca0cf3d6e", [:mix], [], "hexpm", "64a2d30189704ae41ca7dbdd587f5291db5d1dda1414e0774c29ffc81088c1bc"}, "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, - "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, + "jose": {:hex, :jose, "1.11.2", "f4c018ccf4fdce22c71e44d471f15f723cb3efab5d909ab2ba202b5bf35557b3", [:mix, :rebar3], [], "hexpm", "98143fbc48d55f3a18daba82d34fe48959d44538e9697c08f34200fa5f0947d2"}, + "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "mix_test_watch": {:hex, :mix_test_watch, "1.1.0", "330bb91c8ed271fe408c42d07e0773340a7938d8a0d281d57a14243eae9dc8c3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "52b6b1c476cbb70fd899ca5394506482f12e5f6b0d6acff9df95c7f1e0812ec3"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.2.1", "264fc6864936b59fedb3ceb89998c64e9bb91945faf1eb115d349b96913cc2ef", [:mix], [], "hexpm", "23c31d0ec38c97bf9adde35bc91bc8e1181ea5202881f48a192f4aa2d2cf4d59"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.2.2", "b99ca56bbce410e9d5ee4f9155a212e942e224e259c7ebbf8f2c86ac21d4fa3c", [:mix], [], "hexpm", "98d51bd64d5f6a2a9c6bb7586ee8129e27dfaab1140b5a4753f24dac0ba27d2f"}, + "oauth2": {:hex, :oauth2, "2.0.0", "338382079fe16c514420fa218b0903f8ad2d4bfc0ad0c9f988867dfa246731b0", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "881b8364ac7385f9fddc7949379cbe3f7081da37233a1aa7aab844670a91e7e7"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "phoenix": {:hex, :phoenix, "1.6.6", "281c8ce8dccc9f60607346b72cdfc597c3dde134dd9df28dff08282f0b751754", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "807bd646e64cd9dc83db016199715faba72758e6db1de0707eef0a2da4924364"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"}, "phoenix_html": {:hex, :phoenix_html, "3.1.0", "0b499df05aad27160d697a9362f0e89fa0e24d3c7a9065c2bd9d38b4d1416c09", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0c0a98a2cefa63433657983a2a594c7dee5927e4391e0f1bfd3a151d1def33fc"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.6", "3665f3ec426ac8d681cd7753ad4c85d2d247094dc4dc6add80dd6e3026045389", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "62f06d4bbfc4dc5595070bc338119ab08e8e67a011e2923f9366419622149b9c"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.7", "05a42377075868a678d446361effba80cefef19ab98941c01a7a4c7560b29121", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "25eaf41028eb351b90d4f69671874643a09944098fefd0d01d442f40a6091b6f"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, "phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"}, "plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"}, @@ -59,5 +63,10 @@ "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"}, + "ueberauth": {:hex, :ueberauth, "0.7.0", "9c44f41798b5fa27f872561b6f7d2bb0f10f03fdd22b90f454232d7b087f4b75", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2efad9022e949834f16cc52cd935165049d81fa9e925690f91035c2e4b58d905"}, + "ueberauth_google": {:hex, :ueberauth_google, "0.10.1", "db7bd2d99d2ff38e7449042a08d9560741b0dcaf1c31191729b97188b025465e", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "b799f547d279bb836e1f7039fc9fbb3a9d008a695e2a25bd06bffe591a168ba1"}, + "ueberauth_identity": {:hex, :ueberauth_identity, "0.4.0", "cbb241884028095dfe7383966821ae9c44c05c3328f451878822af74065a3ba1", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "e980d840d0bda0d10b5ab695ca5c59d750682f1af343ab70829b25aed4b0e32f"}, + "ueberauth_keycloak_strategy": {:git, "https://github.com/firezone/ueberauth_keycloak.git", "8a028d6c949604b25a083d4137affa0472ff919c", []}, + "ueberauth_okta": {:hex, :ueberauth_okta, "0.3.0", "236175e9f6bd5b13fcefc9f291b74e380a5698a4b699105b6e74f2747c137363", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oauth2, "~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "f871dc6f41da34ea4d3cbc6cad3f8c4f2ebf7b5d3fef38f861ca99ecae3607b2"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, } diff --git a/omnibus/cookbooks/firezone/attributes/default.rb b/omnibus/cookbooks/firezone/attributes/default.rb index 52eb5f74b..312d5fb7e 100644 --- a/omnibus/cookbooks/firezone/attributes/default.rb +++ b/omnibus/cookbooks/firezone/attributes/default.rb @@ -40,6 +40,11 @@ default['firezone']['group'] = 'firezone' # Email for the primary admin user. default['firezone']['admin_email'] = 'firezone@localhost' +# The maximum number of devices a user can have. +# Max: 100 +# Default: 10 +default['firezone']['max_devices_per_user'] = 10 + # The outgoing interface name. # This is where tunneled traffic will exit the WireGuard tunnel. # If set to nil, this is will be set to the interface for the machine's @@ -69,6 +74,35 @@ default['firezone']['install_path'] = node['firezone']['install_directory'] # (for the file) sequence of 1-4 characters. default['firezone']['sysvinit_id'] = 'SUP' +# ## Authentication + +# These settings control authentication-related aspects of Firezone. +# For more information, see https://docs.firez.one/docs/user-guides/authentication/ +# +# When local email/password authentication is used, users must be created by an Administrator +# before they can log in. +# +# When SSO authentication methods are used, users are automatically added to Firezone +# when logging in for the first time via the SSO provider. +# +# Users are uniquely identified by their email address, and may log in via multiple providers +# if configured. + +# Local email/password authentication is enabled by default +default['firezone']['authentication']['local']['enabled'] = true + +# If using the 'okta' authentication method, set 'enabeld' to true and configure relevant settings below. +default['firezone']['authentication']['okta']['enabled'] = false +default['firezone']['authentication']['okta']['client_id'] = nil +default['firezone']['authentication']['okta']['client_secret'] = nil +default['firezone']['authentication']['okta']['site'] = 'https://your-domain.okta.com' + +# If using the 'google' authentication method, set 'enabled' to true and configure relevant settings below. +default['firezone']['authentication']['google']['enabled'] = false +default['firezone']['authentication']['google']['client_id'] = nil +default['firezone']['authentication']['google']['client_secret'] = nil +default['firezone']['authentication']['google']['redirect_uri'] = nil + # ## Nginx # These attributes control Firezone-specific portions of the Nginx diff --git a/omnibus/cookbooks/firezone/libraries/config.rb b/omnibus/cookbooks/firezone/libraries/config.rb index 32d57ffd9..b1616cb20 100644 --- a/omnibus/cookbooks/firezone/libraries/config.rb +++ b/omnibus/cookbooks/firezone/libraries/config.rb @@ -80,25 +80,28 @@ class Firezone # Read in the filename (as JSON) and add its attributes to the node object. # If it doesn't exist, create it with generated secrets. - # rubocop:disable Metrics/MethodLength def self.load_or_create_secrets!(filename, node) create_directory!(filename) - secrets = Chef::JSONCompat.from_json(File.read(filename)) - node.consume_attributes('firezone' => secrets) - rescue Errno::ENOENT secrets = build_secrets(node) - begin - File.open(filename, 'w') do |file| - file.puts Chef::JSONCompat.to_json_pretty(secrets) - end - Chef::Log.info("Creating secrets file #{filename}") - rescue Errno::EACCES, Errno::ENOENT => e - Chef::Log.warn "Could not create #{filename}: #{e}" - end + # Merge in existing secrets from JSON file + File.exist?(filename) && secrets.merge!(Chef::JSONCompat.from_json(File.read(filename))) + + # Apply to running system node.consume_attributes('firezone' => secrets) + + # Save them for next run + write_secrets(filename, secrets) + end + + def self.write_secrets(filename, secrets) + File.open(filename, 'w') do |file| + file.puts Chef::JSONCompat.to_json_pretty(secrets) + end + Chef::Log.info("Creating secrets file #{filename}") + rescue Errno::EACCES, Errno::ENOENT => e + Chef::Log.warn "Could not create #{filename}: #{e}" end - # rubocop:enable Metrics/MethodLength # rubocop:disable Metrics/PerceivedComplexity # rubocop:disable Metrics/MethodLength @@ -106,6 +109,7 @@ class Firezone # rubocop:disable Metrics/AbcSize def self.build_secrets(node) { + 'guardian_secret_key' => node['firezone'] && node['firezone']['guardian_secret_key'] || SecureRandom.base64(48), 'secret_key_base' => node['firezone'] && node['firezone']['secret_key_base'] || SecureRandom.base64(48), 'live_view_signing_salt' => node['firezone'] && node['firezone']['live_view_signing_salt'] || \ SecureRandom.base64(24), @@ -233,6 +237,7 @@ class Firezone 'WIREGUARD_IPV6_ENABLED' => attributes['wireguard']['ipv6']['enabled'].to_s, 'WIREGUARD_IPV6_NETWORK' => attributes['wireguard']['ipv6']['network'], 'WIREGUARD_IPV6_ADDRESS' => attributes['wireguard']['ipv6']['address'], + 'MAX_DEVICES_PER_USER' => attributes['max_devices_per_user'].to_s, # Allow env var to override config 'TELEMETRY_ENABLED' => ENV.fetch('TELEMETRY_ENABLED', attributes['telemetry']['enabled'] == false ? 'false' : 'true'), @@ -240,7 +245,19 @@ class Firezone 'CONNECTIVITY_CHECKS_ENABLED' => attributes['connectivity_checks']['enabled'].to_s, 'CONNECTIVITY_CHECKS_INTERVAL' => attributes['connectivity_checks']['interval'].to_s, + # Auth + 'LOCAL_AUTH_ENABLED' => attributes['authentication']['local']['enabled'].to_s, + 'OKTA_AUTH_ENABLED' => attributes['authentication']['okta']['enabled'].to_s, + 'OKTA_CLIENT_ID' => attributes['authentication']['okta']['client_id'], + 'OKTA_CLIENT_SECRET' => attributes['authentication']['okta']['client_secret'], + 'OKTA_SITE' => attributes['authentication']['okta']['site'], + 'GOOGLE_AUTH_ENABLED' => attributes['authentication']['google']['enabled'].to_s, + 'GOOGLE_CLIENT_ID' => attributes['authentication']['google']['client_id'], + 'GOOGLE_CLIENT_SECRET' => attributes['authentication']['google']['client_secret'], + 'GOOGLE_REDIRECT_URI' => attributes['authentication']['google']['redirect_uri'], + # secrets + 'GUARDIAN_SECRET_KEY' => attributes['guardian_secret_key'], 'SECRET_KEY_BASE' => attributes['secret_key_base'], 'LIVE_VIEW_SIGNING_SALT' => attributes['live_view_signing_salt'], 'COOKIE_SIGNING_SALT' => attributes['cookie_signing_salt'], diff --git a/scripts/generate_keypairs.sh b/scripts/generate_keypairs.sh deleted file mode 100755 index a8de8098a..000000000 --- a/scripts/generate_keypairs.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env zsh -set -e - -# Generates 10 WireGuard keypairs for use in Dev/Test environments. -repeat 10 { - key=$(wg genkey | tee >(wg pubkey)) - parts=("${(f)key}") - echo $parts -}