diff --git a/.codespellrc b/.codespellrc index 4cfb1b56b..e4fde4156 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,3 +1,3 @@ [codespell] -skip = ./cover,./vendor,./omnibus,*.json,yarn.lock,seeds.exs,./**/node_modules,./deps,./priv/static,./priv/plts,./**/priv/static,./.git,./docs/build,./_build +skip = ./erl_crash.dump,./apps/fz_http/erl_crash.dump,./cover,./vendor,./omnibus,*.json,yarn.lock,seeds.exs,./**/node_modules,./deps,./priv/static,./priv/plts,./**/priv/static,./.git,./docs/build,./_build ignore-words-list = keypair,keypairs,iif,statics,wee diff --git a/.credo.exs b/.credo.exs index a7e937340..b91764922 100644 --- a/.credo.exs +++ b/.credo.exs @@ -98,7 +98,6 @@ {Credo.Check.Readability.LargeNumbers, []}, {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, {Credo.Check.Readability.ModuleAttributeNames, []}, - {Credo.Check.Readability.ModuleDoc, []}, {Credo.Check.Readability.ModuleNames, []}, {Credo.Check.Readability.ParenthesesInCondition, []}, {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0da2f9dc6..c684de9fe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,4 +61,4 @@ repos: - -b - master - --pattern - - '^(?!((chore|feat|feature|bug|fix|build|ci|docs|style|refactor|perf|test|revert)\/[a-zA-Z0-9\-\.\/]+)$).*' + - '^(?!((chore|feat|feature|bug|fix|build|ci|docs|style|refactor|perf|test|revert)\/[@a-zA-Z0-9\-\.\/]+)$).*' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f287f63da..b67e860d8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -109,27 +109,6 @@ We use [pre-commit](https://pre-commit.com) to catch any static analysis issues before code is committed. Install with Homebrew: `brew install pre-commit` or pip: `pip install pre-commit`. -### The ENV file - -For running tests and developing Firezone outside of Docker, you'll need some -environment variables present in your shell's env. - -See .env.sample an example of what variables you need. We recommend copying this -file to `.env` and using a dotenv loader to apply this to your current shell -env. - -For example, run the following command to 'source' the environment variables -from .env on `mix test`: - -`env $(cat .env | grep -v \# | xargs) mix test` - -This will initialize everything and run the test suite. If you have no -failures, Firezone should be properly set up 🥳. - -At this point you should be able to sign in to -[http://localhost:4000](http://localhost:4000) with email `firezone@localhost` and -password `firezone1234`. - ### Bootstrapping To start the local development cluster, follow these steps: @@ -137,13 +116,17 @@ To start the local development cluster, follow these steps: ``` docker compose build docker compose up -d postgres -docker compose run --rm firezone mix ecto.setup +docker compose run --rm firezone mix do ecto.setup, ecto.seed docker compose up ``` Now you should be able to connect to `https://localhost/` and sign in with email `firezone@localhost` and password `firezone1234`. +The [`docker-compose.yml`](docker-compose.yml) file configures the Docker +development environment. If you make any changes you feel would benefit +all developers, feel free to open a PR to get them merged! + ### Ensure Everything Works There is a `client` container in the docker-compose configuration that diff --git a/Dockerfile.dev b/Dockerfile.dev index 459b5c841..91f0f2056 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -36,12 +36,12 @@ COPY apps/fz_vpn/mix.exs /var/app/apps/fz_vpn/mix.exs COPY apps/fz_wall/mix.exs /var/app/apps/fz_wall/mix.exs COPY mix.exs /var/app/mix.exs COPY mix.lock /var/app/mix.lock -RUN mix do deps.get --only dev, deps.compile --only dev, compile --only dev -RUN mix do deps.get --only test, deps.compile --only test, compile --only test +RUN mix do deps.get, deps.compile, compile # Copy more granular, dependency management files first to prevent # busting the Docker build cache unnecessarily COPY apps/fz_http/assets/package.json /var/app/apps/fz_http/assets/package.json +COPY apps/fz_http/assets/local_modules /var/app/apps/fz_http/assets/local_modules COPY apps/fz_http/assets/package-lock.json /var/app/apps/fz_http/assets/package-lock.json RUN npm install --prefix apps/fz_http/assets diff --git a/apps/fz_common/lib/fz_net.ex b/apps/fz_common/lib/fz_net.ex index 69ece9854..9b1a8dd71 100644 --- a/apps/fz_common/lib/fz_net.ex +++ b/apps/fz_common/lib/fz_net.ex @@ -2,19 +2,9 @@ defmodule FzCommon.FzNet do @moduledoc """ Network utility functions. """ - # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength - @ip_regex ~r/((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/ + import FzCommon.FzRegex - @cidr4_regex ~r/^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))$/ - - # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength - @cidr6_regex ~r/^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))$/ - - # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength - @host_regex ~r/\A(([a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?)\.)*([a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?)$\Z/ - @fqdn_regex ~r/\A(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)\z/ - - # XXX: Consider using InetCidr for this + # XXX: Consider using CIDR for this def ip_type(str) when is_binary(str) do charlist = str @@ -36,11 +26,11 @@ defmodule FzCommon.FzNet do end def valid_cidr?(cidr) when is_binary(cidr) do - String.match?(cidr, @cidr4_regex) or String.match?(cidr, @cidr6_regex) + String.match?(cidr, cidr4_regex()) or String.match?(cidr, cidr6_regex()) end def valid_ip?(ip) when is_binary(ip) do - String.match?(ip, @ip_regex) + String.match?(ip, ip_regex()) end @doc """ @@ -50,8 +40,8 @@ defmodule FzCommon.FzNet do if String.contains?(inet, "/") do inet # normalize CIDR - |> InetCidr.parse(true) - |> InetCidr.to_string() + |> CIDR.parse() + |> to_string() else {:ok, addr} = inet |> String.to_charlist() |> :inet.parse_address() :inet.ntoa(addr) |> List.to_string() @@ -59,11 +49,11 @@ defmodule FzCommon.FzNet do end def valid_fqdn?(fqdn) when is_binary(fqdn) do - String.match?(fqdn, @fqdn_regex) + String.match?(fqdn, fqdn_regex()) end def valid_hostname?(hostname) when is_binary(hostname) do - String.match?(hostname, @host_regex) + String.match?(hostname, host_regex()) end def to_complete_url(str) when is_binary(str) do diff --git a/apps/fz_common/lib/fz_regex.ex b/apps/fz_common/lib/fz_regex.ex new file mode 100644 index 000000000..5f6c1075b --- /dev/null +++ b/apps/fz_common/lib/fz_regex.ex @@ -0,0 +1,25 @@ +defmodule FzCommon.FzRegex do + @moduledoc """ + A common module to store RegExps used in FzCommon in order + to avoid linting and editor slowdown issues in other modules. + + What a time to be a developer, right? + """ + # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength + @ip_regex ~r/((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/ + + @cidr4_regex ~r/^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))$/ + + # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength + @cidr6_regex ~r/^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))$/ + + # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength + @host_regex ~r/\A(([a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?)\.)*([a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?)$\Z/ + @fqdn_regex ~r/\A(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)\z/ + + def ip_regex, do: @ip_regex + def cidr4_regex, do: @cidr4_regex + def cidr6_regex, do: @cidr6_regex + def host_regex, do: @host_regex + def fqdn_regex, do: @fqdn_regex +end diff --git a/apps/fz_common/mix.exs b/apps/fz_common/mix.exs index dca143e99..7eb9a65b9 100644 --- a/apps/fz_common/mix.exs +++ b/apps/fz_common/mix.exs @@ -34,7 +34,7 @@ defmodule FzCommon.MixProject do {:file_size, "~> 3.0.1"}, {:posthog, "~> 0.1"}, {:jason, "~> 1.2"}, - {:inet_cidr, "~> 1.0.0"} + {:cidr, github: "firezone/cidr-elixir"} # {:dep_from_hexpm, "~> 0.3.0"}, # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}, # {:sibling_app_in_umbrella, in_umbrella: true} diff --git a/apps/fz_common/test/fz_net_test.exs b/apps/fz_common/test/fz_net_test.exs index 83ac6fb27..67419a4ee 100644 --- a/apps/fz_common/test/fz_net_test.exs +++ b/apps/fz_common/test/fz_net_test.exs @@ -127,11 +127,7 @@ defmodule FzCommon.FzNetTest do end end - @tag cases: [ - "<", - "{", - "[" - ] + @tag cases: ["<", "{", "["] test "returns {:error, _} for invalid URIs", %{cases: cases} do for subject <- cases do assert {:error, _} = FzNet.to_complete_url(subject) diff --git a/apps/fz_http/assets/css/main.scss b/apps/fz_http/assets/css/main.scss index 61fd64e0c..343cb87d4 100644 --- a/apps/fz_http/assets/css/main.scss +++ b/apps/fz_http/assets/css/main.scss @@ -8,6 +8,14 @@ nav.navbar { @extend .is-family-monospace; } +pre.multiline { + white-space: pre-wrap; /* Since CSS 2.1 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ +} + .is-horizontally-scrollable { overflow-x: auto; } diff --git a/apps/fz_http/assets/js/event_listeners.js b/apps/fz_http/assets/js/event_listeners.js index 3d572f166..ad46fd487 100644 --- a/apps/fz_http/assets/js/event_listeners.js +++ b/apps/fz_http/assets/js/event_listeners.js @@ -16,3 +16,23 @@ document.addEventListener('DOMContentLoaded', () => { $span.innerHTML = FormatTimestamp($span.dataset.timestamp) }) }) + +document.addEventListener("firezone:clipcopy", (event) => { + if ("clipboard" in navigator) { + const text = event.target.textContent + navigator.clipboard.writeText(text) + const dispatcher = event.detail.dispatcher + const span = dispatcher.getElementsByTagName("span")[0] + const icon = dispatcher.getElementsByTagName("i")[0] + + span.classList.add("has-text-success") + icon.classList.replace("mdi-content-copy", "mdi-check-bold") + + setTimeout(() => { + span.classList.remove("has-text-success") + icon.classList.replace("mdi-check-bold", "mdi-content-copy") + }, 1000) + } else { + alert("Sorry, your browser does not support clipboard copy.") + } +}) diff --git a/apps/fz_http/assets/package-lock.json b/apps/fz_http/assets/package-lock.json index 71582ee62..aac7f5f18 100644 --- a/apps/fz_http/assets/package-lock.json +++ b/apps/fz_http/assets/package-lock.json @@ -36,31 +36,6 @@ "node": ">= 14.0.0" } }, - "../../../deps/phoenix": { - "version": "1.7.0-rc.0", - "license": "MIT" - }, - "../../../deps/phoenix_html": { - "version": "3.2.0" - }, - "../../../deps/phoenix_live_view": { - "version": "0.18.3", - "license": "MIT" - }, - "local_modules/admin-one-bulma-dashboard": { - "version": "1.5.5", - "dev": true, - "license": "MIT", - "dependencies": { - "bulma": "^0.9.0", - "bulma-checkbox": "^1.1.1", - "bulma-radio": "^1.1.1", - "bulma-responsive-tables": "^1.2.3", - "bulma-switch-control": "^1.1.1", - "bulma-upload-control": "^1.2.0", - "node-sass": "^7.0.1" - } - }, "node_modules/@babel/code-frame": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", @@ -271,8 +246,19 @@ "dev": true }, "node_modules/admin-one-bulma-dashboard": { - "resolved": "local_modules/admin-one-bulma-dashboard", - "link": true + "version": "1.5.5", + "resolved": "file:local_modules/admin-one-bulma-dashboard", + "dev": true, + "license": "MIT", + "dependencies": { + "bulma": "^0.9.0", + "bulma-checkbox": "^1.1.1", + "bulma-radio": "^1.1.1", + "bulma-responsive-tables": "^1.2.3", + "bulma-switch-control": "^1.1.1", + "bulma-upload-control": "^1.2.0", + "node-sass": "^7.0.1" + } }, "node_modules/agent-base": { "version": "6.0.2", @@ -2526,16 +2512,18 @@ "dev": true }, "node_modules/phoenix": { - "resolved": "../../../deps/phoenix", - "link": true + "version": "1.7.0-rc.0", + "resolved": "file:../../../deps/phoenix", + "license": "MIT" }, "node_modules/phoenix_html": { - "resolved": "../../../deps/phoenix_html", - "link": true + "version": "3.2.0", + "resolved": "file:../../../deps/phoenix_html" }, "node_modules/phoenix_live_view": { - "resolved": "../../../deps/phoenix_live_view", - "link": true + "version": "0.18.3", + "resolved": "file:../../../deps/phoenix_live_view", + "license": "MIT" }, "node_modules/picomatch": { "version": "2.3.1", @@ -3690,7 +3678,8 @@ "dev": true }, "admin-one-bulma-dashboard": { - "version": "file:local_modules/admin-one-bulma-dashboard", + "version": "1.5.5", + "dev": true, "requires": { "bulma": "^0.9.0", "bulma-checkbox": "^1.1.1", @@ -5336,13 +5325,13 @@ "dev": true }, "phoenix": { - "version": "file:../../../deps/phoenix" + "version": "1.7.0-rc.0" }, "phoenix_html": { - "version": "file:../../../deps/phoenix_html" + "version": "3.2.0" }, "phoenix_live_view": { - "version": "file:../../../deps/phoenix_live_view" + "version": "0.18.3" }, "picomatch": { "version": "2.3.1", diff --git a/apps/fz_http/lib/fz_http.ex b/apps/fz_http/lib/fz_http.ex index cd38e2e7b..913700aa9 100644 --- a/apps/fz_http/lib/fz_http.ex +++ b/apps/fz_http/lib/fz_http.ex @@ -1,9 +1,21 @@ defmodule FzHttp do - @moduledoc """ - FzHttp keeps the contexts that define your domain - and business logic. + def schema do + quote do + use Ecto.Schema - Contexts are also responsible for managing your data, regardless - if it comes from the database, an external API or others. + @primary_key {:id, :binary_id, autogenerate: true} + @foreign_key_type :binary_id + + @timestamps_opts [type: :utc_datetime_usec] + + @type id :: binary() + end + end + + @doc """ + When used, dispatch to the appropriate schema/context/changeset/query/etc. """ + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end end diff --git a/apps/fz_http/lib/fz_http/api_tokens.ex b/apps/fz_http/lib/fz_http/api_tokens.ex new file mode 100644 index 000000000..36aefa84a --- /dev/null +++ b/apps/fz_http/lib/fz_http/api_tokens.ex @@ -0,0 +1,62 @@ +defmodule FzHttp.ApiTokens do + @moduledoc """ + The ApiTokens context. + """ + + import Ecto.Query, warn: false + alias FzHttp.Repo + + alias FzHttp.ApiTokens.ApiToken + + def count_by_user_id(user_id) do + Repo.aggregate(from(a in ApiToken, where: a.user_id == ^user_id), :count) + end + + def list_api_tokens do + Repo.all(ApiToken) + end + + def list_api_tokens(user_id) do + Repo.all(from a in ApiToken, where: a.user_id == ^user_id) + end + + def get_api_token(id), do: Repo.get(ApiToken, id) + + def get_api_token!(id), do: Repo.get!(ApiToken, id) + + def get_unexpired_api_token(api_token_id) do + now = DateTime.utc_now() + + Repo.one( + from a in ApiToken, + where: a.id == ^api_token_id and a.expires_at >= ^now + ) + end + + def new_api_token(attrs \\ %{}) do + ApiToken.create_changeset(attrs) + end + + def create_user_api_token(%FzHttp.Users.User{} = user, params) do + changeset = + params + |> Enum.into(%{"user_id" => user.id}) + |> ApiToken.create_changeset(count_per_user: count_by_user_id(user.id)) + + with {:ok, api_token} <- Repo.insert(changeset) do + FzHttp.Telemetry.create_api_token() + {:ok, api_token} + end + end + + def api_token_expired?(%ApiToken{} = api_token) do + DateTime.diff(api_token.expires_at, DateTime.utc_now()) < 0 + end + + def delete_api_token(%ApiToken{} = api_token) do + with {:ok, api_token} <- Repo.delete(api_token) do + FzHttp.Telemetry.delete_api_token(api_token) + {:ok, api_token} + end + end +end diff --git a/apps/fz_http/lib/fz_http/api_tokens/api_token.ex b/apps/fz_http/lib/fz_http/api_tokens/api_token.ex new file mode 100644 index 000000000..3a111cbc6 --- /dev/null +++ b/apps/fz_http/lib/fz_http/api_tokens/api_token.ex @@ -0,0 +1,51 @@ +defmodule FzHttp.ApiTokens.ApiToken do + use FzHttp, :schema + import Ecto.Changeset + + @max_per_user 25 + + schema "api_tokens" do + field :expires_at, :utc_datetime_usec + + # Developer-friendly way to set expires_at + field :expires_in, :integer, virtual: true, default: 30 + + belongs_to :user, FzHttp.Users.User + + timestamps(updated_at: false) + end + + def create_changeset(attrs, opts \\ []) do + %__MODULE__{} + |> cast(attrs, ~w[ + user_id + expires_in + expires_at + ]a) + |> validate_required([:user_id, :expires_in]) + |> validate_number(:expires_in, greater_than_or_equal_to: 1, less_than_or_equal_to: 90) + |> resolve_expires_at() + |> validate_required(:expires_at) + |> assoc_constraint(:user) + |> maybe_validate_count_per_user(@max_per_user, opts[:count_per_user]) + end + + def max_per_user, do: @max_per_user + + defp resolve_expires_at(changeset) do + expires_at = + DateTime.utc_now() + |> DateTime.add(get_field(changeset, :expires_in), :day) + + put_change(changeset, :expires_at, expires_at) + end + + defp maybe_validate_count_per_user(changeset, max, num) when is_integer(num) and num >= max do + # XXX: This suffers from a race condition because the count happens in a separate transaction. + # At the moment it's not a big concern. Fixing it would require locking against INSERTs or DELETEs + # while counts are happening. + add_error(changeset, :base, "token limit of #{@max_per_user} reached") + end + + defp maybe_validate_count_per_user(changeset, _, _), do: changeset +end diff --git a/apps/fz_http/lib/fz_http/application.ex b/apps/fz_http/lib/fz_http/application.ex index 60f9381d3..ce56ebe38 100644 --- a/apps/fz_http/lib/fz_http/application.ex +++ b/apps/fz_http/lib/fz_http/application.ex @@ -22,17 +22,15 @@ defmodule FzHttp.Application do :ok end - defp children, do: children(Application.fetch_env!(:fz_http, :supervision_tree_mode)) + defp children, do: children(FzHttp.Config.fetch_env!(:fz_http, :supervision_tree_mode)) defp children(:full) do [ - {Cachex, name: :conf}, FzHttp.Server, FzHttp.Repo, {Postgrex.Notifications, [name: FzHttp.Repo.Notifications] ++ FzHttp.Repo.config()}, FzHttp.Repo.Notifier, FzHttp.Vault, - FzHttp.Configurations.Cache, FzHttpWeb.Endpoint, {Phoenix.PubSub, name: FzHttp.PubSub}, {FzHttp.Notifications, name: FzHttp.Notifications}, @@ -41,25 +39,23 @@ defmodule FzHttp.Application do FzHttp.TelemetryPingService, FzHttp.VpnSessionScheduler, FzHttp.OIDC.StartProxy, + FzHttp.SAML.StartProxy, {DynamicSupervisor, name: FzHttp.RefresherSupervisor, strategy: :one_for_one}, - FzHttp.OIDC.RefreshManager, - FzHttp.SAML.StartProxy + FzHttp.OIDC.RefreshManager ] end defp children(:test) do [ - {Cachex, name: :conf}, FzHttp.Server, FzHttp.Repo, FzHttp.Vault, - FzHttp.Configurations.Cache, FzHttpWeb.Endpoint, {FzHttp.OIDC.StartProxy, :test}, + {FzHttp.SAML.StartProxy, :test}, {Phoenix.PubSub, name: FzHttp.PubSub}, {FzHttp.Notifications, name: FzHttp.Notifications}, - FzHttpWeb.Presence, - FzHttp.SAML.StartProxy + FzHttpWeb.Presence ] end end diff --git a/apps/fz_http/lib/fz_http/conf/cache.ex b/apps/fz_http/lib/fz_http/conf/cache.ex deleted file mode 100644 index f807f4fa9..000000000 --- a/apps/fz_http/lib/fz_http/conf/cache.ex +++ /dev/null @@ -1,55 +0,0 @@ -defmodule FzHttp.Configurations.Cache do - @moduledoc """ - Manipulate cached configurations. - """ - - use GenServer, restart: :transient - - alias FzHttp.Configurations, as: Conf - - @name :conf - - def get(key) do - Cachex.get(@name, key) - end - - def get!(key) do - Cachex.get!(@name, key) - end - - def put(key, value) do - Cachex.put(@name, key, value) - end - - def put!(key, value) do - Cachex.put!(@name, key, value) - end - - def start_link(_) do - GenServer.start_link(__MODULE__, []) - end - - @no_fallback [:logo] - - @impl true - def init(_) do - configurations = - Conf.get_configuration!() - |> Map.from_struct() - |> Map.delete(:id) - - for {k, v} <- configurations do - # XXX: Remove fallbacks before 1.0? - v = - with nil <- v, true <- k not in @no_fallback do - Application.fetch_env!(:fz_http, k) - else - _ -> v - end - - {:ok, _} = put(k, v) - end - - :ignore - end -end diff --git a/apps/fz_http/lib/fz_http/conf/configuration.ex b/apps/fz_http/lib/fz_http/conf/configuration.ex deleted file mode 100644 index cc11d6c6e..000000000 --- a/apps/fz_http/lib/fz_http/conf/configuration.ex +++ /dev/null @@ -1,37 +0,0 @@ -defmodule FzHttp.Configurations.Configuration do - @moduledoc """ - App global configuration, singleton resource - """ - - use Ecto.Schema - import Ecto.Changeset - alias FzHttp.Configurations.Logo - - @primary_key {:id, :binary_id, autogenerate: true} - - schema "configurations" do - embeds_one :logo, Logo, on_replace: :update - field :local_auth_enabled, :boolean - field :allow_unprivileged_device_management, :boolean - field :allow_unprivileged_device_configuration, :boolean - field :openid_connect_providers, :map - field :saml_identity_providers, :map - field :disable_vpn_on_oidc_error, :boolean - - timestamps(type: :utc_datetime_usec) - end - - @doc false - def changeset(configuration, attrs) do - configuration - |> cast(attrs, [ - :local_auth_enabled, - :allow_unprivileged_device_management, - :allow_unprivileged_device_configuration, - :openid_connect_providers, - :saml_identity_providers, - :disable_vpn_on_oidc_error - ]) - |> cast_embed(:logo) - end -end diff --git a/apps/fz_http/lib/fz_http/conf/html_safe_map.ex b/apps/fz_http/lib/fz_http/conf/html_safe_map.ex deleted file mode 100644 index e81cd4b69..000000000 --- a/apps/fz_http/lib/fz_http/conf/html_safe_map.ex +++ /dev/null @@ -1,5 +0,0 @@ -defimpl Phoenix.HTML.Safe, for: Map do - def to_iodata(%{} = map) do - Jason.encode_to_iodata!(map, pretty: true) - end -end diff --git a/apps/fz_http/lib/fz_http/config.ex b/apps/fz_http/lib/fz_http/config.ex new file mode 100644 index 000000000..da29549b1 --- /dev/null +++ b/apps/fz_http/lib/fz_http/config.ex @@ -0,0 +1,70 @@ +defmodule FzHttp.Config do + @moduledoc """ + This module provides set of helper functions that are useful when reading application runtime configuration overrides + in test environment. + """ + + if Mix.env() != :test do + def maybe_put_env_override(_key, _value), do: :ok + def fetch_env!(app, key), do: Application.fetch_env!(app, key) + else + def maybe_put_env_override(key, value) do + _ = Process.put(key, value) + :ok + end + + @doc """ + Attempts to override application env configuration from one of 3 sources (in this exact order): + * takes it from process dictionary of a current process; + * takes it from process dictionary of a last process in $ancestors stack. + * takes it from process dictionary of a last process in $callers stack; + + This function is especially useful when some options (eg. request endpoint) needs to be overridden + in test environment (eg. to send those requests to Bypass). + """ + def fetch_env!(app, key) do + application_env = Application.fetch_env!(app, key) + + with :error <- fetch_process_value(key), + :error <- fetch_process_value(get_last_pid_from_pdict_list(:"$ancestors"), key), + :error <- fetch_process_value(get_last_pid_from_pdict_list(:"$callers"), key) do + application_env + else + {:ok, override} -> override + end + end + + defp fetch_process_value(key) do + case Process.get(key) do + nil -> :error + value -> {:ok, value} + end + end + + defp fetch_process_value(nil, _key) do + :error + end + + defp fetch_process_value(atom, key) when is_atom(atom) do + atom + |> Process.whereis() + |> fetch_process_value(key) + end + + defp fetch_process_value(pid, key) do + case :erlang.process_info(pid, :dictionary) do + {:dictionary, pdict} -> + Keyword.fetch(pdict, key) + + _other -> + :error + end + end + + defp get_last_pid_from_pdict_list(stack) do + if values = Process.get(stack) do + List.last(values) + end + end + end +end diff --git a/apps/fz_http/lib/fz_http/configurations.ex b/apps/fz_http/lib/fz_http/configurations.ex index ac4295ce5..d4896ac80 100644 --- a/apps/fz_http/lib/fz_http/configurations.ex +++ b/apps/fz_http/lib/fz_http/configurations.ex @@ -4,20 +4,39 @@ defmodule FzHttp.Configurations do """ import Ecto.Query, warn: false - import Ecto.Changeset - alias FzHttp.{Repo, Configurations.Configuration, Configurations.Cache} - defdelegate get(key), to: FzHttp.Configurations.Cache - defdelegate get!(key), to: FzHttp.Configurations.Cache + alias FzHttp.{Repo, Configurations.Configuration} + + def get!(key) do + Map.get(get_configuration!(), key) + end + + def put!(key, val) do + get_configuration!() + |> Configuration.changeset(%{key => val}) + |> Repo.update!() + end def get_configuration! do Repo.one!(Configuration) end - def auto_create_users?(field, provider) do - get!(field) - |> Map.get(provider) - |> Map.get("auto_create_users") + def get_provider_by_id(field, provider_id) do + FzHttp.Configurations.get!(field) + |> Enum.find(&(&1.id == provider_id)) + end + + def auto_create_users?(field, provider_id) do + FzHttp.Configurations.get!(field) + |> Enum.find(&(&1.id == provider_id)) + |> case do + nil -> raise RuntimeError, "Unknown provider #{provider_id}" + provider -> provider.auto_create_users + end + end + + def new_configuration(attrs \\ %{}) do + Configuration.changeset(%Configuration{}, attrs) end def change_configuration(%Configuration{} = config \\ get_configuration!()) do @@ -25,27 +44,30 @@ defmodule FzHttp.Configurations do end def update_configuration(%Configuration{} = config \\ get_configuration!(), attrs) do - config - |> Configuration.changeset(attrs) - |> prepare_changes(fn changeset -> - for {k, v} <- changeset.changes do - case v do - %Ecto.Changeset{} -> - Cache.put!(k, v.changes) + case Repo.update(Configuration.changeset(config, attrs)) do + {:ok, configuration} -> + FzHttp.SAML.StartProxy.restart() + FzHttp.OIDC.StartProxy.restart() - _ -> - Cache.put!(k, v) - end - end + {:ok, configuration} - changeset - end) - |> Repo.update() + error -> + error + end end def logo_types, do: ~w(Default URL Upload) def logo_type(nil), do: "Default" - def logo_type(%{url: _url}), do: "URL" - def logo_type(%{data: _data}), do: "Upload" + def logo_type(%{url: url}) when not is_nil(url), do: "URL" + def logo_type(%{data: data}) when not is_nil(data), do: "Upload" + + def vpn_sessions_expire? do + freq = vpn_duration() + freq > 0 && freq < Configuration.max_vpn_session_duration() + end + + def vpn_duration do + get_configuration!().vpn_session_duration + end end diff --git a/apps/fz_http/lib/fz_http/configurations/configuration.ex b/apps/fz_http/lib/fz_http/configurations/configuration.ex new file mode 100644 index 000000000..0236c0a2e --- /dev/null +++ b/apps/fz_http/lib/fz_http/configurations/configuration.ex @@ -0,0 +1,107 @@ +defmodule FzHttp.Configurations.Configuration do + @moduledoc """ + App global configuration, singleton resource + """ + use FzHttp, :schema + import Ecto.Changeset + + alias FzHttp.{ + Configurations.Logo, + Validators.Common + } + + @min_mtu 576 + @max_mtu 1500 + @min_persistent_keepalive 0 + @max_persistent_keepalive 120 + + # Postgres max int size is 4 bytes + @max_pg_integer 2_147_483_647 + @min_vpn_session_duration 0 + @max_vpn_session_duration @max_pg_integer + + schema "configurations" do + field :allow_unprivileged_device_management, :boolean + field :allow_unprivileged_device_configuration, :boolean + + field :local_auth_enabled, :boolean + field :disable_vpn_on_oidc_error, :boolean + + # The defaults for these fields are set in the following migration: + # apps/fz_http/priv/repo/migrations/20221224210654_fix_sites_nullable_fields.exs + # + # This will be changing in 0.8 and again when we have client apps, + # so this works for the time being. The important thing is allowing users + # to update these fields via the REST API since they were removed as + # environment variables in the above migration. This is important for users + # wishing to configure Firezone with automated Infrastructure tools like + # Terraform. + field :default_client_persistent_keepalive, :integer + field :default_client_mtu, :integer + field :default_client_endpoint, :string + field :default_client_dns, :string + field :default_client_allowed_ips, :string + + # XXX: Remove when this feature is refactored into config expiration feature + # and WireGuard keys are decoupled from devices to facilitate rotation. + # + # See https://github.com/firezone/firezone/issues/1236 + field :vpn_session_duration, :integer, read_after_writes: true + + embeds_one :logo, Logo, on_replace: :delete + + embeds_many :openid_connect_providers, + FzHttp.Configurations.Configuration.OpenIDConnectProvider, + on_replace: :delete + + embeds_many :saml_identity_providers, + FzHttp.Configurations.Configuration.SAMLIdentityProvider, + on_replace: :delete + + timestamps() + end + + @doc false + def changeset(configuration, attrs) do + configuration + |> cast(attrs, ~w[ + local_auth_enabled + allow_unprivileged_device_management + allow_unprivileged_device_configuration + disable_vpn_on_oidc_error + default_client_persistent_keepalive + default_client_mtu + default_client_endpoint + default_client_dns + default_client_allowed_ips + vpn_session_duration + ]a) + |> cast_embed(:logo) + |> cast_embed(:openid_connect_providers, + with: {FzHttp.Configurations.Configuration.OpenIDConnectProvider, :changeset, []} + ) + |> cast_embed(:saml_identity_providers, + with: {FzHttp.Configurations.Configuration.SAMLIdentityProvider, :changeset, []} + ) + |> Common.trim_change(:default_client_dns) + |> Common.trim_change(:default_client_allowed_ips) + |> Common.trim_change(:default_client_endpoint) + |> Common.validate_no_duplicates(:default_client_dns) + |> Common.validate_list_of_ips_or_cidrs(:default_client_allowed_ips) + |> Common.validate_no_duplicates(:default_client_allowed_ips) + |> validate_number(:default_client_mtu, + greater_than_or_equal_to: @min_mtu, + less_than_or_equal_to: @max_mtu + ) + |> validate_number(:default_client_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 diff --git a/apps/fz_http/lib/fz_http/conf/oidc_config.ex b/apps/fz_http/lib/fz_http/configurations/configuration/openid_connect_provider.ex similarity index 79% rename from apps/fz_http/lib/fz_http/conf/oidc_config.ex rename to apps/fz_http/lib/fz_http/configurations/configuration/openid_connect_provider.ex index 0abb9fe43..26f988511 100644 --- a/apps/fz_http/lib/fz_http/conf/oidc_config.ex +++ b/apps/fz_http/lib/fz_http/configurations/configuration/openid_connect_provider.ex @@ -1,12 +1,10 @@ -defmodule FzHttp.Conf.OIDCConfig do +defmodule FzHttp.Configurations.Configuration.OpenIDConnectProvider do @moduledoc """ OIDC Config virtual schema """ - use Ecto.Schema - + use FzHttp, :schema import Ecto.Changeset - import FzHttp.Validators.OpenIDConnect - import FzHttp.Validators.Common, only: [validate_uri: 2] + alias FzHttp.Validators @reserved_config_ids [ "identity", @@ -27,8 +25,8 @@ defmodule FzHttp.Conf.OIDCConfig do field :auto_create_users, :boolean, default: true end - def changeset(data) do - %__MODULE__{} + def changeset(struct \\ %__MODULE__{}, data) do + struct |> cast( data, [ @@ -55,8 +53,8 @@ defmodule FzHttp.Conf.OIDCConfig do ]) # Don't allow users to enter reserved config ids |> validate_exclusion(:id, @reserved_config_ids) - |> validate_discovery_document_uri() - |> validate_uri([ + |> Validators.OpenIDConnect.validate_discovery_document_uri() + |> Validators.Common.validate_uri([ :redirect_uri ]) end diff --git a/apps/fz_http/lib/fz_http/conf/saml_config.ex b/apps/fz_http/lib/fz_http/configurations/configuration/saml_identity_provider.ex similarity index 79% rename from apps/fz_http/lib/fz_http/conf/saml_config.ex rename to apps/fz_http/lib/fz_http/configurations/configuration/saml_identity_provider.ex index 1cd472c1b..9317b8930 100644 --- a/apps/fz_http/lib/fz_http/conf/saml_config.ex +++ b/apps/fz_http/lib/fz_http/configurations/configuration/saml_identity_provider.ex @@ -1,11 +1,10 @@ -defmodule FzHttp.Conf.SAMLConfig do +defmodule FzHttp.Configurations.Configuration.SAMLIdentityProvider do @moduledoc """ SAML Config virtual schema """ - use Ecto.Schema - + use FzHttp, :schema import Ecto.Changeset - import FzHttp.Validators.SAML + alias FzHttp.Validators @primary_key false embedded_schema do @@ -20,8 +19,8 @@ defmodule FzHttp.Conf.SAMLConfig do field :auto_create_users, :boolean, default: true end - def changeset(data) do - %__MODULE__{} + def changeset(struct \\ %__MODULE__{}, data) do + struct |> cast(data, [ :id, :label, @@ -39,6 +38,6 @@ defmodule FzHttp.Conf.SAMLConfig do :metadata, :auto_create_users ]) - |> validate_metadata() + |> Validators.SAML.validate_metadata() end end diff --git a/apps/fz_http/lib/fz_http/conf/logo.ex b/apps/fz_http/lib/fz_http/configurations/logo.ex similarity index 81% rename from apps/fz_http/lib/fz_http/conf/logo.ex rename to apps/fz_http/lib/fz_http/configurations/logo.ex index 066d9f458..1cf0fd52b 100644 --- a/apps/fz_http/lib/fz_http/conf/logo.ex +++ b/apps/fz_http/lib/fz_http/configurations/logo.ex @@ -2,10 +2,12 @@ defmodule FzHttp.Configurations.Logo do @moduledoc """ Embedded Schema for logo """ - - use Ecto.Schema + use FzHttp, :schema import Ecto.Changeset + # Singleton per configuration + @primary_key false + embedded_schema do field :url, :string field :data, :string diff --git a/apps/fz_http/lib/fz_http/connectivity_check_service.ex b/apps/fz_http/lib/fz_http/connectivity_check_service.ex index 1e78eac6e..6bd6a8a01 100644 --- a/apps/fz_http/lib/fz_http/connectivity_check_service.ex +++ b/apps/fz_http/lib/fz_http/connectivity_check_service.ex @@ -61,11 +61,11 @@ defmodule FzHttp.ConnectivityCheckService do end defp url do - Application.fetch_env!(:fz_http, :connectivity_checks_url) <> version() + FzHttp.Config.fetch_env!(:fz_http, :connectivity_checks_url) <> version() end defp http_client do - Application.fetch_env!(:fz_http, :http_client) + FzHttp.Config.fetch_env!(:fz_http, :http_client) end defp version do @@ -73,10 +73,10 @@ defmodule FzHttp.ConnectivityCheckService do end defp interval do - Application.fetch_env!(:fz_http, :connectivity_checks_interval) * 1_000 + FzHttp.Config.fetch_env!(:fz_http, :connectivity_checks_interval) * 1_000 end defp enabled? do - Application.fetch_env!(:fz_http, :connectivity_checks_enabled) + FzHttp.Config.fetch_env!(:fz_http, :connectivity_checks_enabled) end end diff --git a/apps/fz_http/lib/fz_http/connectivity_checks/connectivity_check.ex b/apps/fz_http/lib/fz_http/connectivity_checks/connectivity_check.ex index 7571d8984..1846f9b75 100644 --- a/apps/fz_http/lib/fz_http/connectivity_checks/connectivity_check.ex +++ b/apps/fz_http/lib/fz_http/connectivity_checks/connectivity_check.ex @@ -2,7 +2,7 @@ defmodule FzHttp.ConnectivityChecks.ConnectivityCheck do @moduledoc """ Manages the connectivity_checks table """ - use Ecto.Schema + use FzHttp, :schema import Ecto.Changeset @url_regex ~r<\Ahttps://ping(?:-dev)?\.firez\.one/\d+\.\d+\.\d+(?:\+git\.\d+\.[0-9a-fA-F]{7,})?\z> @@ -13,7 +13,7 @@ defmodule FzHttp.ConnectivityChecks.ConnectivityCheck do field :response_headers, :map field :url, :string - timestamps(type: :utc_datetime_usec) + timestamps(updated_at: false) end @doc false diff --git a/apps/fz_http/lib/fz_http/devices.ex b/apps/fz_http/lib/fz_http/devices.ex index 1b7f0489c..868278845 100644 --- a/apps/fz_http/lib/fz_http/devices.ex +++ b/apps/fz_http/lib/fz_http/devices.ex @@ -1,13 +1,17 @@ defmodule FzHttp.Devices do - @moduledoc """ - The Devices context. - """ - import Ecto.Changeset import Ecto.Query, warn: false - alias EctoNetwork.INET - alias FzHttp.{Devices.Device, Devices.DeviceSetting, Repo, Sites, Telemetry, Users, Users.User} + + alias FzHttp.{ + Configurations, + Devices.Device, + Devices.DeviceSetting, + Repo, + Telemetry, + Users, + Users.User + } require Logger @@ -22,7 +26,13 @@ defmodule FzHttp.Devices do end def count do - Repo.one(from d in Device, select: count(d.id)) + Repo.aggregate(Device, :count) + end + + def count(nil), do: 0 + + def count(user_id) do + Repo.one(from d in Device, where: d.user_id == ^user_id, select: count()) end def max_count_by_user_id do @@ -52,14 +62,11 @@ defmodule FzHttp.Devices do end def setting_projection(device) do - DeviceSetting.parse(device) + device + |> DeviceSetting.parse() |> Map.from_struct() end - def count(user_id) do - Repo.one(from d in Device, where: d.user_id == ^user_id, select: count()) - end - def get_device!(id), do: Repo.get!(Device, id) def create_device(attrs \\ %{}) do @@ -113,7 +120,7 @@ defmodule FzHttp.Devices do end def to_peer_list do - vpn_duration = Sites.vpn_duration() + vpn_duration = Configurations.vpn_duration() Repo.all( from d in Device, @@ -132,16 +139,7 @@ defmodule FzHttp.Devices do end def new_device(attrs \\ %{}) do - change_device( - %Device{}, - Map.merge( - %{ - "name" => Device.new_name(), - "preshared_key" => FzCommon.FzCrypto.psk() - }, - attrs - ) - ) + change_device(%Device{}, attrs) end def allowed_ips(device), do: config(device, :allowed_ips) @@ -150,9 +148,9 @@ defmodule FzHttp.Devices do def mtu(device), do: config(device, :mtu) def persistent_keepalive(device), do: config(device, :persistent_keepalive) - defp config(device, key) do - if Map.get(device, String.to_atom("use_site_#{key}")) do - Map.get(Sites.get_site!(), key) + def config(device, key) do + if Map.get(device, String.to_atom("use_default_#{key}")) do + Map.get(Configurations.get_configuration!(), String.to_atom("default_client_#{key}")) else Map.get(device, key) end @@ -160,11 +158,11 @@ defmodule FzHttp.Devices do def defaults(changeset) do ~w( - use_site_allowed_ips - use_site_dns - use_site_endpoint - use_site_mtu - use_site_persistent_keepalive + use_default_allowed_ips + use_default_dns + use_default_endpoint + use_default_mtu + use_default_persistent_keepalive )a |> Map.new(&{&1, get_field(changeset, &1)}) end @@ -197,8 +195,24 @@ defmodule FzHttp.Devices do end def decode(nil), do: nil + def decode(inet) when is_binary(inet), do: inet def decode(inet), do: INET.decode(inet) + @hash_range 2 ** 16 + def new_name(name \\ FzCommon.NameGenerator.generate()) do + hash = + name + |> :erlang.phash2(@hash_range) + |> Integer.to_string(16) + |> String.pad_leading(4, "0") + + if String.length(name) > 15 do + String.slice(name, 0..10) <> hash + else + name + end + end + defp psk_config(device) do if device.preshared_key do "PresharedKey = #{device.preshared_key}" @@ -287,10 +301,10 @@ defmodule FzHttp.Devices do defp field_empty?(_), do: false defp ipv4? do - Application.fetch_env!(:fz_http, :wireguard_ipv4_enabled) + FzHttp.Config.fetch_env!(:fz_http, :wireguard_ipv4_enabled) end defp ipv6? do - Application.fetch_env!(:fz_http, :wireguard_ipv6_enabled) + FzHttp.Config.fetch_env!(:fz_http, :wireguard_ipv6_enabled) end end diff --git a/apps/fz_http/lib/fz_http/devices/device.ex b/apps/fz_http/lib/fz_http/devices/device.ex index fb8b6b5ae..1ae8d9193 100644 --- a/apps/fz_http/lib/fz_http/devices/device.ex +++ b/apps/fz_http/lib/fz_http/devices/device.ex @@ -2,141 +2,114 @@ defmodule FzHttp.Devices.Device do @moduledoc """ Manages Device things """ - - use Ecto.Schema + use FzHttp, :schema import Ecto.Changeset + alias FzHttp.Validators.Common + alias FzHttp.Devices require Logger - import FzHttp.Validators.Common, - only: [ - trim: 2, - validate_omitted: 2, - validate_no_duplicates: 2, - validate_no_mask: 2, - validate_list_of_ips_or_cidrs: 2 - ] - - import FzHttp.Queries.INET - - alias FzHttp.{Devices, Users.User} - @description_max_length 2048 - # Fields for which to trim whitespace after cast, before validation - @whitespace_trimmed_fields ~w( - allowed_ips - dns - endpoint - name - description - )a + # WireGuard base64-encoded string length + @key_length 44 schema "devices" do field :rx_bytes, :integer field :tx_bytes, :integer - field :uuid, Ecto.UUID, autogenerate: true field :name, :string field :description, :string field :public_key, :string field :preshared_key, FzHttp.Encrypted.Binary - 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 :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 :endpoint, :string field :mtu, :integer field :persistent_keepalive, :integer field :allowed_ips, :string field :dns, :string field :remote_ip, EctoNetwork.INET - field :ipv4, EctoNetwork.INET, read_after_writes: true - field :ipv6, EctoNetwork.INET, read_after_writes: true + field :ipv4, EctoNetwork.INET + field :ipv6, EctoNetwork.INET + field :latest_handshake, :utc_datetime_usec field :key_regenerated_at, :utc_datetime_usec, read_after_writes: true - belongs_to :user, User + belongs_to :user, FzHttp.Users.User - timestamps(type: :utc_datetime_usec) + timestamps() end - def create_changeset(device \\ %__MODULE__{}, attrs) do - device - |> shared_cast(attrs) - |> put_next_ip(:ipv4) - |> put_next_ip(:ipv6) - |> shared_changeset() + @fields ~w[ + latest_handshake + rx_bytes + tx_bytes + use_default_allowed_ips + use_default_dns + use_default_endpoint + use_default_mtu + use_default_persistent_keepalive + allowed_ips + dns + endpoint + mtu + persistent_keepalive + remote_ip + ipv4 + ipv6 + user_id + name + description + public_key + preshared_key + key_regenerated_at + ]a + + @required_fields ~w[user_id name public_key]a + + def create_changeset(attrs) do + %__MODULE__{} + |> cast(attrs, @fields) + |> Common.put_default_value(:name, &FzHttp.Devices.new_name/0) + |> Common.put_default_value(:preshared_key, &FzCommon.FzCrypto.psk/0) + |> changeset() |> validate_max_devices() + |> validate_required(@required_fields) end def update_changeset(device, attrs) do device - |> shared_cast(attrs) - |> shared_changeset() + |> cast(attrs, @fields) + |> changeset() + |> validate_required(@required_fields) end - @hash_range 2 ** 16 - - def new_name(name \\ FzCommon.NameGenerator.generate()) do - hash = - name - |> :erlang.phash2(@hash_range) - |> Integer.to_string(16) - |> String.pad_leading(4, "0") - - if String.length(name) > 15 do - String.slice(name, 0..10) <> hash - else - name - end - end - - defp shared_cast(device, attrs) do - device - |> cast(attrs, [ - :latest_handshake, - :rx_bytes, - :tx_bytes, - :use_site_allowed_ips, - :use_site_dns, - :use_site_endpoint, - :use_site_mtu, - :use_site_persistent_keepalive, - :allowed_ips, - :dns, - :endpoint, - :mtu, - :persistent_keepalive, - :remote_ip, - :ipv4, - :ipv6, - :user_id, - :name, - :description, - :public_key, - :preshared_key, - :key_regenerated_at - ]) - |> trim(@whitespace_trimmed_fields) - end - - defp shared_changeset(changeset) do + defp changeset(changeset) do changeset - |> validate_required([ - :user_id, - :name, - :public_key - ]) - |> validate_required_unless_site([:endpoint]) - |> validate_omitted_if_site([ - :allowed_ips, - :dns, - :endpoint, - :persistent_keepalive, - :mtu - ]) - |> validate_list_of_ips_or_cidrs(:allowed_ips) - |> validate_no_duplicates(:dns) + |> Common.trim_change(:allowed_ips) + |> Common.trim_change(:dns) + |> Common.trim_change(:endpoint) + |> Common.trim_change(:name) + |> Common.trim_change(:description) + |> Common.validate_base64(:public_key) + |> Common.validate_base64(:preshared_key) + |> validate_length(:public_key, is: @key_length) + |> validate_length(:preshared_key, is: @key_length) + |> validate_length(:description, max: @description_max_length) + |> validate_length(:name, min: 1) + |> assoc_constraint(:user) + |> validate_required_unless_default([:endpoint]) + |> validate_omitted_if_default(~w[ + allowed_ips + dns + endpoint + persistent_keepalive + mtu + ]a) + |> Common.validate_list_of_ips_or_cidrs(:allowed_ips) + |> Common.validate_no_duplicates(:dns) |> validate_number(:persistent_keepalive, greater_than_or_equal_to: 0, less_than_or_equal_to: 120 @@ -145,25 +118,69 @@ defmodule FzHttp.Devices.Device do greater_than_or_equal_to: 576, less_than_or_equal_to: 1500 ) - |> validate_length(:description, max: @description_max_length) - |> validate_ipv4_required() - |> validate_ipv6_required() + |> prepare_changes(fn changeset -> + changeset + |> maybe_put_default_ip(:ipv4) + |> maybe_put_default_ip(:ipv6) + |> validate_exclusion(:ipv4, [ipv4_address()]) + |> validate_exclusion(:ipv6, [ipv6_address()]) + end) |> unique_constraint(:ipv4) |> unique_constraint(:ipv6) - |> validate_exclusion(:ipv4, [ipv4_address()]) - |> validate_exclusion(:ipv6, [ipv6_address()]) - |> validate_in_network(:ipv4) - |> validate_in_network(:ipv6) - |> validate_no_mask(:ipv4) - |> validate_no_mask(:ipv6) |> unique_constraint(:public_key) |> unique_constraint([:user_id, :name]) end + defp maybe_put_default_ip(changeset, field) do + if FzHttp.Config.fetch_env!(:fz_http, :"wireguard_#{field}_enabled") == true do + case fetch_field(changeset, field) do + {:data, nil} -> put_default_ip(changeset, field) + :error -> put_default_ip(changeset, field) + _ -> changeset + end + |> validate_required(field) + else + changeset + end + end + + defp put_default_ip(changeset, field) do + cidr_string = FzHttp.Config.fetch_env!(:fz_http, :"wireguard_#{field}_network") + {:ok, cidr_inet} = EctoNetwork.INET.cast(cidr_string) + cidr = CIDR.parse(cidr_string) + offset = Enum.random(2..(cidr.hosts - 2)) + + {:ok, gateway_address} = + FzHttp.Config.fetch_env!(:fz_http, :"wireguard_#{field}_address") + |> EctoNetwork.INET.cast() + + Devices.Device.Query.next_available_address(cidr_inet, offset, [gateway_address]) + |> FzHttp.Repo.one() + |> case do + nil -> add_error(changeset, :base, "CIDR #{cidr} is exhausted") + ip -> put_change(changeset, field, ip) + end + end + + defp ipv4_address do + FzHttp.Config.fetch_env!(:fz_http, :wireguard_ipv4_address) + |> EctoNetwork.INET.cast() + end + + defp ipv6_address do + FzHttp.Config.fetch_env!(:fz_http, :wireguard_ipv6_address) + |> EctoNetwork.INET.cast() + 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) + # XXX: This suffers from a race condition because the count happens in a separate transaction. + # At the moment it's not a big concern. Fixing it would require locking against INSERTs or DELETEs + # while counts are happening. + count = + get_field(changeset, :user_id) + |> Devices.count() + + max_devices = FzHttp.Config.fetch_env!(:fz_http, :max_devices_per_user) if count >= max_devices do add_error( @@ -176,99 +193,27 @@ defmodule FzHttp.Devices.Device do end end - defp validate_omitted_if_site(changeset, fields) when is_list(fields) do - validate_omitted(changeset, filter_site_fields(changeset, fields, use_site: true)) + defp validate_omitted_if_default(changeset, fields) when is_list(fields) do + Common.validate_omitted( + changeset, + filter_default_fields(changeset, fields, use_default: true) + ) end - defp validate_required_unless_site(changeset, fields) when is_list(fields) do - validate_required(changeset, filter_site_fields(changeset, fields, use_site: false)) + defp validate_required_unless_default(changeset, fields) when is_list(fields) do + validate_required(changeset, filter_default_fields(changeset, fields, use_default: false)) end - defp filter_site_fields(changeset, fields, use_site: use_site) when is_boolean(use_site) do + defp filter_default_fields(changeset, fields, use_default: use_default) + when is_boolean(use_default) do fields - |> 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 -> String.to_atom("use_default_#{field}") end) + |> Enum.filter(fn default_field -> get_field(changeset, default_field) == use_default end) |> Enum.map(fn field -> field |> Atom.to_string() - |> String.trim("use_site_") + |> String.trim("use_default_") |> String.to_atom() end) end - - defp validate_ipv4_required(changeset) do - if Application.fetch_env!(:fz_http, :wireguard_ipv4_enabled) do - validate_required(changeset, :ipv4) - else - changeset - end - end - - defp validate_ipv6_required(changeset) do - if Application.fetch_env!(:fz_http, :wireguard_ipv6_enabled) do - validate_required(changeset, :ipv6) - else - changeset - end - end - - defp validate_in_network(%Ecto.Changeset{changes: %{ipv4: ip}} = changeset, :ipv4) do - net = Application.fetch_env!(:fz_http, :wireguard_ipv4_network) - 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) - add_net_error_if_outside_bounds(changeset, net, ip, :ipv6) - end - - defp validate_in_network(changeset, :ipv6), do: changeset - - defp add_net_error_if_outside_bounds(changeset, net, ip, ip_type) do - %{address: address} = ip - cidr = CIDR.parse(net) - - if CIDR.match!(cidr, address) do - changeset - else - add_error(changeset, ip_type, "IP must be contained within network #{net}") - end - end - - 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 - - _ -> - if ip = next_available(ip_type) do - put_change(changeset, ip_type, ip) - else - add_error( - changeset, - :base, - "#{ip_type} address pool is exhausted. Increase network size or remove some devices." - ) - end - end - end - - defp ipv4_address do - {:ok, inet} = - Application.fetch_env!(:fz_http, :wireguard_ipv4_address) - |> EctoNetwork.INET.cast() - - inet - end - - defp ipv6_address do - {:ok, inet} = - Application.fetch_env!(:fz_http, :wireguard_ipv6_address) - |> EctoNetwork.INET.cast() - - inet - end end diff --git a/apps/fz_http/lib/fz_http/devices/device/query.ex b/apps/fz_http/lib/fz_http/devices/device/query.ex new file mode 100644 index 000000000..31088887c --- /dev/null +++ b/apps/fz_http/lib/fz_http/devices/device/query.ex @@ -0,0 +1,126 @@ +defmodule FzHttp.Devices.Device.Query do + import Ecto.Query + + @doc """ + Returns IP address at given integer offset relative to start of CIDR range. + """ + defmacro offset_to_ip(field, cidr) do + quote do + fragment("host(?)::inet + ?", unquote(cidr), unquote(field)) + end + end + + @doc """ + Returns index of last IP address available for allocation in CIDR sequence. + + Notice: the very last address in CIDR is typically a broadcast address that we won't allow to use for devices. + """ + defmacro cidr_end_offset(cidr) do + quote do + fragment( + "host(broadcast(?))::inet - host(?)::inet - 1", + unquote(cidr), + unquote(cidr) + ) + end + end + + @doc """ + Acquires a transactional advisory lock for an IP address using "devices" table oid as namespace. + + To fit bigint offset into int lock identifier we rollover at the integer max value. + """ + defmacro acquire_advisory_lock(field) do + quote do + fragment( + "pg_try_advisory_xact_lock('devices'::regclass::oid::int, mod(?, 2147483647)::int)", + unquote(field) + ) + end + end + + def all do + from(device in FzHttp.Devices.Device, as: :device) + end + + @doc """ + This function returns a query to fetch next available IP address, it works in 2 steps: + + 1. It starts by forward-scanning starting for available addresses at `offset` in a given `network_cidr` + up to the end of CIDR range; + + 2. If forward-scan failed, scan backwards from the offset (exclusive) to start of CIDR range. + + During the search, addresses occupied by other devices or reserved are skipped. + + We also exclude first (X.X.X.0) and last (broadcast) address in a CIDR from a search range, + to prevent issues with legacy firewalls that consider them "class C" space network addresses. + """ + def next_available_address(network_cidr, offset, reserved_address) do + forward_search_queryable = + series_from_offset_inclusive_to_end_of_cidr(network_cidr, offset) + |> select_not_used_ips(network_cidr, reserved_address) + + reverse_search_queryable = + series_from_start_of_cidr_to_offset_exclusive(network_cidr, offset) + |> select_not_used_ips(network_cidr, reserved_address) + + union_all(forward_search_queryable, ^reverse_search_queryable) + |> limit(1) + end + + # Although sequences can work with inet types, we iterate over the sequence using an + # offset relative to start of the given CIDR range. + # + # This way is chosen because IPv6 can not be cast to bigint, so by using it directly + # we won't be able to increment/decrement it while building a sequence. + # + # At the same time offset will fit to bigint even for largest CIDR ranges that Firezone supports. + # + # XXX: We can make this code prettier once https://github.com/elixir-ecto/ecto/commit/8f7bb2665bce30dfab18cfed01585c96495575a6 is released. + defp series_from_offset_inclusive_to_end_of_cidr(network_cidr, offset) do + from( + i in fragment( + "SELECT generate_series((?)::bigint, (?)::bigint, ?) AS ip", + ^offset, + cidr_end_offset(^network_cidr), + 1 + ), + as: :q + ) + end + + defp series_from_start_of_cidr_to_offset_exclusive(_network_cidr, offset) do + from( + i in fragment( + "SELECT generate_series((?)::bigint, (?)::bigint, ?) AS ip", + ^(offset - 1), + 2, + -1 + ), + as: :q + ) + end + + defp select_not_used_ips(queryable, network_cidr, reserved_ips) do + queryable + |> where( + [q: q], + offset_to_ip(q.ip, ^network_cidr) not in subquery(used_ips_subquery(network_cidr)) + ) + |> where([q: q], offset_to_ip(q.ip, ^network_cidr) not in ^reserved_ips) + |> where([q: q], acquire_advisory_lock(q.ip) == true) + |> select([q: q], offset_to_ip(q.ip, ^network_cidr)) + end + + defp used_ips_subquery(queryable \\ all(), address) + + defp used_ips_subquery(queryable, %Postgrex.INET{address: address}) + when tuple_size(address) == 4 do + select(queryable, [device: device], device.ipv4) + end + + defp used_ips_subquery(queryable, %Postgrex.INET{address: _address}) do + select(queryable, [device: device], device.ipv6) + end +end diff --git a/apps/fz_http/lib/fz_http/devices/device_setting.ex b/apps/fz_http/lib/fz_http/devices/device_setting.ex index 1c25a2644..42e3eec20 100644 --- a/apps/fz_http/lib/fz_http/devices/device_setting.ex +++ b/apps/fz_http/lib/fz_http/devices/device_setting.ex @@ -1,35 +1,19 @@ defmodule FzHttp.Devices.DeviceSetting do - @moduledoc """ - Device setting parsed from either a Device struct or map. - """ - use Ecto.Schema - - import Ecto.Changeset - import FzHttp.Devices, only: [decode: 1] + use FzHttp, :schema + alias FzHttp.Devices @primary_key false embedded_schema do field :ip, :string field :ip6, :string - field :user_id, :integer + field :user_id, Ecto.UUID end - def parse(device) when is_struct(device) do + def parse(device_or_device_as_map) do %__MODULE__{ - ip: decode(device.ipv4), - ip6: decode(device.ipv6), - user_id: device.user_id + ip: Devices.decode(device_or_device_as_map.ipv4), + ip6: Devices.decode(device_or_device_as_map.ipv6), + user_id: device_or_device_as_map.user_id } end - - def parse(device) when is_map(device) do - device = - device - |> Map.put(:ip, Map.get(device, :ipv4)) - |> Map.put(:ip6, Map.get(device, :ipv6)) - - %__MODULE__{} - |> cast(device, [:ip, :ip6, :user_id]) - |> apply_changes() - end end diff --git a/apps/fz_http/lib/fz_http/devices/stats_updater.ex b/apps/fz_http/lib/fz_http/devices/stats_updater.ex index 22e0043ac..70d78de6a 100644 --- a/apps/fz_http/lib/fz_http/devices/stats_updater.ex +++ b/apps/fz_http/lib/fz_http/devices/stats_updater.ex @@ -33,7 +33,7 @@ defmodule FzHttp.Devices.StatsUpdater do # XXX: Come up with a better way to update devices in Sandbox mode defp device_to_update(public_key) do - if Application.fetch_env!(:fz_http, :sandbox) do + if FzHttp.Config.fetch_env!(:fz_http, :sandbox) do Repo.one( from Device, order_by: fragment("RANDOM()"), diff --git a/apps/fz_http/lib/fz_http/events.ex b/apps/fz_http/lib/fz_http/events.ex index 53cd8dd61..3cf31902e 100644 --- a/apps/fz_http/lib/fz_http/events.ex +++ b/apps/fz_http/lib/fz_http/events.ex @@ -9,7 +9,7 @@ defmodule FzHttp.Events do # 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 add(subject, device) when subject == "devices" do + def add("devices", device) do with :ok <- GenServer.call(wall_pid(), {:add_device, Devices.setting_projection(device)}), :ok <- GenServer.call(vpn_pid(), {:set_config, Devices.to_peer_list()}) do :ok @@ -28,11 +28,11 @@ defmodule FzHttp.Events do end end - def add(subject, rule) when subject == "rules" do + def add("rules", rule) do GenServer.call(wall_pid(), {:add_rule, Rules.setting_projection(rule)}) end - def add(subject, user) when subject == "users" do + def add("users", user) do # Security note: It's important to let an exception here crash this service # otherwise, nft could have succeeded in adding the user's set but not the rules # this means that in `update_device` add_device can succeed adding the device to the user's set @@ -40,7 +40,7 @@ defmodule FzHttp.Events do GenServer.call(wall_pid(), {:add_user, Users.setting_projection(user)}) end - def delete(subject, device) when subject == "devices" do + def delete("devices", device) do with :ok <- GenServer.call(wall_pid(), {:delete_device, Devices.setting_projection(device)}), :ok <- GenServer.call(vpn_pid(), {:remove_peer, device.public_key}) do :ok @@ -59,11 +59,11 @@ defmodule FzHttp.Events do end end - def delete(subject, rule) when subject == "rules" do + def delete("rules", rule) do GenServer.call(wall_pid(), {:delete_rule, Rules.setting_projection(rule)}) end - def delete(subject, user) when subject == "users" do + def delete("users", user) do GenServer.call(wall_pid(), {:delete_user, Users.setting_projection(user)}) end diff --git a/apps/fz_http/lib/fz_http/int4range.ex b/apps/fz_http/lib/fz_http/int4range.ex index 8f489273b..512d6343c 100644 --- a/apps/fz_http/lib/fz_http/int4range.ex +++ b/apps/fz_http/lib/fz_http/int4range.ex @@ -5,7 +5,8 @@ defmodule FzHttp.Int4Range do # Note: we represent a port range as a string: lower - upper for ease of use # with Phoenix LiveView and nftables use Ecto.Type - @format_error "Range Error: Bad format" + @format_error "bad format" + @cast_error "lower value cannot be higher than upper value" def type, do: :int4range @@ -30,7 +31,7 @@ defmodule FzHttp.Int4Range do end def cast([lower, upper]) when upper >= lower, do: {:ok, "#{lower} - #{upper}"} - def cast([_, _]), do: {:error, message: "Range Error: Lower bound higher than upper bound"} + def cast([_, _]), do: {:error, message: @cast_error} def load(%Postgrex.Range{ lower: lower, diff --git a/apps/fz_http/lib/fz_http/mailer.ex b/apps/fz_http/lib/fz_http/mailer.ex index 6f1ef70df..79cc4c1a9 100644 --- a/apps/fz_http/lib/fz_http/mailer.ex +++ b/apps/fz_http/lib/fz_http/mailer.ex @@ -18,7 +18,7 @@ defmodule FzHttpWeb.Mailer do def default_email do Email.new() - |> Email.from(Application.fetch_env!(:fz_http, FzHttpWeb.Mailer)[:from_email]) + |> Email.from(FzHttp.Config.fetch_env!(:fz_http, FzHttpWeb.Mailer)[:from_email]) end def configs_for(provider) do diff --git a/apps/fz_http/lib/fz_http/mfa/method.ex b/apps/fz_http/lib/fz_http/mfa/method.ex index a88ccc887..7a024932e 100644 --- a/apps/fz_http/lib/fz_http/mfa/method.ex +++ b/apps/fz_http/lib/fz_http/mfa/method.ex @@ -2,13 +2,8 @@ defmodule FzHttp.MFA.Method do @moduledoc """ Multi Factor Authentication methods """ - - use Ecto.Schema + use FzHttp, :schema import Ecto.Changeset - import FzHttp.Validators.Common, only: [trim: 2] - - @primary_key {:id, :binary_id, autogenerate: true} - @whitespace_trimmed_fields :name schema "mfa_methods" do field :name, :string @@ -16,18 +11,19 @@ defmodule FzHttp.MFA.Method do field :credential_id, :string field :last_used_at, :utc_datetime_usec field :payload, FzHttp.Encrypted.Map - field :user_id, :id field :secret, :string, virtual: true field :code, :string, virtual: true - timestamps(type: :utc_datetime_usec) + belongs_to :user, FzHttp.Users.User + + timestamps() end @doc false def changeset(method, attrs) do method |> cast(attrs, [:name, :type, :credential_id, :payload, :last_used_at, :secret, :code]) - |> trim(@whitespace_trimmed_fields) + |> update_change(:name, &String.trim/1) |> cast_payload() |> validate_required([:name, :type, :payload]) |> validate_code() diff --git a/apps/fz_http/lib/fz_http/oidc/connection.ex b/apps/fz_http/lib/fz_http/oidc/connection.ex index 19233845c..5aec6c8a0 100644 --- a/apps/fz_http/lib/fz_http/oidc/connection.ex +++ b/apps/fz_http/lib/fz_http/oidc/connection.ex @@ -2,8 +2,7 @@ defmodule FzHttp.OIDC.Connection do @moduledoc """ OIDC connections """ - - use Ecto.Schema + use FzHttp, :schema import Ecto.Changeset schema "oidc_connections" do @@ -11,9 +10,10 @@ defmodule FzHttp.OIDC.Connection do field :refresh_response, :map field :refresh_token, :string field :refreshed_at, :utc_datetime_usec - field :user_id, :id - timestamps(type: :utc_datetime_usec) + belongs_to :user, FzHttp.Users.User + + timestamps() end @doc false diff --git a/apps/fz_http/lib/fz_http/oidc/refresher.ex b/apps/fz_http/lib/fz_http/oidc/refresher.ex index 2ff6a4175..9180cc31f 100644 --- a/apps/fz_http/lib/fz_http/oidc/refresher.ex +++ b/apps/fz_http/lib/fz_http/oidc/refresher.ex @@ -6,7 +6,7 @@ defmodule FzHttp.OIDC.Refresher do import Ecto.{Changeset, Query} import FzHttpWeb.OIDC.Helpers - alias FzHttp.Configurations, as: Conf + alias FzHttp.{OIDC, OIDC.Connection, Repo, Users} require Logger @@ -33,14 +33,12 @@ defmodule FzHttp.OIDC.Refresher do {:stop, :shutdown, user_id} end - defp do_refresh(user_id, %{provider: provider_key, refresh_token: refresh_token} = conn) do - {:ok, provider} = atomize_provider(provider_key) - - Logger.info("Refreshing user\##{user_id} @ #{provider}...") + defp do_refresh(user_id, %{provider: provider_id, refresh_token: refresh_token} = conn) do + Logger.info("Refreshing user\##{user_id} @ #{provider_id}...") result = openid_connect().fetch_tokens( - provider, + provider_id, %{grant_type: "refresh_token", refresh_token: refresh_token} ) @@ -79,6 +77,6 @@ defmodule FzHttp.OIDC.Refresher do end defp enabled? do - Conf.get!(:disable_vpn_on_oidc_error) + FzHttp.Configurations.get!(:disable_vpn_on_oidc_error) end end diff --git a/apps/fz_http/lib/fz_http/oidc/start_proxy.ex b/apps/fz_http/lib/fz_http/oidc/start_proxy.ex index 2609d9c23..df6e7a8b9 100644 --- a/apps/fz_http/lib/fz_http/oidc/start_proxy.ex +++ b/apps/fz_http/lib/fz_http/oidc/start_proxy.ex @@ -1,11 +1,8 @@ defmodule FzHttp.OIDC.StartProxy do @moduledoc """ This proxy simply gets the relevant config at an appropriate timing - (after `FzHttp.Configurations.Cache` has started) and pass to `OpenIDConnect.Worker` """ - alias FzHttp.Configurations, as: Conf - require Logger def child_spec(arg) do @@ -13,43 +10,44 @@ defmodule FzHttp.OIDC.StartProxy do end def start_link(:test) do - auth_oidc_env = Conf.get!(:openid_connect_providers) - Conf.Cache.put!(:parsed_openid_connect_providers, parse(auth_oidc_env)) :ignore end def start_link(_) do - auth_oidc_env = Conf.get!(:openid_connect_providers) + FzHttp.Configurations.get!(:openid_connect_providers) + |> parse() + |> OpenIDConnect.Worker.start_link() + end - if parsed = auth_oidc_env && parse(auth_oidc_env) do - Conf.Cache.put!(:parsed_openid_connect_providers, parsed) - OpenIDConnect.Worker.start_link(parsed) - else - :ignore + # XXX: Remove when configurations support test fixtures + if Mix.env() == :test do + def restart, do: :ignore + else + def restart do + :ok = Supervisor.terminate_child(FzHttp.Supervisor, __MODULE__) + Supervisor.restart_child(FzHttp.Supervisor, __MODULE__) end end - defp parse(auth_oidc_env) when is_binary(auth_oidc_env) do - auth_oidc_env |> Jason.decode!() |> parse() - end + # Convert the configuration record to something openid_connect expects, + # atom-keyed configs eg. [provider: [client_id: "CLIENT_ID" ...]] + defp parse(nil), do: [] - defp parse(auth_oidc_config) when is_map(auth_oidc_config) do - external_url = Application.fetch_env!(:fz_http, :external_url) + defp parse(auth_oidc_config) when is_list(auth_oidc_config) do + external_url = FzHttp.Config.fetch_env!(:fz_http, :external_url) - # Convert Map to something openid_connect expects, atomic keyed configs - # eg. [provider: [client_id: "CLIENT_ID" ...]] - Enum.map(auth_oidc_config, fn {provider, settings} -> + Enum.map(auth_oidc_config, fn provider -> { - String.to_atom(provider), + provider.id, [ - discovery_document_uri: settings["discovery_document_uri"], - client_id: settings["client_id"], - client_secret: settings["client_secret"], + discovery_document_uri: provider.discovery_document_uri, + client_id: provider.client_id, + client_secret: provider.client_secret, redirect_uri: - settings["redirect_uri"] || "#{external_url}/auth/oidc/#{provider}/callback/", - response_type: settings["response_type"], - scope: settings["scope"], - label: settings["label"] + provider.redirect_uri || "#{external_url}/auth/oidc/#{provider.id}/callback/", + response_type: provider.response_type, + scope: provider.scope, + label: provider.label ] } end) diff --git a/apps/fz_http/lib/fz_http/queries/inet.ex b/apps/fz_http/lib/fz_http/queries/inet.ex deleted file mode 100644 index 84447f833..000000000 --- a/apps/fz_http/lib/fz_http/queries/inet.ex +++ /dev/null @@ -1,80 +0,0 @@ -defmodule FzHttp.Queries.INET do - @moduledoc """ - Raw SQL INET queries - """ - - # XXX: This needs to be an insert to avoid the deadlocks - @next_available_ipv4_query """ - WITH combined AS ( - SELECT $2 AS ipv4 - UNION ALL - SELECT devices.ipv4 FROM devices - ) - SELECT combined.ipv4 + 1 AS ipv4 - FROM combined - WHERE combined.ipv4 + 1 < host(broadcast($1))::INET - AND combined.ipv4 + 1 != $2 - AND combined.ipv4 >= host($1)::INET - AND NOT EXISTS ( - SELECT 1 from combined d2 WHERE d2.ipv4 = combined.ipv4 + 1 - ) - ORDER BY combined.ipv4 - LIMIT 1 - """ - - # XXX: This needs to be an insert to avoid the deadlocks - @next_available_ipv6_query """ - WITH combined AS ( - SELECT $2 AS ipv6 - UNION ALL - SELECT devices.ipv6 FROM devices - ) - SELECT combined.ipv6 + 1 AS ipv6 - FROM combined - WHERE combined.ipv6 + 1 < host(broadcast($1))::INET - AND combined.ipv6 + 1 != $2 - AND combined.ipv6 >= host($1)::INET - AND NOT EXISTS ( - SELECT 1 from combined d2 WHERE d2.ipv6 = combined.ipv6 + 1 - ) - ORDER BY combined.ipv6 - LIMIT 1 - """ - - def next_available(type) do - network = wireguard_network(type) - address = wireguard_address(type) - query = next_available_query(type) - - case FzHttp.Repo.query(query, [network, address]) do - {:ok, %Postgrex.Result{rows: [[%Postgrex.INET{} = inet]]}} -> - inet - - {:ok, %Postgrex.Result{rows: []}} -> - nil - - {:error, error} -> - raise(error) - end - end - - defp wireguard_network(type) do - network_key = "wireguard_#{type}_network" |> String.to_existing_atom() - {:ok, network} = EctoNetwork.INET.cast(Application.fetch_env!(:fz_http, network_key)) - network - end - - defp wireguard_address(type) do - address_key = "wireguard_#{type}_address" |> String.to_existing_atom() - {:ok, address} = EctoNetwork.INET.cast(Application.fetch_env!(:fz_http, address_key)) - address - end - - defp next_available_query(:ipv4) do - @next_available_ipv4_query - end - - defp next_available_query(:ipv6) do - @next_available_ipv6_query - end -end diff --git a/apps/fz_http/lib/fz_http/release.ex b/apps/fz_http/lib/fz_http/release.ex index ed43f38f0..d5d7133d0 100644 --- a/apps/fz_http/lib/fz_http/release.ex +++ b/apps/fz_http/lib/fz_http/release.ex @@ -21,16 +21,22 @@ defmodule FzHttp.Release do def create_admin_user do load_app() - if Repo.exists?(from u in User, where: u.email == ^email()) do - change_password(email(), default_password()) - reset_role(email(), :admin) - else - Users.create_admin_user( - email: email(), - password: default_password(), - password_confirmation: default_password() - ) - end + reply = + if Repo.exists?(from u in User, where: u.email == ^email()) do + change_password(email(), default_password()) + reset_role(email(), :admin) + else + Users.create_admin_user( + email: email(), + password: default_password(), + password_confirmation: default_password() + ) + end + + # Notify the user + IO.puts("Password reset! Check $HOME/.firezone/.env for sign in credentials.") + + reply end def change_password(email, password) do @@ -50,11 +56,11 @@ defmodule FzHttp.Release do end def repos do - Application.fetch_env!(@app, :ecto_repos) + FzHttp.Config.fetch_env!(@app, :ecto_repos) end defp email do - Application.fetch_env!(@app, :admin_email) + FzHttp.Config.fetch_env!(@app, :admin_email) end defp load_app do @@ -66,6 +72,6 @@ defmodule FzHttp.Release do end defp default_password do - Application.fetch_env!(@app, :default_admin_password) + FzHttp.Config.fetch_env!(@app, :default_admin_password) end end diff --git a/apps/fz_http/lib/fz_http/rules.ex b/apps/fz_http/lib/fz_http/rules.ex index 45c567c20..17128420a 100644 --- a/apps/fz_http/lib/fz_http/rules.ex +++ b/apps/fz_http/lib/fz_http/rules.ex @@ -7,7 +7,7 @@ defmodule FzHttp.Rules do import Ecto.Changeset alias FzHttp.{Repo, Rules.Rule, Rules.RuleSetting, Telemetry} - def port_rules_supported?, do: Application.fetch_env!(:fz_wall, :port_based_rules_supported) + def port_rules_supported?, do: FzHttp.Config.fetch_env!(:fz_wall, :port_based_rules_supported) defp scope(port_based_rules) when port_based_rules == true do Rule @@ -75,6 +75,12 @@ defmodule FzHttp.Rules do result end + def update_rule(%Rule{} = rule, attrs \\ %{}) do + rule + |> Rule.changeset(attrs) + |> Repo.update() + end + def delete_rule(%Rule{} = rule) do Telemetry.delete_rule() Repo.delete(rule) diff --git a/apps/fz_http/lib/fz_http/rules/rule.ex b/apps/fz_http/lib/fz_http/rules/rule.ex index 301e28ebb..dbd7fcff4 100644 --- a/apps/fz_http/lib/fz_http/rules/rule.ex +++ b/apps/fz_http/lib/fz_http/rules/rule.ex @@ -2,23 +2,22 @@ defmodule FzHttp.Rules.Rule do @moduledoc """ Not really sure what to write here. I'll update this later. """ - - use Ecto.Schema + use FzHttp, :schema import Ecto.Changeset - @exclusion_msg "Destination overlaps with an existing rule" - @port_range_msg "Port is not within valid range" - @port_type_msg "Please specify a port-range for the given port type" + @exclusion_msg "destination overlaps with an existing rule" + @port_range_msg "port is not within valid range" + @port_type_msg "port_type must be specified with port_range" schema "rules" do - field :uuid, Ecto.UUID, autogenerate: true field :destination, EctoNetwork.INET, read_after_writes: true field :action, Ecto.Enum, values: [:drop, :accept], default: :drop field :port_type, Ecto.Enum, values: [:tcp, :udp], default: nil field :port_range, FzHttp.Int4Range, default: nil + belongs_to :user, FzHttp.Users.User - timestamps(type: :utc_datetime_usec) + timestamps() end def changeset(rule, attrs) do diff --git a/apps/fz_http/lib/fz_http/rules/rule_setting.ex b/apps/fz_http/lib/fz_http/rules/rule_setting.ex index 253dd7401..dc26044d8 100644 --- a/apps/fz_http/lib/fz_http/rules/rule_setting.ex +++ b/apps/fz_http/lib/fz_http/rules/rule_setting.ex @@ -2,8 +2,7 @@ defmodule FzHttp.Rules.RuleSetting do @moduledoc """ Rule setting parsed from either a Rule struct or map. """ - use Ecto.Schema - + use FzHttp, :schema import Ecto.Changeset import FzHttp.Devices, only: [decode: 1] @@ -11,7 +10,7 @@ defmodule FzHttp.Rules.RuleSetting do embedded_schema do field :action, Ecto.Enum, values: [:drop, :accept] field :destination, :string - field :user_id, :integer + field :user_id, Ecto.UUID field :port_type, Ecto.Enum, values: [:tcp, :udp], default: nil field :port_range, FzHttp.Int4Range, default: nil end diff --git a/apps/fz_http/lib/fz_http/saml/start_proxy.ex b/apps/fz_http/lib/fz_http/saml/start_proxy.ex index 2cd7ecfe0..abf93ed41 100644 --- a/apps/fz_http/lib/fz_http/saml/start_proxy.ex +++ b/apps/fz_http/lib/fz_http/saml/start_proxy.ex @@ -1,30 +1,32 @@ defmodule FzHttp.SAML.StartProxy do @moduledoc """ This proxy starts Samly.Provider with proper configs - (after `FzHttp.Conf.Cache` has started) """ - alias FzHttp.Configurations, as: Conf - def child_spec(arg) do %{id: __MODULE__, start: {__MODULE__, :start_link, [arg]}} end + def start_link(:test) do + start_link(nil) + end + def start_link(_) do + providers = FzHttp.Configurations.get!(:saml_identity_providers) samly = Samly.Provider.start_link() - Application.fetch_env!(:samly, Samly.Provider) + FzHttp.Config.fetch_env!(:samly, Samly.Provider) |> set_service_provider() - |> set_identity_providers() + |> set_identity_providers(providers) |> refresh() samly end def set_service_provider(samly_configs) do - entity_id = Application.fetch_env!(:fz_http, :saml_entity_id) - keyfile = Application.fetch_env!(:fz_http, :saml_keyfile_path) - certfile = Application.fetch_env!(:fz_http, :saml_certfile_path) + entity_id = FzHttp.Config.fetch_env!(:fz_http, :saml_entity_id) + keyfile = FzHttp.Config.fetch_env!(:fz_http, :saml_keyfile_path) + certfile = FzHttp.Config.fetch_env!(:fz_http, :saml_certfile_path) # Only one service provider definition: us. Keyword.put(samly_configs, :service_providers, [ @@ -37,21 +39,23 @@ defmodule FzHttp.SAML.StartProxy do ]) end - def set_identity_providers(samly_configs, providers \\ Conf.get!(:saml_identity_providers)) do - external_url = Application.fetch_env!(:fz_http, :external_url) + def set_identity_providers(samly_configs, providers) do + external_url = FzHttp.Config.fetch_env!(:fz_http, :external_url) identity_providers = providers - |> Enum.map(fn {id, setting} -> + |> Enum.map(fn provider -> + # XXX We should not set default values here, instead they should be part + # of the changeset and always valid in database %{ - id: id, + id: provider.id, sp_id: "firezone", - metadata: Map.get(setting, "metadata"), - base_url: Map.get(setting, "base_url", Path.join(external_url, "/auth/saml")), - sign_requests: Map.get(setting, "sign_requests", true), - sign_metadata: Map.get(setting, "sign_metadata", true), - signed_assertion_in_resp: Map.get(setting, "signed_assertion_in_resp", true), - signed_envelopes_in_resp: Map.get(setting, "signed_envelopes_in_resp", true) + metadata: provider.metadata, + base_url: provider.base_url || Path.join(external_url, "/auth/saml"), + sign_requests: provider.sign_requests || true, + sign_metadata: provider.sign_metadata || true, + signed_assertion_in_resp: provider.signed_assertion_in_resp || true, + signed_envelopes_in_resp: provider.signed_envelopes_in_resp || true } end) @@ -62,4 +66,17 @@ defmodule FzHttp.SAML.StartProxy do Application.put_env(:samly, Samly.Provider, samly_configs) Samly.Provider.refresh_providers() end + + # XXX: This should be removed when the configurations singleton record is removed. + # + # Needed to prevent the test suite from recursively restarting this module as + # it put!()'s mock data + if Mix.env() == :test do + def restart, do: :ignore + else + def restart do + :ok = Supervisor.terminate_child(FzHttp.Supervisor, __MODULE__) + Supervisor.restart_child(FzHttp.Supervisor, __MODULE__) + end + end end diff --git a/apps/fz_http/lib/fz_http/sites.ex b/apps/fz_http/lib/fz_http/sites.ex deleted file mode 100644 index f61c5964b..000000000 --- a/apps/fz_http/lib/fz_http/sites.ex +++ /dev/null @@ -1,46 +0,0 @@ -defmodule FzHttp.Sites do - @moduledoc """ - The Sites context. - """ - - import Ecto.Query, warn: false - alias FzHttp.{Repo, Sites.Site} - - def new_site(attrs \\ %{}) do - Site.changeset(%Site{}, attrs) - end - - 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 -end diff --git a/apps/fz_http/lib/fz_http/sites/site.ex b/apps/fz_http/lib/fz_http/sites/site.ex deleted file mode 100644 index 5cfbf9d6c..000000000 --- a/apps/fz_http/lib/fz_http/sites/site.ex +++ /dev/null @@ -1,74 +0,0 @@ -defmodule FzHttp.Sites.Site do - @moduledoc """ - Represents a VPN / Firewall site and its config. - """ - - use Ecto.Schema - import Ecto.Changeset - - import FzHttp.Validators.Common, - only: [ - trim: 2, - validate_list_of_ips_or_cidrs: 2, - validate_no_duplicates: 2 - ] - - @primary_key {:id, :binary_id, autogenerate: true} - @foreign_key_type :binary_id - - # Postgres max int size is 4 bytes - @max_pg_integer 2_147_483_647 - - @minute 60 - @hour 60 * @minute - @min_mtu 576 - @max_mtu 1500 - @min_persistent_keepalive 0 - @max_persistent_keepalive 1 * @hour - @min_vpn_session_duration 0 - @max_vpn_session_duration @max_pg_integer - @whitespace_trimmed_fields ~w(name dns allowed_ips endpoint)a - - 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 - ]) - |> trim(@whitespace_trimmed_fields) - |> validate_required(:name) - |> validate_no_duplicates(:dns) - |> validate_list_of_ips_or_cidrs(:allowed_ips) - |> validate_no_duplicates(:allowed_ips) - |> 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/telemetry.ex b/apps/fz_http/lib/fz_http/telemetry.ex index c6b536fa3..ba685ce45 100644 --- a/apps/fz_http/lib/fz_http/telemetry.ex +++ b/apps/fz_http/lib/fz_http/telemetry.ex @@ -5,9 +5,25 @@ defmodule FzHttp.Telemetry do require Logger - alias FzHttp.Configurations, as: Conf alias FzHttp.{Devices, MFA, Users} + def create_api_token do + telemetry_module().capture( + "add_api_token", + common_fields() + ) + end + + def delete_api_token(api_token) do + telemetry_module().capture( + "delete_api_token", + common_fields() ++ + [ + api_token_created_at: api_token.inserted_at + ] + ) + end + def add_device do telemetry_module().capture( "add_device", @@ -75,9 +91,6 @@ defmodule FzHttp.Telemetry do telemetry_module().capture("ping", ping_data()) end - defp count(subject) when is_map(subject), do: count(Map.keys(subject)) - defp count(subject) when is_list(subject), do: length(subject) - # How far back to count handshakes as an active device @active_device_window 86_400 def ping_data do @@ -91,15 +104,18 @@ defmodule FzHttp.Telemetry do max_devices_for_users: Devices.max_count_by_user_id(), users_with_mfa: MFA.count_distinct_by_user_id(), users_with_mfa_totp: MFA.count_distinct_totp_by_user_id(), - openid_providers: count(Conf.get!(:parsed_openid_connect_providers)), - saml_providers: count(Conf.get!(:saml_identity_providers)), - unprivileged_device_management: Conf.get!(:allow_unprivileged_device_management), - unprivileged_device_configuration: Conf.get!(:allow_unprivileged_device_configuration), - local_authentication: Conf.get!(:local_auth_enabled), - disable_vpn_on_oidc_error: Conf.get!(:disable_vpn_on_oidc_error), + openid_providers: length(FzHttp.Configurations.get!(:openid_connect_providers)), + saml_providers: length(FzHttp.Configurations.get!(:saml_identity_providers)), + unprivileged_device_management: + FzHttp.Configurations.get!(:allow_unprivileged_device_management), + unprivileged_device_configuration: + FzHttp.Configurations.get!(:allow_unprivileged_device_configuration), + local_authentication: FzHttp.Configurations.get!(:local_auth_enabled), + disable_vpn_on_oidc_error: FzHttp.Configurations.get!(:disable_vpn_on_oidc_error), outbound_email: outbound_email?(), - external_database: external_database?(Map.new(conf(FzHttp.Repo))), - logo_type: Conf.logo_type(Conf.get!(:logo)) + external_database: + external_database?(Map.new(FzHttp.Config.fetch_env!(:fz_http, FzHttp.Repo))), + logo_type: FzHttp.Configurations.logo_type(FzHttp.Configurations.get!(:logo)) ] end @@ -109,7 +125,7 @@ defmodule FzHttp.Telemetry do defp common_fields do [ - distinct_id: conf(:telemetry_id), + distinct_id: FzHttp.Config.fetch_env!(:fz_http, :telemetry_id), fqdn: fqdn(), version: version(), kernel_version: "#{os_type()} #{os_version()}" @@ -117,12 +133,12 @@ defmodule FzHttp.Telemetry do end defp telemetry_module do - Application.fetch_env!(:fz_http, :telemetry_module) + FzHttp.Config.fetch_env!(:fz_http, :telemetry_module) end defp fqdn do :fz_http - |> Application.fetch_env!(FzHttpWeb.Endpoint) + |> FzHttp.Config.fetch_env!(FzHttpWeb.Endpoint) |> Keyword.get(:url) |> Keyword.get(:host) end @@ -146,7 +162,7 @@ defmodule FzHttp.Telemetry do end defp outbound_email? do - from_email = conf(FzHttpWeb.Mailer)[:from_email] + from_email = FzHttp.Config.fetch_env!(:fz_http, FzHttpWeb.Mailer)[:from_email] !is_nil(from_email) end @@ -170,8 +186,4 @@ defmodule FzHttp.Telemetry do "0.0.0" end end - - defp conf(key) do - Application.fetch_env!(:fz_http, key) - end end diff --git a/apps/fz_http/lib/fz_http/users.ex b/apps/fz_http/lib/fz_http/users.ex index 01dba9080..899d15d35 100644 --- a/apps/fz_http/lib/fz_http/users.ex +++ b/apps/fz_http/lib/fz_http/users.ex @@ -8,7 +8,6 @@ defmodule FzHttp.Users do alias FzHttp.Devices.Device alias FzHttp.Repo - alias FzHttp.Sites.Site alias FzHttp.Telemetry alias FzHttp.Users.User alias FzHttpWeb.Mailer @@ -57,6 +56,10 @@ defmodule FzHttp.Users do Repo.get_by(User, email: email) end + 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 @@ -215,7 +218,7 @@ defmodule FzHttp.Users do end def vpn_session_expired?(user, duration) do - max = Site.max_vpn_session_duration() + max = FzHttp.Configurations.Configuration.max_vpn_session_duration() case duration do 0 -> diff --git a/apps/fz_http/lib/fz_http/users/user.ex b/apps/fz_http/lib/fz_http/users/user.ex index 15c93ab1e..b64b76bb4 100644 --- a/apps/fz_http/lib/fz_http/users/user.ex +++ b/apps/fz_http/lib/fz_http/users/user.ex @@ -2,22 +2,21 @@ defmodule FzHttp.Users.User do @moduledoc """ Represents a User. """ + use FzHttp, :schema + import Ecto.Changeset + import FzHttp.Users.PasswordHelpers + + alias FzHttp.{ + ApiTokens.ApiToken, + Devices.Device, + OIDC.Connection, + Validators.Common + } @min_password_length 12 @max_password_length 64 - use Ecto.Schema - import Ecto.Changeset - import FzHttp.Users.PasswordHelpers - import FzHttp.Validators.Common, only: [trim: 2] - - alias FzHttp.{Devices.Device, OIDC.Connection} - - # Fields for which to trim whitespace after cast, before validation - @whitespace_trimmed_fields :email - schema "users" do - field :uuid, Ecto.UUID, autogenerate: true field :role, Ecto.Enum, values: [:unprivileged, :admin], default: :unprivileged field :email, :string field :last_signed_in_at, :utc_datetime_usec @@ -33,10 +32,11 @@ defmodule FzHttp.Users.User do field :password_confirmation, :string, virtual: true field :current_password, :string, virtual: true - has_many :devices, Device, on_delete: :delete_all - has_many :oidc_connections, Connection, on_delete: :delete_all + has_many :devices, Device + has_many :oidc_connections, Connection + has_many :api_tokens, ApiToken - timestamps(type: :utc_datetime_usec) + timestamps() end def create_changeset(user, attrs \\ %{}) do @@ -47,7 +47,7 @@ defmodule FzHttp.Users.User do :password, :password_confirmation ]) - |> trim(@whitespace_trimmed_fields) + |> update_change(:email, &String.trim/1) |> validate_required([:email]) |> validate_password_equality() |> validate_length(:password, min: @min_password_length, max: @max_password_length) @@ -87,7 +87,7 @@ defmodule FzHttp.Users.User do def update_email(user, attrs) do user |> cast(attrs, [:email]) - |> trim(:email) + |> Common.trim_change(:email) |> validate_required([:email]) |> validate_format(:email, ~r/@/) end diff --git a/apps/fz_http/lib/fz_http/validators/common.ex b/apps/fz_http/lib/fz_http/validators/common.ex index 1b8d3340a..8a9a618bd 100644 --- a/apps/fz_http/lib/fz_http/validators/common.ex +++ b/apps/fz_http/lib/fz_http/validators/common.ex @@ -11,20 +11,6 @@ defmodule FzHttp.Validators.Common do valid_cidr?: 1 ] - defp do_trim(nil), do: nil - - defp do_trim(str) when is_binary(str), do: String.trim(str) - - def trim(changeset, field) when is_atom(field) do - trim(changeset, [field]) - end - - def trim(changeset, fields) when is_list(fields) do - Enum.reduce(fields, changeset, fn field, cs -> - update_change(cs, field, &do_trim/1) - end) - end - def validate_uri(changeset, fields) when is_list(fields) do Enum.reduce(fields, changeset, fn field, accumulated_changeset -> validate_uri(accumulated_changeset, field) @@ -80,6 +66,15 @@ defmodule FzHttp.Validators.Common do end) end + def validate_base64(changeset, field) do + validate_change(changeset, field, fn _cur, value -> + case Base.decode64(value) do + :error -> [{field, "must be a base64-encoded string"}] + {:ok, _decoded} -> [] + end + end) + end + def validate_omitted(changeset, fields) when is_list(fields) do Enum.reduce(fields, changeset, fn field, accumulated_changeset -> validate_omitted(accumulated_changeset, field) @@ -96,22 +91,6 @@ defmodule FzHttp.Validators.Common do end) end - def validate_no_mask(%Ecto.Changeset{changes: %{ipv4: %{netmask: nm}}} = changeset, :ipv4) - when nm in [nil, 32], - do: changeset - - def validate_no_mask(%Ecto.Changeset{changes: %{ipv6: %{netmask: nm}}} = changeset, :ipv6) - when nm in [nil, 128], - do: changeset - - def validate_no_mask(%Ecto.Changeset{changes: %{ipv4: %{netmask: _}}} = changeset, :ipv4), - do: netmask_error(changeset, :ipv4) - - def validate_no_mask(%Ecto.Changeset{changes: %{ipv6: %{netmask: _}}} = changeset, :ipv6), - do: netmask_error(changeset, :ipv6) - - def validate_no_mask(changeset, _), do: changeset - defp split_comma_list(text) do text |> String.split(",") @@ -126,11 +105,25 @@ defmodule FzHttp.Validators.Common do end end - defp netmask_error(changeset, ip_type) do - add_error( - changeset, - ip_type, - "Only IPs without netmask are supported." - ) + @doc """ + Puts the change if field is not changed or it's value is set to `nil`. + """ + def put_default_value(changeset, _field, nil) do + changeset + end + + def put_default_value(changeset, field, value) do + case fetch_field(changeset, field) do + {:data, nil} -> put_change(changeset, field, maybe_apply(value)) + :error -> put_change(changeset, field, maybe_apply(value)) + _ -> changeset + end + end + + defp maybe_apply(fun) when is_function(fun, 0), do: fun.() + defp maybe_apply(value), do: value + + def trim_change(changeset, field) do + update_change(changeset, field, &if(!is_nil(&1), do: String.trim(&1))) end end diff --git a/apps/fz_http/lib/fz_http_web.ex b/apps/fz_http/lib/fz_http_web.ex index f031820ae..adfa3123b 100644 --- a/apps/fz_http/lib/fz_http_web.ex +++ b/apps/fz_http/lib/fz_http_web.ex @@ -25,7 +25,6 @@ defmodule FzHttpWeb do import FzHttpWeb.Gettext import Phoenix.LiveView.Controller import FzHttpWeb.ControllerHelpers - alias FzHttp.Configurations, as: Conf unquote(verified_routes()) end @@ -52,10 +51,6 @@ defmodule FzHttpWeb do import FzHttpWeb.LiveHelpers unquote(verified_routes()) - - def render_common(template, assigns \\ []) do - render(FzHttpWeb.CommonView, template, assigns) - end end end @@ -63,7 +58,7 @@ defmodule FzHttpWeb do quote do use Phoenix.LiveView, layout: {FzHttpWeb.LayoutView, :live} import FzHttpWeb.LiveHelpers - alias FzHttp.Configurations, as: Conf + alias Phoenix.LiveView.JS unquote(view_helpers()) @@ -74,7 +69,7 @@ defmodule FzHttpWeb do quote do use Phoenix.LiveView, layout: nil import FzHttpWeb.LiveHelpers - alias FzHttp.Configurations, as: Conf + alias Phoenix.LiveView.JS unquote(view_helpers()) @@ -87,7 +82,6 @@ defmodule FzHttpWeb do use Phoenix.LiveComponent use Phoenix.Component, global_prefixes: ~w(x-) import FzHttpWeb.LiveHelpers - alias FzHttp.Configurations, as: Conf unquote(view_helpers()) end diff --git a/apps/fz_http/lib/fz_http_web/authentication.ex b/apps/fz_http/lib/fz_http_web/auth/html/authentication.ex similarity index 60% rename from apps/fz_http/lib/fz_http_web/authentication.ex rename to apps/fz_http/lib/fz_http_web/auth/html/authentication.ex index d877f9ce1..7111b5f8e 100644 --- a/apps/fz_http/lib/fz_http_web/authentication.ex +++ b/apps/fz_http/lib/fz_http_web/auth/html/authentication.ex @@ -1,23 +1,26 @@ -defmodule FzHttpWeb.Authentication do +defmodule FzHttpWeb.Auth.HTML.Authentication do @moduledoc """ - Authentication helpers. + HTML Authentication implementation module for Guardian. """ use Guardian, otp_app: :fz_http use FzHttpWeb, :controller - alias FzHttp.Configurations, as: Conf alias FzHttp.Telemetry alias FzHttp.Users alias FzHttp.Users.User import FzHttpWeb.OIDC.Helpers + require Logger + @guardian_token_name "guardian_default_token" + @impl Guardian def subject_for_token(resource, _claims) do - {:ok, to_string(resource.id)} + {:ok, resource.id} end + @impl Guardian def resource_from_claims(%{"sub" => id}) do case Users.get_user(id) do nil -> {:error, :resource_not_found} @@ -56,34 +59,32 @@ defmodule FzHttpWeb.Authentication do def sign_in(conn, user, auth) do Telemetry.login() Users.update_last_signed_in(user, auth) - %{provider: provider} = auth + %{provider: provider_id} = auth conn = - with :identity <- provider, + with :identity <- provider_id, true <- FzHttp.MFA.exists?(user) do Plug.Conn.put_session(conn, "mfa_required_at", DateTime.utc_now()) else _ -> conn end - |> Plug.Conn.put_session("login_method", provider) + # XXX: OIDC and SAML provider IDs can be strings, so normalize to string here + |> Plug.Conn.put_session("login_method", to_string(provider_id)) __MODULE__.Plug.sign_in(conn, user) end def sign_out(conn) do - with {:ok, provider_key} <- parse_provider(Plug.Conn.get_session(conn, "login_method")), - {:ok, provider} <- atomize_provider(provider_key), - {:ok, client_id} <- - parse_client_id(Conf.get!(:parsed_openid_connect_providers)[provider]), - {:ok, token} <- parse_token(Plug.Conn.get_session(conn, "id_token")), - {:ok, end_session_uri} <- - parse_end_session_uri( - openid_connect().end_session_uri(provider, %{ - client_id: client_id, - id_token_hint: token, - post_logout_redirect_uri: url(~p"/") - }) - ) do + with provider_id when not is_nil(provider_id) <- Plug.Conn.get_session(conn, "login_method"), + provider when not is_nil(provider) <- + FzHttp.Configurations.get_provider_by_id(:openid_connect_providers, provider_id), + token when not is_nil(token) <- Plug.Conn.get_session(conn, "id_token"), + end_session_uri when not is_nil(end_session_uri) <- + openid_connect().end_session_uri(provider_id, %{ + client_id: provider.client_id, + id_token_hint: token, + post_logout_redirect_uri: url(~p"/") + }) do conn |> __MODULE__.Plug.sign_out() |> Plug.Conn.configure_session(drop: true) @@ -97,19 +98,6 @@ defmodule FzHttpWeb.Authentication do end end - defp parse_provider(nil), do: {:error, "provider not present"} - defp parse_provider(p) when is_binary(p), do: {:ok, p} - defp parse_provider(p) when is_atom(p), do: {:ok, "#{p}"} - - defp parse_client_id(nil), do: {:error, "client_id missing"} - defp parse_client_id(c), do: {:ok, c[:client_id]} - - defp parse_token(nil), do: {:error, "token missing"} - defp parse_token(t), do: {:ok, t} - - defp parse_end_session_uri(nil), do: {:error, "end_session_uri missing"} - defp parse_end_session_uri(e), do: {:ok, e} - def get_current_user(%Plug.Conn{} = conn) do __MODULE__.Plug.current_resource(conn) end diff --git a/apps/fz_http/lib/fz_http_web/authentication/error_handler.ex b/apps/fz_http/lib/fz_http_web/auth/html/error_handler.ex similarity index 55% rename from apps/fz_http/lib/fz_http_web/authentication/error_handler.ex rename to apps/fz_http/lib/fz_http_web/auth/html/error_handler.ex index f14f06d30..eebfa9992 100644 --- a/apps/fz_http/lib/fz_http_web/authentication/error_handler.ex +++ b/apps/fz_http/lib/fz_http_web/auth/html/error_handler.ex @@ -1,11 +1,11 @@ -defmodule FzHttpWeb.Authentication.ErrorHandler do +defmodule FzHttpWeb.Auth.HTML.ErrorHandler do @moduledoc """ - Error Handler module implementation for Guardian. + HTML Error Handler module implementation for Guardian. """ use FzHttpWeb, :controller - alias FzHttpWeb.Authentication - import FzHttpWeb.ControllerHelpers, only: [root_path_for_role: 1] + alias FzHttpWeb.Auth.HTML.Authentication + import FzHttpWeb.ControllerHelpers, only: [root_path_for_user: 1] require Logger @behaviour Guardian.Plug.ErrorHandler @@ -15,7 +15,7 @@ defmodule FzHttpWeb.Authentication.ErrorHandler do user = Authentication.get_current_user(conn) conn - |> redirect(to: root_path_for_role(user.role)) + |> redirect(to: root_path_for_user(user)) end @impl Guardian.Plug.ErrorHandler @@ -25,13 +25,8 @@ defmodule FzHttpWeb.Authentication.ErrorHandler do end @impl Guardian.Plug.ErrorHandler - def auth_error(conn, {type, reason}, opts) do - Logger.warn(""" - ErrorHandler.auth_error: Could not validate user. - Type: #{type} - Reason: #{reason} - Opts: #{opts} - """) + def auth_error(conn, {type, reason}, _opts) do + Logger.info("Web auth error. Type: #{type}. Reason: #{reason}.") conn |> put_resp_content_type("text/plain") diff --git a/apps/fz_http/lib/fz_http_web/authentication/pipeline.ex b/apps/fz_http/lib/fz_http_web/auth/html/pipeline.ex similarity index 56% rename from apps/fz_http/lib/fz_http_web/authentication/pipeline.ex rename to apps/fz_http/lib/fz_http_web/auth/html/pipeline.ex index 8dfa41485..1704d5324 100644 --- a/apps/fz_http/lib/fz_http_web/authentication/pipeline.ex +++ b/apps/fz_http/lib/fz_http_web/auth/html/pipeline.ex @@ -1,12 +1,12 @@ -defmodule FzHttpWeb.Authentication.Pipeline do +defmodule FzHttpWeb.Auth.HTML.Pipeline do @moduledoc """ - Plug implementation module for Guardian. + HTML Plug implementation module for Guardian. """ use Guardian.Plug.Pipeline, otp_app: :fz_http, - error_handler: FzHttpWeb.Authentication.ErrorHandler, - module: FzHttpWeb.Authentication + error_handler: FzHttpWeb.Auth.HTML.ErrorHandler, + module: FzHttpWeb.Auth.HTML.Authentication @claims %{"typ" => "access"} diff --git a/apps/fz_http/lib/fz_http_web/auth/json/authentication.ex b/apps/fz_http/lib/fz_http_web/auth/json/authentication.ex new file mode 100644 index 000000000..2983ddd97 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/auth/json/authentication.ex @@ -0,0 +1,38 @@ +defmodule FzHttpWeb.Auth.JSON.Authentication do + @moduledoc """ + API Authentication implementation module for Guardian. + """ + use Guardian, otp_app: :fz_http + + alias FzHttp.{ + ApiTokens.ApiToken, + ApiTokens, + Users.User, + Users + } + + @impl Guardian + def subject_for_token(user, _claims) do + {:ok, user.id} + end + + @impl Guardian + def resource_from_claims(%{"api" => api_token_id}) do + with %ApiTokens.ApiToken{} = api_token <- ApiTokens.get_unexpired_api_token(api_token_id), + %Users.User{} = user <- Users.get_user(api_token.user_id) do + {:ok, user} + else + _ -> + {:error, :resource_not_found} + end + end + + def fz_encode_and_sign(%ApiToken{} = api_token, %User{} = user) do + claims = %{ + "api" => api_token.id, + "exp" => DateTime.to_unix(api_token.expires_at) + } + + Guardian.encode_and_sign(__MODULE__, user, claims) + end +end diff --git a/apps/fz_http/lib/fz_http_web/auth/json/error_handler.ex b/apps/fz_http/lib/fz_http_web/auth/json/error_handler.ex new file mode 100644 index 000000000..40f24cb3e --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/auth/json/error_handler.ex @@ -0,0 +1,18 @@ +defmodule FzHttpWeb.Auth.JSON.ErrorHandler do + @moduledoc """ + API Error Handler module implementation for Guardian. + """ + use FzHttpWeb, :controller + require Logger + + @behaviour Guardian.Plug.ErrorHandler + + @impl Guardian.Plug.ErrorHandler + def auth_error(conn, {type, reason}, _opts) do + Logger.info("API auth error. Type: #{type}. Reason: #{reason}.") + + conn + |> put_resp_content_type("application/json") + |> send_resp(401, to_string(type)) + end +end diff --git a/apps/fz_http/lib/fz_http_web/auth/json/pipeline.ex b/apps/fz_http/lib/fz_http_web/auth/json/pipeline.ex new file mode 100644 index 000000000..16d858a38 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/auth/json/pipeline.ex @@ -0,0 +1,17 @@ +defmodule FzHttpWeb.Auth.JSON.Pipeline do + @moduledoc """ + API Plug implementation module for Guardian. + """ + + use Guardian.Plug.Pipeline, + otp_app: :fz_http, + error_handler: FzHttpWeb.Auth.JSON.ErrorHandler, + module: FzHttpWeb.Auth.JSON.Authentication + + # 90 days + @max_age 60 * 60 * 24 * 90 + + plug Guardian.Plug.VerifyHeader, max_age: @max_age + plug Guardian.Plug.EnsureAuthenticated + plug Guardian.Plug.LoadResource +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 970da7074..b44affeda 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 @@ -52,7 +52,7 @@ defmodule FzHttpWeb.NotificationChannel do end defp presence_list(socket) do - ids_to_show = [Integer.to_string(socket.assigns.current_user.id)] + ids_to_show = [socket.assigns.current_user.id] Presence.list(socket) |> Map.take(ids_to_show) 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 0f813046b..4583ab967 100644 --- a/apps/fz_http/lib/fz_http_web/controller_helpers.ex +++ b/apps/fz_http/lib/fz_http_web/controller_helpers.ex @@ -4,11 +4,13 @@ defmodule FzHttpWeb.ControllerHelpers do """ use FzHttpWeb, :helper - def root_path_for_role(:admin) do + alias FzHttp.Users.User + + def root_path_for_user(%User{role: :admin}) do ~p"/users" end - def root_path_for_role(:unprivileged) do + def root_path_for_user(%User{role: :unprivileged}) do ~p"/user_devices" 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 index b32364a13..8ba003a67 100644 --- a/apps/fz_http/lib/fz_http_web/controllers/auth_controller.ex +++ b/apps/fz_http/lib/fz_http_web/controllers/auth_controller.ex @@ -8,7 +8,7 @@ defmodule FzHttpWeb.AuthController do @local_auth_providers [:identity, :magic_link] alias FzHttp.Users - alias FzHttpWeb.Authentication + alias FzHttpWeb.Auth.HTML.Authentication alias FzHttpWeb.OAuth.PKCE alias FzHttpWeb.OIDC.State alias FzHttpWeb.UserFromAuth @@ -21,8 +21,6 @@ defmodule FzHttpWeb.AuthController do plug Ueberauth def request(conn, _params) do - # XXX: Helpers.callback_url/1 generates the wrong URL behind nginx. - # This is a bug in Ueberauth. auth_path is used instead. path = ~p"/auth/identity/callback" conn @@ -30,9 +28,7 @@ defmodule FzHttpWeb.AuthController do end def callback(%{assigns: %{ueberauth_failure: %{errors: errors}}} = conn, _params) do - msg = - errors - |> Enum.map_join(". ", fn error -> error.message end) + msg = Enum.map_join(errors, ". ", fn error -> error.message end) conn |> put_flash(:error, msg) @@ -61,24 +57,24 @@ defmodule FzHttpWeb.AuthController do end end - def callback(conn, %{"provider" => provider_key, "state" => state} = params) do + def callback(conn, %{"provider" => provider_id, "state" => state} = params) + when is_binary(provider_id) do token_params = Map.merge(params, PKCE.token_params(conn)) - with {:ok, provider} <- atomize_provider(provider_key), - :ok <- State.verify_state(conn, state), - {:ok, tokens} <- openid_connect().fetch_tokens(provider, token_params), - {:ok, claims} <- openid_connect().verify(provider, tokens["id_token"]) do - case UserFromAuth.find_or_create(provider_key, claims) do + with :ok <- State.verify_state(conn, state), + {:ok, tokens} <- openid_connect().fetch_tokens(provider_id, token_params), + {:ok, claims} <- openid_connect().verify(provider_id, tokens["id_token"]) do + case UserFromAuth.find_or_create(provider_id, claims) do {:ok, user} -> # only first-time connect will include refresh token # XXX: Remove this when SCIM 2.0 is implemented with %{"refresh_token" => refresh_token} <- tokens do - FzHttp.OIDC.create_connection(user.id, provider_key, refresh_token) + FzHttp.OIDC.create_connection(user.id, provider_id, refresh_token) end conn |> put_session("id_token", tokens["id_token"]) - |> maybe_sign_in(user, %{provider: provider}) + |> maybe_sign_in(user, %{provider: provider_id}) {:error, reason} -> conn @@ -104,9 +100,16 @@ defmodule FzHttpWeb.AuthController do end end - def delete(conn, _params) do + # This can be called if the user attempts to visit one of the callback redirect URLs + # directly. + def callback(conn, params) do conn - |> Authentication.sign_out() + |> put_flash(:error, inspect(params) <> inspect(conn.assigns)) + |> redirect(to: ~p"/") + end + + def delete(conn, _params) do + Authentication.sign_out(conn) end def reset_password(conn, _params) do @@ -139,7 +142,7 @@ defmodule FzHttpWeb.AuthController do end end - def redirect_oidc_auth_uri(conn, %{"provider" => provider_key}) do + def redirect_oidc_auth_uri(conn, %{"provider" => provider_id}) when is_binary(provider_id) do verifier = PKCE.code_verifier() params = %{ @@ -149,27 +152,17 @@ defmodule FzHttpWeb.AuthController do code_challenge: PKCE.code_challenge(verifier) } - with {:ok, provider} <- atomize_provider(provider_key), - uri <- openid_connect().authorization_uri(provider, params) do - conn - |> PKCE.put_cookie(verifier) - |> State.put_cookie(params.state) - |> redirect(external: uri) - else - _ -> - msg = "OpenIDConnect error: provider #{provider_key} not found in config" - Logger.warn(msg) + uri = openid_connect().authorization_uri(provider_id, params) - conn - |> put_resp_content_type("text/plain") - |> send_resp(400, "OIDC Error. Check logs.") - |> halt() - end + conn + |> PKCE.put_cookie(verifier) + |> State.put_cookie(params.state) + |> redirect(external: uri) end - defp maybe_sign_in(conn, user, %{provider: provider} = auth) - when provider in @local_auth_providers do - if Conf.get!(:local_auth_enabled) do + defp maybe_sign_in(conn, user, %{provider: provider_key} = auth) + when is_atom(provider_key) and provider_key in @local_auth_providers do + if FzHttp.Configurations.get!(:local_auth_enabled) do do_sign_in(conn, user, auth) else conn @@ -186,6 +179,6 @@ defmodule FzHttpWeb.AuthController do |> Authentication.sign_in(user, auth) |> configure_session(renew: true) |> put_session(:live_socket_id, "users_socket:#{user.id}") - |> redirect(to: root_path_for_role(user.role)) + |> redirect(to: root_path_for_user(user)) end end diff --git a/apps/fz_http/lib/fz_http_web/controllers/json/configuration_controller.ex b/apps/fz_http/lib/fz_http_web/controllers/json/configuration_controller.ex new file mode 100644 index 000000000..4e56f3a3d --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/controllers/json/configuration_controller.ex @@ -0,0 +1,21 @@ +defmodule FzHttpWeb.JSON.ConfigurationController do + use FzHttpWeb, :controller + + alias FzHttp.{Configurations.Configuration, Configurations} + + action_fallback FzHttpWeb.JSON.FallbackController + + def show(conn, _params) do + configuration = Configurations.get_configuration!() + render(conn, "show.json", configuration: configuration) + end + + def update(conn, %{"configuration" => params}) do + configuration = Configurations.get_configuration!() + + with {:ok, %Configuration{} = configuration} <- + Configurations.update_configuration(configuration, params) do + render(conn, "show.json", configuration: configuration) + end + end +end diff --git a/apps/fz_http/lib/fz_http_web/controllers/json/device_controller.ex b/apps/fz_http/lib/fz_http_web/controllers/json/device_controller.ex new file mode 100644 index 000000000..c3f65846e --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/controllers/json/device_controller.ex @@ -0,0 +1,46 @@ +defmodule FzHttpWeb.JSON.DeviceController do + @moduledoc """ + REST API Controller for Devices. + """ + + use FzHttpWeb, :controller + + action_fallback FzHttpWeb.JSON.FallbackController + + alias FzHttp.Devices + + def index(conn, _params) do + devices = Devices.list_devices() + render(conn, "index.json", devices: devices) + end + + def create(conn, %{"device" => device_params}) do + with {:ok, device} <- Devices.create_device(device_params) do + conn + |> put_status(:created) + |> put_resp_header("location", ~p"/v0/devices/#{device}") + |> render("show.json", device: device) + end + end + + def show(conn, %{"id" => id}) do + device = Devices.get_device!(id) + render(conn, "show.json", device: device) + end + + def update(conn, %{"id" => id, "device" => device_params}) do + device = Devices.get_device!(id) + + with {:ok, device} <- Devices.update_device(device, device_params) do + render(conn, "show.json", device: device) + end + end + + def delete(conn, %{"id" => id}) do + device = Devices.get_device!(id) + + with {:ok, _device} <- Devices.delete_device(device) do + send_resp(conn, :no_content, "") + end + end +end diff --git a/apps/fz_http/lib/fz_http_web/controllers/json/fallback_controller.ex b/apps/fz_http/lib/fz_http_web/controllers/json/fallback_controller.ex new file mode 100644 index 000000000..052767cf3 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/controllers/json/fallback_controller.ex @@ -0,0 +1,30 @@ +defmodule FzHttpWeb.JSON.FallbackController do + @moduledoc """ + Translates controller action results into valid `Plug.Conn` responses. + + See `Phoenix.Controller.action_fallback/1` for more details. + """ + use FzHttpWeb, :controller + + # This clause is an example of how to handle resources that cannot be found. + def call(conn, {:error, :not_found}) do + conn + |> put_status(:not_found) + |> put_view(FzHttpWeb.ErrorView) + |> render("404.json") + end + + def call(conn, {:error, :internal_server_error}) do + conn + |> put_status(:internal_server_error) + |> put_view(FzHttpWeb.ErrorView) + |> render("500.json") + end + + def call(conn, {:error, %Ecto.Changeset{valid?: false} = changeset}) do + conn + |> put_status(422) + |> put_view(FzHttpWeb.JSON.ChangesetView) + |> render("error.json", changeset: changeset) + end +end diff --git a/apps/fz_http/lib/fz_http_web/controllers/json/rule_controller.ex b/apps/fz_http/lib/fz_http_web/controllers/json/rule_controller.ex new file mode 100644 index 000000000..7e05f4826 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/controllers/json/rule_controller.ex @@ -0,0 +1,47 @@ +defmodule FzHttpWeb.JSON.RuleController do + @moduledoc """ + REST API Controller for Rules. + """ + + use FzHttpWeb, :controller + + action_fallback FzHttpWeb.JSON.FallbackController + + alias FzHttp.Rules + + def index(conn, _params) do + # XXX: Add user-scoped rules + rules = Rules.list_rules() + render(conn, "index.json", rules: rules) + end + + def create(conn, %{"rule" => rule_params}) do + with {:ok, rule} <- Rules.create_rule(rule_params) do + conn + |> put_status(:created) + |> put_resp_header("location", ~p"/v0/rules/#{rule}") + |> render("show.json", rule: rule) + end + end + + def show(conn, %{"id" => id}) do + rule = Rules.get_rule!(id) + render(conn, "show.json", rule: rule) + end + + def update(conn, %{"id" => id, "rule" => rule_params}) do + rule = Rules.get_rule!(id) + + with {:ok, rule} <- Rules.update_rule(rule, rule_params) do + render(conn, "show.json", rule: rule) + end + end + + def delete(conn, %{"id" => id}) do + rule = Rules.get_rule!(id) + + with {:ok, _rule} <- Rules.delete_rule(rule) do + send_resp(conn, :no_content, "") + end + end +end diff --git a/apps/fz_http/lib/fz_http_web/controllers/json/user_controller.ex b/apps/fz_http/lib/fz_http_web/controllers/json/user_controller.ex new file mode 100644 index 000000000..a80d047c7 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/controllers/json/user_controller.ex @@ -0,0 +1,51 @@ +defmodule FzHttpWeb.JSON.UserController do + use FzHttpWeb, :controller + + alias FzHttp.Users + alias FzHttp.Users.User + + action_fallback FzHttpWeb.JSON.FallbackController + + def index(conn, _params) do + users = Users.list_users() + render(conn, "index.json", users: users) + end + + def create(conn, %{"user" => user_params}) do + with {:ok, %User{} = user} <- Users.create_user(user_params) do + conn + |> put_status(:created) + |> put_resp_header("location", ~p"/v0/users/#{user}") + |> render("show.json", user: user) + end + end + + def show(conn, %{"id" => id_or_email}) do + user = get_user_by_id_or_email(id_or_email) + render(conn, "show.json", user: user) + end + + def update(conn, %{"id" => id, "user" => user_params}) do + user = Users.get_user!(id) + + with {:ok, %User{} = user} <- Users.admin_update_user(user, user_params) do + render(conn, "show.json", user: user) + end + end + + def delete(conn, %{"id" => id}) do + user = Users.get_user!(id) + + with {:ok, %User{}} <- Users.delete_user(user) do + send_resp(conn, :no_content, "") + end + end + + defp get_user_by_id_or_email(id_or_email) do + if String.contains?(id_or_email, "@") do + Users.get_by_email!(id_or_email) + else + Users.get_user!(id_or_email) + end + 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 16f0561d3..5bf9a99f7 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 @@ -4,15 +4,13 @@ defmodule FzHttpWeb.RootController do """ use FzHttpWeb, :controller - alias FzHttp.Configurations, as: Conf - def index(conn, _params) do conn |> render( "auth.html", - local_enabled: Conf.get!(:local_auth_enabled), - openid_connect_providers: Conf.get!(:parsed_openid_connect_providers), - saml_identity_providers: Conf.get!(:saml_identity_providers) + local_enabled: FzHttp.Configurations.get!(:local_auth_enabled), + openid_connect_providers: FzHttp.Configurations.get!(:openid_connect_providers), + saml_identity_providers: FzHttp.Configurations.get!(:saml_identity_providers) ) 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 2a66866cc..30c20283a 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,7 +4,7 @@ defmodule FzHttpWeb.UserController do """ alias FzHttp.Users - alias FzHttpWeb.Authentication + alias FzHttpWeb.Auth.HTML.Authentication use FzHttpWeb, :controller require Logger diff --git a/apps/fz_http/lib/fz_http_web/header_helpers.ex b/apps/fz_http/lib/fz_http_web/header_helpers.ex index 6c93b9212..d38268cec 100644 --- a/apps/fz_http/lib/fz_http_web/header_helpers.ex +++ b/apps/fz_http/lib/fz_http_web/header_helpers.ex @@ -5,9 +5,9 @@ defmodule FzHttpWeb.HeaderHelpers do @remote_ip_headers ["x-forwarded-for"] - def external_trusted_proxies, do: conf(:external_trusted_proxies) + def external_trusted_proxies, do: FzHttp.Config.fetch_env!(:fz_http, :external_trusted_proxies) - def clients, do: conf(:private_clients) + def clients, do: FzHttp.Config.fetch_env!(:fz_http, :private_clients) def proxied?, do: not (external_trusted_proxies() == false) @@ -18,8 +18,4 @@ defmodule FzHttpWeb.HeaderHelpers do clients: clients() ] end - - defp conf(key) when is_atom(key) do - Application.fetch_env!(:fz_http, key) - end end 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 index 90e888793..b35dcbebe 100644 --- 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 @@ -51,7 +51,7 @@ defmodule FzHttpWeb.DeviceLive.Admin.Show do allowed_ips: Devices.allowed_ips(device), dns: Devices.dns(device), endpoint: Devices.endpoint(device), - port: Application.fetch_env!(:fz_vpn, :wireguard_port), + port: FzHttp.Config.fetch_env!(:fz_vpn, :wireguard_port), mtu: Devices.mtu(device), persistent_keepalive: Devices.persistent_keepalive(device), config: Devices.as_config(device) 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 index 7ec85bfa3..bce96ed69 100644 --- 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 @@ -4,9 +4,7 @@ defmodule FzHttpWeb.DeviceLive.NewFormComponent do """ use FzHttpWeb, :live_component - alias FzHttp.Configurations, as: Conf alias FzHttp.Devices - alias FzHttp.Sites alias FzHttpWeb.ErrorHelpers @impl Phoenix.LiveComponent @@ -17,6 +15,13 @@ defmodule FzHttpWeb.DeviceLive.NewFormComponent do |> assign(:config, nil)} end + @default_fields ~w( + default_client_mtu + default_client_endpoint + default_client_persistent_keepalive + default_client_dns + default_client_allowed_ips + )a @impl Phoenix.LiveComponent def update(assigns, socket) do changeset = new_changeset(socket) @@ -25,9 +30,7 @@ defmodule FzHttpWeb.DeviceLive.NewFormComponent do socket |> assign(assigns) |> assign(:changeset, changeset) - |> assign( - Map.take(Sites.get_site!(), [:mtu, :endpoint, :persistent_keepalive, :dns, :allowed_ips]) - ) + |> assign(Map.take(FzHttp.Configurations.get_configuration!(), @default_fields)) |> assign(Devices.defaults(changeset))} end @@ -78,8 +81,8 @@ defmodule FzHttpWeb.DeviceLive.NewFormComponent do defp authorized_to_create?(socket) do has_role?(socket, :admin) || - (Conf.get!(:allow_unprivileged_device_management) && - to_string(socket.assigns.current_user.id) == to_string(socket.assigns.target_user_id)) + (FzHttp.Configurations.get!(:allow_unprivileged_device_management) && + socket.assigns.current_user.id == socket.assigns.target_user_id) end # update/2 is called twice: on load and then connect. @@ -87,9 +90,10 @@ defmodule FzHttpWeb.DeviceLive.NewFormComponent do # XXX: Clean this up using assign_new/3 defp new_changeset(socket) do if connected?(socket) do - Devices.new_device() + %{name: FzHttp.Devices.new_name()} else - Devices.new_device(%{"name" => nil}) + %{} end + |> Devices.new_device() 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 index e20f26478..5891bd191 100644 --- 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 @@ -95,17 +95,17 @@ <%= if FzHttpWeb.DeviceView.can_configure_devices?(@current_user) do %>
- <%= label(f, :use_site_allowed_ips, "Use Default Allowed IPs", class: "label") %> + <%= label(f, :use_default_allowed_ips, "Use Default Allowed IPs", class: "label") %>

- Default: <%= @allowed_ips %> + Default: <%= @default_client_allowed_ips %>

@@ -114,7 +114,7 @@
<%= textarea(f, :allowed_ips, class: "textarea #{input_error_class(f, :allowed_ips)}", - disabled: @use_site_allowed_ips + disabled: @use_default_allowed_ips ) %>

@@ -123,17 +123,17 @@

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

- Default: <%= @dns %> + Default: <%= @default_client_dns %>

@@ -142,7 +142,7 @@
<%= text_input(f, :dns, class: "input #{input_error_class(f, :dns)}", - disabled: @use_site_dns + disabled: @use_default_dns ) %>

@@ -151,17 +151,17 @@

- <%= label(f, :use_site_endpoint, "Use Default Endpoint", class: "label") %> + <%= label(f, :use_default_endpoint, "Use Default Endpoint", class: "label") %>

- Default: <%= @endpoint %> + Default: <%= @default_client_endpoint %>

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

@@ -180,17 +180,17 @@

- <%= label(f, :use_site_mtu, "Use Default MTU", class: "label") %> + <%= label(f, :use_default_mtu, "Use Default MTU", class: "label") %>

- Default: <%= @mtu %> + Default: <%= @default_client_mtu %>

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

@@ -209,19 +209,19 @@

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

- Default: <%= @persistent_keepalive %> + Default: <%= @default_client_persistent_keepalive %>

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

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 index 1e2a5c48d..20a925b79 100644 --- 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 @@ -29,12 +29,13 @@

- <%= if length(@devices) > 0 do %> + <%= unless Enum.empty?(@devices) do %> - + + @@ -47,10 +48,8 @@ <%= device.name %> - + + - - - - + + + + - + - <%= if Application.fetch_env!(:fz_http, :wireguard_ipv4_enabled) do %> + <%= if FzHttp.Config.fetch_env!(:fz_http, :wireguard_ipv4_enabled) do %> - + <% end %> - <%= if Application.fetch_env!(:fz_http, :wireguard_ipv6_enabled) do %> + <%= if FzHttp.Config.fetch_env!(:fz_http, :wireguard_ipv6_enabled) do %> - + <% end %> 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 051364bc8..feecc6b50 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 @@ -5,7 +5,8 @@ <%= if @show_user do %> <% end %> - + + @@ -29,11 +30,8 @@ ) %> <% 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 index ec92ba3d1..9f43ffec8 100644 --- a/apps/fz_http/lib/fz_http_web/user_from_auth.ex +++ b/apps/fz_http/lib/fz_http_web/user_from_auth.ex @@ -3,42 +3,40 @@ defmodule FzHttpWeb.UserFromAuth do Authenticates users. """ - alias FzHttp.Configurations, as: Conf alias FzHttp.Users - alias FzHttpWeb.Authentication - alias Ueberauth.Auth + alias FzHttpWeb.Auth.HTML.Authentication def find_or_create( - %Auth{ + %Ueberauth.Auth{ provider: :identity, - info: %Auth.Info{email: email}, - credentials: %Auth.Credentials{other: %{password: password}} + info: %Ueberauth.Auth.Info{email: email}, + credentials: %Ueberauth.Auth.Credentials{other: %{password: password}} } = _auth ) do Users.get_by_email(email) |> Authentication.authenticate(password) end # SAML - def find_or_create(:saml, provider_key, %{"email" => email}) do + def find_or_create(:saml, provider_id, %{"email" => email}) do case Users.get_by_email(email) do - nil -> maybe_create_user(:saml_identity_providers, provider_key, email) + nil -> maybe_create_user(:saml_identity_providers, provider_id, email) user -> {:ok, user} end end # OIDC - def find_or_create(provider_key, %{"email" => email, "sub" => _sub}) do + def find_or_create(provider_id, %{"email" => email, "sub" => _sub}) do case Users.get_by_email(email) do - nil -> maybe_create_user(:openid_connect_providers, provider_key, email) + nil -> maybe_create_user(:openid_connect_providers, provider_id, email) user -> {:ok, user} end end - defp maybe_create_user(idp_field, provider_key, email) do - if Conf.auto_create_users?(idp_field, provider_key) do + defp maybe_create_user(idp_field, provider_id, email) do + if FzHttp.Configurations.auto_create_users?(idp_field, provider_id) do Users.create_unprivileged_user(%{email: email}) else - {:error, "not found"} + {:error, "user not found and auto_create_users disabled"} end end end diff --git a/apps/fz_http/lib/fz_http_web/views/device_view.ex b/apps/fz_http/lib/fz_http_web/views/device_view.ex index 87354445f..5cfb48a4c 100644 --- a/apps/fz_http/lib/fz_http_web/views/device_view.ex +++ b/apps/fz_http/lib/fz_http_web/views/device_view.ex @@ -1,13 +1,12 @@ defmodule FzHttpWeb.DeviceView do use FzHttpWeb, :view - alias FzHttp.Configurations, as: Conf - def can_manage_devices?(user) do - has_role?(user, :admin) || Conf.get!(:allow_unprivileged_device_management) + has_role?(user, :admin) || FzHttp.Configurations.get!(:allow_unprivileged_device_management) end def can_configure_devices?(user) do - has_role?(user, :admin) || Conf.get!(:allow_unprivileged_device_configuration) + has_role?(user, :admin) || + FzHttp.Configurations.get!(:allow_unprivileged_device_configuration) end end diff --git a/apps/fz_http/lib/fz_http_web/views/json/changeset_view.ex b/apps/fz_http/lib/fz_http_web/views/json/changeset_view.ex new file mode 100644 index 000000000..bf29f87c6 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/views/json/changeset_view.ex @@ -0,0 +1,19 @@ +defmodule FzHttpWeb.JSON.ChangesetView do + use FzHttpWeb, :view + + @doc """ + Traverses and translates changeset errors. + + See `Ecto.Changeset.traverse_errors/2` and + `FzHttpWeb.ErrorHelpers.translate_error/1` for more details. + """ + def translate_errors(changeset) do + Ecto.Changeset.traverse_errors(changeset, &translate_error/1) + end + + def render("error.json", %{changeset: changeset}) do + # When encoded, the changeset returns its errors + # as a JSON object. So we just pass it forward. + %{errors: translate_errors(changeset)} + end +end diff --git a/apps/fz_http/lib/fz_http_web/views/json/configuration_view.ex b/apps/fz_http/lib/fz_http_web/views/json/configuration_view.ex new file mode 100644 index 000000000..b24142561 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/views/json/configuration_view.ex @@ -0,0 +1,36 @@ +defmodule FzHttpWeb.JSON.ConfigurationView do + @moduledoc """ + Handles JSON rendering of Configuration records. + """ + use FzHttpWeb, :view + + def render("index.json", %{configurations: configurations}) do + %{data: render_many(configurations, __MODULE__, "configuration.json")} + end + + def render("show.json", %{configuration: configuration}) do + %{data: render_one(configuration, __MODULE__, "configuration.json")} + end + + @keys_to_render ~w[ + id + logo + local_auth_enabled + allow_unprivileged_device_management + allow_unprivileged_device_configuration + openid_connect_providers + saml_identity_providers + disable_vpn_on_oidc_error + vpn_session_duration + default_client_persistent_keepalive + default_client_mtu + default_client_endpoint + default_client_dns + default_client_allowed_ips + inserted_at + updated_at + ]a + def render("configuration.json", %{configuration: configuration}) do + Map.take(configuration, @keys_to_render) + end +end diff --git a/apps/fz_http/lib/fz_http_web/views/json/device_view.ex b/apps/fz_http/lib/fz_http_web/views/json/device_view.ex new file mode 100644 index 000000000..16a23fe78 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/views/json/device_view.ex @@ -0,0 +1,56 @@ +defmodule FzHttpWeb.JSON.DeviceView do + @moduledoc """ + Handles JSON rendering of Device records. + """ + use FzHttpWeb, :view + + alias FzHttp.Devices + + def render("index.json", %{devices: devices}) do + %{data: render_many(devices, __MODULE__, "device.json")} + end + + def render("show.json", %{device: device}) do + %{data: render_one(device, __MODULE__, "device.json")} + end + + @keys_to_render ~w[ + id + rx_bytes + tx_bytes + name + description + public_key + preshared_key + use_default_allowed_ips + use_default_dns + use_default_endpoint + use_default_mtu + use_default_persistent_keepalive + endpoint + mtu + persistent_keepalive + allowed_ips + dns + remote_ip + ipv4 + ipv6 + latest_handshake + updated_at + inserted_at + user_id + ]a + def render("device.json", %{device: device}) do + Map.merge( + Map.take(device, @keys_to_render), + %{ + server_public_key: Application.get_env(:fz_vpn, :wireguard_public_key), + endpoint: Devices.config(device, :endpoint), + allowed_ips: Devices.config(device, :allowed_ips), + dns: Devices.config(device, :dns), + persistent_keepalive: Devices.config(device, :persistent_keepalive), + mtu: Devices.config(device, :mtu) + } + ) + end +end diff --git a/apps/fz_http/lib/fz_http_web/views/json/encoders/postgrex_inet.ex b/apps/fz_http/lib/fz_http_web/views/json/encoders/postgrex_inet.ex new file mode 100644 index 000000000..1a2042128 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/views/json/encoders/postgrex_inet.ex @@ -0,0 +1,5 @@ +defimpl Jason.Encoder, for: Postgrex.INET do + def encode(%Postgrex.INET{} = struct, opts) do + Jason.Encode.string("#{struct}", opts) + end +end diff --git a/apps/fz_http/lib/fz_http_web/views/json/rule_view.ex b/apps/fz_http/lib/fz_http_web/views/json/rule_view.ex new file mode 100644 index 000000000..ad16ce0f4 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/views/json/rule_view.ex @@ -0,0 +1,28 @@ +defmodule FzHttpWeb.JSON.RuleView do + @moduledoc """ + Handles JSON rendering of Rule records. + """ + use FzHttpWeb, :view + + def render("index.json", %{rules: rules}) do + %{data: render_many(rules, __MODULE__, "rule.json")} + end + + def render("show.json", %{rule: rule}) do + %{data: render_one(rule, __MODULE__, "rule.json")} + end + + @keys_to_render ~w[ + id + destination + action + port_type + port_range + user_id + inserted_at + updated_at + ]a + def render("rule.json", %{rule: rule}) do + Map.take(rule, @keys_to_render) + end +end diff --git a/apps/fz_http/lib/fz_http_web/views/json/user_view.ex b/apps/fz_http/lib/fz_http_web/views/json/user_view.ex new file mode 100644 index 000000000..b52fc1a1d --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/views/json/user_view.ex @@ -0,0 +1,28 @@ +defmodule FzHttpWeb.JSON.UserView do + @moduledoc """ + Handles JSON rendering of User records. + """ + use FzHttpWeb, :view + + def render("index.json", %{users: users}) do + %{data: render_many(users, __MODULE__, "user.json")} + end + + def render("show.json", %{user: user}) do + %{data: render_one(user, __MODULE__, "user.json")} + end + + @keys_to_render ~w[ + id + role + email + last_signed_in_at + last_signed_in_method + disabled_at + inserted_at + updated_at + ]a + def render("user.json", %{user: user}) do + Map.take(user, @keys_to_render) + end +end diff --git a/apps/fz_http/mix.exs b/apps/fz_http/mix.exs index 6f328a144..c8e95d992 100644 --- a/apps/fz_http/mix.exs +++ b/apps/fz_http/mix.exs @@ -55,6 +55,7 @@ defmodule FzHttp.MixProject do defp deps do [ {:fz_common, in_umbrella: true}, + {:bypass, "~> 2.1", only: :test}, {:decimal, "~> 2.0"}, {:cloak, "~> 1.1"}, {:cloak_ecto, "~> 1.2"}, @@ -63,6 +64,7 @@ defmodule FzHttp.MixProject do {:mox, "~> 1.0.1", only: :test}, {:guardian, "~> 2.0"}, {:guardian_db, "~> 2.0"}, + # XXX: All github deps should use ref instead of always updating from master branch {:openid_connect, github: "firezone/openid_connect"}, {:esaml, github: "firezone/esaml", override: true}, {:samly, github: "firezone/samly"}, @@ -73,7 +75,8 @@ defmodule FzHttp.MixProject do {:phoenix_pubsub, "~> 2.0"}, {:phoenix_ecto, "~> 4.4"}, {:ecto_sql, "~> 3.7"}, - {:ecto_network, "~> 1.3"}, + {:ecto_network, + github: "firezone/ecto_network", ref: "7dfe65bcb6506fb0ed6050871b433f3f8b1c10cb"}, {:inflex, "~> 2.1"}, {:plug, "~> 1.13"}, {:postgrex, "~> 0.16"}, @@ -87,7 +90,6 @@ defmodule FzHttp.MixProject do {:phoenix_swoosh, "~> 1.0"}, {:gen_smtp, "~> 1.0"}, {:nimble_totp, "~> 0.2"}, - {:cachex, "~> 3.4"}, # XXX: Change this when hex package is updated {:cidr, github: "firezone/cidr-elixir"}, {:telemetry, "~> 1.0"}, diff --git a/apps/fz_http/priv/repo/migrations/20221129002233_update_on_delete_behavior.exs b/apps/fz_http/priv/repo/migrations/20221129002233_update_on_delete_behavior.exs new file mode 100644 index 000000000..5485e052c --- /dev/null +++ b/apps/fz_http/priv/repo/migrations/20221129002233_update_on_delete_behavior.exs @@ -0,0 +1,33 @@ +defmodule FzHttp.Repo.Migrations.UpdateOnDeleteBehavior do + use Ecto.Migration + + def change do + drop(constraint(:oidc_connections, "oidc_connections_user_id_fkey")) + + alter table(:oidc_connections) do + modify( + :user_id, + references(:users, on_delete: :delete_all), + null: false, + from: { + references(:users, on_delete: :nothing), + null: false + } + ) + end + + drop(constraint(:mfa_methods, "mfa_methods_user_id_fkey")) + + alter table(:mfa_methods) do + modify( + :user_id, + references(:users, on_delete: :delete_all), + null: false, + from: { + references(:users, on_delete: :nothing), + null: false + } + ) + end + end +end diff --git a/apps/fz_http/priv/repo/migrations/20221223190406_migrate_pks_to_uuid.exs b/apps/fz_http/priv/repo/migrations/20221223190406_migrate_pks_to_uuid.exs new file mode 100644 index 000000000..4dca044c0 --- /dev/null +++ b/apps/fz_http/priv/repo/migrations/20221223190406_migrate_pks_to_uuid.exs @@ -0,0 +1,186 @@ +defmodule FzHttp.Repo.Migrations.MigratePksToUuid do + use Ecto.Migration + + def change do + ## connectivity_checks + alter table("connectivity_checks") do + remove(:id) + add(:id, :binary_id, primary_key: true) + end + + ## devices + execute("DROP INDEX devices_uuid_index") + rename(table("devices"), :id, to: :id_tmp) + rename(table("devices"), :uuid, to: :id) + + alter table("devices") do + modify(:id, :binary_id, primary_key: true) + remove(:id_tmp) + end + + ## rules + execute("DROP INDEX rules_uuid_index") + rename(table("rules"), :id, to: :id_tmp) + rename(table("rules"), :uuid, to: :id) + + alter table("rules") do + modify(:id, :binary_id, primary_key: true) + remove(:id_tmp) + end + + ## oidc_connections + alter table("oidc_connections") do + remove(:id) + add(:id, :binary_id, primary_key: true) + end + + ## users + rename(table("users"), :id, to: :id_tmp) + rename(table("users"), :uuid, to: :id) + + ### devices refs to users + rename(table("devices"), :user_id, to: :user_id_tmp) + + execute( + "ALTER TABLE devices RENAME CONSTRAINT devices_user_id_fkey TO devices_user_id_tmp_fkey" + ) + + alter table("devices") do + add(:user_id, references(:users, type: :binary_id, on_delete: :delete_all)) + end + + execute( + "UPDATE devices SET user_id = (SELECT users.id FROM users WHERE users.id_tmp = devices.user_id_tmp)" + ) + + execute("ALTER TABLE devices ALTER COLUMN user_id SET NOT NULL") + + alter table("devices") do + remove(:user_id_tmp) + end + + create(index(:devices, [:user_id])) + create(unique_index(:devices, [:user_id, :name])) + + ### rules refs to users + rename(table("rules"), :user_id, to: :user_id_tmp) + + execute("ALTER TABLE rules RENAME CONSTRAINT rules_user_id_fkey TO rules_user_id_tmp_fkey") + + alter table("rules") do + add(:user_id, references(:users, type: :binary_id, on_delete: :delete_all)) + end + + execute( + "UPDATE rules SET user_id = (SELECT users.id FROM users WHERE users.id_tmp = rules.user_id_tmp)" + ) + + execute(""" + ALTER TABLE rules + DROP CONSTRAINT destination_overlap_excl + """) + + execute(""" + ALTER TABLE rules + ADD CONSTRAINT destination_overlap_excl + EXCLUDE USING gist (destination inet_ops WITH &&, action WITH =) + WHERE (user_id IS NULL AND port_range IS NULL) + """) + + execute(""" + ALTER TABLE rules + DROP CONSTRAINT destination_overlap_excl_port + """) + + execute(""" + ALTER TABLE rules + ADD CONSTRAINT destination_overlap_excl_port + EXCLUDE USING gist (destination inet_ops WITH &&, action WITH =, port_range WITH &&, port_type WITH =) + WHERE (user_id IS NULL AND port_range IS NOT NULL) + """) + + execute(""" + ALTER TABLE rules + DROP CONSTRAINT destination_overlap_excl_usr_rule + """) + + execute(""" + ALTER TABLE rules + ADD CONSTRAINT destination_overlap_excl_usr_rule + EXCLUDE USING gist (destination inet_ops WITH &&, user_id WITH =, action WITH =) + WHERE (user_id IS NOT NULL AND port_range IS NULL) + """) + + execute(""" + ALTER TABLE rules + DROP CONSTRAINT destination_overlap_excl_usr_rule_port + """) + + execute(""" + ALTER TABLE rules + ADD CONSTRAINT destination_overlap_excl_usr_rule_port + EXCLUDE USING gist (destination inet_ops WITH &&, user_id WITH =, action WITH =, port_range WITH &&, port_type WITH =) + WHERE (user_id IS NOT NULL AND port_range IS NOT NULL) + """) + + alter table("rules") do + remove(:user_id_tmp) + end + + ### oidc_connections refs to users + rename(table("oidc_connections"), :user_id, to: :user_id_tmp) + + execute( + "ALTER TABLE oidc_connections RENAME CONSTRAINT oidc_connections_user_id_fkey TO oidc_connections_user_id_tmp_fkey" + ) + + alter table("oidc_connections") do + add(:user_id, references(:users, type: :binary_id, on_delete: :delete_all)) + end + + execute( + "UPDATE oidc_connections SET user_id = (SELECT users.id FROM users WHERE users.id_tmp = oidc_connections.user_id_tmp)" + ) + + execute("ALTER TABLE oidc_connections ALTER COLUMN user_id SET NOT NULL") + + alter table("oidc_connections") do + remove(:user_id_tmp) + end + + create(unique_index(:oidc_connections, [:user_id, :provider])) + + ### mfa_methods refs to users + rename(table("mfa_methods"), :user_id, to: :user_id_tmp) + + execute( + "ALTER TABLE mfa_methods RENAME CONSTRAINT mfa_methods_user_id_fkey TO mfa_methods_user_id_tmp_fkey" + ) + + alter table("mfa_methods") do + add(:user_id, references(:users, type: :binary_id, on_delete: :delete_all)) + end + + execute( + "UPDATE mfa_methods SET user_id = (SELECT users.id FROM users WHERE users.id_tmp = mfa_methods.user_id_tmp)" + ) + + execute("ALTER TABLE mfa_methods ALTER COLUMN user_id SET NOT NULL") + + alter table("mfa_methods") do + remove(:user_id_tmp) + end + + create(index(:mfa_methods, [:user_id])) + + alter table("users") do + remove(:id_tmp) + end + + execute("ALTER INDEX users_uuid_index RENAME TO users_pkey") + + alter table("users") do + modify(:id, :binary_id, primary_key: true) + end + end +end diff --git a/apps/fz_http/priv/repo/migrations/20221223223357_migrate_datetimes_to_timestamptz.exs b/apps/fz_http/priv/repo/migrations/20221223223357_migrate_datetimes_to_timestamptz.exs new file mode 100644 index 000000000..1809f7fc3 --- /dev/null +++ b/apps/fz_http/priv/repo/migrations/20221223223357_migrate_datetimes_to_timestamptz.exs @@ -0,0 +1,47 @@ +defmodule FzHttp.Repo.Migrations.MigrateDatetimesToTimestamptz do + use Ecto.Migration + + def change do + alter table("configurations") do + modify(:inserted_at, :timestamptz, from: :utc_datetime_usec) + modify(:updated_at, :timestamptz, from: :utc_datetime_usec) + end + + alter table("sites") do + modify(:inserted_at, :timestamptz, from: :utc_datetime_usec) + modify(:updated_at, :timestamptz, from: :utc_datetime_usec) + end + + alter table("mfa_methods") do + modify(:inserted_at, :timestamptz, from: :utc_datetime_usec) + modify(:updated_at, :timestamptz, from: :utc_datetime_usec) + modify(:last_used_at, :timestamptz, from: :utc_datetime_usec) + end + + alter table("devices") do + modify(:inserted_at, :timestamptz, from: :utc_datetime_usec) + modify(:updated_at, :timestamptz, from: :utc_datetime_usec) + modify(:latest_handshake, :timestamptz, from: :utc_datetime_usec) + modify(:key_regenerated_at, :timestamptz, from: :utc_datetime_usec) + end + + alter table("oidc_connections") do + modify(:inserted_at, :timestamptz, from: :utc_datetime_usec) + modify(:updated_at, :timestamptz, from: :utc_datetime_usec) + modify(:refreshed_at, :timestamptz, from: :utc_datetime_usec) + end + + alter table("connectivity_checks") do + modify(:inserted_at, :timestamptz, from: :utc_datetime_usec) + remove(:updated_at, :timestamptz, null: false) + end + + alter table("users") do + modify(:inserted_at, :timestamptz, from: :utc_datetime_usec) + modify(:updated_at, :timestamptz, from: :utc_datetime_usec) + modify(:last_signed_in_at, :timestamptz, from: :utc_datetime_usec) + modify(:sign_in_token_created_at, :timestamptz, from: :utc_datetime_usec) + modify(:disabled_at, :timestamptz, from: :utc_datetime_usec) + end + end +end diff --git a/apps/fz_http/priv/repo/migrations/20221223223931_order_connectivity_checks_inserted_at_index.exs b/apps/fz_http/priv/repo/migrations/20221223223931_order_connectivity_checks_inserted_at_index.exs new file mode 100644 index 000000000..4d15bc9b6 --- /dev/null +++ b/apps/fz_http/priv/repo/migrations/20221223223931_order_connectivity_checks_inserted_at_index.exs @@ -0,0 +1,11 @@ +defmodule FzHttp.Repo.Migrations.OrderConnectivityChecksInsertedAtIndex do + use Ecto.Migration + + def change do + drop(index(:connectivity_checks, :inserted_at)) + + execute( + "CREATE INDEX connectivity_checks_inserted_at_index ON connectivity_checks (inserted_at DESC)" + ) + end +end diff --git a/apps/fz_http/priv/repo/migrations/20221224125052_users_fk_delete_all_constraint.exs b/apps/fz_http/priv/repo/migrations/20221224125052_users_fk_delete_all_constraint.exs deleted file mode 100644 index ca0cc981c..000000000 --- a/apps/fz_http/priv/repo/migrations/20221224125052_users_fk_delete_all_constraint.exs +++ /dev/null @@ -1,17 +0,0 @@ -defmodule FzHttp.Repo.Migrations.UsersFkDeleteAllConstraint do - use Ecto.Migration - - def change do - drop(constraint(:oidc_connections, "oidc_connections_user_id_fkey")) - - alter table(:oidc_connections) do - modify(:user_id, references(:users, on_delete: :delete_all), null: false) - end - - drop(constraint(:mfa_methods, "mfa_methods_user_id_fkey")) - - alter table(:mfa_methods) do - modify(:user_id, references(:users, on_delete: :delete_all), null: false) - end - end -end diff --git a/apps/fz_http/priv/repo/migrations/20221226044850_create_api_tokens.exs b/apps/fz_http/priv/repo/migrations/20221226044850_create_api_tokens.exs new file mode 100644 index 000000000..eafd4c5ed --- /dev/null +++ b/apps/fz_http/priv/repo/migrations/20221226044850_create_api_tokens.exs @@ -0,0 +1,17 @@ +defmodule FzHttp.Repo.Migrations.CreateApiTokens do + use Ecto.Migration + + def change do + create table(:api_tokens, primary_key: false) do + add(:id, :binary_id, primary_key: true) + add(:expires_at, :timestamptz) + + add(:user_id, references(:users, type: :binary_id, on_delete: :delete_all), null: false) + + timestamps(updated_at: false) + end + + create(index(:api_tokens, [:expires_at])) + create(index(:api_tokens, [:user_id])) + end +end diff --git a/apps/fz_http/priv/repo/migrations/20221226143651_move_sites_fields_to_configurations.exs b/apps/fz_http/priv/repo/migrations/20221226143651_move_sites_fields_to_configurations.exs new file mode 100644 index 000000000..e87743856 --- /dev/null +++ b/apps/fz_http/priv/repo/migrations/20221226143651_move_sites_fields_to_configurations.exs @@ -0,0 +1,91 @@ +defmodule FzHttp.Repo.Migrations.MoveSitesFieldsToConfigurations do + use Ecto.Migration + + @doc """ + XXX: The following env vars are used to configure interface settings + on bootup and so we don't want to store them in the DB or update them + at runtime. Leave them out of this migration. + + WIREGUARD_IPV4_ENABLED + WIREGUARD_IPV6_ENABLED + WIREGUARD_IPV4_NETWORK + WIREGUARD_IPV6_NETWORK + WIREGUARD_IPV4_ADDRESS + WIREGUARD_IPV6_ADDRESS + WIREGUARD_MTU + """ + def change do + alter table(:configurations) do + add(:default_client_persistent_keepalive, :integer) + add(:default_client_endpoint, :string) + add(:default_client_dns, :string) + add(:default_client_allowed_ips, :string) + + # XXX: Note this is different than the WIREGUARD_MTU env var which + # configures the server interface MTU. + add(:default_client_mtu, :integer) + + add(:vpn_session_duration, :integer, null: false, default: 0) + end + + # persistent_keepalive + execute(""" + UPDATE configurations + SET default_client_persistent_keepalive = ( + SELECT persistent_keepalive + FROM sites + WHERE sites.name = 'default' + ) + """) + + # endpoint + execute(""" + UPDATE configurations + SET default_client_endpoint = ( + SELECT endpoint + FROM sites + WHERE sites.name = 'default' + ) + """) + + # dns + execute(""" + UPDATE configurations + SET default_client_dns = ( + SELECT dns + FROM sites + WHERE sites.name = 'default' + ) + """) + + # allowed_ips + execute(""" + UPDATE configurations + SET default_client_allowed_ips = ( + SELECT allowed_ips + FROM sites + WHERE sites.name = 'default' + ) + """) + + # mtu + execute(""" + UPDATE configurations + SET default_client_mtu = ( + SELECT mtu + FROM sites + WHERE sites.name = 'default' + ) + """) + + # vpn_session_duration + execute(""" + UPDATE configurations + SET vpn_session_duration = ( + SELECT vpn_session_duration + FROM sites + WHERE sites.name = 'default' + ) + """) + end +end diff --git a/apps/fz_http/priv/repo/migrations/20221226171558_rename_use_site_to_use_default.exs b/apps/fz_http/priv/repo/migrations/20221226171558_rename_use_site_to_use_default.exs new file mode 100644 index 000000000..83a6caa29 --- /dev/null +++ b/apps/fz_http/priv/repo/migrations/20221226171558_rename_use_site_to_use_default.exs @@ -0,0 +1,11 @@ +defmodule FzHttp.Repo.Migrations.RenameUseSiteToUseDefault do + use Ecto.Migration + + def change do + rename(table(:devices), :use_site_allowed_ips, to: :use_default_allowed_ips) + rename(table(:devices), :use_site_dns, to: :use_default_dns) + rename(table(:devices), :use_site_endpoint, to: :use_default_endpoint) + rename(table(:devices), :use_site_mtu, to: :use_default_mtu) + rename(table(:devices), :use_site_persistent_keepalive, to: :use_default_persistent_keepalive) + end +end diff --git a/apps/fz_http/priv/repo/migrations/20221226193228_drop_sites.exs b/apps/fz_http/priv/repo/migrations/20221226193228_drop_sites.exs new file mode 100644 index 000000000..1ed36c3f7 --- /dev/null +++ b/apps/fz_http/priv/repo/migrations/20221226193228_drop_sites.exs @@ -0,0 +1,7 @@ +defmodule FzHttp.Repo.Migrations.DropSites do + use Ecto.Migration + + def change do + drop(table(:sites)) + end +end diff --git a/apps/fz_http/priv/repo/migrations/20221227181727_move_cache_fallbacks_to_configurations.exs b/apps/fz_http/priv/repo/migrations/20221227181727_move_cache_fallbacks_to_configurations.exs new file mode 100644 index 000000000..21269f861 --- /dev/null +++ b/apps/fz_http/priv/repo/migrations/20221227181727_move_cache_fallbacks_to_configurations.exs @@ -0,0 +1,56 @@ +defmodule FzHttp.Repo.Migrations.MoveCacheFallbacksToConfigurations do + use Ecto.Migration + + alias FzCommon.FzString + + def change do + local_auth_enabled = FzString.to_boolean(System.get_env("LOCAL_AUTH_ENABLED", "true")) + + disable_vpn_on_oidc_error = + FzString.to_boolean(System.get_env("DISABLE_VPN_ON_OIDC_ERROR", "false")) + + allow_unprivileged_device_management = + FzString.to_boolean(System.get_env("ALLOW_UNPRIVILEGED_DEVICE_MANAGEMENT", "true")) + + allow_unprivileged_device_configuration = + FzString.to_boolean(System.get_env("ALLOW_UNPRIVILEGED_DEVICE_CONFIGURATION", "true")) + + execute(""" + UPDATE configurations + SET local_auth_enabled = '#{local_auth_enabled}' WHERE configurations.local_auth_enabled IS NULL + """) + + execute(""" + UPDATE configurations + SET disable_vpn_on_oidc_error = '#{disable_vpn_on_oidc_error}' WHERE configurations.disable_vpn_on_oidc_error IS NULL + """) + + execute(""" + UPDATE configurations + SET allow_unprivileged_device_management = '#{allow_unprivileged_device_management}' WHERE configurations.allow_unprivileged_device_management IS NULL + """) + + execute(""" + UPDATE configurations + SET allow_unprivileged_device_configuration = '#{allow_unprivileged_device_configuration}' WHERE configurations.allow_unprivileged_device_configuration IS NULL + """) + + # Set defaults for providers + execute(""" + UPDATE configurations + SET openid_connect_providers = '{}'::json + WHERE openid_connect_providers IS NULL + """) + + execute(""" + UPDATE configurations + SET saml_identity_providers = '{}'::json + WHERE saml_identity_providers IS NULL + """) + + alter table(:configurations) do + modify(:openid_connect_providers, :map, default: %{}, null: false) + modify(:saml_identity_providers, :map, default: %{}, null: false) + end + end +end diff --git a/apps/fz_http/priv/repo/migrations/20221229154115_migrate_providers_configs.exs b/apps/fz_http/priv/repo/migrations/20221229154115_migrate_providers_configs.exs new file mode 100644 index 000000000..ec243825f --- /dev/null +++ b/apps/fz_http/priv/repo/migrations/20221229154115_migrate_providers_configs.exs @@ -0,0 +1,11 @@ +defmodule FzHttp.Repo.Migrations.MigrateProvidersConfigs do + use Ecto.Migration + + def change do + execute(""" + UPDATE configurations + SET openid_connect_providers = COALESCE((select jsonb_agg(o.item::jsonb || jsonb_build_object('id', key)) from jsonb_each(openid_connect_providers) as o(key, item)), '[]'::jsonb), + saml_identity_providers = COALESCE((select jsonb_agg(s.item::jsonb || jsonb_build_object('id', key)) from jsonb_each(saml_identity_providers) as s(key, item)), '[]'::jsonb) + """) + end +end diff --git a/apps/fz_http/priv/repo/seeds.exs b/apps/fz_http/priv/repo/seeds.exs index eb37f1304..bc97c9915 100644 --- a/apps/fz_http/priv/repo/seeds.exs +++ b/apps/fz_http/priv/repo/seeds.exs @@ -10,7 +10,11 @@ # We recommend using the bang functions (`insert!`, `update!` # and so on) as they will fail if something goes wrong. -alias FzHttp.{Devices, ConnectivityChecks, Users} +alias FzHttp.{ + ConnectivityChecks, + Devices, + Users +} {:ok, user} = Users.create_admin_user(%{ @@ -28,18 +32,16 @@ alias FzHttp.{Devices, ConnectivityChecks, Users} """, preshared_key: "C+Tte1echarIObr6rq+nFeYQ1QO5xo5N29ygDjMlpS8=", public_key: "pSLWbPiQ2mKh26IG1dMFQQWuAstFJXV91dNk+olzEjA=", - ipv4: "10.3.2.6", - ipv6: "fd00::3:2:6", mtu: 1280, persistent_keepalive: 25, allowed_ips: "0.0.0.0,::/0", endpoint: "elixir", dns: "127.0.0.11", - use_site_allowed_ips: false, - use_site_dns: false, - use_site_endpoint: false, - use_site_mtu: false, - use_site_persistent_keepalive: false + use_default_allowed_ips: false, + use_default_dns: false, + use_default_endpoint: false, + use_default_mtu: false, + use_default_persistent_keepalive: false }) {:ok, _device} = diff --git a/apps/fz_http/test/fz_http/api_tokens_test.exs b/apps/fz_http/test/fz_http/api_tokens_test.exs new file mode 100644 index 000000000..a76670980 --- /dev/null +++ b/apps/fz_http/test/fz_http/api_tokens_test.exs @@ -0,0 +1,50 @@ +defmodule FzHttp.ApiTokensTest do + use FzHttp.DataCase + + alias FzHttp.ApiTokens + + describe "api_tokens" do + alias FzHttp.ApiTokens.ApiToken + + import FzHttp.ApiTokensFixtures + import FzHttp.UsersFixtures + + @invalid_params %{"expires_in" => 0} + + test "list_api_tokens/0 returns all api_tokens" do + api_token = api_token() + assert ApiTokens.list_api_tokens() == [api_token] + end + + test "list_api_tokens/1 returns api_tokens scoped to a user" do + api_token1 = api_token() + api_token2 = api_token() + assert [api_token1] == ApiTokens.list_api_tokens(api_token1.user_id) + assert [api_token2] == ApiTokens.list_api_tokens(api_token2.user_id) + end + + test "get_api_token!/1 returns the api_token with given id" do + api_token = api_token() + assert ApiTokens.get_api_token!(api_token.id) == api_token + end + + test "create_user_api_token/2 with valid data creates a api_token" do + valid_params = %{ + "expires_in" => 1 + } + + assert {:ok, %ApiToken{} = api_token} = + ApiTokens.create_user_api_token(user(), valid_params) + + # Within 10 seconds + assert_in_delta DateTime.to_unix(api_token.expires_at), + DateTime.to_unix(DateTime.add(DateTime.utc_now(), 1, :day)), + 10 + end + + test "create_user_api_token/2 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = + ApiTokens.create_user_api_token(user(), @invalid_params) + end + end +end diff --git a/apps/fz_http/test/fz_http/configurations_test.exs b/apps/fz_http/test/fz_http/configurations_test.exs new file mode 100644 index 000000000..448a02e69 --- /dev/null +++ b/apps/fz_http/test/fz_http/configurations_test.exs @@ -0,0 +1,110 @@ +defmodule FzHttp.ConfigurationsTest do + use FzHttp.DataCase + + alias FzHttp.Configurations.Configuration + alias FzHttp.Configurations + + describe "trimmed fields" do + test "trims expected fields" do + changeset = + Configurations.new_configuration(%{ + "default_client_allowed_ips" => " foo ", + "default_client_dns" => " foo ", + "default_client_endpoint" => " foo " + }) + + assert %Ecto.Changeset{ + changes: %{ + default_client_allowed_ips: "foo", + default_client_dns: "foo", + default_client_endpoint: "foo" + } + } = changeset + end + end + + describe "update_configuration/2 with name-based default_client_dns" do + import FzHttp.ConfigurationsFixtures + + setup do + {:ok, configuration: configuration(%{})} + end + + @tag attrs: %{default_client_dns: "foobar.com"} + test "update_configuration/2 allows hosts for DNS", %{ + configuration: configuration, + attrs: attrs + } do + assert {:ok, _configuration} = Configurations.update_configuration(configuration, attrs) + end + + @tag attrs: %{default_client_dns: "foobar.com, google.com"} + test "update_configuration/2 allows list hosts for DNS", %{ + configuration: configuration, + attrs: attrs + } do + assert {:ok, _configuration} = Configurations.update_configuration(configuration, attrs) + end + end + + describe "configurations" do + import FzHttp.ConfigurationsFixtures + + @valid_configurations [ + %{ + "default_client_dns" => "8.8.8.8", + "default_client_allowed_ips" => "::/0", + "default_client_endpoint" => "172.10.10.10", + "default_client_persistent_keepalive" => "20", + "default_client_mtu" => "1280" + }, + %{ + "default_client_dns" => "8.8.8.8", + "default_client_allowed_ips" => "::/0", + "default_client_endpoint" => "foobar.example.com", + "default_client_persistent_keepalive" => "15", + "default_client_mtu" => "1280" + } + ] + @invalid_configuration %{ + "default_client_dns" => "foobar", + "default_client_allowed_ips" => "foobar", + "default_client_endpoint" => "foobar", + "default_client_persistent_keepalive" => "-120", + "default_client_mtu" => "1501" + } + + test "get_configuration/1 returns the configuration" do + configuration = configuration(%{}) + assert Configurations.get_configuration!() == configuration + end + + test "update_configuration/2 with valid data updates the configuration via provided configuration" do + configuration = Configurations.get_configuration!() + + for attrs <- @valid_configurations do + assert {:ok, %Configuration{}} = Configurations.update_configuration(configuration, attrs) + end + end + + test "update_configuration/2 with invalid data returns error changeset" do + configuration = Configurations.get_configuration!() + + assert {:error, %Ecto.Changeset{}} = + Configurations.update_configuration(configuration, @invalid_configuration) + + configuration = Configurations.get_configuration!() + + refute configuration.default_client_dns == "foobar" + refute configuration.default_client_allowed_ips == "foobar" + refute configuration.default_client_endpoint == "foobar" + refute configuration.default_client_persistent_keepalive == -120 + refute configuration.default_client_mtu == 1501 + end + + test "change_configuration/1 returns a configuration changeset" do + configuration = configuration(%{}) + assert %Ecto.Changeset{} = Configurations.change_configuration(configuration) + end + end +end diff --git a/apps/fz_http/test/fz_http/connectivity_check_service_test.exs b/apps/fz_http/test/fz_http/connectivity_check_service_test.exs index 7f716d783..ccf5bf384 100644 --- a/apps/fz_http/test/fz_http/connectivity_check_service_test.exs +++ b/apps/fz_http/test/fz_http/connectivity_check_service_test.exs @@ -2,9 +2,10 @@ defmodule FzHttp.ConnectivityCheckServiceTest do @moduledoc """ Tests the ConnectivityCheckService module. """ + use FzHttp.DataCase, async: true + alias Ecto.Adapters.SQL.Sandbox alias FzHttp.{ConnectivityChecks, ConnectivityCheckService, Repo} - use FzHttp.DataCase, async: true describe "post_request/0 valid url" do @expected_check %{ diff --git a/apps/fz_http/test/fz_http/devices/device/query_test.exs b/apps/fz_http/test/fz_http/devices/device/query_test.exs new file mode 100644 index 000000000..512ef6cd9 --- /dev/null +++ b/apps/fz_http/test/fz_http/devices/device/query_test.exs @@ -0,0 +1,131 @@ +defmodule FzHttp.Devices.Device.QueryTest do + use FzHttp.DataCase, async: true + import FzHttp.Devices.Device.Query + alias FzHttp.DevicesFixtures + + describe "next_available_address/3" do + test "selects available IPv4 in CIDR range at the offset" do + cidr = string_to_inet("10.3.2.0/29") + gateway_ip = string_to_inet("10.3.2.0") + offset = 3 + + queryable = next_available_address(cidr, offset, [gateway_ip]) + + assert Repo.one(queryable) == %Postgrex.INET{address: {10, 3, 2, 3}} + end + + test "skips addresses taken by the gateway" do + cidr = string_to_inet("10.3.3.0/29") + gateway_ip = string_to_inet("10.3.3.3") + offset = 3 + + queryable = next_available_address(cidr, offset, [gateway_ip]) + + assert Repo.one(queryable) == %Postgrex.INET{address: {10, 3, 3, 4}} + end + + test "forward scans available address after offset it it's assigned to a device" do + cidr = string_to_inet("10.3.4.0/29") + gateway_ip = string_to_inet("10.3.4.0") + offset = 3 + + queryable = next_available_address(cidr, offset, [gateway_ip]) + + DevicesFixtures.device(%{ipv4: "10.3.4.3"}) + DevicesFixtures.device(%{ipv4: "10.3.4.4"}) + assert Repo.one(queryable) == %Postgrex.INET{address: {10, 3, 4, 5}} + + DevicesFixtures.device(%{ipv4: "10.3.4.5"}) + assert Repo.one(queryable) == %Postgrex.INET{address: {10, 3, 4, 6}} + end + + test "backward scans available address if forward scan found not available IPs" do + cidr = string_to_inet("10.3.5.0/29") + gateway_ip = string_to_inet("10.3.5.0") + offset = 5 + + queryable = next_available_address(cidr, offset, [gateway_ip]) + + DevicesFixtures.device(%{ipv4: "10.3.5.5"}) + DevicesFixtures.device(%{ipv4: "10.3.5.6"}) + # Notice: end of range is 10.3.5.7 + # but it's a broadcast address that we don't allow to assign + assert Repo.one(queryable) == %Postgrex.INET{address: {10, 3, 5, 4}} + + DevicesFixtures.device(%{ipv4: "10.3.5.4"}) + assert Repo.one(queryable) == %Postgrex.INET{address: {10, 3, 5, 3}} + end + + test "selects nothing when CIDR range is exhausted" do + cidr = string_to_inet("10.3.6.0/30") + gateway_ip = string_to_inet("10.3.6.1") + offset = 1 + + DevicesFixtures.device(%{ipv4: "10.3.6.2"}) + queryable = next_available_address(cidr, offset, [gateway_ip]) + assert is_nil(Repo.one(queryable)) + + DevicesFixtures.device(%{ipv4: "10.3.6.1"}) + queryable = next_available_address(cidr, offset, []) + assert is_nil(Repo.one(queryable)) + + # Notice: real start of range is 10.3.6.0, + # but it's a typical gateway address that we don't allow to assign + end + + test "prevents two concurrent transactions from acquiring the same address" do + cidr = string_to_inet("10.3.7.0/29") + gateway_ip = string_to_inet("10.3.7.3") + offset = 3 + + queryable = next_available_address(cidr, offset, [gateway_ip]) + + test_pid = self() + + spawn(fn -> + Ecto.Adapters.SQL.Sandbox.unboxed_run(Repo, fn -> + Repo.transaction(fn -> + ip = Repo.one(queryable) + send(test_pid, {:ip, ip}) + Process.sleep(200) + end) + end) + end) + + ip1 = Repo.one(queryable) + assert_receive {:ip, ip2}, 1_000 + + assert Enum.sort([ip1, ip2]) == + Enum.sort([ + %Postgrex.INET{address: {10, 3, 7, 4}}, + %Postgrex.INET{address: {10, 3, 7, 5}} + ]) + end + + test "selects available IPv6 in CIDR range at the offset" do + cidr = string_to_inet("fd00::3:3:0/120") + gateway_ip = string_to_inet("fd00::3:3:3") + offset = 3 + + queryable = next_available_address(cidr, offset, [gateway_ip]) + + assert Repo.one(queryable) == %Postgrex.INET{address: {64_768, 0, 0, 0, 0, 3, 3, 4}} + end + + test "selects nothing when IPv6 CIDR range is exhausted" do + cidr = string_to_inet("fd00::3:2:0/126") + gateway_ip = string_to_inet("fd00::3:2:1") + offset = 3 + + DevicesFixtures.device(%{ipv6: "fd00::3:2:2"}) + + queryable = next_available_address(cidr, offset, [gateway_ip]) + assert is_nil(Repo.one(queryable)) + end + end + + defp string_to_inet(string) do + {:ok, inet} = EctoNetwork.INET.cast(string) + inet + end +end diff --git a/apps/fz_http/test/fz_http/devices_test.exs b/apps/fz_http/test/fz_http/devices_test.exs index 14dc773cd..1e1ffdebd 100644 --- a/apps/fz_http/test/fz_http/devices_test.exs +++ b/apps/fz_http/test/fz_http/devices_test.exs @@ -1,33 +1,10 @@ defmodule FzHttp.DevicesTest do - # XXX: Update the device IP query to be an insert - use FzHttp.DataCase, async: false + use FzHttp.DataCase, async: true + alias FzHttp.Devices alias FzHttp.DevicesFixtures alias FzHttp.Users - describe "trimmed fields" do - test "trims expected fields" do - changeset = - Devices.new_device(%{ - "allowed_ips" => " foo ", - "dns" => " foo ", - "endpoint" => " foo ", - "name" => " foo ", - "description" => " foo " - }) - - assert %Ecto.Changeset{ - changes: %{ - allowed_ips: "foo", - dns: "foo", - endpoint: "foo", - name: "foo", - description: "foo" - } - } = changeset - end - end - describe "count/0" do setup :create_devices @@ -64,38 +41,17 @@ defmodule FzHttp.DevicesTest do describe "create_device/1" do setup [:create_user, :create_device] - setup context do - if ipv4_network = context[:ipv4_network] do - restore_env(:wireguard_ipv4_network, ipv4_network, &on_exit/1) - else - context - end - end - - setup context do - if ipv6_network = context[:ipv6_network] do - restore_env(:wireguard_ipv6_network, ipv6_network, &on_exit/1) - else - context - 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", - user_id: nil + public_key: "CHqFuS+iL3FTog5F4Ceumqlk0CU4Cl/dyUP/9F9NDnI=", + user_id: nil, + ipv4: "100.64.0.2", + ipv6: "fd00::2" } - @tag max_devices: 1 test "prevents creating more than max_devices_per_user", %{device: device} do + FzHttp.Config.maybe_put_env_override(:max_devices_per_user, 1) + assert {:error, %Ecto.Changeset{ errors: [ @@ -113,49 +69,28 @@ defmodule FzHttp.DevicesTest do end test "creates devices with default ipv4", %{device: device} do - assert device.ipv4 == %Postgrex.INET{address: {10, 3, 2, 2}, netmask: 32} + refute is_nil(device.ipv4) end test "creates device with default ipv6", %{device: device} do - assert device.ipv6 == %Postgrex.INET{address: {64_768, 0, 0, 0, 0, 3, 2, 2}, netmask: 128} + refute is_nil(device.ipv6) end - test "generates preshared_key" do - assert String.length(Devices.new_device().changes.preshared_key) == 44 - end - - @tag ipv4_network: "10.3.2.0/30", - errors: [ - ipv4: {"can't be blank", [validation: :required]}, - base: - {"ipv4 address pool is exhausted. Increase network size or remove some devices.", []} - ] - test "sets error when ipv4 address pool is exhausted", %{ - ipv4_network: ipv4_network, - user: user, - errors: errors + test "returns error when device IP can't be assigned due to CIDR pool exhaustion", %{ + device: device } do - restore_env(:wireguard_ipv4_network, ipv4_network, &on_exit/1) + FzHttp.Config.maybe_put_env_override(:wireguard_ipv4_network, "10.3.2.0/30") + attrs = %{@device_attrs | ipv4: nil, ipv6: nil, user_id: device.user_id} - {:error, changeset} = Devices.create_device(%{@device_attrs | user_id: user.id}) - assert errors == changeset.errors + assert {:ok, _device} = Devices.create_device(attrs) + assert {:error, changeset} = Devices.create_device(attrs) + refute changeset.valid? + assert "CIDR 10.3.2.0/30 is exhausted" in errors_on(changeset).base end - @tag ipv6_network: "fd00::3:2:0/126", - errors: [ - ipv6: {"can't be blank", [validation: :required]}, - base: - {"ipv6 address pool is exhausted. Increase network size or remove some devices.", []} - ] - test "sets error when ipv6 address pool is exhausted", %{ - ipv6_network: ipv6_network, - user: user, - errors: errors - } do - restore_env(:wireguard_ipv6_network, ipv6_network, &on_exit/1) - - {:error, changeset} = Devices.create_device(%{@device_attrs | user_id: user.id}) - assert errors == changeset.errors + test "autogenerates preshared_key", %{user: user} do + assert {:ok, device} = Devices.create_device(%{@device_attrs | user_id: user.id}) + assert byte_size(device.preshared_key) == 44 end end @@ -187,11 +122,11 @@ defmodule FzHttp.DevicesTest do @attrs %{ name: "Go hard or go home.", allowed_ips: "0.0.0.0", - use_site_allowed_ips: false + use_default_allowed_ips: false } @valid_dns_attrs %{ - use_site_dns: false, + use_default_dns: false, dns: "1.1.1.1, 1.0.0.1, 2606:4700:4700::1111, 2606:4700:4700::1001" } @@ -200,27 +135,27 @@ defmodule FzHttp.DevicesTest do } @valid_allowed_ips_attrs %{ - use_site_allowed_ips: false, + use_default_allowed_ips: false, allowed_ips: "0.0.0.0/0, ::/0, ::0/0, 192.168.1.0/24" } @valid_endpoint_ipv4_attrs %{ - use_site_endpoint: false, + use_default_endpoint: false, endpoint: "5.5.5.5" } @valid_endpoint_ipv6_attrs %{ - use_site_endpoint: false, + use_default_endpoint: false, endpoint: "fd00::1" } @valid_endpoint_host_attrs %{ - use_site_endpoint: false, + use_default_endpoint: false, endpoint: "valid-endpoint.example.com" } @empty_endpoint_attrs %{ - use_site_endpoint: false, + use_default_endpoint: false, endpoint: "" } @@ -228,46 +163,14 @@ defmodule FzHttp.DevicesTest do 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} + @fields_use_default [ + %{use_default_allowed_ips: true, allowed_ips: "1.1.1.1"}, + %{use_default_dns: true, dns: "1.1.1.1"}, + %{use_default_endpoint: true, endpoint: "1.1.1.1"}, + %{use_default_persistent_keepalive: true, persistent_keepalive: 1}, + %{use_default_mtu: true, mtu: 1000} ] - test "updates device with /32 netmask", %{device: device} do - ipv4 = "10.3.2.9/32" - {:ok, test_device} = Devices.update_device(device, %{ipv4: ipv4}) - assert "#{test_device.ipv4}" == "10.3.2.9" - end - - test "updates device with /128 netmask", %{device: device} do - ipv6 = "fd00::3:2:9/128" - {:ok, test_device} = Devices.update_device(device, %{ipv6: ipv6}) - assert "#{test_device.ipv6}" == "fd00::3:2:9" - end - - test "prevents updating device with ipv4 netmask", %{device: device} do - attrs = %{ipv4: "10.3.2.9/24"} - {:error, changeset} = Devices.update_device(device, attrs) - - assert changeset.errors[:ipv4] == { - "Only IPs without netmask are supported.", - [] - } - end - - test "prevents updating device with ipv6 netmask", %{device: device} do - attrs = %{ipv6: "fd00::3:2:9/120"} - {:error, changeset} = Devices.update_device(device, attrs) - - assert changeset.errors[:ipv6] == { - "Only IPs without netmask are supported.", - [] - } - end - test "updates device", %{device: device} do {:ok, test_device} = Devices.update_device(device, @attrs) assert @attrs = test_device @@ -293,11 +196,11 @@ defmodule FzHttp.DevicesTest do assert @valid_endpoint_host_attrs = test_device end - test "prevents updating fields if use_site_", %{device: device} do - for attrs <- @fields_use_site do + test "prevents updating fields if use_default_", %{device: device} do + for attrs <- @fields_use_default do field = Map.keys(attrs) - |> Enum.filter(fn attr -> !String.starts_with?(Atom.to_string(attr), "use_site_") end) + |> Enum.filter(fn attr -> !String.starts_with?(Atom.to_string(attr), "use_default_") end) |> List.first() assert {:error, changeset} = Devices.update_device(device, attrs) @@ -309,7 +212,7 @@ defmodule FzHttp.DevicesTest do end end - @tag attrs: %{use_site_dns: false, dns: "foobar.com"} + @tag attrs: %{use_default_dns: false, dns: "foobar.com"} test "allows hosts for DNS", %{attrs: attrs, device: device} do assert {:ok, _device} = Devices.update_device(device, attrs) end @@ -345,52 +248,6 @@ defmodule FzHttp.DevicesTest do [] } end - - test "prevents updating ipv4 to out of network", %{device: device} do - {:error, changeset} = Devices.update_device(device, %{ipv4: "172.16.0.1"}) - - assert changeset.errors[:ipv4] == { - "IP must be contained within network 10.3.2.0/24", - [] - } - end - - test "prevents updating ipv6 to out of network", %{device: device} do - {:error, changeset} = Devices.update_device(device, %{ipv6: "fd00::2:1:1"}) - - assert changeset.errors[:ipv6] == { - "IP must be contained within network fd00::3:2:0/120", - [] - } - end - - test "prevents updating ipv4 to wireguard address", %{device: device} do - ip = Application.fetch_env!(:fz_http, :wireguard_ipv4_address) - {:error, changeset} = Devices.update_device(device, %{ipv4: ip}) - - assert changeset.errors[:ipv4] == { - "is reserved", - [ - {:validation, :exclusion}, - {:enum, [%Postgrex.INET{address: {10, 3, 2, 1}, netmask: 32}]} - ] - } - end - - test "prevents updating ipv6 to wireguard address", %{device: device} do - {:error, changeset} = - Devices.update_device(device, %{ - ipv6: Application.fetch_env!(:fz_http, :wireguard_ipv6_address) - }) - - assert changeset.errors[:ipv6] == { - "is reserved", - [ - {:validation, :exclusion}, - {:enum, [%Postgrex.INET{address: {64_768, 0, 0, 0, 0, 3, 2, 1}, netmask: 128}]} - ] - } - end end describe "delete_device/1" do @@ -421,8 +278,7 @@ defmodule FzHttp.DevicesTest do setup [:create_device] test "renders all peers", %{device: device} do - assert Devices.to_peer_list() |> List.first() == %{ - preshared_key: nil, + assert Devices.to_peer_list() |> List.first() |> Map.delete(:preshared_key) == %{ public_key: device.public_key, inet: "#{device.ipv4}/32,#{device.ipv6}/128" } @@ -431,12 +287,12 @@ defmodule FzHttp.DevicesTest do describe "Device.new_name/0,1" do test "retains name with less than or equal to 15 chars" do - assert Devices.Device.new_name("12345") == "12345" - assert Devices.Device.new_name("1234567890ABCDE") == "1234567890ABCDE" + assert Devices.new_name("12345") == "12345" + assert Devices.new_name("1234567890ABCDE") == "1234567890ABCDE" end test "truncates long names that exceed 15 chars" do - assert Devices.Device.new_name("1234567890ABCDEF") == "1234567890A4772" + assert Devices.new_name("1234567890ABCDEF") == "1234567890A4772" end end @@ -446,8 +302,7 @@ defmodule FzHttp.DevicesTest do test "projects expected fields with device", %{device: device, user: user} do user_id = user.id - assert %{ip: "10.3.2.2", ip6: "fd00::3:2:2", user_id: ^user_id} = - Devices.setting_projection(device) + assert %{ip: _, ip6: _, user_id: ^user_id} = Devices.setting_projection(device) end test "projects expected fields with device map", %{device: device, user: user} do @@ -459,8 +314,7 @@ defmodule FzHttp.DevicesTest do |> Map.put(:ipv4, FzHttp.Devices.decode(device.ipv4)) |> Map.put(:ipv6, FzHttp.Devices.decode(device.ipv6)) - assert %{ip: "10.3.2.2", ip6: "fd00::3:2:2", user_id: ^user_id} = - Devices.setting_projection(device_map) + assert %{ip: _, ip6: _, user_id: ^user_id} = Devices.setting_projection(device_map) end end diff --git a/apps/fz_http/test/fz_http/events_test.exs b/apps/fz_http/test/fz_http/events_test.exs index ec8d4fce9..273ffa85c 100644 --- a/apps/fz_http/test/fz_http/events_test.exs +++ b/apps/fz_http/test/fz_http/events_test.exs @@ -26,14 +26,15 @@ defmodule FzHttp.EventsTest do assert :sys.get_state(Events.wall_pid()) == %{ users: MapSet.new(), - devices: MapSet.new([%{ip: "10.3.2.2", ip6: "fd00::3:2:2", user_id: user.id}]), + devices: + MapSet.new([%{ip: "#{device.ipv4}", ip6: "#{device.ipv6}", user_id: user.id}]), rules: MapSet.new() } assert :sys.get_state(Events.vpn_pid()) == %{ - "1" => %{ - allowed_ips: "10.3.2.2/32,fd00::3:2:2/128", - preshared_key: nil + device.public_key => %{ + allowed_ips: "#{device.ipv4}/32,#{device.ipv6}/128", + preshared_key: device.preshared_key } } end diff --git a/apps/fz_http/test/fz_http/queries_test.exs b/apps/fz_http/test/fz_http/queries_test.exs deleted file mode 100644 index 148b448ca..000000000 --- a/apps/fz_http/test/fz_http/queries_test.exs +++ /dev/null @@ -1,84 +0,0 @@ -defmodule FzHttp.QueriesTest do - use FzHttp.DataCase, async: false - - alias FzHttp.Queries.INET - - describe "next_available/1 when none available" do - @expected_ipv4 %Postgrex.INET{address: {10, 3, 2, 2}, netmask: nil} - @expected_ipv6 %Postgrex.INET{address: {64_768, 0, 0, 0, 0, 3, 2, 2}, netmask: nil} - - setup context do - if ipv4_network = context[:ipv4_network] do - restore_env(:wireguard_ipv4_network, ipv4_network, &on_exit/1) - else - context - end - end - - setup context do - if ipv6_network = context[:ipv6_network] do - restore_env(:wireguard_ipv6_network, ipv6_network, &on_exit/1) - else - context - end - end - - @tag ipv4_network: "10.3.2.2/32" - test "when ipv4 network is /32 returns null" do - assert is_nil(INET.next_available(:ipv4)) - end - - @tag ipv6_network: "fd00::3:2:2/128" - test "when ipv6 network is /128 returns null" do - assert is_nil(INET.next_available(:ipv6)) - end - end - - describe "next_available/1 when edge case" do - setup :create_device - - @expected_ipv4 %Postgrex.INET{address: {10, 3, 2, 2}, netmask: 32} - @expected_ipv6 %Postgrex.INET{address: {64_768, 0, 0, 0, 0, 3, 2, 2}, netmask: 128} - - setup context do - if ipv4_network = context[:ipv4_network] do - restore_env(:wireguard_ipv4_network, ipv4_network, &on_exit/1) - else - context - end - end - - setup context do - if ipv6_network = context[:ipv6_network] do - restore_env(:wireguard_ipv6_network, ipv6_network, &on_exit/1) - else - context - end - end - - @tag ipv4_network: "10.3.2.0/30" - test "when ipv4 network is /30 returns null", %{device: device} do - assert device.ipv4 == @expected_ipv4 - assert is_nil(INET.next_available(:ipv4)) - end - - @tag ipv6_network: "fd00::3:2:0/126" - test "when ipv6 network is /126 returns null", %{device: device} do - assert device.ipv6 == @expected_ipv6 - assert is_nil(INET.next_available(:ipv6)) - end - end - - describe "next_available/1 when available" do - @expected_ipv4 %Postgrex.INET{address: {10, 3, 2, 2}, netmask: nil} - @expected_ipv6 %Postgrex.INET{address: {64_768, 0, 0, 0, 0, 3, 2, 2}, netmask: nil} - - test "when ipv4 network is /24 returns 10.3.2.2" do - assert INET.next_available(:ipv4) == @expected_ipv4 - end - - test "when ipv6 network is /120 returns fd00::3:2:2" do - assert INET.next_available(:ipv6) == @expected_ipv6 - end - end -end diff --git a/apps/fz_http/test/fz_http/repo/notifier_test.exs b/apps/fz_http/test/fz_http/repo/notifier_test.exs index 3682ccd41..f19efda10 100644 --- a/apps/fz_http/test/fz_http/repo/notifier_test.exs +++ b/apps/fz_http/test/fz_http/repo/notifier_test.exs @@ -88,16 +88,16 @@ defmodule FzHttp.Repo.NotifierTest do Notifier.handle_event("devices", %{op: "INSERT", row: device}) expected_vpn_state = %{ - "1" => %{ - allowed_ips: "10.3.2.2/32,fd00::3:2:2/128", - preshared_key: nil + device.public_key => %{ + allowed_ips: "#{device.ipv4}/32,#{device.ipv6}/128", + preshared_key: device.preshared_key } } expected_wall_state = %{ users: MapSet.new([]), rules: MapSet.new([]), - devices: MapSet.new([%{ip: "10.3.2.2", ip6: "fd00::3:2:2", user_id: user.id}]) + devices: MapSet.new([%{ip: "#{device.ipv4}", ip6: "#{device.ipv6}", user_id: user.id}]) } assert :sys.get_state(Events.vpn_pid()) == expected_vpn_state diff --git a/apps/fz_http/test/fz_http/rules_test.exs b/apps/fz_http/test/fz_http/rules_test.exs index 9c1c6eea7..056192a83 100644 --- a/apps/fz_http/test/fz_http/rules_test.exs +++ b/apps/fz_http/test/fz_http/rules_test.exs @@ -3,6 +3,13 @@ defmodule FzHttp.RulesTest do alias FzHttp.Rules + setup do + FzHttp.Config.maybe_put_env_override(:wireguard_ipv4_network, "100.64.0.0/10") + FzHttp.Config.maybe_put_env_override(:wireguard_ipv6_network, "fd00::0/106") + + :ok + end + describe "list_rules/0" do setup [:create_rules] @@ -34,7 +41,7 @@ defmodule FzHttp.RulesTest do test "raises error when id does not exist", %{rule: _rule} do assert_raise(Ecto.NoResultsError, fn -> - Rules.get_rule!(0) + Rules.get_rule!(Ecto.UUID.generate()) end) end end @@ -75,7 +82,7 @@ defmodule FzHttp.RulesTest do {:error, changeset} = Rules.create_rule(%{destination: "10.0.0.0/24", port_range: "10-20"}) assert changeset.errors[:port_type] == - {"Please specify a port-range for the given port type", + {"port_type must be specified with port_range", [constraint: :check, constraint_name: "port_range_needs_type"]} end @@ -83,7 +90,7 @@ defmodule FzHttp.RulesTest do {:error, changeset} = Rules.create_rule(%{destination: "10.0.0.0/24", port_type: :tcp}) assert changeset.errors[:port_type] == - {"Please specify a port-range for the given port type", + {"port_type must be specified with port_range", [constraint: :check, constraint_name: "port_range_needs_type"]} end @@ -92,7 +99,7 @@ defmodule FzHttp.RulesTest do Rules.create_rule(%{destination: "10.0.0.0/24", port_type: :tcp, port_range: "10-90000"}) assert changeset.errors[:port_range] == - {"Port is not within valid range", + {"port is not within valid range", [constraint: :check, constraint_name: "port_range_is_within_valid_values"]} end @@ -101,11 +108,45 @@ defmodule FzHttp.RulesTest do Rules.create_rule(%{destination: "10.0.0.0/24", port_type: :tcp, port_range: "20-10"}) assert changeset.errors[:port_range] == - {"Range Error: Lower bound higher than upper bound", + {"lower value cannot be higher than upper value", [type: FzHttp.Int4Range, validation: :cast]} end end + describe "update_rule/2" do + setup [:create_rule] + + @tag params: %{ + "destination" => "123.123.123.123", + "action" => "accept", + "port_type" => "udp", + "port_range" => [1, 65_000] + } + test "updates rule with string params", %{rule: rule, params: params} do + assert {:ok, rule} = Rules.update_rule(rule, params) + assert rule.destination == %Postgrex.INET{address: {123, 123, 123, 123}} + assert rule.action == :accept + assert rule.port_type == :udp + assert rule.port_range == "1 - 65000" + end + + @tag attrs: %{ + destination: "123.123.123.123", + action: "accept", + port_type: "udp", + port_range: [1, 65_000] + } + test "updates rule with atom attrs", %{rule: rule, attrs: attrs} do + assert {:ok, rule} = Rules.update_rule(rule, attrs) + assert rule.destination == %Postgrex.INET{address: {123, 123, 123, 123}} + assert rule.action == :accept + assert rule.port_type == :udp + assert rule.port_range == "1 - 65000" + end + + # XXX: Do we want to allow changing a rule's user_id? + end + describe "delete_rule/1" do setup [:create_rule] diff --git a/apps/fz_http/test/fz_http/sites_test.exs b/apps/fz_http/test/fz_http/sites_test.exs deleted file mode 100644 index b30043742..000000000 --- a/apps/fz_http/test/fz_http/sites_test.exs +++ /dev/null @@ -1,105 +0,0 @@ -defmodule FzHttp.SitesTest do - use FzHttp.DataCase - - alias FzHttp.Sites.Site - alias FzHttp.Sites - import FzHttp.SitesFixtures - - describe "trimmed fields" do - test "trims expected fields" do - changeset = - Sites.new_site(%{ - "allowed_ips" => " foo ", - "dns" => " foo ", - "endpoint" => " foo ", - "name" => " foo " - }) - - assert %Ecto.Changeset{ - changes: %{ - allowed_ips: "foo", - dns: "foo", - endpoint: "foo", - name: "foo" - } - } = changeset - end - end - - describe "update_site/2 with name-based dns" do - setup do - {:ok, site: site_fixture()} - end - - @tag attrs: %{dns: "foobar.com"} - test "update_site/2 allows hosts for DNS", %{site: site, attrs: attrs} do - assert {:ok, _site} = Sites.update_site(site, attrs) - end - - @tag attrs: %{dns: "foobar.com, google.com"} - test "update_site/2 allows list hosts for DNS", %{site: site, attrs: attrs} do - assert {:ok, _site} = Sites.update_site(site, attrs) - end - end - - describe "sites" do - @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" => "1280" - } - ] - @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/telemetry_test.exs b/apps/fz_http/test/fz_http/telemetry_test.exs index 33a48e1a4..e27e19dc7 100644 --- a/apps/fz_http/test/fz_http/telemetry_test.exs +++ b/apps/fz_http/test/fz_http/telemetry_test.exs @@ -40,65 +40,68 @@ defmodule FzHttp.TelemetryTest do end describe "auth" do - setup context do - if context[:config] do - {key, value} = context[:config] - restore_env(key, value, &on_exit/1) - else - context - end - end - test "count openid providers" do + FzHttp.Configurations.put!( + :openid_connect_providers, + FzHttp.ConfigurationsFixtures.openid_connect_providers_attrs() + ) + ping_data = Telemetry.ping_data() assert ping_data[:openid_providers] == 7 end - @tag config: {:disable_vpn_on_oidc_error, true} test "disable vpn on oidc error enabled" do + FzHttp.Configurations.put!(:disable_vpn_on_oidc_error, true) + ping_data = Telemetry.ping_data() assert ping_data[:disable_vpn_on_oidc_error] end - @tag config: {:disable_vpn_on_oidc_error, false} test "disable vpn on oidc error disabled" do + FzHttp.Configurations.put!(:disable_vpn_on_oidc_error, false) + ping_data = Telemetry.ping_data() refute ping_data[:disable_vpn_on_oidc_error] end - @tag config: {:local_auth_enabled, true} test "local authentication enabled" do + FzHttp.Configurations.put!(:local_auth_enabled, true) + ping_data = Telemetry.ping_data() assert ping_data[:local_authentication] end - @tag config: {:local_auth_enabled, false} test "local authentication disabled" do + FzHttp.Configurations.put!(:local_auth_enabled, false) + ping_data = Telemetry.ping_data() refute ping_data[:local_authentication] end - @tag config: {:allow_unprivileged_device_management, true} test "unprivileged device management enabled" do + FzHttp.Configurations.put!(:allow_unprivileged_device_management, true) + ping_data = Telemetry.ping_data() assert ping_data[:unprivileged_device_management] end - @tag config: {:allow_unprivileged_device_configuration, true} test "unprivileged device configuration enabled" do + FzHttp.Configurations.put!(:allow_unprivileged_device_configuration, true) + ping_data = Telemetry.ping_data() assert ping_data[:unprivileged_device_configuration] end - @tag config: {:allow_unprivileged_device_configuration, false} test "unprivileged device configuration disabled" do + FzHttp.Configurations.put!(:allow_unprivileged_device_configuration, false) + ping_data = Telemetry.ping_data() refute ping_data[:unprivileged_device_configuration] @@ -106,33 +109,33 @@ defmodule FzHttp.TelemetryTest do end describe "database" do - setup context do - restore_env(FzHttp.Repo, context[:db_config], &on_exit/1) - end - - @tag db_config: [hostname: "localhost"] test "local hostname" do + FzHttp.Config.maybe_put_env_override(FzHttp.Repo, hostname: "localhost") + ping_data = Telemetry.ping_data() refute ping_data[:external_database] end - @tag db_config: [url: "postgres://127.0.0.1"] test "local url" do + FzHttp.Config.maybe_put_env_override(FzHttp.Repo, url: "postgres://127.0.0.1") + ping_data = Telemetry.ping_data() refute ping_data[:external_database] end - @tag db_config: [hostname: "firezone.dev"] test "external hostname" do + FzHttp.Config.maybe_put_env_override(FzHttp.Repo, hostname: "firezone.dev") + ping_data = Telemetry.ping_data() assert ping_data[:external_database] end - @tag db_config: [url: "postgres://firezone.dev"] test "external url" do + FzHttp.Config.maybe_put_env_override(FzHttp.Repo, url: "postgres://firezone.dev") + ping_data = Telemetry.ping_data() assert ping_data[:external_database] @@ -140,19 +143,17 @@ defmodule FzHttp.TelemetryTest do end describe "email" do - setup context do - restore_env(FzHttpWeb.Mailer, [from_email: context[:from_email]], &on_exit/1) - end - - @tag from_email: "test@firezone.dev" test "outbound set" do + FzHttp.Config.maybe_put_env_override(FzHttpWeb.Mailer, from_email: "test@firezone.dev") + ping_data = Telemetry.ping_data() assert ping_data[:outbound_email] end - @tag from_email: nil test "outbound unset" do + FzHttp.Config.maybe_put_env_override(FzHttpWeb.Mailer, from_email: nil) + ping_data = Telemetry.ping_data() refute ping_data[:outbound_email] diff --git a/apps/fz_http/test/fz_http/users_test.exs b/apps/fz_http/test/fz_http/users_test.exs index 5ddf46e89..0f1f5d06a 100644 --- a/apps/fz_http/test/fz_http/users_test.exs +++ b/apps/fz_http/test/fz_http/users_test.exs @@ -78,7 +78,7 @@ defmodule FzHttp.UsersTest do test "raises Ecto.NoResultsError for missing Users", %{user: _user} do assert_raise(Ecto.NoResultsError, fn -> - Users.get_user!(0) + Users.get_user!(Ecto.UUID.generate()) end) end end @@ -91,7 +91,7 @@ defmodule FzHttp.UsersTest do end test "returns nil if not found" do - assert nil == Users.get_user(0) + assert nil == Users.get_user(Ecto.UUID.generate()) end end diff --git a/apps/fz_http/test/fz_http_web/controllers/auth_controller_test.exs b/apps/fz_http/test/fz_http_web/controllers/auth_controller_test.exs index f79befacf..5e7c0144e 100644 --- a/apps/fz_http/test/fz_http_web/controllers/auth_controller_test.exs +++ b/apps/fz_http/test/fz_http_web/controllers/auth_controller_test.exs @@ -1,8 +1,21 @@ defmodule FzHttpWeb.AuthControllerTest do use FzHttpWeb.ConnCase, async: true - import Mox + setup do + FzHttp.Configurations.put!( + :openid_connect_providers, + FzHttp.ConfigurationsFixtures.openid_connect_providers_attrs() + ) + + FzHttp.Configurations.put!( + :saml_identity_providers, + [FzHttp.SAMLIdentityProviderFixtures.saml_attrs() |> Map.put("label", "SAML")] + ) + + %{} + end + describe "new" do setup [:create_user] @@ -11,10 +24,16 @@ defmodule FzHttpWeb.AuthControllerTest do test_conn = get(conn, ~p"/") - # Assert that we email, OIDC and Oauth2 buttons provided + # Assert that we have email, OIDC and Oauth2 buttons provided for expected <- [ "Sign in with email", "Sign in with OIDC Google", + "Sign in with OIDC Okta", + "Sign in with OIDC Auth0", + "Sign in with OIDC Azure", + "Sign in with OIDC Onelogin", + "Sign in with OIDC Keycloak", + "Sign in with OIDC Vault", "Sign in with SAML" ] do assert html_response(test_conn, 200) =~ expected @@ -37,6 +56,10 @@ defmodule FzHttpWeb.AuthControllerTest do describe "create session" do setup [:create_user] + test "GET /auth/identity/callback redirects to /", %{unauthed_conn: conn} do + assert redirected_to(get(conn, ~p"/auth/identity/callback")) == ~p"/" + end + test "invalid email", %{unauthed_conn: conn} do params = %{ "email" => "invalid@test", @@ -83,7 +106,7 @@ defmodule FzHttpWeb.AuthControllerTest do "password" => "password1234" } - restore_env(:local_auth_enabled, false, &on_exit/1) + FzHttp.Configurations.put!(:local_auth_enabled, false) test_conn = post(conn, ~p"/auth/identity/callback", params) assert text_response(test_conn, 401) == "Local auth disabled" @@ -221,7 +244,7 @@ defmodule FzHttpWeb.AuthControllerTest do end test "prevents signing in when local_auth_disabled", %{unauthed_conn: conn, user: user} do - restore_env(:local_auth_enabled, false, &on_exit/1) + FzHttp.Configurations.put!(:local_auth_enabled, false) test_conn = get(conn, ~p"/auth/magic/#{user.sign_in_token}") assert text_response(test_conn, 401) == "Local auth disabled" @@ -259,7 +282,7 @@ defmodule FzHttpWeb.AuthControllerTest do test "redirects to oidc auth uri", %{unauthed_conn: conn} do expect(OpenIDConnect.Mock, :authorization_uri, fn provider, _ -> case provider do - :google -> @oidc_auth_uri + "google" -> @oidc_auth_uri end end) diff --git a/apps/fz_http/test/fz_http_web/controllers/json/configuration_controller_test.exs b/apps/fz_http/test/fz_http_web/controllers/json/configuration_controller_test.exs new file mode 100644 index 000000000..2afbc933b --- /dev/null +++ b/apps/fz_http/test/fz_http_web/controllers/json/configuration_controller_test.exs @@ -0,0 +1,31 @@ +defmodule FzHttpWeb.JSON.ConfigurationControllerTest do + use FzHttpWeb.ConnCase, async: true + @moduletag api: true + + describe "show configuration" do + test "renders configuration", %{api_conn: conn} do + conn = get(conn, ~p"/v0/configuration") + assert json_response(conn, 200)["data"] + end + end + + describe "update configuration" do + test "renders configuration when data is valid", %{api_conn: conn} do + conn = put(conn, ~p"/v0/configuration", configuration: %{"local_auth_enabled" => true}) + + assert %{"local_auth_enabled" => true} = json_response(conn, 200)["data"] + assert FzHttp.Configurations.get!(:local_auth_enabled) == true + + conn = put(conn, ~p"/v0/configuration", configuration: %{"local_auth_enabled" => false}) + + assert %{"local_auth_enabled" => false} = json_response(conn, 200)["data"] + assert FzHttp.Configurations.get!(:local_auth_enabled) == false + end + + test "renders errors when data is invalid", %{api_conn: conn} do + conn = put(conn, ~p"/v0/configuration", configuration: %{"local_auth_enabled" => 123}) + + assert json_response(conn, 422)["errors"] == %{"local_auth_enabled" => ["is invalid"]} + end + end +end diff --git a/apps/fz_http/test/fz_http_web/controllers/json/device_controller_test.exs b/apps/fz_http/test/fz_http_web/controllers/json/device_controller_test.exs new file mode 100644 index 000000000..9787d3575 --- /dev/null +++ b/apps/fz_http/test/fz_http_web/controllers/json/device_controller_test.exs @@ -0,0 +1,80 @@ +defmodule FzHttpWeb.JSON.DeviceControllerTest do + use FzHttpWeb.ConnCase, async: true + @moduletag api: true + + describe "show device" do + setup :create_device + + test "shows device", %{api_conn: conn, device: %{id: id}} do + conn = get(conn, ~p"/v0/devices/#{id}") + assert %{"id" => ^id} = json_response(conn, 200)["data"] + end + end + + describe "create device" do + @params %{ + "name" => "create-name", + "description" => "create-description", + "public_key" => "CHqFuS+iL3FTog5F4Ceumqlk0CU4Cl/dyUP/9F9NDnI=", + "preshared_key" => "CHqFuS+iL3FTog5F4Ceumqlk0CU4Cl/dyUP/9F9NDnI=", + "use_default_allowed_ips" => false, + "use_default_dns" => false, + "use_default_endpoint" => false, + "use_default_mtu" => false, + "use_default_persistent_keepalive" => false, + "endpoint" => "9.9.9.9", + "mtu" => 999, + "persistent_keepalive" => 9, + "allowed_ips" => "0.0.0.0/0, ::/0, 1.1.1.1", + "dns" => "9.9.9.8", + "ipv4" => "100.64.0.2", + "ipv6" => "fd00::2" + } + + @tag params: @params + test "creates device", %{api_conn: conn, unprivileged_user: %{id: id}, params: params} do + conn = post(conn, ~p"/v0/devices", device: Map.merge(params, %{"user_id" => id})) + assert @params = json_response(conn, 201)["data"] + end + end + + describe "update device" do + setup :create_device + + @tag params: %{ + "name" => "json-update-device" + } + test "updates device", %{api_conn: conn, params: params, device: %{id: id}} do + conn = put(conn, ~p"/v0/devices/#{id}", device: params) + assert %{"id" => ^id} = json_response(conn, 200)["data"] + + conn = get(conn, ~p"/v0/devices/#{id}") + + assert %{ + "name" => "json-update-device" + } = json_response(conn, 200)["data"] + end + end + + describe "list devices" do + setup :create_devices + + test "lists devices", %{api_conn: conn, devices: devices} do + conn = get(conn, ~p"/v0/devices") + assert length(json_response(conn, 200)["data"]) == length(devices) + end + end + + describe "delete device" do + setup :create_device + + test "deletes device", %{api_conn: conn, device: device} do + conn = delete(conn, ~p"/v0/devices/#{device}") + assert response(conn, 204) + + assert_error_sent 404, fn -> + get(conn, ~p"/v0/devices/#{device}") + end + end + end +end diff --git a/apps/fz_http/test/fz_http_web/controllers/json/rule_controller_test.exs b/apps/fz_http/test/fz_http_web/controllers/json/rule_controller_test.exs new file mode 100644 index 000000000..34e7d33e0 --- /dev/null +++ b/apps/fz_http/test/fz_http_web/controllers/json/rule_controller_test.exs @@ -0,0 +1,64 @@ +defmodule FzHttpWeb.JSON.RuleControllerTest do + use FzHttpWeb.ConnCase, async: true + @moduletag api: true + + @rule_params %{ + "destination" => "5.5.5.5/24", + "action" => "accept", + "port_type" => "tcp", + "port_range" => "1 - 65000" + } + + describe "show rule" do + setup :create_rule + + test "shows rule", %{api_conn: conn, rule: %{id: id}} do + conn = get(conn, ~p"/v0/rules/#{id}") + assert %{"id" => ^id} = json_response(conn, 200)["data"] + end + end + + describe "create rule" do + @tag params: @rule_params + test "creates rule", %{api_conn: conn, unprivileged_user: user, params: params} do + conn = post(conn, ~p"/v0/rules", rule: Map.merge(params, %{"user_id" => user.id})) + assert @rule_params = json_response(conn, 201)["data"] + end + end + + describe "update rule" do + setup :create_rule + + @tag params: @rule_params + test "updates rule", %{api_conn: conn, params: params, rule: %{id: id}} do + conn = put(conn, ~p"/v0/rules/#{id}", rule: params) + assert %{"id" => ^id} = json_response(conn, 200)["data"] + + conn = get(conn, ~p"/v0/rules/#{id}") + + assert @rule_params = json_response(conn, 200)["data"] + end + end + + describe "list rules" do + setup :create_rules + + test "lists rules", %{api_conn: conn, rules: rules} do + conn = get(conn, ~p"/v0/rules") + assert length(json_response(conn, 200)["data"]) == length(rules) + end + end + + describe "delete rule" do + setup :create_rule + + test "deletes rule", %{api_conn: conn, rule: rule} do + conn = delete(conn, ~p"/v0/rules/#{rule}") + assert response(conn, 204) + + assert_error_sent 404, fn -> + get(conn, ~p"/v0/rules/#{rule}") + end + end + end +end diff --git a/apps/fz_http/test/fz_http_web/controllers/json/user_controller_test.exs b/apps/fz_http/test/fz_http_web/controllers/json/user_controller_test.exs new file mode 100644 index 000000000..b6d782095 --- /dev/null +++ b/apps/fz_http/test/fz_http_web/controllers/json/user_controller_test.exs @@ -0,0 +1,90 @@ +defmodule FzHttpWeb.JSON.UserControllerTest do + use FzHttpWeb.ConnCase, async: true + @moduletag api: true + + alias FzHttp.{ + Users, + Users.User + } + + @create_attrs %{ + "email" => "test@test.com", + "password" => "test1234test", + "password_confirmation" => "test1234test" + } + @update_attrs %{ + "email" => "test2@test.com" + } + @invalid_attrs %{ + "email" => "test@test.com", + "password" => "test1234" + } + + describe "index" do + test "lists all users", %{api_conn: conn} do + conn = get(conn, ~p"/v0/users") + + actual = + Users.list_users() + |> Enum.map(fn u -> u.id end) + |> Enum.sort() + + expected = + json_response(conn, 200)["data"] + |> Enum.map(fn m -> m["id"] end) + |> Enum.sort() + + assert actual == expected + end + end + + describe "create user" do + test "renders user when data is valid", %{api_conn: conn} do + conn = post(conn, ~p"/v0/users", user: @create_attrs) + assert %{"id" => id} = json_response(conn, 201)["data"] + + conn = get(conn, ~p"/v0/users/#{id}") + + assert %{ + "id" => ^id + } = json_response(conn, 200)["data"] + end + + test "renders errors when data is invalid", %{api_conn: conn} do + conn = post(conn, ~p"/v0/users", user: @invalid_attrs) + assert json_response(conn, 422)["errors"] != %{} + end + end + + describe "update user" do + test "renders user when data is valid", %{ + api_conn: conn, + unprivileged_user: %User{id: id} = user + } do + conn = put(conn, ~p"/v0/users/#{user}", user: @update_attrs) + assert %{"id" => ^id} = json_response(conn, 200)["data"] + + conn = get(conn, ~p"/v0/users/#{id}") + + assert %{ + "id" => ^id + } = json_response(conn, 200)["data"] + end + + test "renders errors when data is invalid", %{api_conn: conn, unprivileged_user: user} do + conn = put(conn, ~p"/v0/users/#{user}", user: @invalid_attrs) + assert json_response(conn, 422)["errors"] != %{} + end + end + + describe "delete user" do + test "deletes chosen user", %{api_conn: conn, unprivileged_user: user} do + conn = delete(conn, ~p"/v0/users/#{user}") + assert response(conn, 204) + + assert_error_sent 404, fn -> + get(conn, ~p"/v0/users/#{user}") + end + end + end +end diff --git a/apps/fz_http/test/fz_http_web/authentication_test.exs b/apps/fz_http/test/fz_http_web/html_authentication_test.exs similarity index 89% rename from apps/fz_http/test/fz_http_web/authentication_test.exs rename to apps/fz_http/test/fz_http_web/html_authentication_test.exs index 76949d4f9..1b50644af 100644 --- a/apps/fz_http/test/fz_http_web/authentication_test.exs +++ b/apps/fz_http/test/fz_http_web/html_authentication_test.exs @@ -1,7 +1,7 @@ -defmodule FzHttpWeb.AuthenticationTest do +defmodule FzHttpWeb.HTMLAuthenticationTest do use FzHttpWeb.ConnCase, async: true - alias FzHttpWeb.Authentication + alias FzHttpWeb.Auth.HTML.Authentication describe "authenticate/2" do setup :create_user 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 index f67fc3999..3f727d14f 100644 --- 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 @@ -1,5 +1,5 @@ defmodule FzHttpWeb.DeviceLive.Admin.IndexTest do - use FzHttpWeb.ConnCase, async: false + use FzHttpWeb.ConnCase, async: true describe "authenticated/device list" do setup :create_devices 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 index a10a45ec0..70f546fee 100644 --- 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 @@ -1,5 +1,5 @@ defmodule FzHttpWeb.DeviceLive.Unprivileged.IndexTest do - use FzHttpWeb.ConnCase, async: false + use FzHttpWeb.ConnCase, async: true describe "authenticated/device list" do test "includes the device name in the list", %{ @@ -28,7 +28,8 @@ defmodule FzHttpWeb.DeviceLive.Unprivileged.IndexTest do describe "authenticated device management disabled" do setup do - restore_env(:allow_unprivileged_device_management, false, &on_exit/1) + FzHttp.Configurations.put!(:allow_unprivileged_device_management, false) + :ok end test "prevents navigating to /user_devices/new", %{unprivileged_conn: conn} do @@ -48,19 +49,20 @@ defmodule FzHttpWeb.DeviceLive.Unprivileged.IndexTest do describe "authenticated device configuration disabled" do setup do - restore_env(:allow_unprivileged_device_configuration, false, &on_exit/1) + FzHttp.Configurations.put!(:allow_unprivileged_device_configuration, false) + :ok end @tag fields: ~w( - use_site_allowed_ips + use_default_allowed_ips allowed_ips - use_site_dns + use_default_dns dns - use_site_endpoint + use_default_endpoint endpoint - use_site_mtu + use_default_mtu mtu - use_site_persistent_keepalive + use_default_persistent_keepalive persistent_keepalive ipv4 ipv6 @@ -88,36 +90,6 @@ defmodule FzHttpWeb.DeviceLive.Unprivileged.IndexTest do assert html =~ "device[#{field}]" end end - - @tag params: %{"device" => %{"public_key" => "test-pubkey", "name" => "test-tunnel"}}, - error: "ipv4 address pool is exhausted. Increase network size or remove some devices." - test "Displays base error when IPv4 pool is exhausted", - %{params: params, unprivileged_conn: conn, error: error} do - path = ~p"/user_devices/new" - {:ok, view, _html} = live(conn, path) - - # A pool of size 1 is always exhausted - restore_env(:wireguard_ipv4_network, "10.0.0.1/32", &on_exit/1) - - assert view - |> element("#create-device") - |> render_submit(params) =~ error - end - - @tag params: %{"device" => %{"public_key" => "test-pubkey", "name" => "test-tunnel"}}, - error: "ipv6 address pool is exhausted. Increase network size or remove some devices." - test "Displays base error when IPv6 pool is exhausted", - %{params: params, unprivileged_conn: conn, error: error} do - path = ~p"/user_devices/new" - {:ok, view, _html} = live(conn, path) - - # A pool of size 1 is always exhausted - restore_env(:wireguard_ipv6_network, "fd00::3:2:0/128", &on_exit/1) - - assert view - |> element("#create-device") - |> render_submit(params) =~ error - end end describe "authenticated/creates device" do @@ -139,7 +111,12 @@ defmodule FzHttpWeb.DeviceLive.Unprivileged.IndexTest do new_view = view |> element("#create-device") - |> render_submit(%{"device" => %{"public_key" => "test-pubkey", "name" => "test-tunnel"}}) + |> render_submit(%{ + "device" => %{ + "public_key" => "8IkpsAXiqhqNdc9PJS76YeJjig4lyTBaf8Rm7gTApXk=", + "name" => "test-tunnel" + } + }) assert new_view =~ "Device added!" end diff --git a/apps/fz_http/test/fz_http_web/live/device_live/unprivileged/show_test.exs b/apps/fz_http/test/fz_http_web/live/device_live/unprivileged/show_test.exs index 36c40f9b3..3aca4ea09 100644 --- a/apps/fz_http/test/fz_http_web/live/device_live/unprivileged/show_test.exs +++ b/apps/fz_http/test/fz_http_web/live/device_live/unprivileged/show_test.exs @@ -42,7 +42,8 @@ defmodule FzHttpWeb.DeviceLive.Unprivileged.ShowTest do unprivileged_conn: conn } do {:ok, device: device} = create_device(user_id: user.id) - restore_env(:allow_unprivileged_device_management, false, &on_exit/1) + + FzHttp.Configurations.put!(:allow_unprivileged_device_management, false) path = ~p"/user_devices/#{device}" {:ok, _view, html} = live(conn, path) diff --git a/apps/fz_http/test/fz_http_web/live/setting_live/client_defaults_test.exs b/apps/fz_http/test/fz_http_web/live/setting_live/client_defaults_test.exs new file mode 100644 index 000000000..b4545b923 --- /dev/null +++ b/apps/fz_http/test/fz_http_web/live/setting_live/client_defaults_test.exs @@ -0,0 +1,168 @@ +defmodule FzHttpWeb.SettingLive.ClientDefaultsTest do + use FzHttpWeb.ConnCase, async: true + + alias FzHttp.Configurations + + describe "authenticated/client_defaults" do + @valid_allowed_ips %{ + "configuration" => %{"default_client_allowed_ips" => "1.1.1.1"} + } + @valid_dns %{ + "configuration" => %{"default_client_dns" => "1.1.1.1"} + } + @valid_endpoint %{ + "configuration" => %{"default_client_endpoint" => "1.1.1.1"} + } + @valid_persistent_keepalive %{ + "configuration" => %{"default_client_persistent_keepalive" => "1"} + } + @valid_mtu %{ + "configuration" => %{"default_client_mtu" => "1000"} + } + + @invalid_allowed_ips %{ + "configuration" => %{"default_client_allowed_ips" => "foobar"} + } + @invalid_persistent_keepalive %{ + "configuration" => %{"default_client_persistent_keepalive" => "-1"} + } + @invalid_mtu %{ + "configuration" => %{"default_client_mtu" => "0"} + } + + setup %{admin_conn: conn} do + path = ~p"/settings/client_defaults" + {:ok, view, html} = live(conn, path) + + %{html: html, view: view} + end + + test "renders current configuration", %{html: html} do + assert html =~ Configurations.get_configuration!().default_client_allowed_ips + assert html =~ Configurations.get_configuration!().default_client_dns + + assert html =~ """ + id="client_defaults_form_component_default_client_endpoint"\ + """ + + assert html =~ """ + id="client_defaults_form_component_default_client_persistent_keepalive"\ + """ + end + + test "updates default client allowed_ips", %{view: view} do + test_view = + view + |> element("#client_defaults_form_component") + |> render_submit(@valid_allowed_ips) + + refute test_view =~ "is invalid" + + assert test_view =~ """ + \ + """ + end + + test "updates default client dns", %{view: view} do + test_view = + view + |> element("#client_defaults_form_component") + |> render_submit(@valid_dns) + + refute test_view =~ "is invalid" + + assert test_view =~ """ + \ + """ + end + + test "updates default client endpoint", %{view: view} do + test_view = + view + |> element("#client_defaults_form_component") + |> render_submit(@valid_endpoint) + + refute test_view =~ "is invalid" + + assert test_view =~ """ + \ + """ + end + + test "updates default client persistent_keepalive", %{view: view} do + test_view = + view + |> element("#client_defaults_form_component") + |> render_submit(@valid_persistent_keepalive) + + refute test_view =~ "is invalid" + + assert test_view =~ """ + \ + """ + end + + test "updates default client mtu", %{view: view} do + test_view = + view + |> element("#client_defaults_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("#client_defaults_form_component") + |> render_submit(@invalid_allowed_ips) + + assert test_view =~ "is invalid" + + assert test_view =~ """ + \ + """ + end + + test "prevents invalid persistent_keepalive", %{view: view} do + test_view = + view + |> element("#client_defaults_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("#client_defaults_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 = ~p"/settings/client_defaults" + expected_path = ~p"/" + assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path) + end + end +end diff --git a/apps/fz_http/test/fz_http_web/live/setting_live/customization_test.exs b/apps/fz_http/test/fz_http_web/live/setting_live/customization_test.exs index 4ef4a1dae..af7f22b3f 100644 --- a/apps/fz_http/test/fz_http_web/live/setting_live/customization_test.exs +++ b/apps/fz_http/test/fz_http_web/live/setting_live/customization_test.exs @@ -1,17 +1,9 @@ defmodule FzHttpWeb.SettingLive.CustomizationTest do use FzHttpWeb.ConnCase, async: true - alias FzHttp.Configurations, as: Conf - describe "logo" do setup %{admin_conn: conn} = context do - Conf.update_configuration(%{logo: context[:logo]}) - - on_exit(fn -> - # this is required because configuration is automatically reset (rolled back) - # after each run, but persistent terms are not. we need to manually reset it here. - Conf.Cache.put!(:logo, nil) - end) + FzHttp.Configurations.put!(:logo, context[:logo]) path = ~p"/settings/customization" {:ok, view, html} = live(conn, path) @@ -24,12 +16,12 @@ defmodule FzHttpWeb.SettingLive.CustomizationTest do assert html =~ ~s|value="Default" checked| end - @tag logo: %{"url" => "test"} + @tag logo: %{url: "test"} test "show url", %{html: html} do assert html =~ ~s|value="URL" checked| end - @tag logo: %{"data" => "test", "type" => "test"} + @tag logo: %{data: "test", type: "test"} test "show upload", %{html: html} do assert html =~ ~s|value="Upload" checked| end @@ -52,13 +44,13 @@ defmodule FzHttpWeb.SettingLive.CustomizationTest do |> render_click() =~ ~s| "test"} + @tag logo: %{url: "test"} test "reset to default", %{view: view, html: html} do html =~ ~s| element("input[value=Default]") |> render_click() view |> element("form") |> render_submit() - assert nil == Conf.get!(:logo) + assert FzHttp.Configurations.get!(:logo) == nil end test "change to url", %{view: view, html: html} do @@ -66,7 +58,7 @@ defmodule FzHttpWeb.SettingLive.CustomizationTest do view |> element("input[value=URL]") |> render_click() view |> render_submit("save", %{"url" => "new"}) - assert %{url: "new"} == Conf.get!(:logo) + assert %{url: "new"} = FzHttp.Configurations.get!(:logo) end test "change to upload", %{view: view, html: html} do @@ -86,7 +78,9 @@ defmodule FzHttpWeb.SettingLive.CustomizationTest do |> render_upload("logo.jpeg") view |> render_submit("save", %{}) - assert %{data: Base.encode64("new"), type: "image/jpeg"} == Conf.get!(:logo) + + data = Base.encode64("new") + assert %{data: ^data, type: "image/jpeg"} = FzHttp.Configurations.get!(:logo) end end end diff --git a/apps/fz_http/test/fz_http_web/live/setting_live/security_test.exs b/apps/fz_http/test/fz_http_web/live/setting_live/security_test.exs index 734e4ba47..2c3adff81 100644 --- a/apps/fz_http/test/fz_http_web/live/setting_live/security_test.exs +++ b/apps/fz_http/test/fz_http_web/live/setting_live/security_test.exs @@ -1,9 +1,9 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do - use FzHttpWeb.ConnCase, async: false + use FzHttpWeb.ConnCase, async: true - alias FzHttp.Configurations, as: Conf + alias FzHttp.Configurations alias FzHttpWeb.SettingLive.Security - import FzHttp.SAMLConfigFixtures + import FzHttp.SAMLIdentityProviderFixtures describe "authenticated mount" do test "loads the active sessions table", %{admin_conn: conn} do @@ -18,7 +18,8 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do {:ok, _view, html} = live(conn, path) assert html =~ ~s|| - FzHttp.Sites.get_site!() |> FzHttp.Sites.update_site(%{vpn_session_duration: 3_600}) + Configurations.get_configuration!() + |> Configurations.update_configuration(%{vpn_session_duration: 3_600}) {:ok, _view, html} = live(conn, path) assert html =~ ~s|| @@ -51,48 +52,64 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do end describe "toggles" do - setup %{config: config, config_val: config_val} do - Conf.update_configuration(%{config => config_val}) - - Conf.Cache.init([]) + import FzHttp.ConfigurationsFixtures + setup %{conf_key: key, conf_val: val} do + FzHttp.Configurations.put!(key, val) {:ok, path: ~p"/settings/security"} end - for {t, val} <- [ - {:local_auth_enabled, true}, - {:allow_unprivileged_device_management, true}, - {:allow_unprivileged_device_configuration, true}, - {:disable_vpn_on_oidc_error, true}, - {:local_auth_enabled, nil}, - {:allow_unprivileged_device_management, nil}, - {:allow_unprivileged_device_configuration, nil}, - {:disable_vpn_on_oidc_error, nil} + for {key, val} <- [ + local_auth_enabled: true, + allow_unprivileged_device_management: true, + allow_unprivileged_device_configuration: true, + disable_vpn_on_oidc_error: true ] do - @tag [config: t, config_val: val] - test "toggle #{t} when value in db is #{val}", %{admin_conn: conn, path: path} do + @tag conf_key: key, conf_val: val + test "toggle #{key} when value in db is true", %{admin_conn: conn, path: path} do {:ok, view, _html} = live(conn, path) - html = view |> element("input[phx-value-config=#{unquote(t)}]") |> render() + html = view |> element("input[phx-value-config=#{unquote(key)}}]") |> render() assert html =~ "checked" - view |> element("input[phx-value-config=#{unquote(t)}]") |> render_click() - assert Conf.get!(unquote(t)) == false + view |> element("input[phx-value-config=#{unquote(key)}]") |> render_click() + assert FzHttp.Configurations.get!(unquote(key)) == false + end + end + for {key, val} <- [ + local_auth_enabled: nil, + allow_unprivileged_device_management: nil, + allow_unprivileged_device_configuration: nil, + disable_vpn_on_oidc_error: nil + ] do + @tag conf_key: key, conf_val: val + test "toggle #{key} when value in db is nil", %{admin_conn: conn, path: path} do {:ok, view, _html} = live(conn, path) - html = view |> element("input[phx-value-config=#{unquote(t)}]") |> render() + html = view |> element("input[phx-value-config=#{unquote(key)}]") |> render() refute html =~ "checked" - view |> element("input[phx-value-config=#{unquote(t)}]") |> render_click() - assert Conf.get!(unquote(t)) == true + view |> element("input[phx-value-config=#{unquote(key)}]") |> render_click() + assert FzHttp.Configurations.get!(unquote(key)) == true end end end describe "oidc configuration" do + import FzHttp.ConfigurationsFixtures + setup %{admin_conn: conn} do - Conf.update_configuration(%{ - openid_connect_providers: %{"test" => %{"label" => "test123"}}, - saml_identity_providers: %{} + configuration(%{ + openid_connect_providers: [ + %{ + "id" => "test", + "label" => "test123", + "client_id" => "foo", + "client_secret" => "bar", + "discovery_document_uri" => + "https://common.auth0.com/.well-known/openid-configuration" + } + ], + saml_identity_providers: [] }) path = ~p"/settings/security" @@ -124,16 +141,27 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do |> element("a", "Edit") |> render_click() - html = + return = view - |> element("#oidc-form") + |> form("#oidc-form") |> render_submit(%{"label" => "updated"}) - # stays on the modal - assert html =~ ~s|| + assert {:error, {:redirect, _}} = return - # not updated - assert Conf.get!(:openid_connect_providers) == %{"test" => %{"label" => "test123"}} + assert FzHttp.Configurations.get!(:openid_connect_providers) == [ + %FzHttp.Configurations.Configuration.OpenIDConnectProvider{ + id: "test", + label: "test123", + scope: "openid email profile", + response_type: "code", + client_id: "foo", + client_secret: "bar", + discovery_document_uri: + "https://common.auth0.com/.well-known/openid-configuration", + redirect_uri: nil, + auto_create_users: true + } + ] end test "delete", %{view: view} do @@ -141,15 +169,18 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do |> element("button", "Delete") |> render_click() - assert Conf.get!(:openid_connect_providers) == %{} + assert FzHttp.Configurations.get!(:openid_connect_providers) == [] end end describe "saml configuration" do + import FzHttp.ConfigurationsFixtures + setup %{admin_conn: conn} do - Conf.update_configuration(%{ - openid_connect_providers: %{}, - saml_identity_providers: %{"test" => saml_attrs()} + # Security views use the DB config, not cached config, so update DB here for testing + configuration(%{ + openid_connect_providers: [], + saml_identity_providers: [saml_attrs()] }) path = ~p"/settings/security" @@ -164,9 +195,54 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do |> render_click() assert html =~ ~s|| + + html = + view + |> form("#saml-form", %{ + saml_identity_provider: %{ + metadata: "XXX", + label: "" + } + }) + |> render_submit() + + assert html =~ "{:fatal, {:expected_element_start_tag," + assert html =~ "can't be blank" + + attrs = saml_attrs() + + return = + view + |> form("#saml-form", %{ + saml_identity_provider: %{ + id: "FAKEID", + metadata: attrs["metadata"], + label: "FOO" + } + }) + |> render_submit() + + assert {:error, {:redirect, _}} = return + + saml_identity_providers = FzHttp.Configurations.get!(:saml_identity_providers) + + assert length(saml_identity_providers) == 2 + + assert %FzHttp.Configurations.Configuration.SAMLIdentityProvider{ + auto_create_users: true, + # XXX this field would be nil if we don't "guess" the url when we load the record in StartServer + base_url: "https://localhost/auth/saml", + id: "FAKEID", + label: "FOO", + metadata: attrs["metadata"], + sign_metadata: true, + sign_requests: true, + signed_assertion_in_resp: true, + signed_envelopes_in_resp: true + } in saml_identity_providers end - test "click edit button", %{view: view} do + test "edit", %{view: view} do html = view |> element("a", "Edit") @@ -174,9 +250,26 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do assert html =~ ~s|| assert html =~ ~s|entityID="http://localhost:8080/realms/firezone| + + html = + view + |> form("#saml-form", %{ + saml_identity_provider: %{ + label: "just-changed" + } + }) + |> render_submit() + + assert html =~ "value=\"just-changed\"" + + # XXX this test fails, figure out why + # assert [saml_identity_provider] = FzHttp.Configurations.get!(:saml_identity_providers) + # assert saml_identity_provider.label == "changed" end test "validate", %{view: view} do + attrs = saml_attrs() + view |> element("a", "Edit") |> render_click() @@ -189,8 +282,19 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do # stays on the modal assert html =~ ~s|| - # not updated - assert Conf.get!(:saml_identity_providers) == %{"test" => saml_attrs()} + assert FzHttp.Configurations.get!(:saml_identity_providers) == [ + %FzHttp.Configurations.Configuration.SAMLIdentityProvider{ + auto_create_users: true, + base_url: nil, + id: attrs["id"], + label: attrs["label"], + metadata: attrs["metadata"], + sign_metadata: true, + sign_requests: true, + signed_assertion_in_resp: true, + signed_envelopes_in_resp: true + } + ] end test "delete", %{view: view} do @@ -198,7 +302,7 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do |> element("button", "Delete") |> render_click() - assert Conf.get!(:saml_identity_providers) == %{} + assert FzHttp.Configurations.get!(:saml_identity_providers) == [] end end end diff --git a/apps/fz_http/test/fz_http_web/live/setting_live/site_test.exs b/apps/fz_http/test/fz_http_web/live/setting_live/site_test.exs deleted file mode 100644 index 4863b7e1f..000000000 --- a/apps/fz_http/test/fz_http_web/live/setting_live/site_test.exs +++ /dev/null @@ -1,168 +0,0 @@ -defmodule FzHttpWeb.SettingLive.SiteTest do - use FzHttpWeb.ConnCase, async: true - - alias FzHttp.Sites - - describe "authenticated/sites default" do - @valid_allowed_ips %{ - "site" => %{"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_persistent_keepalive %{ - "site" => %{"persistent_keepalive" => "-1"} - } - @invalid_mtu %{ - "site" => %{"mtu" => "0"} - } - - setup %{admin_conn: conn} do - path = ~p"/settings/site" - {: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 - assert html =~ Sites.get_site!().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 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 = ~p"/settings/site" - expected_path = ~p"/" - 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/show_test.exs b/apps/fz_http/test/fz_http_web/live/user_live/show_test.exs index 56f5141f1..1a57faf55 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 @@ -1,6 +1,5 @@ defmodule FzHttpWeb.UserLive.ShowTest do - # XXX: Setting to true causes deadlocks. Figure out why. - use FzHttpWeb.ConnCase, async: false + use FzHttpWeb.ConnCase, async: true alias FzHttp.UsersFixtures @@ -28,121 +27,117 @@ defmodule FzHttpWeb.UserLive.ShowTest do end describe "authenticated new device" do + @test_pubkey "8IkpsAXiqhqNdc9PJS76YeJjig4lyTBaf8Rm7gTApXk=" + @device_id_regex ~r/device-(?.*)-inserted-at/ @valid_params %{ "device" => %{ - "public_key" => "test-pubkey", + "public_key" => @test_pubkey, "name" => "new_name", "description" => "new_description" } } - @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", + "public_key" => @test_pubkey, + "use_default_allowed_ips" => "false", "allowed_ips" => @allowed_ips } } @allowed_ips_unchanged %{ "device" => %{ - "public_key" => "test-pubkey", - "use_site_allowed_ips" => "true", + "public_key" => @test_pubkey, + "use_default_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", + "public_key" => @test_pubkey, + "use_default_dns" => "false", "dns" => @dns } } @dns_unchanged %{ "device" => %{ - "public_key" => "test-pubkey", - "use_site_dns" => "true", + "public_key" => @test_pubkey, + "use_default_dns" => "true", "dns" => @dns } } @wireguard_endpoint "6.6.6.6" @endpoint_change %{ "device" => %{ - "public_key" => "test-pubkey", - "use_site_endpoint" => "false", + "public_key" => @test_pubkey, + "use_default_endpoint" => "false", "endpoint" => @wireguard_endpoint } } @endpoint_unchanged %{ "device" => %{ - "public_key" => "test-pubkey", - "use_site_endpoint" => "true", + "public_key" => @test_pubkey, + "use_default_endpoint" => "true", "endpoint" => @wireguard_endpoint } } @mtu_change %{ "device" => %{ - "public_key" => "test-pubkey", - "use_site_mtu" => "false", + "public_key" => @test_pubkey, + "use_default_mtu" => "false", "mtu" => "1280" } } @mtu_unchanged %{ "device" => %{ - "public_key" => "test-pubkey", - "use_site_mtu" => "true", + "public_key" => @test_pubkey, + "use_default_mtu" => "true", "mtu" => "1280" } } @persistent_keepalive_change %{ "device" => %{ - "public_key" => "test-pubkey", - "use_site_persistent_keepalive" => "false", + "public_key" => @test_pubkey, + "use_default_persistent_keepalive" => "false", "persistent_keepalive" => "120" } } @persistent_keepalive_unchanged %{ "device" => %{ - "public_key" => "test-pubkey", - "use_site_persistent_keepalive" => "true", + "public_key" => @test_pubkey, + "use_default_persistent_keepalive" => "true", "persistent_keepalive" => "5" } } @default_allowed_ips_change %{ "device" => %{ - "public_key" => "test-pubkey", - "use_site_allowed_ips" => "false" + "public_key" => @test_pubkey, + "use_default_allowed_ips" => "false" } } @default_dns_change %{ "device" => %{ - "public_key" => "test-pubkey", - "use_site_dns" => "false" + "public_key" => @test_pubkey, + "use_default_dns" => "false" } } @default_endpoint_change %{ "device" => %{ - "public_key" => "test-pubkey", - "use_site_endpoint" => "false" + "public_key" => @test_pubkey, + "use_default_endpoint" => "false" } } @default_mtu_change %{ "device" => %{ - "public_key" => "test-pubkey", - "use_site_mtu" => "false" + "public_key" => @test_pubkey, + "use_default_mtu" => "false" } } @default_persistent_keepalive_change %{ "device" => %{ - "public_key" => "test-pubkey", - "use_site_persistent_keepalive" => "false" + "public_key" => @test_pubkey, + "use_default_persistent_keepalive" => "false" } } @@ -175,7 +170,7 @@ defmodule FzHttpWeb.UserLive.ShowTest do assert test_view =~ @valid_params["device"]["name"] end - test "prevents allowed_ips changes when use_site_allowed_ips is true", %{ + test "prevents allowed_ips changes when use_default_allowed_ips is true", %{ admin_conn: conn, admin_user: user } do @@ -190,7 +185,7 @@ defmodule FzHttpWeb.UserLive.ShowTest do assert test_view =~ "must not be present" end - test "prevents dns changes when use_site_dns is true", %{ + test "prevents dns changes when use_default_dns is true", %{ admin_conn: conn, admin_user: user } do @@ -205,7 +200,7 @@ defmodule FzHttpWeb.UserLive.ShowTest do assert test_view =~ "must not be present" end - test "prevents endpoint changes when use_site_endpoint is true", %{ + test "prevents endpoint changes when use_default_endpoint is true", %{ admin_conn: conn, admin_user: user } do @@ -220,7 +215,7 @@ defmodule FzHttpWeb.UserLive.ShowTest do assert test_view =~ "must not be present" end - test "prevents mtu changes when use_site_mtu is true", %{ + test "prevents mtu changes when use_default_mtu is true", %{ admin_conn: conn, admin_user: user } do @@ -235,7 +230,7 @@ defmodule FzHttpWeb.UserLive.ShowTest do assert test_view =~ "must not be present" end - test "prevents persistent_keepalive changes when use_site_persistent_keepalive is true", + test "prevents persistent_keepalive changes when use_default_persistent_keepalive is true", %{ admin_conn: conn, admin_user: user @@ -341,19 +336,21 @@ defmodule FzHttpWeb.UserLive.ShowTest do assert html =~ "120" end - test "prevents empty names", %{admin_conn: conn, admin_user: user} do + test "generates a name when it's empty", %{admin_conn: conn, admin_user: user} do path = ~p"/users/#{user.id}/new_device" {:ok, view, _html} = live(conn, path) + params = Map.put(@valid_params, "name", "") + test_view = view |> form("#create-device") - |> render_submit(@invalid_params) + |> render_submit(params) - assert test_view =~ "can't be blank" + assert test_view =~ "Device added!" end - test "on use_site_allowed_ips change", %{admin_conn: conn, admin_user: user} do + test "on use_default_allowed_ips change", %{admin_conn: conn, admin_user: user} do path = ~p"/users/#{user.id}/new_device" {:ok, view, _html} = live(conn, path) @@ -368,7 +365,7 @@ defmodule FzHttpWeb.UserLive.ShowTest do """ end - test "on use_site_dns change", %{admin_conn: conn, admin_user: user} do + test "on use_default_dns change", %{admin_conn: conn, admin_user: user} do path = ~p"/users/#{user.id}/new_device" {:ok, view, _html} = live(conn, path) @@ -382,7 +379,7 @@ defmodule FzHttpWeb.UserLive.ShowTest do """ end - test "on use_site_endpoint change", %{admin_conn: conn, admin_user: user} do + test "on use_default_endpoint change", %{admin_conn: conn, admin_user: user} do path = ~p"/users/#{user.id}/new_device" {:ok, view, _html} = live(conn, path) @@ -396,7 +393,7 @@ defmodule FzHttpWeb.UserLive.ShowTest do """ end - test "on use_site_mtu change", %{admin_conn: conn, admin_user: user} do + test "on use_default_mtu change", %{admin_conn: conn, admin_user: user} do path = ~p"/users/#{user.id}/new_device" {:ok, view, _html} = live(conn, path) @@ -410,7 +407,7 @@ defmodule FzHttpWeb.UserLive.ShowTest do """ end - test "on use_site_persistent_keepalive change", %{admin_conn: conn, admin_user: user} do + test "on use_default_persistent_keepalive change", %{admin_conn: conn, admin_user: user} do path = ~p"/users/#{user.id}/new_device" {:ok, view, _html} = live(conn, path) diff --git a/apps/fz_http/test/fz_http_web/mailer_test.exs b/apps/fz_http/test/fz_http_web/mailer_test.exs index bc698649b..a3ebb691e 100644 --- a/apps/fz_http/test/fz_http_web/mailer_test.exs +++ b/apps/fz_http/test/fz_http_web/mailer_test.exs @@ -53,7 +53,8 @@ defmodule FzHttpWeb.MailerTest do end test "delivery" do - SampleEmail.test_heex(0) |> Mailer.deliver!() + SampleEmail.test_heex(0) + |> Mailer.deliver!() assert_email_sent( subject: "testing", diff --git a/apps/fz_http/test/fz_http_web/user_from_auth_test.exs b/apps/fz_http/test/fz_http_web/user_from_auth_test.exs index f1c0f5e74..8e2e2ce01 100644 --- a/apps/fz_http/test/fz_http_web/user_from_auth_test.exs +++ b/apps/fz_http/test/fz_http_web/user_from_auth_test.exs @@ -25,48 +25,67 @@ defmodule FzHttpWeb.UserFromAuthTest do end describe "find_or_create/2 via OIDC with auto create enabled" do - @tag config: %{"oidc_test" => %{"auto_create_users" => true}} - test "sign in creates user", %{config: config, email: email} do - restore_env(:openid_connect_providers, config, &on_exit/1) + test "sign in creates user", %{email: email} do + openid_connect_provider = + List.first(FzHttp.ConfigurationsFixtures.openid_connect_providers_attrs()) + + FzHttp.Configurations.put!( + :openid_connect_providers, + [openid_connect_provider] + ) assert {:ok, result} = - UserFromAuth.find_or_create("oidc_test", %{"email" => email, "sub" => :noop}) + UserFromAuth.find_or_create(openid_connect_provider["id"], %{ + "email" => email, + "sub" => :noop + }) assert result.email == email end end describe "find_or_create/2 via OIDC with auto create disabled" do - @tag config: %{"oidc_test" => %{"auto_create_users" => false}} - test "sign in returns error", %{email: email, config: config} do - restore_env(:openid_connect_providers, config, &on_exit/1) + test "sign in returns error", %{email: email} do + openid_connect_provider = + List.first(FzHttp.ConfigurationsFixtures.openid_connect_providers_attrs()) + |> Map.put("auto_create_users", false) - assert {:error, "not found"} = - UserFromAuth.find_or_create("oidc_test", %{"email" => email, "sub" => :noop}) + FzHttp.Configurations.put!( + :openid_connect_providers, + [openid_connect_provider] + ) + + assert {:error, "user not found and auto_create_users disabled"} = + UserFromAuth.find_or_create(openid_connect_provider["id"], %{ + "email" => email, + "sub" => :noop + }) assert Users.get_by_email(email) == nil end end describe "find_or_create/2 via SAML with auto create enabled" do - @tag config: %{"saml_test" => %{"auto_create_users" => true}} + @tag config: [FzHttp.SAMLIdentityProviderFixtures.saml_attrs()] test "sign in creates user", %{config: config, email: email} do - restore_env(:saml_identity_providers, config, &on_exit/1) + FzHttp.Configurations.put!(:saml_identity_providers, config) assert {:ok, result} = - UserFromAuth.find_or_create(:saml, "saml_test", %{"email" => email, "sub" => :noop}) + UserFromAuth.find_or_create(:saml, "test", %{"email" => email, "sub" => :noop}) assert result.email == email end end describe "find_or_create/2 via SAML with auto create disabled" do - @tag config: %{"saml_test" => %{"auto_create_users" => false}} + @tag config: [ + FzHttp.SAMLIdentityProviderFixtures.saml_attrs() |> Map.put("auto_create_users", false) + ] test "sign in returns error", %{email: email, config: config} do - restore_env(:saml_identity_providers, config, &on_exit/1) + FzHttp.Configurations.put!(:saml_identity_providers, config) - assert {:error, "not found"} = - UserFromAuth.find_or_create(:saml, "saml_test", %{"email" => email, "sub" => :noop}) + assert {:error, "user not found and auto_create_users disabled"} = + UserFromAuth.find_or_create(:saml, "test", %{"email" => email, "sub" => :noop}) assert Users.get_by_email(email) == nil end diff --git a/apps/fz_http/test/support/case_template.ex b/apps/fz_http/test/support/case_template.ex new file mode 100644 index 000000000..62bc18073 --- /dev/null +++ b/apps/fz_http/test/support/case_template.ex @@ -0,0 +1,23 @@ +defmodule FzHttp.CaseTemplate do + @moduledoc """ + Our wrapper for the ExUnit.CaseTemplate to provide metaprogrammed + helpers to all tests. + """ + + use ExUnit.CaseTemplate + alias Ecto.Adapters.SQL.Sandbox + + using do + quote do + setup tags do + :ok = Sandbox.checkout(FzHttp.Repo) + + unless tags[:async] do + Sandbox.mode(FzHttp.Repo, {:shared, self()}) + end + + :ok + end + end + end +end diff --git a/apps/fz_http/test/support/channel_case.ex b/apps/fz_http/test/support/channel_case.ex index 5f32149d5..1461259ec 100644 --- a/apps/fz_http/test/support/channel_case.ex +++ b/apps/fz_http/test/support/channel_case.ex @@ -16,8 +16,7 @@ defmodule FzHttpWeb.ChannelCase do """ use ExUnit.CaseTemplate - - alias Ecto.Adapters.SQL.Sandbox + use FzHttp.CaseTemplate using do quote do @@ -29,14 +28,4 @@ defmodule FzHttpWeb.ChannelCase do @endpoint FzHttpWeb.Endpoint end end - - setup tags do - :ok = Sandbox.checkout(FzHttp.Repo) - - unless tags[:async] do - Sandbox.mode(FzHttp.Repo, {:shared, self()}) - end - - :ok - end end diff --git a/apps/fz_http/test/support/conn_case.ex b/apps/fz_http/test/support/conn_case.ex index 750998da2..87944f352 100644 --- a/apps/fz_http/test/support/conn_case.ex +++ b/apps/fz_http/test/support/conn_case.ex @@ -16,10 +16,14 @@ defmodule FzHttpWeb.ConnCase do """ use ExUnit.CaseTemplate + use FzHttp.CaseTemplate - alias Ecto.Adapters.SQL.Sandbox + alias FzHttpWeb.Auth.HTML.Authentication - alias FzHttp.UsersFixtures + alias FzHttp.{ + ApiTokensFixtures, + UsersFixtures + } using do quote do @@ -36,7 +40,7 @@ defmodule FzHttpWeb.ConnCase do def current_user(test_conn) do get_session(test_conn) - |> FzHttpWeb.Authentication.get_current_user() + |> Authentication.get_current_user() end end end @@ -45,6 +49,16 @@ defmodule FzHttpWeb.ConnCase do Phoenix.ConnTest.build_conn() end + def api_conn do + user = UsersFixtures.user() + api_token = ApiTokensFixtures.api_token(%{"user_id" => user.id}) + {:ok, token, _claims} = FzHttpWeb.Auth.JSON.Authentication.fz_encode_and_sign(api_token, user) + + new_conn() + |> Plug.Conn.put_req_header("accept", "application/json") + |> Plug.Conn.put_req_header("authorization", "bearer #{token}") + end + def admin_conn(tags) do authed_conn(:admin, tags) end @@ -59,14 +73,12 @@ defmodule FzHttpWeb.ConnCase do conn = new_conn() |> Plug.Test.init_test_session(%{}) - |> FzHttpWeb.Authentication.sign_in(user, %{provider: :identity}) + |> Authentication.sign_in(user, %{provider: :identity}) |> maybe_put_session(tags) {user, conn - |> Plug.Test.init_test_session(%{ - "guardian_default_token" => conn.private.guardian_default_token - })} + |> Plug.Conn.put_session("guardian_default_token", conn.private.guardian_default_token)} end defp maybe_put_session(conn, %{session: session}) do @@ -79,20 +91,18 @@ defmodule FzHttpWeb.ConnCase do end setup tags do - :ok = Sandbox.checkout(FzHttp.Repo) - - unless tags[:async] do - Sandbox.mode(FzHttp.Repo, {:shared, self()}) - end - {unprivileged_user, unprivileged_conn} = unprivileged_conn(tags) {admin_user, admin_conn} = admin_conn(tags) - {:ok, - unauthed_conn: new_conn(), - admin_user: admin_user, - unprivileged_user: unprivileged_user, - admin_conn: admin_conn, - unprivileged_conn: unprivileged_conn} + conns = + [ + unauthed_conn: new_conn(), + admin_user: admin_user, + unprivileged_user: unprivileged_user, + admin_conn: admin_conn, + unprivileged_conn: unprivileged_conn + ] ++ if tags[:api], do: [api_conn: api_conn()], else: [] + + {:ok, conns} end end diff --git a/apps/fz_http/test/support/data_case.ex b/apps/fz_http/test/support/data_case.ex index 2a98db84f..c6cdf2014 100644 --- a/apps/fz_http/test/support/data_case.ex +++ b/apps/fz_http/test/support/data_case.ex @@ -15,8 +15,7 @@ defmodule FzHttp.DataCase do """ use ExUnit.CaseTemplate - - alias Ecto.Adapters.SQL.Sandbox + use FzHttp.CaseTemplate using do quote do @@ -30,16 +29,6 @@ defmodule FzHttp.DataCase do end end - setup tags do - :ok = Sandbox.checkout(FzHttp.Repo) - - unless tags[:async] do - Sandbox.mode(FzHttp.Repo, {:shared, self()}) - end - - :ok - end - @doc """ A helper that transforms changeset errors into a map of messages. diff --git a/apps/fz_http/test/support/fixtures/api_tokens_fixtures.ex b/apps/fz_http/test/support/fixtures/api_tokens_fixtures.ex new file mode 100644 index 000000000..40f457dc2 --- /dev/null +++ b/apps/fz_http/test/support/fixtures/api_tokens_fixtures.ex @@ -0,0 +1,25 @@ +defmodule FzHttp.ApiTokensFixtures do + @moduledoc """ + This module defines test helpers for creating + entities via the `FzHttp.ApiTokens` context. + """ + + @doc """ + Generate a api_token. + """ + def api_token(params \\ %{}) do + user_id = + Map.get_lazy( + params, + "user_id", + fn -> + FzHttp.UsersFixtures.user().id + end + ) + + {:ok, api_token} = + FzHttp.ApiTokens.create_user_api_token(%FzHttp.Users.User{id: user_id}, params) + + api_token + end +end diff --git a/apps/fz_http/test/support/fixtures/configurations_fixtures.ex b/apps/fz_http/test/support/fixtures/configurations_fixtures.ex new file mode 100644 index 000000000..84073856f --- /dev/null +++ b/apps/fz_http/test/support/fixtures/configurations_fixtures.ex @@ -0,0 +1,191 @@ +defmodule FzHttp.ConfigurationsFixtures do + @moduledoc """ + Allows for easily updating configuration in tests. + """ + + alias FzHttp.{ + Configurations, + Configurations.Configuration, + Repo + } + + @doc "Configurations table holds a singleton record." + def configuration(%Configuration{} = conf \\ Configurations.get_configuration!(), attrs) do + {:ok, configuration} = + conf + |> Configuration.changeset(attrs) + |> Repo.update() + + configuration + end + + def openid_connect_providers_attrs do + discovery_document_url = discovery_document_server() + + [ + %{ + "id" => "google", + "discovery_document_uri" => discovery_document_url, + "client_id" => "google-client-id", + "client_secret" => "google-client-secret", + "redirect_uri" => "https://firezone.example.com/auth/oidc/google/callback/", + "response_type" => "code", + "scope" => "openid email profile", + "label" => "OIDC Google" + }, + %{ + "id" => "okta", + "discovery_document_uri" => discovery_document_url, + "client_id" => "okta-client-id", + "client_secret" => "okta-client-secret", + "redirect_uri" => "https://firezone.example.com/auth/oidc/okta/callback/", + "response_type" => "code", + "scope" => "openid email profile offline_access", + "label" => "OIDC Okta" + }, + %{ + "id" => "auth0", + "discovery_document_uri" => discovery_document_url, + "client_id" => "auth0-client-id", + "client_secret" => "auth0-client-secret", + "redirect_uri" => "https://firezone.example.com/auth/oidc/auth0/callback/", + "response_type" => "code", + "scope" => "openid email profile", + "label" => "OIDC Auth0" + }, + %{ + "id" => "azure", + "discovery_document_uri" => discovery_document_url, + "client_id" => "azure-client-id", + "client_secret" => "azure-client-secret", + "redirect_uri" => "https://firezone.example.com/auth/oidc/azure/callback/", + "response_type" => "code", + "scope" => "openid email profile offline_access", + "label" => "OIDC Azure" + }, + %{ + "id" => "onelogin", + "discovery_document_uri" => discovery_document_url, + "client_id" => "onelogin-client-id", + "client_secret" => "onelogin-client-secret", + "redirect_uri" => "https://firezone.example.com/auth/oidc/onelogin/callback/", + "response_type" => "code", + "scope" => "openid email profile offline_access", + "label" => "OIDC Onelogin" + }, + %{ + "id" => "keycloak", + "discovery_document_uri" => discovery_document_url, + "client_id" => "keycloak-client-id", + "client_secret" => "keycloak-client-secret", + "redirect_uri" => "https://firezone.example.com/auth/oidc/keycloak/callback/", + "response_type" => "code", + "scope" => "openid email profile offline_access", + "label" => "OIDC Keycloak" + }, + %{ + "id" => "vault", + "discovery_document_uri" => discovery_document_url, + "client_id" => "vault-client-id", + "client_secret" => "vault-client-secret", + "redirect_uri" => "https://firezone.example.com/auth/oidc/vault/callback/", + "response_type" => "code", + "scope" => "openid email profile offline_access", + "label" => "OIDC Vault" + } + ] + end + + def discovery_document_server do + bypass = Bypass.open() + + Bypass.expect(bypass, "GET", "/.well-known/openid-configuration", fn conn -> + attrs = %{ + "issuer" => "https://common.auth0.com/", + "authorization_endpoint" => "https://common.auth0.com/authorize", + "token_endpoint" => "https://common.auth0.com/oauth/token", + "device_authorization_endpoint" => "https://common.auth0.com/oauth/device/code", + "userinfo_endpoint" => "https://common.auth0.com/userinfo", + "mfa_challenge_endpoint" => "https://common.auth0.com/mfa/challenge", + "jwks_uri" => "https://common.auth0.com/.well-known/jwks.json", + "registration_endpoint" => "https://common.auth0.com/oidc/register", + "revocation_endpoint" => "https://common.auth0.com/oauth/revoke", + "scopes_supported" => [ + "openid", + "profile", + "offline_access", + "name", + "given_name", + "family_name", + "nickname", + "email", + "email_verified", + "picture", + "created_at", + "identities", + "phone", + "address" + ], + "response_types_supported" => [ + "code", + "token", + "id_token", + "code token", + "code id_token", + "token id_token", + "code token id_token" + ], + "code_challenge_methods_supported" => [ + "S256", + "plain" + ], + "response_modes_supported" => [ + "query", + "fragment", + "form_post" + ], + "subject_types_supported" => [ + "public" + ], + "id_token_signing_alg_values_supported" => [ + "HS256", + "RS256" + ], + "token_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "client_secret_post" + ], + "claims_supported" => [ + "aud", + "auth_time", + "created_at", + "email", + "email_verified", + "exp", + "family_name", + "given_name", + "iat", + "identities", + "iss", + "name", + "nickname", + "phone_number", + "picture", + "sub" + ], + "request_uri_parameter_supported" => false, + "request_parameter_supported" => false + } + + Plug.Conn.resp(conn, 200, Jason.encode!(attrs)) + end) + + "http://localhost:#{bypass.port}/.well-known/openid-configuration" + end + + def saml_identity_providers_attrs do + [ + %{"id" => "test", "label" => "SAML"} + ] + 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 11434f0bb..f7045300e 100644 --- a/apps/fz_http/test/support/fixtures/devices_fixtures.ex +++ b/apps/fz_http/test/support/fixtures/devices_fixtures.ex @@ -4,7 +4,10 @@ defmodule FzHttp.DevicesFixtures do entities via the `FzHttp.Devices` context. """ - alias FzHttp.{Devices, UsersFixtures} + alias FzHttp.{ + Devices, + UsersFixtures + } @doc """ Generate a device. @@ -15,7 +18,7 @@ defmodule FzHttp.DevicesFixtures do default_attrs = %{ user_id: user_id, - public_key: "test-pubkey-#{counter()}", + public_key: public_key(), name: "factory #{counter()}", description: "factory description" } @@ -24,6 +27,11 @@ defmodule FzHttp.DevicesFixtures do device end + def public_key do + :crypto.strong_rand_bytes(32) + |> Base.encode64() + end + defp counter do System.unique_integer([:positive]) end diff --git a/apps/fz_http/test/support/fixtures/oidc_config_fixtures.ex b/apps/fz_http/test/support/fixtures/oidc_config_fixtures.ex index 545061d96..23e25a31f 100644 --- a/apps/fz_http/test/support/fixtures/oidc_config_fixtures.ex +++ b/apps/fz_http/test/support/fixtures/oidc_config_fixtures.ex @@ -1,4 +1,4 @@ -defmodule FzHttp.OIDCConfigFixtures do +defmodule FzHttp.OpenIDConnectProviderFixtures do @moduledoc """ Fixtures for OIDC configs. """ diff --git a/apps/fz_http/test/support/fixtures/saml_config_fixtures.ex b/apps/fz_http/test/support/fixtures/saml_config_fixtures.ex index 3fdac835a..a8beaa4ef 100644 --- a/apps/fz_http/test/support/fixtures/saml_config_fixtures.ex +++ b/apps/fz_http/test/support/fixtures/saml_config_fixtures.ex @@ -1,4 +1,4 @@ -defmodule FzHttp.SAMLConfigFixtures do +defmodule FzHttp.SAMLIdentityProviderFixtures do @moduledoc """ Fixtures for SAML configs. """ diff --git a/apps/fz_http/test/support/fixtures/sites_fixtures.ex b/apps/fz_http/test/support/fixtures/sites_fixtures.ex deleted file mode 100644 index 6aeba667f..000000000 --- a/apps/fz_http/test/support/fixtures/sites_fixtures.ex +++ /dev/null @@ -1,15 +0,0 @@ -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/mailer_case.ex b/apps/fz_http/test/support/mailer_case.ex index 53487995d..dea0aa9c7 100644 --- a/apps/fz_http/test/support/mailer_case.ex +++ b/apps/fz_http/test/support/mailer_case.ex @@ -3,8 +3,7 @@ defmodule FzHttpWeb.MailerCase do A case template for Mailers. """ use ExUnit.CaseTemplate - - alias Ecto.Adapters.SQL.Sandbox + use FzHttp.CaseTemplate using do quote do @@ -19,14 +18,4 @@ defmodule FzHttpWeb.MailerCase do use FzHttpWeb, :verified_routes end end - - setup tags do - :ok = Sandbox.checkout(FzHttp.Repo) - - unless tags[:async] do - Sandbox.mode(FzHttp.Repo, {:shared, self()}) - end - - :ok - end end diff --git a/apps/fz_http/test/support/test_helpers.ex b/apps/fz_http/test/support/test_helpers.ex index 77c80c0dc..72b6a1562 100644 --- a/apps/fz_http/test/support/test_helpers.ex +++ b/apps/fz_http/test/support/test_helpers.ex @@ -4,7 +4,6 @@ defmodule FzHttp.TestHelpers do """ alias FzHttp.{ - Configurations.Cache, ConnectivityChecksFixtures, DevicesFixtures, MFA, @@ -16,19 +15,6 @@ defmodule FzHttp.TestHelpers do UsersFixtures } - def restore_env(key, val, cb), do: restore_env(:fz_http, key, val, cb) - - def restore_env(app, key, val, cb) do - old = Application.fetch_env!(app, key) - Application.put_env(app, key, val) - Cache.put!(key, val) - - cb.(fn -> - Application.put_env(app, key, old) - Cache.put!(key, old) - end) - end - def clear_users do Repo.delete_all(User) end @@ -54,8 +40,7 @@ defmodule FzHttp.TestHelpers do device = DevicesFixtures.device(%{ user_id: user_id, - name: "other device", - public_key: "other-pubkey" + name: "other device" }) {:ok, other_device: device} @@ -82,7 +67,6 @@ defmodule FzHttp.TestHelpers do Enum.map(1..5, fn num -> DevicesFixtures.device(%{ name: "device #{num}", - public_key: "#{num}", user_id: user_id }) end) @@ -137,10 +121,7 @@ defmodule FzHttp.TestHelpers do device = DevicesFixtures.device(%{ name: "device #{num}", - public_key: "#{num}", - user_id: user.id, - ipv4: "10.3.2.#{num}", - ipv6: "fd00::3:2:#{num}" + user_id: user.id }) rule = RulesFixtures.rule(%{destination: destination, user_id: user.id}) @@ -167,10 +148,7 @@ defmodule FzHttp.TestHelpers do device = DevicesFixtures.device(%{ name: "device", - public_key: "1", - user_id: user.id, - ipv4: "10.3.2.2", - ipv6: "fd00::3:2:2" + user_id: user.id }) {:ok, rule: rule, user: user, device: device} diff --git a/apps/fz_vpn/test/fz_vpn/server_test.exs b/apps/fz_vpn/test/fz_vpn/server_test.exs index fc7cbec1c..58935eb69 100644 --- a/apps/fz_vpn/test/fz_vpn/server_test.exs +++ b/apps/fz_vpn/test/fz_vpn/server_test.exs @@ -9,38 +9,46 @@ defmodule FzVpn.ServerTest do end describe "state" do + @pubkey "2Bp11cX3ETPs4/bbKdn44OywJAqD6XuzWG6VCrlSzXI=" + @psk "sGaRdRnjo58qCuNnb4zIwAfZa0mOmD6aDfsxye9Tw3s=" + @key1 "KGIx2Yt8S+dc2886Y9H4lrFzm3Hh7f//Ix0Ip/mdX2k=" + @key2 "MDxx3EkWIBI1KfBhnAdwfdqGcFMKz32+PgIOro4g9Eo=" + @key3 "wMc2ntAv2w233Qsy+VMfFHzF4J4rPaj2+HYeFV99YH8=" + @key4 "wN2yynjMdSzFcVrzfl7v89YOuBfNWhMAklgfeA3PQG0=" + @key5 "8IkpsAXiqhqNdc9PJS76YeJjig4lyTBaf8Rm7gTApXk=" + @single_peer [ - %{public_key: "test-pubkey", preshared_key: "foobar", inet: "127.0.0.1/32,::1/128"} + %{public_key: @pubkey, preshared_key: @psk, inet: "127.0.0.1/32,::1/128"} ] @many_peers [ - %{public_key: "key1", preshared_key: "foobar", inet: "0.0.0.0/32,::1/128"}, - %{public_key: "key2", preshared_key: "foobar", inet: "127.0.0.1/32,::1/128"}, - %{public_key: "key3", preshared_key: "foobar", inet: "127.0.0.1/32,::1/128"}, - %{public_key: "key4", preshared_key: "foobar", inet: "127.0.0.1/32,::1/128"} + %{public_key: @key1, preshared_key: @psk, inet: "0.0.0.0/32,::1/128"}, + %{public_key: @key2, preshared_key: @psk, inet: "127.0.0.1/32,::1/128"}, + %{public_key: @key3, preshared_key: @psk, inet: "127.0.0.1/32,::1/128"}, + %{public_key: @key4, preshared_key: @psk, inet: "127.0.0.1/32,::1/128"} ] @tag stubbed_config: @single_peer test "removes peers from config when removed", %{test_pid: test_pid} do - GenServer.call(test_pid, {:remove_peer, "test-pubkey"}) + GenServer.call(test_pid, {:remove_peer, @pubkey}) assert :sys.get_state(test_pid) == %{} end @tag stubbed_config: @many_peers test "calcs diff and sets only the diff", %{test_pid: test_pid} do - new_peers = [%{public_key: "key5", inet: "1.1.1.1/32,::2/128", preshared_key: "foobar"}] + new_peers = [%{public_key: @key5, inet: "1.1.1.1/32,::2/128", preshared_key: @psk}] assert :sys.get_state(test_pid) == %{ - "key1" => %{allowed_ips: "0.0.0.0/32,::1/128", preshared_key: "foobar"}, - "key2" => %{allowed_ips: "127.0.0.1/32,::1/128", preshared_key: "foobar"}, - "key3" => %{allowed_ips: "127.0.0.1/32,::1/128", preshared_key: "foobar"}, - "key4" => %{allowed_ips: "127.0.0.1/32,::1/128", preshared_key: "foobar"} + @key1 => %{allowed_ips: "0.0.0.0/32,::1/128", preshared_key: @psk}, + @key2 => %{allowed_ips: "127.0.0.1/32,::1/128", preshared_key: @psk}, + @key3 => %{allowed_ips: "127.0.0.1/32,::1/128", preshared_key: @psk}, + @key4 => %{allowed_ips: "127.0.0.1/32,::1/128", preshared_key: @psk} } :ok = GenServer.call(test_pid, {:set_config, new_peers}) assert :sys.get_state(test_pid) == %{ - "key5" => %{allowed_ips: "1.1.1.1/32,::2/128", preshared_key: "foobar"} + @key5 => %{allowed_ips: "1.1.1.1/32,::2/128", preshared_key: @psk} } end end diff --git a/config/config.exs b/config/config.exs index 7517168d0..91da016da 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,28 +1,5 @@ -# This file is responsible for configuring your umbrella -# and **all applications** and their dependencies with the -# help of the Config module. -# -# *Note*: -# This configuration is generated on compile time. To configure the application during runtime, -# use releases.exs. These configuration options are overridden by environment-specific -# configuration files. -# -# Note that all applications in your umbrella share the -# same configuration and dependencies, which is why they -# all use the same configuration file. If you want different -# configurations or dependencies per app, it is best to -# move said applications out of the umbrella. import Config -require Logger - -# Sample configuration: -# -# config :logger, :console, -# level: :info, -# format: "$date $time [$level] $metadata$message\n", -# metadata: [:user_id] - # Use Jason for JSON parsing in Phoenix config :phoenix, :json_library, Jason @@ -32,25 +9,30 @@ config :posthog, api_key: "phc_ubuPhiqqjMdedpmbWpG2Ak3axqv5eMVhFDNBaXl9UZK" # Guardian configuration -config :fz_http, FzHttpWeb.Authentication, +config :fz_http, FzHttpWeb.Auth.HTML.Authentication, issuer: "fz_http", # Generate with mix guardian.gen.secret secret_key: "GApJ4c4a/KJLrBePgTDUk0n67AbjCvI9qdypKZEaJFXl6s9H3uRcIhTt49Fij5UO" +config :fz_http, FzHttpWeb.Auth.JSON.Authentication, + issuer: "fz_http", + # Generate with mix guardian.gen.secret + secret_key: "GApJ4c4a/KJLrBePgTDUk0n67AbjCvI9qdypKZEaJFXl6s9H3uRcIhTt49Fij5UO" + +# Use timestamptz for all timestamp fields +config :fz_http, FzHttp.Repo, migration_timestamps: [type: :timestamptz] + config :fz_http, external_trusted_proxies: [], private_clients: [], - disable_vpn_on_oidc_error: true, sandbox: true, - allow_unprivileged_device_management: true, - allow_unprivileged_device_configuration: true, telemetry_id: "543aae08-5a2b-428d-b704-2956dd3f5a57", wireguard_ipv4_enabled: true, - wireguard_ipv4_network: "10.3.2.0/24", - wireguard_ipv4_address: "10.3.2.1", + wireguard_ipv4_network: "100.64.0.0/10", + wireguard_ipv4_address: "100.64.0.1", wireguard_ipv6_enabled: true, - wireguard_ipv6_network: "fd00::3:2:0/120", - wireguard_ipv6_address: "fd00::3:2:1", + wireguard_ipv6_network: "fd00::/106", + wireguard_ipv6_address: "fd00::1", max_devices_per_user: 10, telemetry_module: FzCommon.Telemetry, supervision_tree_mode: :full, @@ -65,11 +47,9 @@ config :fz_http, admin_email: "firezone@localhost", default_admin_password: "firezone1234", server_process_opts: [name: {:global, :fz_http_server}], - openid_connect_providers: "{}", - saml_identity_providers: %{}, saml_entity_id: "urn:firezone.dev:firezone-app", - saml_certfile_path: "apps/fz_http/priv/cert/saml_selfsigned.pem", - saml_keyfile_path: "apps/fz_http/priv/cert/saml_selfsigned_key.pem", + saml_certfile_path: Path.expand("../apps/fz_http/priv/cert/saml_selfsigned.pem", __DIR__), + saml_keyfile_path: Path.expand("../apps/fz_http/priv/cert/saml_selfsigned_key.pem", __DIR__), openid_connect: OpenIDConnect config :fz_wall, @@ -97,6 +77,7 @@ config :fz_http, FzHttpWeb.Endpoint, # Configures Elixir's Logger config :logger, :console, + level: String.to_atom(System.get_env("LOG_LEVEL", "info")), format: "$time $metadata[$level] $message\n", metadata: [:request_id, :remote_ip] diff --git a/config/dev.exs b/config/dev.exs index 135bee1db..89dcf703f 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -63,23 +63,15 @@ config :fz_vpn, # Auth local_auth_enabled = System.get_env("LOCAL_AUTH_ENABLED") == "true" -# Configure strategies -identity_strategy = - {:identity, - {Ueberauth.Strategy.Identity, - [ - callback_methods: ["POST"], - uid_field: :email - ]}} - -providers = - [ - {local_auth_enabled, identity_strategy} +config :ueberauth, Ueberauth, + providers: [ + identity: + {Ueberauth.Strategy.Identity, + [ + callback_methods: ["POST"], + uid_field: :email + ]} ] - |> Enum.filter(fn {key, _val} -> key end) - |> Enum.map(fn {_key, val} -> val end) - -config :ueberauth, Ueberauth, providers: providers # ## SSL Support # @@ -133,7 +125,6 @@ config :phoenix, :plug_init_mode, :runtime config :fz_http, private_clients: ["172.28.0.0/16"], cookie_secure: false, - telemetry_module: FzCommon.MockTelemetry, - local_auth_enabled: local_auth_enabled + telemetry_module: FzCommon.MockTelemetry config :fz_http, FzHttpWeb.Mailer, adapter: Swoosh.Adapters.Local, from_email: "dev@firez.one" diff --git a/config/prod.exs b/config/prod.exs index 8fcd0bcee..4e5030d56 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -41,7 +41,6 @@ config :logger, level: :info config :fz_http, sandbox: false, - local_auth_enabled: true, connectivity_checks_url: "https://ping.firez.one/" config :ueberauth, Ueberauth, diff --git a/config/runtime.exs b/config/runtime.exs index 67ae959be..3bdbf22ca 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -65,6 +65,7 @@ if config_env() == :prod do nft_path = System.get_env("NFT_PATH", "nft") egress_interface = System.get_env("EGRESS_INTERFACE", "eth0") wireguard_ipv4_enabled = FzString.to_boolean(System.get_env("WIREGUARD_IPV4_ENABLED", "true")) + wireguard_ipv6_enabled = FzString.to_boolean(System.get_env("WIREGUARD_IPV6_ENABLED", "true")) wireguard_ipv4_masquerade = FzString.to_boolean(System.get_env("WIREGUARD_IPV4_MASQUERADE", "true")) @@ -72,24 +73,16 @@ if config_env() == :prod do wireguard_ipv6_masquerade = FzString.to_boolean(System.get_env("WIREGUARD_IPV6_MASQUERADE", "true")) + # On fresh installs, these should now be populated in the ENV to be 100.64.0.0/10 and fd00::/106 wireguard_ipv4_network = System.get_env("WIREGUARD_IPV4_NETWORK", "10.3.2.0/24") wireguard_ipv4_address = System.get_env("WIREGUARD_IPV4_ADDRESS", "10.3.2.1") - wireguard_ipv6_enabled = FzString.to_boolean(System.get_env("WIREGUARD_IPV6_ENABLED", "true")) wireguard_ipv6_network = System.get_env("WIREGUARD_IPV6_NETWORK", "fd00::3:2:0/120") wireguard_ipv6_address = System.get_env("WIREGUARD_IPV6_ADDRESS", "fd00::3:2:1") + telemetry_enabled = FzString.to_boolean(System.get_env("TELEMETRY_ENABLED", "true")) - disable_vpn_on_oidc_error = - FzString.to_boolean(System.get_env("DISABLE_VPN_ON_OIDC_ERROR", "false")) - cookie_secure = FzString.to_boolean(System.get_env("SECURE_COOKIES", "true")) - allow_unprivileged_device_management = - FzString.to_boolean(System.get_env("ALLOW_UNPRIVILEGED_DEVICE_MANAGEMENT", "true")) - - allow_unprivileged_device_configuration = - FzString.to_boolean(System.get_env("ALLOW_UNPRIVILEGED_DEVICE_CONFIGURATION", "true")) - # Outbound Email from_email = System.get_env("OUTBOUND_EMAIL_FROM") @@ -101,9 +94,6 @@ if config_env() == :prod do [from_email: from_email] ++ FzHttpWeb.Mailer.configs_for(provider) end - # Local auth - local_auth_enabled = FzString.to_boolean(System.get_env("LOCAL_AUTH_ENABLED", "true")) - max_devices_per_user = System.get_env("MAX_DEVICES_PER_USER", "10") |> String.to_integer() @@ -213,7 +203,12 @@ if config_env() == :prod do wireguard_port: wireguard_port # Guardian configuration - config :fz_http, FzHttpWeb.Authentication, + # XXX: Use different secret keys here when config / secret generation is refactored + config :fz_http, FzHttpWeb.Auth.HTML.Authentication, + issuer: "fz_http", + secret_key: guardian_secret_key + + config :fz_http, FzHttpWeb.Auth.JSON.Authentication, issuer: "fz_http", secret_key: guardian_secret_key @@ -223,14 +218,10 @@ if config_env() == :prod do saml_keyfile_path: saml_keyfile_path, external_trusted_proxies: external_trusted_proxies, private_clients: private_clients, - disable_vpn_on_oidc_error: disable_vpn_on_oidc_error, cookie_signing_salt: cookie_signing_salt, cookie_encryption_salt: cookie_encryption_salt, cookie_secure: cookie_secure, - allow_unprivileged_device_management: allow_unprivileged_device_management, - allow_unprivileged_device_configuration: allow_unprivileged_device_configuration, max_devices_per_user: max_devices_per_user, - local_auth_enabled: local_auth_enabled, wireguard_ipv4_enabled: wireguard_ipv4_enabled, wireguard_ipv4_network: wireguard_ipv4_network, wireguard_ipv4_address: wireguard_ipv4_address, @@ -254,19 +245,11 @@ if config_env() == :prod do uid_field: :email ]}} - providers = - [ - {local_auth_enabled, identity_strategy} - ] - |> Enum.filter(fn {key, _val} -> key end) - |> Enum.map(fn {_key, val} -> val end) - - config :ueberauth, Ueberauth, providers: providers -end - -# OIDC Auth -auth_oidc_env = System.get_env("AUTH_OIDC_JSON", "{}") - -if config_env() != :test && auth_oidc_env do - config :fz_http, :openid_connect_providers, auth_oidc_env + # Local auth can be disabled at runtime. We check for that in multiple + # places to ensure this strategy is noop'd when local_auth_enabled = false + # without having to conditionally reconfigure Ueberauth strategies. + # + # Local auth is likely to removed in the future, so it's not worth + # refactoring this. + config :ueberauth, Ueberauth, providers: identity_strategy end diff --git a/config/test.exs b/config/test.exs index 7fe6baa32..14535215c 100644 --- a/config/test.exs +++ b/config/test.exs @@ -39,7 +39,6 @@ config :fz_http, FzHttpWeb.Endpoint, config :fz_http, mock_events_module_errors: false, - local_auth_enabled: true, telemetry_module: FzCommon.MockTelemetry, supervision_tree_mode: :test, connectivity_checks_interval: 86_400, @@ -51,81 +50,10 @@ config :logger, level: :warn config :ueberauth, Ueberauth, providers: [ - {:identity, {Ueberauth.Strategy.Identity, [callback_methods: ["POST"], uid_field: :email]}} + identity: {Ueberauth.Strategy.Identity, [callback_methods: ["POST"], uid_field: :email]} ] -# OIDC auth for testing -config :fz_http, :openid_connect_providers, """ -{ - "google": { - "discovery_document_uri": "https://google/.well-known/openid-configuration", - "client_id": "google-client-id", - "client_secret": "google-client-secret", - "redirect_uri": "https://firezone.example.com/auth/oidc/google/callback/", - "response_type": "code", - "scope": "openid email profile", - "label": "OIDC Google" - }, - "okta": { - "discovery_document_uri": "https://okta/.well-known/openid-configuration", - "client_id": "okta-client-id", - "client_secret": "okta-client-secret", - "redirect_uri": "https://firezone.example.com/auth/oidc/okta/callback/", - "response_type": "code", - "scope": "openid email profile offline_access", - "label": "OIDC Okta" - }, - "auth0": { - "discovery_document_uri": "https://auth0/.well-known/openid-configuration", - "client_id": "auth0-client-id", - "client_secret": "auth0-client-secret", - "redirect_uri": "https://firezone.example.com/auth/oidc/google/callback/", - "response_type": "code", - "scope": "openid email profile", - "label": "OIDC Google" - }, - "azure": { - "discovery_document_uri": "https://azure/.well-known/openid-configuration", - "client_id": "azure-client-id", - "client_secret": "azure-client-secret", - "redirect_uri": "https://firezone.example.com/auth/oidc/okta/callback/", - "response_type": "code", - "scope": "openid email profile offline_access", - "label": "OIDC Okta" - }, - "onelogin": { - "discovery_document_uri": "https://onelogin/.well-known/openid-configuration", - "client_id": "onelogin-client-id", - "client_secret": "onelogin-client-secret", - "redirect_uri": "https://firezone.example.com/auth/oidc/okta/callback/", - "response_type": "code", - "scope": "openid email profile offline_access", - "label": "OIDC Okta" - }, - "keycloak": { - "discovery_document_uri": "https://keycloak/.well-known/openid-configuration", - "client_id": "keycloak-client-id", - "client_secret": "keycloak-client-secret", - "redirect_uri": "https://firezone.example.com/auth/oidc/keycloak/callback/", - "response_type": "code", - "scope": "openid email profile offline_access", - "label": "Keycloak" - }, - "vault": { - "discovery_document_uri": "https://vault/.well-known/openid-configuration", - "client_id": "vault-client-id", - "client_secret": "vault-client-secret", - "redirect_uri": "https://firezone.example.com/auth/oidc/vault/callback/", - "response_type": "code", - "scope": "openid email profile offline_access", - "label": "Vault" - } -} -""" - -config :fz_http, :saml_identity_providers, %{"test" => %{"label" => "SAML"}} - -# Provide mock for HTTPClient +# Provide mock for OpenIDConnect config :fz_http, :openid_connect, OpenIDConnect.Mock config :fz_http, FzHttpWeb.Mailer, adapter: Swoosh.Adapters.Test, from_email: "test@firez.one" diff --git a/docs/docs/administer/upgrade.mdx b/docs/docs/administer/upgrade.mdx index 409ce2207..efea75ccd 100644 --- a/docs/docs/administer/upgrade.mdx +++ b/docs/docs/administer/upgrade.mdx @@ -47,13 +47,24 @@ docker compose up -d If you hit any issues, please let us know by [filing an issue](https://github.com/firezone/firezone/issues/new/choose). +## Upgrading to 0.7.x + +Firezone 0.7.0 introduces a new REST API that replaces many of the legacy environment +variables. + +Due to this, If you're running Firezone < 0.6, we recommend updating to the latest +0.6.x release **before** upgrading to 0.7. This will ensure any environment variables +are properly parsed and migrated into the DB as runtime `configurations`. + + ## Upgrading to 0.6.12 ### WIREGUARD_* env vars Firezone 0.6.12 moves the `WIREGUARD_ALLOWED_IPS`, `WIREGUARD_PERSISTENT_KEEPALIVE`, and `WIREGUARD_DNS` environment variables to the `sites` table to be configured in the -UI at `/settings/site`. If the corresponding value at `/settings/site` was empty, the -environment variable's value was used to populate the field. +UI at `/settings/client_defaults`. If the corresponding value at +`/settings/client_defaults` was empty, the environment variable's value was used to +populate the field. This is a baby step in our quest to move more runtime configuration from environment variables to the DB. diff --git a/docs/docs/deploy/advanced/build-from-source.mdx b/docs/docs/deploy/advanced/build-from-source.mdx index 0d2931869..0de44e906 100644 --- a/docs/docs/deploy/advanced/build-from-source.mdx +++ b/docs/docs/deploy/advanced/build-from-source.mdx @@ -51,7 +51,7 @@ MIX_ENV=prod mix deps.get MIX_ENV=prod mix release ``` -After the release build finishes, you should have a shiny new Firezone instance at +After the release build finishes, you should have a shiny new Firezone release artifact in `/_build/dev/rel/firezone`. In the `bin` folder, the `firezone` binary can be used to start up Firezone. If you run it without any arguments you should see a list of available commands like this: @@ -73,7 +73,10 @@ The known commands are: pid Prints the operating system PID of the running system via a remote command version Prints the release name and version to be booted ``` -Most deployment-related configuration is handled with environment variables. You'll probably want to at least set variables related to your reverse proxy and database. See the [ENV var reference](/reference/env-vars/) for an exhaustive list. + +Most deployment-related configuration is handled with environment variables. +You'll probably want to at least set variables related to your reverse proxy +and database. See the [ENV var reference](/reference/env-vars/) for an exhaustive list. Now all you need are the database and reverse proxy that you've previously set up. Once that's done, you can use `firezone start` to start Firezone and run diff --git a/docs/docs/reference/configuration-file.mdx b/docs/docs/reference/configuration-file.mdx index 56e5cbe85..8a006fe3c 100644 --- a/docs/docs/reference/configuration-file.mdx +++ b/docs/docs/reference/configuration-file.mdx @@ -147,10 +147,6 @@ Shown below is a complete listing of the configuration options available in | `default['firezone']['wireguard']['log_rotation']['num_to_keep']` | Number of WireGuard log files to keep. | `10` | | `default['firezone']['wireguard']['interface_name']` | WireGuard interface name. **Changing this parameter may cause a temporary loss in VPN connectivity**. | `'wg-firezone'` | | `default['firezone']['wireguard']['port']` | WireGuard listen port. | `51820` | -| `default['firezone']['wireguard']['mtu']` | WireGuard interface MTU for this server and for device configurations. | `1280` | -| `default['firezone']['wireguard']['endpoint']` | WireGuard `Endpoint` to use for generating device configurations. If `nil`, defaults to the server's public IP address. | `nil` | -| `default['firezone']['wireguard']['dns']` | WireGuard `DNS` to use for generated device configurations. | `'1.1.1.1, 1.0.0.1'` | -| `default['firezone']['wireguard']['allowed_ips']` | WireGuard `AllowedIPs` to use for generated device configurations. | `'0.0.0.0/0, ::/0'` | | `default['firezone']['wireguard']['persistent_keepalive']` | Default PersistentKeepalive setting for generated device configurations. A value of 0 disables. | `0` | | `default['firezone']['wireguard']['ipv4']['enabled']` | Enable or disable IPv4 for WireGuard network. | `true` | | `default['firezone']['wireguard']['ipv4']['masquerade']` | Enable or disable masquerade for packets leaving the IPv4 tunnel. | `true` | diff --git a/docs/docs/reference/env-vars.mdx b/docs/docs/reference/env-vars.mdx index 6dd86d13b..1bb3d7db8 100644 --- a/docs/docs/reference/env-vars.mdx +++ b/docs/docs/reference/env-vars.mdx @@ -20,7 +20,7 @@ We recommend setting these in your Docker ENV file (`$HOME/.firezone/.env` by default). Required fields in **bold**. | Name | Description | Format | Default | -| --- | --- | --- | --- | +| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | **`EXTERNAL_URL`** | The external URL the web UI will be accessible at. Must be a valid FQDN for ACME SSL issuance to function. | String | | | **`ADMIN_EMAIL`** | Primary administrator email. | String | | | **`DEFAULT_ADMIN_PASSWORD`** | Default password that will be used for creating or resetting the primary administrator account. | String | Randomly generated upon install with `docker run firezone/firezone bin/gen-env`. | @@ -52,14 +52,10 @@ default). Required fields in **bold**. | `OUTBOUND_EMAIL_CONFIGS` | Email provider-specific config. | JSON-encoded hash of provider config. E.g. `{"gmail": {"access_token": "..."}, "smtp": {"relay": "smtp.example.com"}}`. See the [swoosh docs](https://hexdocs.pm/swoosh/). | `{}` | | `PHOENIX_PORT` | Internal port to listen on for the Phoenix web server. | Integer | `13000` | | `PRIVATE_CLIENTS` | List of IPs / CIDRs to consider trusted for purposes of correctly parsing the `X-Forwarded-For` header. | JSON-encoded list of IPs / CIDRs. | `[]` | -| `WIREGUARD_IPV4_ADDRESS` | Tunnel-side IPv4 address of Firezone. | String | `10.3.2.1` | | `WIREGUARD_IPV4_ENABLED` | Enable / disable tunnel-side IPv4 connectivity. | Boolean | `true` | | `WIREGUARD_IPV4_MASQUERADE` | Enable / disable IPv4 masquerade. | String | `true` | -| `WIREGUARD_IPV4_NETWORK` | Tunnel-side IPv4 network to use. | String | `10.3.2.0/24` | -| `WIREGUARD_IPV6_ADDRESS` | Tunnel-side IPv6 address of Firezone. | String | `fd00::3:2:1` | | `WIREGUARD_IPV6_ENABLED` | Enable / disable tunnel IPv6 addresses. | Boolean | `true` | | `WIREGUARD_IPV6_MASQUERADE` | Enable / disable IPv6 masquerade. | Boolean | `true` | -| `WIREGUARD_IPV6_NETWORK` | Tunnel-side IPv6 network to use. | String | `fd00::3:2:0/120` | | `WIREGUARD_MTU` | MTU to use for the server-side WireGuard MTU interface. | String | `1280` | | `WIREGUARD_PORT` | Port to listen on for WireGuard connections. | Integer | `51820` | | `SECURE_COOKIES` | Enable or disable requiring secure cookies. Required for HTTPS. | Boolean | `true` | diff --git a/mix.lock b/mix.lock index a4a9cdecb..fa20d5b98 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.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, - "cachex": {:hex, :cachex, "3.4.0", "868b2959ea4aeb328c6b60ff66c8d5123c083466ad3c33d3d8b5f142e13101fb", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "370123b1ab4fba4d2965fb18f87fd758325709787c8c5fce35b3fe80645ccbe5"}, + "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, "castore": {:hex, :castore, "0.1.20", "62a0126cbb7cb3e259257827b9190f88316eb7aa3fdac01fd6f2dfd64e7f46e9", [:mix], [], "hexpm", "a020b7650529c986c454a4035b6b13a328e288466986307bea3aadb4c95ac98a"}, "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, "cidr": {:git, "https://github.com/firezone/cidr-elixir.git", "a32125127a7910f476734f45391ba6d37036ee11", []}, @@ -18,13 +18,12 @@ "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"}, "earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"}, - "ecto": {:hex, :ecto, "3.9.3", "d98ed414404f96eb07261074fef0548782628e3fbb06c7d1e4ea152a441b8493", [: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", "7695b41246b78d5043f1e06836965e2aa5d065ba7667263782e557da5bf07ccb"}, - "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": {:hex, :ecto, "3.9.4", "3ee68e25dbe0c36f980f1ba5dd41ee0d3eb0873bccae8aeaf1a2647242bffa35", [: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", "de5f988c142a3aa4ec18b85a4ec34a2390b65b24f02385c1144252ff6ff8ee75"}, + "ecto_network": {:git, "https://github.com/firezone/ecto_network.git", "7dfe65bcb6506fb0ed6050871b433f3f8b1c10cb", [ref: "7dfe65bcb6506fb0ed6050871b433f3f8b1c10cb"]}, "ecto_sql": {:hex, :ecto_sql, "3.9.2", "34227501abe92dba10d9c3495ab6770e75e79b836d114c41108a4bf2ce200ad5", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 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", "1eb5eeb4358fdbcd42eac11c1fbd87e3affd7904e639d77903c1358b2abd3f70"}, - "elixir_make": {:hex, :elixir_make, "0.7.2", "e83548b0500e654d1a595f1134af4862a2e92ec3282ec4c2a17641e9aa45ee73", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "05fb44abf9582381c2eb1b73d485a55288c581071de0ee3ee1084ee69d6a8e5f"}, + "elixir_make": {:hex, :elixir_make, "0.7.3", "c37fdae1b52d2cc51069713a58c2314877c1ad40800a57efb213f77b078a460d", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "24ada3e3996adbed1fa024ca14995ef2ba3d0d17b678b0f3f2b1f66e6ce2b274"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "esaml": {:git, "https://github.com/firezone/esaml.git", "4294a3ac5262582144e117c10a1537287b6c1fe8", []}, - "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, "ex_doc": {:hex, :ex_doc, "0.29.1", "b1c652fa5f92ee9cf15c75271168027f92039b3877094290a75abcaac82a9f77", [: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", "b7745fa6374a36daf484e2a2012274950e084815b936b1319aeebcf7809574f6"}, "excoveralls": {:hex, :excoveralls, "0.15.1", "83c8cf7973dd9d1d853dce37a2fb98aaf29b564bf7d01866e409abf59dac2c0e", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f8416bd90c0082d56a2178cf46c837595a06575f70a5624f164a1ffe37de07e7"}, "file_size": {:hex, :file_size, "3.0.1", "ad447a69442a82fc701765a73992d7b1110136fa0d4a9d3190ea685d60034dcd", [:mix], [{:decimal, ">= 1.0.0 and < 3.0.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:number, "~> 1.0", [hex: :number, repo: "hexpm", optional: false]}], "hexpm", "64dd665bc37920480c249785788265f5d42e98830d757c6a477b3246703b8e20"}, @@ -37,11 +36,9 @@ "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"}, "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "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.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, "jose": {:hex, :jose, "1.11.5", "3bc2d75ffa5e2c941ca93e5696b54978323191988eb8d225c2e663ddfefd515e", [:mix, :rebar3], [], "hexpm", "dcd3b215bafe02ea7c5b23dafd3eb8062a5cd8f2d904fd9caa323d37034ab384"}, - "jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"}, "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.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, @@ -53,7 +50,7 @@ "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, "nimble_totp": {:hex, :nimble_totp, "0.2.0", "010ad5a6627f62e070f753752680550ba9e5744d96fc4101683cd037f1f5ee18", [:mix], [], "hexpm", "7fecd15ff14637ccd2fb3bda68476a6a7f107af731c51b1714436b687e5b50b3"}, "number": {:hex, :number, "1.0.3", "932c8a2d478a181c624138958ca88a78070332191b8061717270d939778c9857", [:mix], [{:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "dd397bbc096b2ca965a6a430126cc9cf7b9ef7421130def69bcf572232ca0f18"}, - "openid_connect": {:git, "https://github.com/firezone/openid_connect.git", "2d7c7ae2ee085d97cd1ea7bac61d322284a3ff6b", []}, + "openid_connect": {:git, "https://github.com/firezone/openid_connect.git", "6b6b91e25f5153bfa3bcca25be356924806eec13", []}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "phoenix": {:hex, :phoenix, "1.7.0-rc.0", "8e328572f496b5170e879da94baa57c5f878f354d50eac052c9a7c6d57c2cf54", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [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]}, {:websock_adapter, "~> 0.4", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "ed503f6c55184afc0a453e44e6ab2a09f014f59b7fdd682313fdc52ec2f82859"}, "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"}, @@ -68,23 +65,20 @@ "plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [: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", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"}, "plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"}, "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"}, - "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"}, "posthog": {:hex, :posthog, "0.1.0", "0abe2af719c0c30fe6a24569a8947a19f0edfa63f6fed61685a219a8d5655786", [:mix], [{:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ee0426999bd35edf3dfa84141bbd3de17ae07e04d62d269fd5ee581925f1c222"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "remote_ip": {:hex, :remote_ip, "1.1.0", "cb308841595d15df3f9073b7c39243a1dd6ca56e5020295cb012c76fbec50f2d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "616ffdf66aaad6a72fc546dabf42eed87e2a99e97b09cbd92b10cc180d02ed74"}, "rustler_precompiled": {:hex, :rustler_precompiled, "0.5.5", "a075a92c8e748ce5c4f7b2cf573a072d206a6d8d99c53f627e81d3f2b10616a3", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "e8a7f1abfec8d68683bb25d14efc88496f091ef113f7f4c45d39f3606f7223f6"}, "samly": {:git, "https://github.com/firezone/samly.git", "4603438ed4a95ed74d6c0232676c24d097e2feec", []}, - "sleeplocks": {:hex, :sleeplocks, "1.1.2", "d45aa1c5513da48c888715e3381211c859af34bee9b8290490e10c90bb6ff0ca", [:rebar3], [], "hexpm", "9fe5d048c5b781d6305c1a3a0f40bb3dfc06f49bf40571f3d2d0c57eaa7f59a5"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "sweet_xml": {:hex, :sweet_xml, "0.7.3", "debb256781c75ff6a8c5cbf7981146312b66f044a2898f453709a53e5031b45b", [:mix], [], "hexpm", "e110c867a1b3fe74bfc7dd9893aa851f0eed5518d0d7cad76d7baafd30e4f5ba"}, - "swoosh": {:hex, :swoosh, "1.9.0", "23b0678abc29158d3881970d2264724ecf960ac12b8657b657a717445ac01a8f", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "548e8f680182064e94354264065275d424eb60bc207fcd1891bcdc3d40845704"}, + "swoosh": {:hex, :swoosh, "1.9.1", "0a5d7bf9954eb41d7e55525bc0940379982b090abbaef67cd8e1fd2ed7f8ca1a", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "76dffff3ffcab80f249d5937a592eaef7cc49ac6f4cdd27e622868326ed6371e"}, "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, "ueberauth": {:hex, :ueberauth, "0.10.3", "4a3bd7ab7b5d93d301d264f0f6858392654ee92171f4437d067d1ae227c051d9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "1394f36a6c64e97f2038cf95228e7e52b4cb75417962e30418fbe9902b30e6d3"}, "ueberauth_identity": {:hex, :ueberauth_identity, "0.4.2", "1ef48b37428d225a2eb0cc453b0d446440d8f62c70dbbfef675ed923986136f2", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "134354bc3da3ece4333f3611fbe283372134b19b2ed8a3d7f43554c6102c4bff"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, - "unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"}, "websock": {:hex, :websock, "0.4.3", "184ac396bdcd3dfceb5b74c17d221af659dd559a95b1b92041ecb51c9b728093", [:mix], [], "hexpm", "5e4dd85f305f43fd3d3e25d70bec4a45228dfed60f0f3b072d8eddff335539cf"}, "websock_adapter": {:hex, :websock_adapter, "0.4.5", "30038a3715067f51a9580562c05a3a8d501126030336ffc6edb53bf57d6d2d26", [:mix], [{:bandit, "~> 0.6", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.4", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "1d9812dc7e703c205049426fd4fe0852a247a825f91b099e53dc96f68bafe4c8"}, "wireguardex": {:hex, :wireguardex, "0.3.6", "163b72693ecb710473c40d3dec3c4c150ba1e7c85ac137114feb93033198c935", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.5.1", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "1bbe747178265b5f5182aa46a61994635a31a6548cf5a965efbf827e7263c673"}, diff --git a/priv/wg0.client.conf b/priv/wg0.client.conf index 247f09b53..f5b469368 100644 --- a/priv/wg0.client.conf +++ b/priv/wg0.client.conf @@ -1,7 +1,7 @@ # This config corresponds to the wireguard-client device # created when the DB is bootstrapped from apps/fz_http/priv/repo/seeds.exs. [Interface] -Address = 10.3.2.6/32,fd00::3:2:6/128 +Address = 100.64.100.1/32,fd00::6/128 DNS = 127.0.0.11 PrivateKey = UJ3WN7k8mnRTj33BAoiA1lw0ag24oB7NsTg01MaeNUI= MTU = 1280 diff --git a/rel/overlays/bin/gen-env b/rel/overlays/bin/gen-env index 315600ef7..2b21eebb0 100755 --- a/rel/overlays/bin/gen-env +++ b/rel/overlays/bin/gen-env @@ -11,4 +11,15 @@ COOKIE_SIGNING_SALT=$(openssl rand -base64 6) COOKIE_ENCRYPTION_SALT=$(openssl rand -base64 6) DATABASE_ENCRYPTION_KEY=$(openssl rand -base64 32) DATABASE_PASSWORD=$(openssl rand -base64 12) + +# The ability to change the IPv4 and IPv6 address pool will be removed +# in a future Firezone release in order to reduce the possible combinations +# of network configurations we need to handle. +# +# Due to the above, we recommend not changing these unless absolutely +# necessary. +WIREGUARD_IPV4_NETWORK=100.64.0.0/10 +WIREGUARD_IPV4_ADDRESS=100.64.0.1 +WIREGUARD_IPV6_NETWORK=fd00::/106 +WIREGUARD_IPV4_ADDRESS=fd00::1 EOF diff --git a/scripts/dev_start.sh b/scripts/dev_start.sh index 5770fbab8..0bdda1dc3 100755 --- a/scripts/dev_start.sh +++ b/scripts/dev_start.sh @@ -1,8 +1,8 @@ #!/bin/sh ip link add dev wg-firezone type wireguard -ip address replace dev wg-firezone 10.3.2.1/24 -ip -6 address replace dev wg-firezone fd00::3:2:1/120 +ip address replace dev wg-firezone 100.64.0.1/10 +ip -6 address replace dev wg-firezone fd00::1/106 ip link set mtu 1280 up dev wg-firezone mix start
NameAssigned Device IPTunnel IPv4Tunnel IPv6 Public key Created
- <%= device.ipv4 %> - <%= device.ipv6 %> - <%= device.ipv4 %><%= device.ipv6 %> <%= device.public_key %> user end)} else {:halt, not_authorized(socket)} diff --git a/apps/fz_http/lib/fz_http_web/live/mfa_live/auth_live.ex b/apps/fz_http/lib/fz_http_web/live/mfa_live/auth_live.ex index 2e2015cd4..96696b588 100644 --- a/apps/fz_http/lib/fz_http_web/live/mfa_live/auth_live.ex +++ b/apps/fz_http/lib/fz_http_web/live/mfa_live/auth_live.ex @@ -117,7 +117,7 @@ defmodule FzHttpWeb.MFALive.Auth do {:ok, _method} -> {:noreply, push_redirect(socket, - to: root_path_for_role(socket.assigns.current_user.role) + to: root_path_for_user(socket.assigns.current_user) )} {:error, changeset} -> diff --git a/apps/fz_http/lib/fz_http_web/live/modal_component.ex b/apps/fz_http/lib/fz_http_web/live/modal_component.ex index 7707701ee..bf57c94ad 100644 --- a/apps/fz_http/lib/fz_http_web/live/modal_component.ex +++ b/apps/fz_http/lib/fz_http_web/live/modal_component.ex @@ -32,7 +32,7 @@ defmodule FzHttpWeb.ModalComponent do
- <%= if !assigns[:hide_footer_content] do %> + <%= if !(assigns[:hide_footer_content] || @opts[:hide_footer_content]) do %> <%= Phoenix.View.render(FzHttpWeb.SharedView, "submit_button.html", button_text: @opts[:button_text], form: @opts[:form] @@ -48,14 +48,4 @@ defmodule FzHttpWeb.ModalComponent do def handle_event("close", _, socket) do {:noreply, push_patch(socket, to: socket.assigns.return_to)} end - - @impl Phoenix.LiveComponent - @doc """ - XXX: This is needed due to a bug on pages with dropdowns. - Basically this modal receives the phx-click-away event and the - server crashes if this is not implemented. - """ - def handle_event("close_dropdown", _params, socket) do - {:noreply, socket} - end end diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/account.html.heex b/apps/fz_http/lib/fz_http_web/live/setting_live/account.html.heex index cbce147aa..9905a8be9 100644 --- a/apps/fz_http/lib/fz_http_web/live/setting_live/account.html.heex +++ b/apps/fz_http/lib/fz_http_web/live/setting_live/account.html.heex @@ -10,6 +10,30 @@ ) %> <% end %> +<%= if @live_action == :new_api_token do %> + <%= live_modal( + FzHttpWeb.SettingLive.NewApiTokenComponent, + return_to: ~p"/settings/account", + title: "Add API Token", + id: "new_api_token", + form: "api-token-form", + user: @current_user, + changeset: FzHttp.ApiTokens.new_api_token() + ) %> +<% end %> + +<%= if @live_action == :show_api_token do %> + <%= live_modal( + FzHttpWeb.SettingLive.ShowApiTokenComponent, + return_to: ~p"/settings/account", + title: "API Token #{@api_token_id}", + id: "show_api_token", + hide_footer_content: true, + user: @current_user, + api_token: @api_token + ) %> +<% end %> + <%= if @live_action == :register_mfa do %> <.live_component module={FzHttpWeb.MFA.RegisterComponent} @@ -96,6 +120,96 @@ +
+

+ API Tokens +

+ +
+

+ Manage API tokens. + + Read more about API tokens -> + +

+
+ +
+ <%= if Enum.any?(@api_tokens) do %> + + + + + + + + + + + <%= for api_token <- @api_tokens do %> + + + + + + + <% end %> + +
Created atIdentifierStatusActions
+ … + + <.link patch={~p"/settings/account/api_token/#{api_token}"}> + <%= api_token.id %> + + + <%= if ApiTokens.api_token_expired?(api_token) do %> + + + + Expired at + <% else %> + + + + Expires at + <% end %> + + … + + + <.link + data-confirm="Are you sure?" + phx-click="delete_api_token" + phx-value-id={api_token.id} + > + Delete + +
+ <% else %> +

+ No API tokens. +

+ <% end %> +
+ + <%= if length(@api_tokens) < FzHttp.ApiTokens.ApiToken.max_per_user() do %> + <.link patch={~p"/settings/account/api_token"} class="button"> + + + + Add API Token + + <% end %> +
+

Multi Factor Authentication 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 1bb27866f..0df82ed13 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 @@ -4,20 +4,31 @@ defmodule FzHttpWeb.SettingLive.Account do """ use FzHttpWeb, :live_view - alias FzHttp.{MFA, Users} - alias FzHttpWeb.{Endpoint, Presence} + alias FzHttp.{ + ApiTokens, + MFA, + Users + } + + alias FzHttpWeb.{ + Endpoint, + Presence + } @live_sessions_topic "notification:session" @page_title "Account Settings" @page_subtitle "Configure settings related to your Firezone web portal account." @impl Phoenix.LiveView - def mount(_params, _session, socket) do + def mount(params, _session, socket) do Endpoint.subscribe(@live_sessions_topic) {:ok, socket + |> assign(:api_token_id, params["api_token_id"]) |> assign(:subscribe_link, subscribe_link()) + |> assign(:allow_delete, length(Users.list_admins()) > 1) + |> assign(:api_tokens, ApiTokens.list_api_tokens(socket.assigns.current_user.id)) |> assign(:changeset, Users.change_user(socket.assigns.current_user)) |> assign(:methods, MFA.list_methods(socket.assigns.current_user)) |> assign(:page_title, @page_title) @@ -29,15 +40,42 @@ defmodule FzHttpWeb.SettingLive.Account do )} end + @impl Phoenix.LiveView + def handle_params(%{"api_token_id" => api_token_id}, _url, socket) do + {:noreply, + socket + |> assign(:api_token, ApiTokens.get_api_token!(api_token_id))} + end + @impl Phoenix.LiveView def handle_params(_params, _url, socket) do - admins = Users.list_admins() - {:noreply, assign(socket, :allow_delete, length(admins) > 1)} + {:noreply, + socket + |> assign(:allow_delete, length(Users.list_admins()) > 1) + |> assign(:api_tokens, ApiTokens.list_api_tokens(socket.assigns.current_user.id))} + end + + @impl Phoenix.LiveView + def handle_event("delete_api_token", %{"id" => id}, socket) do + api_token = ApiTokens.get_api_token!(id) + + if api_token.user_id == socket.assigns.current_user.id do + {:ok, _deleted} = ApiTokens.delete_api_token(api_token) + end + + {:noreply, + socket + |> assign(:api_tokens, ApiTokens.list_api_tokens(socket.assigns.current_user.id))} end @impl Phoenix.LiveView def handle_event("delete_authenticator", %{"id" => id}, socket) do - {:ok, _deleted} = id |> MFA.get_method!() |> MFA.delete_method() + method = MFA.get_method!(id) + + # A user can only delete his/her own MFA method! + if method.user_id == socket.assigns.current_user.id do + {:ok, _deleted} = MFA.delete_method(method) + end {:noreply, socket @@ -58,7 +96,7 @@ defmodule FzHttpWeb.SettingLive.Account do end defp get_metas(presences, user_id) do - get_in(presences, [to_string(user_id), :metas]) || [] + get_in(presences, [user_id, :metas]) || [] end defp subscribe_link do 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/client_defaults.html.heex similarity index 69% rename from apps/fz_http/lib/fz_http_web/live/setting_live/site.html.heex rename to apps/fz_http/lib/fz_http_web/live/setting_live/client_defaults.html.heex index fa8869fe6..2e436f7bd 100644 --- 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/client_defaults.html.heex @@ -6,13 +6,13 @@
<%= render(FzHttpWeb.SharedView, "flash.html", assigns) %> -

Site Defaults

+

Client Defaults

<%= live_component( - FzHttpWeb.SettingLive.SiteFormComponent, + FzHttpWeb.SettingLive.ClientDefaultsFormComponent, changeset: @changeset, - id: :site_form_component + id: :client_defaults_form_component ) %>
diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/client_defaults_form_component.ex b/apps/fz_http/lib/fz_http_web/live/setting_live/client_defaults_form_component.ex new file mode 100644 index 000000000..1808ba4a4 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/live/setting_live/client_defaults_form_component.ex @@ -0,0 +1,32 @@ +defmodule FzHttpWeb.SettingLive.ClientDefaultsFormComponent do + @moduledoc """ + Handles updating client defaults form. + """ + use FzHttpWeb, :live_component + + alias FzHttp.Configurations + + @impl Phoenix.LiveComponent + def update(assigns, socket) do + {:ok, + socket + |> assign(assigns)} + end + + @impl Phoenix.LiveComponent + def handle_event("save", %{"configuration" => configuration_params}, socket) do + configuration = Configurations.get_configuration!() + + case Configurations.update_configuration(configuration, configuration_params) do + {:ok, configuration} -> + {:noreply, + socket + |> assign(:changeset, Configurations.change_configuration(configuration))} + + {: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/client_defaults_form_component.html.heex similarity index 57% rename from apps/fz_http/lib/fz_http_web/live/setting_live/site_form_component.html.heex rename to apps/fz_http/lib/fz_http_web/live/setting_live/client_defaults_form_component.html.heex index 3bc1eaadd..88d40081e 100644 --- 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/client_defaults_form_component.html.heex @@ -1,19 +1,19 @@
<.form :let={f} for={@changeset} id={@id} phx-target={@myself} phx-submit="save">
- <%= label(f, :allowed_ips, "Allowed IPs", class: "label") %> + <%= label(f, :default_client_allowed_ips, "Allowed IPs", class: "label") %>
<%= textarea( f, - :allowed_ips, + :default_client_allowed_ips, placeholder: "0.0.0.0/0, ::/0", - class: "textarea #{input_error_class(f, :allowed_ips)}" + class: "textarea #{input_error_class(f, :default_client_allowed_ips)}" ) %>

- <%= error_tag(f, :allowed_ips) %> + <%= error_tag(f, :default_client_allowed_ips) %>

Configures the default AllowedIPs setting for devices. @@ -25,40 +25,40 @@

- <%= label(f, :dns, "DNS Servers", class: "label") %> + <%= label(f, :default_client_dns, "DNS Servers", class: "label") %>
<%= text_input( f, - :dns, + :default_client_dns, placeholder: "1.1.1.1, 1.0.0.1", - class: "input #{input_error_class(f, :dns)}" + class: "input #{input_error_class(f, :default_client_dns)}" ) %>

- <%= error_tag(f, :dns) %> + <%= error_tag(f, :default_client_dns) %>

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

- <%= label(f, :endpoint, "Endpoint", class: "label") %> + <%= label(f, :default_client_endpoint, "Endpoint", class: "label") %>
<%= text_input( f, - :endpoint, + :default_client_endpoint, placeholder: "firezone.example.com", - class: "input #{input_error_class(f, :endpoint)}" + class: "input #{input_error_class(f, :default_client_endpoint)}" ) %>

- <%= error_tag(f, :endpoint) %> + <%= error_tag(f, :default_client_endpoint) %>

IPv4, IPv6 address, or FQDN that devices will be configured to connect @@ -67,41 +67,41 @@

- <%= label(f, :persistent_keepalive, "Persistent Keepalive", class: "label") %> + <%= label(f, :default_client_persistent_keepalive, "Persistent Keepalive", class: "label") %>
<%= text_input( f, - :persistent_keepalive, + :default_client_persistent_keepalive, placeholder: "25", - class: "input #{input_error_class(f, :persistent_keepalive)}" + class: "input #{input_error_class(f, :default_client_persistent_keepalive)}" ) %>

- <%= error_tag(f, :persistent_keepalive) %> + <%= error_tag(f, :default_client_persistent_keepalive) %>

- Interval in seconds to send persistent keepalive packets from clients. Most users won't need to change - this. Leave this blank to omit this field from generated configs. + Interval in seconds to send persistent keepalive packets from devices. Most users won't + need to change this. Leave this blank to omit this field from generated configs.

- <%= label(f, :mtu, "MTU", class: "label") %> + <%= label(f, :default_client_mtu, "MTU", class: "label") %>
<%= text_input( f, - :mtu, + :default_client_mtu, placeholder: "1280", - class: "input #{input_error_class(f, :mtu)}" + class: "input #{input_error_class(f, :default_client_mtu)}" ) %>

- <%= error_tag(f, :mtu) %> + <%= error_tag(f, :default_client_mtu) %>

- WireGuard interface MTU for client configs. 1280 is a safe bet for most networks. + WireGuard interface MTU for devices. 1280 is a safe bet for most networks. Leave this blank to omit this field from generated configs.

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/client_defaults_live.ex similarity index 54% rename from apps/fz_http/lib/fz_http_web/live/setting_live/site_live.ex rename to apps/fz_http/lib/fz_http_web/live/setting_live/client_defaults_live.ex index 1407e26df..7124d8090 100644 --- 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/client_defaults_live.ex @@ -1,13 +1,13 @@ -defmodule FzHttpWeb.SettingLive.Site do +defmodule FzHttpWeb.SettingLive.ClientDefaults do @moduledoc """ Manages the defaults view. """ use FzHttpWeb, :live_view - alias FzHttp.Sites + alias FzHttp.Configurations - @page_title "Site Settings" - @page_subtitle "Configure default WireGuard settings for this site." + @page_title "Client Defaults" + @page_subtitle "Configure default values for generating WireGuard client configurations." @impl Phoenix.LiveView def mount(_params, _session, socket) do @@ -19,6 +19,6 @@ defmodule FzHttpWeb.SettingLive.Site do end defp changeset do - Sites.get_site!() |> Sites.change_site() + Configurations.get_configuration!() |> Configurations.change_configuration() end end diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/customization.html.heex b/apps/fz_http/lib/fz_http_web/live/setting_live/customization.html.heex index 968ddaec7..2166b2b7d 100644 --- a/apps/fz_http/lib/fz_http_web/live/setting_live/customization.html.heex +++ b/apps/fz_http/lib/fz_http_web/live/setting_live/customization.html.heex @@ -18,7 +18,7 @@
- <%= for type <- Conf.logo_types do %> + <%= for type <- FzHttp.Configurations.logo_types do %>

<%= k %><%= v["label"] %><%= v["client_id"] %><%= v["discovery_document_uri"] %><%= v["scope"] %><%= v.label %><%= v.client_id %><%= v.discovery_document_uri %><%= v.scope %> <%= live_patch(to: ~p"/settings/security/oidc/#{k}/edit", class: "button") do %> @@ -223,9 +231,9 @@ <%= for {k, v} <- @saml_configs do %>
<%= k %><%= v["label"] %><%= v.label %> -
<%= v["metadata"] %>
+
<%= v.metadata %>
<%= live_patch(to: ~p"/settings/security/saml/#{k}/edit", 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 0d2677b00..a4ea31e42 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 @@ -7,29 +7,36 @@ defmodule FzHttpWeb.SettingLive.Security do import Ecto.Changeset import FzCommon.FzCrypto, only: [rand_string: 1] - alias FzHttp.Configurations, as: Conf - alias FzHttp.{Sites, Sites.Site} + alias FzHttp.Configurations @page_title "Security Settings" @page_subtitle "Configure security-related settings." @impl Phoenix.LiveView def mount(_params, _session, socket) do - config_changeset = Conf.change_configuration() + config_changeset = Configurations.change_configuration() {:ok, socket |> assign(:form_changed, false) |> assign(:session_duration_options, session_duration_options()) - |> assign(:site_changeset, site_changeset()) + |> assign(:configuration_changeset, configuration_changeset()) |> assign(:config_changeset, config_changeset) - |> assign(:oidc_configs, config_changeset.data.openid_connect_providers || %{}) - |> assign(:saml_configs, config_changeset.data.saml_identity_providers || %{}) + |> assign(:oidc_configs, map_providers(config_changeset.data.openid_connect_providers)) + |> assign(:saml_configs, map_providers(config_changeset.data.saml_identity_providers)) |> assign(:field_titles, field_titles(config_changeset)) |> assign(:page_subtitle, @page_subtitle) |> assign(:page_title, @page_title)} end + defp map_providers(nil), do: %{} + + defp map_providers(providers) when is_list(providers) do + for provider <- providers, + into: %{}, + do: {provider.id, Map.from_struct(provider)} + end + @impl Phoenix.LiveView def handle_params(params, _uri, socket) do {:noreply, assign(socket, :id, params["id"])} @@ -44,30 +51,32 @@ defmodule FzHttpWeb.SettingLive.Security do @impl Phoenix.LiveView def handle_event( - "save_site", - %{"site" => %{"vpn_session_duration" => vpn_session_duration}}, + "save_configuration", + %{"configuration" => %{"vpn_session_duration" => vpn_session_duration}}, socket ) do - site = Sites.get_site!() + configuration = Configurations.get_configuration!() - case Sites.update_site(site, %{vpn_session_duration: vpn_session_duration}) do - {:ok, site} -> + case Configurations.update_configuration(configuration, %{ + vpn_session_duration: vpn_session_duration + }) do + {:ok, configuration} -> {:noreply, socket |> assign(:form_changed, false) - |> assign(:site_changeset, Sites.change_site(site))} + |> assign(:configuration_changeset, Configurations.change_configuration(configuration))} - {:error, site_changeset} -> + {:error, configuration_changeset} -> {:noreply, socket - |> assign(:site_changeset, site_changeset)} + |> assign(:configuration_changeset, configuration_changeset)} end end @impl Phoenix.LiveView def handle_event("toggle", %{"config" => config} = params, socket) do toggle_value = !!params["value"] - {:ok, _conf} = Conf.update_configuration(%{config => toggle_value}) + {:ok, _conf} = Configurations.update_configuration(%{config => toggle_value}) {:noreply, socket} end @@ -78,9 +87,11 @@ defmodule FzHttpWeb.SettingLive.Security do field_key = Map.fetch!(@types, type) providers = - get_in(socket.assigns.config_changeset, [Access.key!(:data), Access.key!(field_key)]) + socket.assigns.config_changeset + |> get_in([Access.key!(:data), Access.key!(field_key)]) + |> Enum.reject(&(&1.id == key)) - {:ok, conf} = Conf.update_configuration(%{field_key => Map.delete(providers, key)}) + {:ok, conf} = Configurations.update_configuration(%{field_key => providers}) {:noreply, socket @@ -94,7 +105,7 @@ defmodule FzHttpWeb.SettingLive.Security do def session_duration_options do [ Never: 0, - Once: Site.max_vpn_session_duration(), + Once: FzHttp.Configurations.Configuration.max_vpn_session_duration(), "Every Hour": @hour, "Every Day": @day, "Every Week": 7 * @day, @@ -103,9 +114,9 @@ defmodule FzHttpWeb.SettingLive.Security do ] end - defp site_changeset do - Sites.get_site!() - |> Sites.change_site() + defp configuration_changeset do + Configurations.get_configuration!() + |> Configurations.change_configuration() end @fields ~w( diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/show_api_token_component.ex b/apps/fz_http/lib/fz_http_web/live/setting_live/show_api_token_component.ex new file mode 100644 index 000000000..860ad0828 --- /dev/null +++ b/apps/fz_http/lib/fz_http_web/live/setting_live/show_api_token_component.ex @@ -0,0 +1,63 @@ +defmodule FzHttpWeb.SettingLive.ShowApiTokenComponent do + use FzHttpWeb, :live_component + + alias Phoenix.LiveView.JS + alias FzHttpWeb.Auth.JSON.Authentication + + def update(assigns, socket) do + if connected?(socket) do + {:ok, secret, _claims} = Authentication.fz_encode_and_sign(assigns.api_token, assigns.user) + + {:ok, + socket + |> assign(:secret, secret)} + else + {:ok, socket} + end + end + + def render(assigns) do + ~H""" +
+ <%= if assigns[:secret] do %> +
+
+
+ API token secret: +
+
+
+ +
+
+
+
<%= @secret %>
+
+
+

Warning! This token is sensitive data. Store it somewhere safe.

+
+
+
+
cURL example:
+
# List all users
+    curl -H 'Content-Type: application/json' \
+         -H 'Authorization: Bearer <%= @secret %>' \
+         <%= Application.fetch_env!(:fz_http, :external_url) %>/v0/users
+
+ + <% end %> +
+ """ + end +end 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 deleted file mode 100644 index 0c438b10b..000000000 --- a/apps/fz_http/lib/fz_http_web/live/setting_live/site_form_component.ex +++ /dev/null @@ -1,32 +0,0 @@ -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/unprivileged/account_live.ex b/apps/fz_http/lib/fz_http_web/live/setting_live/unprivileged/account_live.ex index 3e25c76b2..a5f5824f0 100644 --- a/apps/fz_http/lib/fz_http_web/live/setting_live/unprivileged/account_live.ex +++ b/apps/fz_http/lib/fz_http_web/live/setting_live/unprivileged/account_live.ex @@ -8,7 +8,6 @@ defmodule FzHttpWeb.SettingLive.Unprivileged.Account do """ use FzHttpWeb, :live_view - alias FzHttp.Configurations, as: Conf alias FzHttp.{MFA, Users} alias FzHttpWeb.{Endpoint, Presence} @@ -22,7 +21,7 @@ defmodule FzHttpWeb.SettingLive.Unprivileged.Account do {:ok, socket - |> assign(:local_auth_enabled, Conf.get!(:local_auth_enabled)) + |> assign(:local_auth_enabled, FzHttp.Configurations.get!(:local_auth_enabled)) |> assign(:changeset, Users.change_user(socket.assigns.current_user)) |> assign(:methods, MFA.list_methods(socket.assigns.current_user)) |> assign(:page_title, @page_title) @@ -61,6 +60,6 @@ defmodule FzHttpWeb.SettingLive.Unprivileged.Account do end defp get_metas(presences, user_id) do - get_in(presences, [to_string(user_id), :metas]) || [] + get_in(presences, [user_id, :metas]) || [] end end diff --git a/apps/fz_http/lib/fz_http_web/live/sidebar_component.ex b/apps/fz_http/lib/fz_http_web/live/sidebar_component.ex index 124bfab4d..13287402f 100644 --- a/apps/fz_http/lib/fz_http_web/live/sidebar_component.ex +++ b/apps/fz_http/lib/fz_http_web/live/sidebar_component.ex @@ -37,7 +37,7 @@ defmodule FzHttpWeb.SidebarComponent do
<%= @device.description %>
Interface IPv4Tunnel IPv4 <%= @device.ipv4 %>
Interface IPv6Tunnel IPv6 <%= @device.ipv6 %>
UserWireGuard IPTunnel IPv4Tunnel IPv6 Remote IP Latest Handshake Transfer - <%= device.ipv4 %> -
- <%= device.ipv6 %> -
<%= device.ipv4 %><%= device.ipv6 %> <%= device.remote_ip %>