diff --git a/.credo.exs b/.credo.exs index f23eb309c..17e9431a7 100644 --- a/.credo.exs +++ b/.credo.exs @@ -125,12 +125,11 @@ {Credo.Check.Refactor.MapJoin, []}, {Credo.Check.Refactor.NegatedConditionsInUnless, []}, {Credo.Check.Refactor.NegatedConditionsWithElse, []}, - {Credo.Check.Refactor.Nesting, []}, + {Credo.Check.Refactor.Nesting, [max_nesting: 3]}, {Credo.Check.Refactor.UnlessWithElse, []}, {Credo.Check.Refactor.WithClauses, []}, {Credo.Check.Refactor.FilterFilter, []}, {Credo.Check.Refactor.RejectReject, []}, - {Credo.Check.Refactor.RedundantWithClauseResult, []}, # ## Warnings diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 557c01642..d768bd21d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -294,8 +294,8 @@ jobs: image: vihangk1/docker-test-saml-idp:latest env: SIMPLESAMLPHP_SP_ENTITY_ID: 'urn:firezone.dev:firezone-app' - SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE: 'http://localhost:4002/auth/saml/sp/consume/mysamlidp' - SIMPLESAMLPHP_SP_SINGLE_LOGOUT_SERVICE: 'http://localhost:4002/auth/saml/sp/logout/mysamlidp' + SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE: 'http://localhost:13000/auth/saml/sp/consume/mysamlidp' + SIMPLESAMLPHP_SP_SINGLE_LOGOUT_SERVICE: 'http://localhost:13000/auth/saml/sp/logout/mysamlidp' SIMPLESAMLPHP_SP_NAME_ID_FORMAT: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' SIMPLESAMLPHP_SP_NAME_ID_ATTRIBUTE: 'email' SIMPLESAMLPHP_IDP_AUTH: 'example-userpass' diff --git a/.gitignore b/.gitignore index 39be23a21..0ad96f9b0 100644 --- a/.gitignore +++ b/.gitignore @@ -63,5 +63,9 @@ npm-debug.log # Test screenshots apps/fz_http/screenshots +# WG configs generated in acceptance tests +*.conf + # Auto generated private key apps/fz_http/priv/wg_dev_private_key +apps/fz_http/priv/static/uploads diff --git a/apps/fz_common/README.md b/apps/fz_common/README.md deleted file mode 100644 index 089488e9b..000000000 --- a/apps/fz_common/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# FzCommon - -**TODO: Add description** - -## Installation - -If [available in Hex](https://hex.pm/docs/publish), the package can be installed -by adding `fz_common` to your list of dependencies in `mix.exs`: - -```elixir -def deps do - [ - {:fz_common, "~> 0.1.0"} - ] -end -``` - -Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) -and published on [HexDocs](https://hexdocs.pm). Once published, the docs can -be found at [https://hexdocs.pm/fz_common](https://hexdocs.pm/fz_common). diff --git a/apps/fz_common/lib/fz_integer.ex b/apps/fz_common/lib/fz_integer.ex deleted file mode 100644 index 791a7880a..000000000 --- a/apps/fz_common/lib/fz_integer.ex +++ /dev/null @@ -1,28 +0,0 @@ -defmodule FzCommon.FzInteger do - @moduledoc """ - Utility functions for working with Integers. - """ - - # Postgres max int size is 4 bytes - @max_integer 2_147_483_647 - - @byte_size_opts [ - precision: 2, - delimiter: "" - ] - - def clamp(num, min, _max) when is_integer(num) and num < min, do: min - def clamp(num, _min, max) when is_integer(num) and num > max, do: max - def clamp(num, _min, _max) when is_integer(num), do: num - - def max_pg_integer do - @max_integer - end - - def to_human_bytes(nil), do: to_human_bytes(0) - - def to_human_bytes(bytes) when is_integer(bytes) do - FileSize.from_bytes(bytes, scale: :iec) - |> FileSize.format(@byte_size_opts) - end -end diff --git a/apps/fz_common/lib/fz_kernel_version.ex b/apps/fz_common/lib/fz_kernel_version.ex deleted file mode 100644 index 1332b5b1e..000000000 --- a/apps/fz_common/lib/fz_kernel_version.ex +++ /dev/null @@ -1,15 +0,0 @@ -defmodule FzCommon.FzKernelVersion do - @moduledoc """ - Helpers related to kernel version - """ - - @doc """ - Compares version tuple to current kernel version - """ - def is_version_greater_than?(val) do - case :os.version() do - v when is_tuple(v) -> v > val - _ -> false - end - end -end diff --git a/apps/fz_common/lib/fz_net.ex b/apps/fz_common/lib/fz_net.ex index 9b1a8dd71..9ff3afdc3 100644 --- a/apps/fz_common/lib/fz_net.ex +++ b/apps/fz_common/lib/fz_net.ex @@ -1,38 +1,4 @@ defmodule FzCommon.FzNet do - @moduledoc """ - Network utility functions. - """ - import FzCommon.FzRegex - - # XXX: Consider using CIDR for this - def ip_type(str) when is_binary(str) do - charlist = - str - # remove CIDR range if exists - |> String.split("/") - |> List.first() - |> String.to_charlist() - - case :inet.parse_ipv4_address(charlist) do - {:ok, _} -> - "IPv4" - - {:error, _} -> - case :inet.parse_ipv6_address(charlist) do - {:ok, _} -> "IPv6" - {:error, _} -> "unknown" - end - end - end - - def valid_cidr?(cidr) when is_binary(cidr) do - 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()) - end - @doc """ Standardize IP addresses and CIDR ranges so that they can be condensed / shortened. """ @@ -48,27 +14,6 @@ defmodule FzCommon.FzNet do end end - def valid_fqdn?(fqdn) when is_binary(fqdn) do - String.match?(fqdn, fqdn_regex()) - end - - def valid_hostname?(hostname) when is_binary(hostname) do - String.match?(hostname, host_regex()) - end - - def to_complete_url(str) when is_binary(str) do - case URI.new(str) do - {:ok, %{host: nil, scheme: nil}} -> - {:ok, "https://" <> str} - - {:ok, _} -> - {:ok, str} - - err -> - err - end - end - def endpoint_to_ip(endpoint) do endpoint |> String.replace(~r{:\d+$}, "") diff --git a/apps/fz_common/lib/fz_regex.ex b/apps/fz_common/lib/fz_regex.ex deleted file mode 100644 index 5f6c1075b..000000000 --- a/apps/fz_common/lib/fz_regex.ex +++ /dev/null @@ -1,25 +0,0 @@ -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/lib/fz_string.ex b/apps/fz_common/lib/fz_string.ex deleted file mode 100644 index 14169e31c..000000000 --- a/apps/fz_common/lib/fz_string.ex +++ /dev/null @@ -1,26 +0,0 @@ -defmodule FzCommon.FzString do - @moduledoc """ - Utility functions for working with Strings. - """ - - def sanitize_filename(str) when is_binary(str) do - str - |> String.replace(~r/[^a-zA-Z0-9]+/, "_") - end - - def to_boolean(str) when is_binary(str) do - as_bool(String.downcase(str)) - end - - defp as_bool("true") do - true - end - - defp as_bool("false") do - false - end - - defp as_bool(unknown) do - raise "Unknown boolean: string #{unknown} not one of ['true', 'false']." - end -end diff --git a/apps/fz_common/mix.exs b/apps/fz_common/mix.exs index 3b96d9522..3216a1ea5 100644 --- a/apps/fz_common/mix.exs +++ b/apps/fz_common/mix.exs @@ -31,7 +31,6 @@ defmodule FzCommon.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:file_size, "~> 3.0.1"}, {:cidr, github: "firezone/cidr-elixir"}, {:posthog, "~> 0.1"}, {:argon2_elixir, "~> 2.0"} diff --git a/apps/fz_common/test/fz_common_test.exs b/apps/fz_common/test/fz_common_test.exs deleted file mode 100644 index 7bf9cacc4..000000000 --- a/apps/fz_common/test/fz_common_test.exs +++ /dev/null @@ -1,6 +0,0 @@ -defmodule FzCommonTest do - use ExUnit.Case - doctest FzCommon - - # XXX: Ensure command injection is NOT POSSIBLE -end diff --git a/apps/fz_common/test/fz_integer_test.exs b/apps/fz_common/test/fz_integer_test.exs deleted file mode 100644 index c283c67cf..000000000 --- a/apps/fz_common/test/fz_integer_test.exs +++ /dev/null @@ -1,54 +0,0 @@ -defmodule FzCommon.FzIntegerTest do - use ExUnit.Case, async: true - - alias FzCommon.FzInteger - - describe "max_pg_integer/0" do - test "returns max integer for postgres" do - assert 2_147_483_647 == FzInteger.max_pg_integer() - end - end - - describe "clamp/3" do - test "clamps to min" do - min = 1 - max = 5 - num = 0 - - assert 1 == FzInteger.clamp(num, min, max) - end - - test "clamps to max" do - min = 1 - max = 5 - num = 7 - - assert 5 == FzInteger.clamp(num, min, max) - end - - test "returns num if in range" do - min = 1 - max = 5 - num = 3 - - assert 3 == FzInteger.clamp(num, min, max) - end - end - - describe "to_human_bytes/1" do - @expected [ - {nil, "0.00 B"}, - {1_023, "1023.00 B"}, - {1_023_999_999_999_999_999_999, "888.18 EiB"}, - {1_000, "1000.00 B"}, - {1_115, "1.09 KiB"}, - {987_654_321_123_456_789_987, "856.65 EiB"} - ] - - test "handles expected cases" do - for {bytes, str} <- @expected do - assert FzInteger.to_human_bytes(bytes) == str - end - end - end -end diff --git a/apps/fz_common/test/fz_net_test.exs b/apps/fz_common/test/fz_net_test.exs index 67419a4ee..ed22a63f2 100644 --- a/apps/fz_common/test/fz_net_test.exs +++ b/apps/fz_common/test/fz_net_test.exs @@ -3,100 +3,6 @@ defmodule FzCommon.FzNetTest do alias FzCommon.FzNet - describe "ip_type/1" do - test "it detects IPv4 addresses" do - assert FzNet.ip_type("127.0.0.1") == "IPv4" - end - - test "it detects IPv6 addresses" do - assert FzNet.ip_type("::1") == "IPv6" - end - - test "it reports \"unknown\" for unknown type" do - assert FzNet.ip_type("invalid") == "unknown" - end - end - - describe "valid_ip?/1" do - test "10 is an invalid IP" do - refute FzNet.valid_ip?("10") - end - - test "1.1.1. is an invalid IP" do - refute FzNet.valid_ip?("1.1.1.") - end - - test "foobar is an invalid IP" do - refute FzNet.valid_ip?("foobar") - end - - test "1.1.1.1 is a valid IP" do - assert FzNet.valid_ip?("1.1.1.1") - end - - test "::1 is a valid IP" do - assert FzNet.valid_ip?("1.1.1.1") - end - end - - describe "valid_host?/1" do - test "foobar is valid" do - assert FzNet.valid_hostname?("foobar") - end - - test "-foobar is invalid" do - refute FzNet.valid_hostname?("-foobar") - end - - test "1234 is valid" do - assert FzNet.valid_hostname?("1234") - end - end - - describe "valid_fqdn?/1" do - test "foobar is invalid" do - refute FzNet.valid_fqdn?("foobar") - end - - test "-foobar is invalid" do - refute FzNet.valid_fqdn?("-foobar") - end - - test "foobar.com is valid" do - assert FzNet.valid_fqdn?("foobar.com") - end - - test "ff99.example.com is valid" do - assert FzNet.valid_fqdn?("ff00.example.com") - end - end - - describe "valid_cidr?/1" do - test "::/0f is an invalid CIDR" do - refute FzNet.valid_cidr?("::/0f") - end - - test "0.0.0.0/0f is an invalid CIDR" do - refute FzNet.valid_cidr?("0.0.0.0/0f") - end - - test "0.0.0.0 is an invalid CIDR" do - refute FzNet.valid_cidr?("0.0.0.0") - end - - test "foobar is an invalid CIDR" do - refute FzNet.valid_cidr?("foobar") - end - - test "0.0.0.0/0 is a valid CIDR" do - assert FzNet.valid_cidr?("::/0") - end - - test "::/0 is a valid CIDR" do - assert FzNet.valid_cidr?("::/0") - end - end - describe "standardized_inet/1" do test "sanitizes CIDRs with invalid start" do assert "10.0.0.0/24" == FzNet.standardized_inet("10.0.0.5/24") @@ -111,30 +17,6 @@ defmodule FzCommon.FzNetTest do end end - describe "to_complete_url/1" do - @tag cases: [ - {"foobar", "https://foobar"}, - {"google.com", "https://google.com"}, - {"127.0.0.1", "https://127.0.0.1"}, - {"8.8.8.8", "https://8.8.8.8"}, - {"https://[fd00::1]", "https://[fd00::1]"}, - {"http://foobar", "http://foobar"}, - {"https://foobar", "https://foobar"} - ] - test "parses valid string URIs", %{cases: cases} do - for {subject, expected} <- cases do - assert {:ok, ^expected} = FzNet.to_complete_url(subject) - end - end - - @tag cases: ["<", "{", "["] - test "returns {:error, _} for invalid URIs", %{cases: cases} do - for subject <- cases do - assert {:error, _} = FzNet.to_complete_url(subject) - end - end - end - describe "endpoint_to_ip/1" do test "IPv4" do assert "192.168.1.1" == FzNet.endpoint_to_ip("192.168.1.1:4562") diff --git a/apps/fz_common/test/fz_string_test.exs b/apps/fz_common/test/fz_string_test.exs deleted file mode 100644 index 072919877..000000000 --- a/apps/fz_common/test/fz_string_test.exs +++ /dev/null @@ -1,29 +0,0 @@ -defmodule FzCommon.FzStringTest do - use ExUnit.Case, async: true - - alias FzCommon.FzString - - describe "sanitize_filename/1" do - test "sanitizes sequential spaces" do - assert "Factory_Device" == FzString.sanitize_filename("Factory Device") - end - end - - describe "to_boolean/1" do - test "converts to true" do - assert true == FzString.to_boolean("True") - end - - test "converts to false" do - assert false == FzString.to_boolean("False") - end - - test "raises exception on unknowns" do - message = "Unknown boolean: string foobar not one of ['true', 'false']." - - assert_raise RuntimeError, message, fn -> - FzString.to_boolean("foobar") - end - end - end -end diff --git a/apps/fz_http/assets/css/main.scss b/apps/fz_http/assets/css/main.scss index 343cb87d4..3012b16a1 100644 --- a/apps/fz_http/assets/css/main.scss +++ b/apps/fz_http/assets/css/main.scss @@ -49,3 +49,18 @@ pre { max-width: 600px; overflow: hidden; } + +.switch { + &:hover input[type=checkbox]:disabled+.check { + background: rgba(181, 181, 181, 1); + } + + &:hover input[type=checkbox]:checked:disabled+.check { + background: rgba(94, 0, 214, 1); + } + + input[type=checkbox]:disabled+.check { + opacity: 0.5; + cursor: not-allowed; + } +} diff --git a/apps/fz_http/lib/fz_http/application.ex b/apps/fz_http/lib/fz_http/application.ex index a7a1af5da..f8f17ca58 100644 --- a/apps/fz_http/lib/fz_http/application.ex +++ b/apps/fz_http/lib/fz_http/application.ex @@ -1,18 +1,17 @@ defmodule FzHttp.Application do - # See https://hexdocs.pm/elixir/Application.html - # for more information on OTP Applications - @moduledoc false - use Application - alias FzHttp.Telemetry - def start(_type, _args) do - # See https://hexdocs.pm/elixir/Supervisor.html - # for other strategies and supported options - Telemetry.fz_http_started() - opts = [strategy: :one_for_one, name: __MODULE__.Supervisor] - Supervisor.start_link(children(), opts) + supervision_tree_mode = FzHttp.Config.fetch_env!(:fz_http, :supervision_tree_mode) + + result = + supervision_tree_mode + |> children() + |> Supervisor.start_link(strategy: :one_for_one, name: __MODULE__.Supervisor) + + :ok = after_start() + + result end # Tell Phoenix to update the endpoint configuration @@ -22,8 +21,6 @@ defmodule FzHttp.Application do :ok end - defp children, do: children(FzHttp.Config.fetch_env!(:fz_http, :supervision_tree_mode)) - defp children(:full) do [ FzHttp.Server, @@ -63,4 +60,14 @@ defmodule FzHttp.Application do FzHttp.Vault ] end + + if Mix.env() == :prod do + defp after_start do + FzHttp.Config.validate_runtime_config!() + end + else + defp after_start do + :ok + end + end end diff --git a/apps/fz_http/lib/fz_http/auth.ex b/apps/fz_http/lib/fz_http/auth.ex new file mode 100644 index 000000000..b04eb7629 --- /dev/null +++ b/apps/fz_http/lib/fz_http/auth.ex @@ -0,0 +1,43 @@ +defmodule FzHttp.Auth do + alias FzHttp.Config + + def fetch_oidc_provider_config(provider_id) do + with {:ok, provider} <- fetch_provider(:openid_connect_providers, provider_id) do + external_url = FzHttp.Config.fetch_env!(:fz_http, :external_url) + redirect_uri = provider.redirect_uri || "#{external_url}/auth/oidc/#{provider.id}/callback/" + + {:ok, + %{ + discovery_document_uri: provider.discovery_document_uri, + client_id: provider.client_id, + client_secret: provider.client_secret, + redirect_uri: redirect_uri, + response_type: provider.response_type, + scope: provider.scope + }} + end + end + + def auto_create_users?(field, provider_id) do + fetch_provider!(field, provider_id).auto_create_users + end + + defp fetch_provider(field, provider_id) do + Config.fetch_config!(field) + |> Enum.find(&(&1.id == provider_id)) + |> case do + nil -> {:error, :not_found} + provider -> {:ok, provider} + end + end + + defp fetch_provider!(field, provider_id) do + case fetch_provider(field, provider_id) do + {:ok, provider} -> + provider + + {:error, :not_found} -> + raise RuntimeError, "Unknown provider #{provider_id}" + end + end +end diff --git a/apps/fz_http/lib/fz_http/config.ex b/apps/fz_http/lib/fz_http/config.ex index 21cc8e1e0..26b9e72fe 100644 --- a/apps/fz_http/lib/fz_http/config.ex +++ b/apps/fz_http/lib/fz_http/config.ex @@ -1,14 +1,173 @@ defmodule FzHttp.Config do - @moduledoc """ - This module provides set of helper functions that are useful when reading application runtime configuration overrides - in test environment. + alias FzHttp.Repo + alias FzHttp.Config.{Definition, Definitions, Validator, Errors, Fetcher} + alias FzHttp.Config.Configuration + + def fetch_source_and_config!(key) do + db_config = maybe_fetch_db_config!(key) + env_config = System.get_env() + + case Fetcher.fetch_source_and_config(Definitions, key, db_config, env_config) do + {:ok, source, config} -> + {source, config} + + {:error, reason} -> + Errors.raise_error!(reason) + end + end + + def fetch_source_and_configs!(keys) when is_list(keys) do + db_config = maybe_fetch_db_config!(keys) + env_config = System.get_env() + + for key <- keys, into: %{} do + case Fetcher.fetch_source_and_config(Definitions, key, db_config, env_config) do + {:ok, source, config} -> + {key, {source, config}} + + {:error, reason} -> + Errors.raise_error!(reason) + end + end + end + + def fetch_config(key) do + db_config = maybe_fetch_db_config!(key) + env_config = System.get_env() + + with {:ok, _source, config} <- + Fetcher.fetch_source_and_config(Definitions, key, db_config, env_config) do + {:ok, config} + end + end + + def fetch_config!(key) do + case fetch_config(key) do + {:ok, config} -> + config + + {:error, reason} -> + Errors.raise_error!(reason) + end + end + + def fetch_configs!(keys) do + for {key, {_source, value}} <- fetch_source_and_configs!(keys), into: %{} do + {key, value} + end + end + + defp maybe_fetch_db_config!(keys) when is_list(keys) do + if Enum.any?(keys, &(&1 in FzHttp.Config.Configuration.__schema__(:fields))) do + fetch_db_config!() + else + %{} + end + end + + defp maybe_fetch_db_config!(key) do + if key in FzHttp.Config.Configuration.__schema__(:fields) do + fetch_db_config!() + else + %{} + end + end + + @doc """ + Similar to `compile_config/2` but raises an error if the configuration is invalid. + + This function does not resolve values from the database because it's intended use is during + compilation and before application boot (in `config/runtime.exs`). + + If you need to resolve values from the database, use `fetch_config/1` or `fetch_config!/1`. """ + def compile_config!(module \\ Definitions, key, env_configurations \\ System.get_env()) do + case Fetcher.fetch_source_and_config(module, key, %{}, env_configurations) do + {:ok, _source, value} -> + value + + {:error, reason} -> + Errors.raise_error!(reason) + end + end + + def validate_runtime_config!( + module \\ Definitions, + db_config \\ fetch_db_config!(), + env_config \\ System.get_env() + ) do + module.configs() + |> Enum.flat_map(fn {module, key} -> + case Fetcher.fetch_source_and_config(module, key, db_config, env_config) do + {:ok, _source, _value} -> [] + {:error, reason} -> [reason] + end + end) + |> case do + [] -> :ok + errors -> Errors.raise_error!(errors) + end + end + + def fetch_db_config! do + Repo.one!(Configuration) + end + + def change_config(%Configuration{} = config \\ fetch_db_config!(), attrs \\ %{}) do + Configuration.Changeset.changeset(config, attrs) + end + + def update_config(%Configuration{} = config, attrs) do + with {:ok, config} <- Repo.update(Configuration.Changeset.changeset(config, attrs)) do + FzHttp.SAML.StartProxy.refresh(config.saml_identity_providers) + {:ok, config} + end + end + + def put_config!(key, value) do + with {:ok, config} <- update_config(fetch_db_config!(), %{key => value}) do + config + else + {:error, reason} -> raise "cannot update config: #{inspect(reason)}" + end + end + + def config_changeset(changeset, schema_key, config_key \\ nil) do + config_key = config_key || schema_key + + {type, {_resolve_opts, validate_opts, _dump_opts, _debug_opts}} = + Definition.fetch_spec_and_opts!(Definitions, config_key) + + with {_data_or_changes, value} <- Ecto.Changeset.fetch_field(changeset, schema_key), + {:error, values_and_errors} <- Validator.validate(config_key, value, type, validate_opts) do + values_and_errors + |> List.wrap() + |> Enum.flat_map(fn {_value, errors} -> errors end) + |> Enum.uniq() + |> Enum.reduce(changeset, fn error, changeset -> + Ecto.Changeset.add_error(changeset, schema_key, error) + end) + else + :error -> changeset + {:ok, _value} -> changeset + end + end + + def vpn_sessions_expire? do + freq = fetch_config!(:vpn_session_duration) + freq > 0 && freq < Configuration.Changeset.max_vpn_session_duration() + end if Mix.env() != :test do defdelegate fetch_env!(app, key), to: Application else def put_env_override(app \\ :fz_http, key, value) do - Process.put(key_function(app, key), value) + Process.put(pdict_key_function(app, key), value) + :ok + end + + def put_system_env_override(key, value) do + Process.put({FzHttp.Config.Resolver, key}, {:env, value}) :ok end @@ -24,51 +183,17 @@ defmodule FzHttp.Config do def fetch_env!(app, key) do application_env = Application.fetch_env!(app, key) - pdict_key = key_function(app, key) + pdict_key_function(app, key) + |> FzHttp.Config.Resolver.fetch_process_env() + |> case do + {:ok, override} -> + override - with :error <- fetch_process_value(pdict_key), - :error <- fetch_process_value(get_last_pid_from_pdict_list(:"$ancestors"), pdict_key), - :error <- fetch_process_value(get_last_pid_from_pdict_list(:"$callers"), pdict_key) do - application_env - else - {:ok, override} -> override + :error -> + application_env 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 - - # String.to_atom/1 is only used in test env - defp key_function(app, key), do: String.to_atom("#{app}-#{key}") + defp pdict_key_function(app, key), do: {app, key} end end diff --git a/apps/fz_http/lib/fz_http/config/caster.ex b/apps/fz_http/lib/fz_http/config/caster.ex new file mode 100644 index 000000000..279032835 --- /dev/null +++ b/apps/fz_http/lib/fz_http/config/caster.ex @@ -0,0 +1,56 @@ +defmodule FzHttp.Config.Caster do + @moduledoc """ + This module allows to cast values to a defined type. + + Notice that only the Ecto types that don't allow to use `c:Ecto.Type.cast/1` + to cast a binary needs to be casted this way, + """ + + def cast(value, {:array, separator, type, _opts}) do + cast(value, {:array, separator, type}) + end + + def cast(value, {:array, separator, type}) when is_binary(value) do + value + |> String.split(separator) + |> Enum.map(&cast(&1, type)) + |> Enum.reduce_while({:ok, []}, fn + {:ok, value}, {:ok, acc} -> {:cont, {:ok, [value | acc]}} + {:error, reason}, {:ok, _acc} -> {:halt, {:error, reason}} + end) + |> case do + {:ok, values} -> {:ok, Enum.reverse(values)} + {:error, reason} -> {:error, reason} + end + end + + def cast(json, :embed) when is_binary(json), do: Jason.decode(json) + def cast(json, {:embed, _schema}) when is_binary(json), do: Jason.decode(json) + def cast(json, :map) when is_binary(json), do: Jason.decode(json) + def cast(json, {:map, _term}) when is_binary(json), do: Jason.decode(json) + def cast(json, :json_array) when is_binary(json), do: Jason.decode(json) + def cast(json, {:json_array, _term}) when is_binary(json), do: Jason.decode(json) + + def cast("true", :boolean), do: {:ok, true} + def cast("false", :boolean), do: {:ok, false} + def cast("", :boolean), do: {:ok, nil} + + def cast(value, :integer) when is_binary(value) do + case Integer.parse(value) do + {value, ""} -> + {:ok, value} + + {value, remainder} -> + {:error, + "cannot be cast to an integer, " <> + "got a reminder #{remainder} after an integer value #{value}"} + + :error -> + {:error, "cannot be cast to an integer"} + end + end + + def cast(nil, :integer), do: {:ok, nil} + + def cast(value, _type), do: {:ok, value} +end diff --git a/apps/fz_http/lib/fz_http/config/configuration.ex b/apps/fz_http/lib/fz_http/config/configuration.ex new file mode 100644 index 000000000..84d012006 --- /dev/null +++ b/apps/fz_http/lib/fz_http/config/configuration.ex @@ -0,0 +1,45 @@ +defmodule FzHttp.Config.Configuration do + use FzHttp, :schema + alias FzHttp.Config.Logo + + 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, {:array, :string}, default: [] + field :default_client_allowed_ips, {:array, FzHttp.Types.INET}, default: [] + + # 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.Config.Configuration.OpenIDConnectProvider, + on_replace: :delete + + embeds_many :saml_identity_providers, + FzHttp.Config.Configuration.SAMLIdentityProvider, + on_replace: :delete + + timestamps() + end +end diff --git a/apps/fz_http/lib/fz_http/config/configuration/changeset.ex b/apps/fz_http/lib/fz_http/config/configuration/changeset.ex new file mode 100644 index 000000000..8e14c7f3d --- /dev/null +++ b/apps/fz_http/lib/fz_http/config/configuration/changeset.ex @@ -0,0 +1,70 @@ +defmodule FzHttp.Config.Configuration.Changeset do + use FzHttp, :changeset + import FzHttp.Config, only: [config_changeset: 2] + + # Postgres max int size is 4 bytes + @max_vpn_session_duration 2_147_483_647 + + @fields ~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 + + @spec changeset( + {map, map} + | %{ + :__struct__ => atom | %{:__changeset__ => map, optional(any) => any}, + optional(atom) => any + }, + :invalid | %{optional(:__struct__) => none, optional(atom | binary) => any} + ) :: any + def changeset(configuration, attrs) do + changeset = + configuration + |> cast(attrs, @fields) + |> cast_embed(:logo) + |> cast_embed(:openid_connect_providers, + with: {FzHttp.Config.Configuration.OpenIDConnectProvider, :changeset, []} + ) + |> cast_embed(:saml_identity_providers, + with: {FzHttp.Config.Configuration.SAMLIdentityProvider, :changeset, []} + ) + |> trim_change(:default_client_dns) + |> trim_change(:default_client_endpoint) + + Enum.reduce(@fields, changeset, fn field, changeset -> + config_changeset(changeset, field) + end) + |> ensure_no_overridden_changes() + end + + defp ensure_no_overridden_changes(changeset) do + changed_keys = Map.keys(changeset.changes) + configs = FzHttp.Config.fetch_source_and_configs!(changed_keys) + + Enum.reduce(changed_keys, changeset, fn key, changeset -> + case Map.fetch!(configs, key) do + {{:env, source_key}, _value} -> + add_error( + changeset, + key, + "cannot be changed; " <> + "it is overridden by #{source_key} environment variable" + ) + + _other -> + changeset + end + end) + end + + def max_vpn_session_duration, do: @max_vpn_session_duration +end diff --git a/apps/fz_http/lib/fz_http/configurations/configuration/openid_connect_provider.ex b/apps/fz_http/lib/fz_http/config/configuration/openid_connect_provider.ex similarity index 84% rename from apps/fz_http/lib/fz_http/configurations/configuration/openid_connect_provider.ex rename to apps/fz_http/lib/fz_http/config/configuration/openid_connect_provider.ex index 15da21664..d92731868 100644 --- a/apps/fz_http/lib/fz_http/configurations/configuration/openid_connect_provider.ex +++ b/apps/fz_http/lib/fz_http/config/configuration/openid_connect_provider.ex @@ -1,4 +1,4 @@ -defmodule FzHttp.Configurations.Configuration.OpenIDConnectProvider do +defmodule FzHttp.Config.Configuration.OpenIDConnectProvider do @moduledoc """ OIDC Config virtual schema """ @@ -59,9 +59,10 @@ defmodule FzHttp.Configurations.Configuration.OpenIDConnectProvider do # Don't allow users to enter reserved config ids |> validate_exclusion(:id, @reserved_config_ids) |> validate_discovery_document_uri() - |> Validator.validate_uri([ - :redirect_uri - ]) + |> Validator.validate_uri(:redirect_uri) + |> validate_inclusion(:response_type, ~w[code]) + |> validate_format(:scope, ~r/openid/, message: "must include openid scope") + |> validate_format(:scope, ~r/email/, message: "must include email scope") end def validate_discovery_document_uri(changeset) do diff --git a/apps/fz_http/lib/fz_http/configurations/configuration/saml_identity_provider.ex b/apps/fz_http/lib/fz_http/config/configuration/saml_identity_provider.ex similarity index 96% rename from apps/fz_http/lib/fz_http/configurations/configuration/saml_identity_provider.ex rename to apps/fz_http/lib/fz_http/config/configuration/saml_identity_provider.ex index a17e7cf6a..a7dfebb51 100644 --- a/apps/fz_http/lib/fz_http/configurations/configuration/saml_identity_provider.ex +++ b/apps/fz_http/lib/fz_http/config/configuration/saml_identity_provider.ex @@ -1,4 +1,4 @@ -defmodule FzHttp.Configurations.Configuration.SAMLIdentityProvider do +defmodule FzHttp.Config.Configuration.SAMLIdentityProvider do @moduledoc """ SAML Config virtual schema """ diff --git a/apps/fz_http/lib/fz_http/config/definition.ex b/apps/fz_http/lib/fz_http/config/definition.ex new file mode 100644 index 000000000..c99f03102 --- /dev/null +++ b/apps/fz_http/lib/fz_http/config/definition.ex @@ -0,0 +1,143 @@ +defmodule FzHttp.Config.Definition do + @moduledoc """ + This module provides a DSL to define application configuration, which can be read from multiple sources, + casted and validated. + + ## Examples + + defmodule MyConfig do + use FzHttp.Config.Definition + + @doc "My config key" + defconfig :my_key, :string, required: true + end + + iex> MyConfig.my_key() + {:string, [required: true]} + + iex> MyConfig.configs() + [:my_key] + + iex> MyConfig.fetch_doc(:my_key) + {:ok, "My config key"} + """ + alias FzHttp.Config.Errors + + @type array_opts :: [{:validate_unique, boolean()} | {:validate_length, Keyword.t()}] + + @type type :: + Ecto.Type.t() + | {:embed, Ecto.Schema.t()} + | {:json_array, type()} + | {:json_array, type(), array_opts()} + | {:array, separator :: String.t(), type()} + | {:array, separator :: String.t(), type(), array_opts()} + | {:one_of, type()} + + @type legacy_key :: {:env, var_name :: String.t(), removed_at :: String.t()} + + @type changeset_callback :: + (changeset :: Ecto.Changeset.t(), key :: atom() -> Ecto.Changeset.t()) + | (type :: term(), changeset :: Ecto.Changeset.t(), key :: atom() -> Ecto.Changeset.t()) + | {module(), atom(), [term()]} + + @type dump_callback :: (value :: term() -> term()) + + @type opts :: [ + default: term, + sensitive: boolean(), + dump: dump_callback(), + legacy_keys: [legacy_key()], + changeset: changeset_callback() + ] + + defmacro __using__(_opts) do + quote do + import FzHttp.Config.Definition + import FzHttp.Config, only: [compile_config!: 1] + + # Accumulator keeps the list of defined config keys + Module.register_attribute(__MODULE__, :configs, accumulate: true) + + # A `configs/0` function is injected before module is compiled + # exporting the aggregated list of config keys + @before_compile FzHttp.Config.Definition + + @doc "See `FzHttp.Config.Definition.fetch_doc/2`" + def fetch_doc(key), do: fetch_doc(__MODULE__, key) + end + end + + @doc """ + Simply exposes the `@configs` attribute as a function after all the `defconfig`'s are compiled. + """ + defmacro __before_compile__(_env) do + quote do + def configs, do: @configs + end + end + + @doc """ + Defines a configuration key. + + Behind the hood it defines a function that returns a tuple with the type and options, a function is used + to allow for `@doc` blocks with markdown to be used to document the configuration key. + + ## Type + + The type can be one of the following: + + * any of the primitive types supported by `Ecto.Schema`; + * a module that implements Ecto.Type behaviour; + * `{:array, binary_separator, type}` - a list of values of the given type, separated by the given separator. + Separator is only used when reading the value from the environment variable or other binary storages; + * `{:one_of, [type]}` - a value of one of the given types. + """ + defmacro defconfig(key, type, opts \\ []) do + quote do + @configs {__MODULE__, unquote(key)} + @spec unquote(key)() :: {FzHttp.Config.Definition.type(), FzHttp.Config.Definition.opts()} + def unquote(key)(), do: {unquote(type), unquote(opts)} + end + end + + def fetch_spec_and_opts!(module, key) do + {type, opts} = apply(module, key, []) + {resolve_opts, opts} = Keyword.split(opts, [:legacy_keys, :default]) + {validate_opts, opts} = Keyword.split(opts, [:changeset]) + {debug_opts, opts} = Keyword.split(opts, [:sensitive]) + {dump_opts, opts} = Keyword.split(opts, [:dump]) + + if opts != [], do: Errors.invalid_spec(key, opts) + + {type, {resolve_opts, validate_opts, dump_opts, debug_opts}} + end + + def fetch_doc(module) do + with {:docs_v1, _, _, _, module_doc, _, _function_docs} <- Code.fetch_docs(module) do + fetch_en_doc(module_doc) + end + end + + @doc """ + Returns EN documentation chunk of a given function in a module. + """ + def fetch_doc(module, key) do + with {:docs_v1, _, _, _, _module_doc, _, function_docs} <- Code.fetch_docs(module) do + function_docs + |> fetch_function_docs(key) + |> fetch_en_doc() + end + end + + defp fetch_function_docs(function_docs, function) do + function_docs + |> Enum.find_value(fn + {{:function, ^function, _}, _, _, doc, _} -> doc + _other -> nil + end) + end + + defp fetch_en_doc(md) when is_map(md), do: Map.fetch(md, "en") + defp fetch_en_doc(_md), do: :error +end diff --git a/apps/fz_http/lib/fz_http/config/definitions.ex b/apps/fz_http/lib/fz_http/config/definitions.ex new file mode 100644 index 000000000..a7ea50fa4 --- /dev/null +++ b/apps/fz_http/lib/fz_http/config/definitions.ex @@ -0,0 +1,807 @@ +defmodule FzHttp.Config.Definitions do + @moduledoc """ + Most day-to-day config of Firezone can be done via the Firezone Web UI, + but for zero-touch deployments we allow to override most of configuration options + using environment variables. + + Read more about configuring Firezone in our [configure guide](/deploy/configure). + + ## Errors + + Firezone will not boot if the configuration is invalid, providing a detailed error message + and a link to the documentation for the configuration key with samples how to set it. + + ## Naming + + If environment variables are used, the configuration key must be uppercased. + The database variables are the same as the configuration keys. + + ## Precedence + + The configuration precedence is as follows: + + 1. Environment variables + 2. Database values + 3. Default values + + It means that if environment variable is set, it will be used, regardless of the database value, + and UI to edit database value will be disabled. + """ + use FzHttp.Config.Definition + alias FzHttp.Config.Dumper + alias FzHttp.Types + alias FzHttp.Config.{Configuration, Logo} + + def doc_sections do + [ + {"WebServer", + [ + :external_url, + :phoenix_secure_cookies, + :phoenix_listen_address, + :phoenix_http_port, + :phoenix_external_trusted_proxies, + :phoenix_private_clients + ]}, + {"Database", + [ + :database_host, + :database_port, + :database_name, + :database_user, + :database_password, + :database_pool_size, + :database_ssl_enabled, + :database_ssl_opts, + :database_parameters + ]}, + {"Admin Setup", + """ + Options responsible for initial admin provisioning and resetting the admin password. + + For more details see [troubleshooting guide](/administer/troubleshoot/#admin-login-isnt-working). + """, + [ + :reset_admin_on_boot, + :default_admin_email, + :default_admin_password + ]}, + {"Secrets and Encryption", + """ + Your secrets should be generated during installation automatically and persisted to `.env` file. + + All secrets should be a **base64-encoded string**. + """, + [ + :guardian_secret_key, + :database_encryption_key, + :secret_key_base, + :live_view_signing_salt, + :cookie_signing_salt, + :cookie_encryption_salt + ]}, + {"Devices", + [ + :allow_unprivileged_device_management, + :allow_unprivileged_device_configuration, + :vpn_session_duration, + :default_client_persistent_keepalive, + :default_client_mtu, + :default_client_endpoint, + :default_client_dns, + :default_client_allowed_ips + ]}, + # {"Limits", + # [ + # :max_devices_per_user + # ]}, + {"Authorization", + [ + :local_auth_enabled, + :disable_vpn_on_oidc_error, + :saml_entity_id, + :saml_keyfile_path, + :saml_certfile_path, + :openid_connect_providers, + :saml_identity_providers + ]}, + {"WireGuard", + [ + :wireguard_port, + :wireguard_ipv4_enabled, + :wireguard_ipv4_masquerade, + :wireguard_ipv4_network, + :wireguard_ipv4_address, + :wireguard_ipv6_enabled, + :wireguard_ipv6_masquerade, + :wireguard_ipv6_network, + :wireguard_ipv6_address, + :wireguard_private_key_path, + :wireguard_interface_name, + :gateway_egress_interface, + :gateway_nft_path + ]}, + {"Outbound Emails", + [ + :outbound_email_from, + :outbound_email_adapter, + :outbound_email_adapter_opts + ]}, + {"Connectivity Checks", + [ + :connectivity_checks_enabled, + :connectivity_checks_interval + ]}, + {"Telemetry", + [ + :telemetry_enabled, + :telemetry_id + ]} + ] + end + + ############################################## + ## Web Server + ############################################## + + @doc """ + The external URL the web UI will be accessible at. + + Must be a valid and public FQDN for ACME SSL issuance to function. + + You can add a path suffix if you want to serve firezone from a non-root path, + eg: `https://firezone.mycorp.com/vpn`. + """ + defconfig(:external_url, :string, + changeset: fn changeset, key -> + changeset + |> FzHttp.Validator.validate_uri(key) + |> FzHttp.Validator.normalize_url(key) + end + ) + + @doc """ + Enable or disable requiring secure cookies. Required for HTTPS. + """ + defconfig(:phoenix_secure_cookies, :boolean, + default: true, + legacy_keys: [{:env, "SECURE_COOKIES", "0.9"}] + ) + + defconfig(:phoenix_listen_address, Types.IP, default: "0.0.0.0") + + @doc """ + Internal port to listen on for the Phoenix web server. + """ + defconfig(:phoenix_http_port, :integer, + default: 13_000, + changeset: fn changeset, key -> + Ecto.Changeset.validate_number(changeset, key, + greater_than: 0, + less_than_or_equal_to: 65_535 + ) + end + ) + + @doc """ + List of trusted reverse proxies. + + This is used to determine the correct IP address of the client when the + application is behind a reverse proxy by skipping a trusted proxy IP + from a list of possible source IPs. + """ + defconfig(:phoenix_external_trusted_proxies, {:array, ",", {:one_of, [Types.IP, Types.CIDR]}}, + default: [], + legacy_keys: [{:env, "EXTERNAL_TRUSTED_PROXIES", "0.9"}] + ) + + @doc """ + List of trusted clients. + + This is used to determine the correct IP address of the client when the + application is behind a reverse proxy by picking a trusted client IP + from a list of possible source IPs. + """ + defconfig(:phoenix_private_clients, {:array, ",", {:one_of, [Types.IP, Types.CIDR]}}, + default: [], + legacy_keys: [{:env, "PRIVATE_CLIENTS", "0.9"}] + ) + + ############################################## + ## Database + ############################################## + + @doc """ + PostgreSQL host. + """ + defconfig(:database_host, :string, default: "postgres") + + @doc """ + PostgreSQL port. + """ + defconfig(:database_port, :integer, default: 5432) + + @doc """ + Name of the PostgreSQL database. + """ + defconfig(:database_name, :string, default: "firezone") + + @doc """ + User that will be used to access the PostgreSQL database. + """ + defconfig(:database_user, :string, default: "postgres", sensitive: true) + + @doc """ + Password that will be used to access the PostgreSQL database. + """ + defconfig(:database_password, :string, sensitive: true) + + @doc """ + Size of the connection pool to the PostgreSQL database. + """ + defconfig(:database_pool_size, :integer, + default: fn -> :erlang.system_info(:logical_processors_available) * 2 end, + legacy_keys: [{:env, "DATABASE_POOL", "0.9"}] + ) + + @doc """ + Whether to connect to the database over SSL. + + If this field is set to `true`, the `database_ssl_opts` config must be set too + with at least `cacertfile` option present. + """ + defconfig(:database_ssl_enabled, :boolean, + default: false, + legacy_keys: [{:env, "DATABASE_SSL", "0.9"}] + ) + + @doc """ + SSL options for connecting to the PostgreSQL database. + + Typically, to enabled SSL you want following options: + + * `cacertfile` - path to the CA certificate file; + * `verify` - set to `verify_peer` to verify the server certificate; + * `fail_if_no_peer_cert` - set to `true` to require the server to present a certificate; + * `server_name_indication` - specify the hostname to be used in TLS Server Name Indication extension. + + See [Ecto.Adapters.Postgres documentation](https://hexdocs.pm/ecto_sql/Ecto.Adapters.Postgres.html#module-connection-options). + For list of all supported options, see the [`ssl`](http://erlang.org/doc/man/ssl.html#type-tls_client_option) module documentation. + """ + defconfig(:database_ssl_opts, :map, + default: %{}, + dump: &Dumper.dump_ssl_opts/1 + ) + + defconfig(:database_parameters, :map, + default: %{application_name: "firezone-#{Application.spec(:fz_http, :vsn)}"}, + dump: &Dumper.keyword/1 + ) + + ############################################## + ## Admin Setup + ############################################## + + @doc """ + Set this variable to `true` to create or reset the admin password every time Firezone + starts. By default, the admin password is only set when Firezone is installed. + + Note: This **will not** change the status of local authentication. + """ + defconfig(:reset_admin_on_boot, :boolean, default: false) + + @doc """ + Primary administrator email. + """ + defconfig(:default_admin_email, :string, + default: nil, + sensitive: true, + legacy_keys: [{:env, "ADMIN_EMAIL", "0.9"}], + changeset: fn changeset, key -> + changeset + |> FzHttp.Validator.trim_change(key) + |> FzHttp.Validator.validate_email(key) + end + ) + + @doc """ + Default password that will be used for creating or resetting the primary administrator account. + """ + defconfig(:default_admin_password, :string, + default: nil, + sensitive: true, + changeset: fn changeset, key -> + Ecto.Changeset.validate_length(changeset, key, min: 5) + end + ) + + ############################################## + ## Secrets + ############################################## + + @doc """ + Secret key used for signing JWTs. + """ + defconfig(:guardian_secret_key, :string, + sensitive: true, + changeset: &FzHttp.Validator.validate_base64/2 + ) + + @doc """ + Secret key used for encrypting sensitive data in the database. + """ + defconfig(:database_encryption_key, :string, + sensitive: true, + changeset: &FzHttp.Validator.validate_base64/2 + ) + + @doc """ + Primary secret key base for the Phoenix application. + """ + defconfig(:secret_key_base, :string, + sensitive: true, + changeset: &FzHttp.Validator.validate_base64/2 + ) + + @doc """ + Signing salt for Phoenix LiveView connection tokens. + """ + defconfig(:live_view_signing_salt, :string, + sensitive: true, + changeset: &FzHttp.Validator.validate_base64/2 + ) + + @doc """ + Encryption salt for cookies issued by the Phoenix web application. + """ + defconfig(:cookie_signing_salt, :string, + sensitive: true, + changeset: &FzHttp.Validator.validate_base64/2 + ) + + @doc """ + Signing salt for cookies issued by the Phoenix web application. + """ + defconfig(:cookie_encryption_salt, :string, + sensitive: true, + changeset: &FzHttp.Validator.validate_base64/2 + ) + + ############################################## + ## Devices + ############################################## + + @doc """ + Enable or disable management of devices on unprivileged accounts. + """ + defconfig(:allow_unprivileged_device_management, :boolean, default: true) + + @doc """ + Enable or disable configuration of device network settings for unprivileged users. + """ + defconfig(:allow_unprivileged_device_configuration, :boolean, default: true) + + @doc """ + Optionally require users to periodically authenticate to the Firezone web UI in order to keep their VPN sessions active. + """ + defconfig(:vpn_session_duration, :integer, + default: 0, + changeset: fn changeset, key -> + Ecto.Changeset.validate_number(changeset, key, + greater_than_or_equal_to: 0, + less_than_or_equal_to: 2_147_483_647 + ) + end + ) + + @doc """ + Interval for WireGuard [persistent keepalive](https://www.wireguard.com/quickstart/#nat-and-firewall-traversal-persistence). + + If you experience NAT or firewall traversal problems, you can enable this to send a keepalive packet every 25 seconds. + Otherwise, keep it disabled with a 0 default value. + """ + defconfig(:default_client_persistent_keepalive, :integer, + default: 25, + changeset: fn changeset, key -> + Ecto.Changeset.validate_number(changeset, key, + greater_than_or_equal_to: 0, + less_than_or_equal_to: 120 + ) + end + ) + + @doc """ + WireGuard interface MTU for devices. 1280 is a safe bet for most networks. + Leave this blank to omit this field from generated configs. + """ + defconfig(:default_client_mtu, :integer, + default: 1280, + legacy_keys: [{:env, "WIREGUARD_MTU", "0.8"}], + changeset: fn changeset, key -> + Ecto.Changeset.validate_number(changeset, key, + greater_than_or_equal_to: 576, + less_than_or_equal_to: 1500 + ) + end + ) + + @doc """ + IPv4, IPv6 address, or FQDN that devices will be configured to connect to. Defaults to this server's FQDN. + """ + defconfig(:default_client_endpoint, {:one_of, [Types.IPPort, :string]}, + default: fn -> + external_uri = URI.parse(compile_config!(:external_url)) + wireguard_port = compile_config!(:wireguard_port) + "#{external_uri.host}:#{wireguard_port}" + end, + changeset: fn + Types.IPPort, changeset, _key -> + changeset + + :string, changeset, key -> + changeset + |> FzHttp.Validator.trim_change(key) + |> FzHttp.Validator.validate_fqdn(key, allow_port: true) + end + ) + + @doc """ + Comma-separated list of DNS servers to use for devices. + + It can be either an IP address or a FQDN if you intend to use a DNS-over-TLS server. + + Leave this blank to omit the `DNS` section from generated configs. + """ + defconfig( + :default_client_dns, + {:array, ",", {:one_of, [Types.IP, :string]}, validate_unique: true}, + default: [], + changeset: fn + Types.IP, changeset, _key -> + changeset + + :string, changeset, key -> + changeset + |> FzHttp.Validator.trim_change(key) + |> FzHttp.Validator.validate_fqdn(key) + end + ) + + @doc """ + Configures the default AllowedIPs setting for devices. + + AllowedIPs determines which destination IPs get routed through Firezone. + + Specify a comma-separated list of IPs or CIDRs here to achieve split tunneling, or use + `0.0.0.0/0, ::/0` to route all device traffic through this Firezone server. + """ + defconfig( + :default_client_allowed_ips, + {:array, ",", {:one_of, [Types.CIDR, Types.IP]}, validate_unique: true}, + default: "0.0.0.0/0, ::/0" + ) + + ############################################## + ## Limits + ############################################## + + defconfig(:max_devices_per_user, :integer, + default: 10, + changeset: fn changeset, key -> + Ecto.Changeset.validate_number(changeset, key, + greater_than_or_equal_to: 0, + less_than_or_equal_to: 100 + ) + end + ) + + ############################################## + ## Userpass / SAML / OIDC authentication + ############################################## + + @doc """ + Enable or disable the local authentication method for all users. + """ + # XXX: This should be replaced with auth_methods config which accepts a list + # of enabled methods. + defconfig(:local_auth_enabled, :boolean, default: true) + + @doc """ + Enable or disable auto disabling VPN connection on OIDC refresh error. + """ + defconfig(:disable_vpn_on_oidc_error, :boolean, default: false) + + @doc """ + Entity ID for SAML authentication. + """ + defconfig(:saml_entity_id, :string, default: "urn:firezone.dev:firezone-app") + + @doc """ + Path to the SAML keyfile inside the container. Should be either a PEM or DER-encoded private key, + with file extension `.pem` or `.key`. + """ + defconfig(:saml_keyfile_path, :string, + default: "/var/firezone/saml.key", + changeset: &FzHttp.Validator.validate_file(&1, &2, extensions: ~w[.pem .key]) + ) + + @doc """ + Path to the SAML certificate file inside the container. Should be either a PEM or DER-encoded certificate, + with file extension `.crt` or `.pem`. + """ + defconfig(:saml_certfile_path, :string, + default: "/var/firezone/saml.crt", + changeset: &FzHttp.Validator.validate_file(&1, &2, extensions: ~w[.crt .pem]) + ) + + @doc """ + List of OpenID Connect identity providers configurations. + + For example: + + ```json + [ + { + "auto_create_users": false, + "id": "google", + "label": "google", + "client_id": "test-id", + "client_secret": "test-secret", + "discovery_document_uri": "https://accounts.google.com/.well-known/openid-configuration", + "redirect_uri": "https://invalid", + "response_type": "response-type", + "scope": "oauth email profile" + } + ] + ``` + + For more details see https://docs.firezone.dev/authenticate/oidc/. + """ + defconfig( + :openid_connect_providers, + {:json_array, {:embed, Configuration.OpenIDConnectProvider}}, + default: [], + changeset: {Configuration.OpenIDConnectProvider, :create_changeset, []} + ) + + @doc """ + List of SAML identity providers configurations. + + For example: + + ```json + [ + { + "auto_create_users": false, + "base_url": "https://saml", + "id": "okta", + "label": "okta", + "metadata": "...", + "sign_metadata": false, + "sign_requests": false, + "signed_assertion_in_resp": false, + "signed_envelopes_in_resp": false + } + ] + ``` + + For more details see https://docs.firezone.dev/authenticate/saml/. + """ + defconfig(:saml_identity_providers, {:json_array, {:embed, Configuration.SAMLIdentityProvider}}, + default: [], + changeset: {Configuration.SAMLIdentityProvider, :create_changeset, []} + ) + + ############################################## + ## Telemetry + ############################################## + + @doc """ + Enable or disable the Firezone telemetry collection. + + For more details see https://docs.firezone.dev/reference/telemetry/. + """ + defconfig(:telemetry_enabled, :boolean, default: true) + + defconfig(:telemetry_id, :string, + default: fn -> + :crypto.hash(:sha256, compile_config!(:external_url)) + |> Base.url_encode64(padding: false) + end, + legacy_keys: [{:env, "TID", nil}] + ) + + ############################################## + ## Connectivity Checks + ############################################## + + @doc """ + Enable / disable periodic checking for egress connectivity. Determines the instance's public IP to populate `Endpoint` fields. + """ + defconfig(:connectivity_checks_enabled, :boolean, default: true) + + @doc """ + Periodicity in seconds to check for egress connectivity. + """ + defconfig(:connectivity_checks_interval, :integer, + default: 43_200, + changeset: fn changeset, key -> + Ecto.Changeset.validate_number(changeset, key, + greater_than_or_equal_to: 60, + less_than_or_equal_to: 86_400 + ) + end + ) + + ############################################## + ## WireGuard + ############################################## + + @doc """ + A port on which WireGuard will listen for incoming connections. + """ + defconfig(:wireguard_port, :integer, + default: 51_820, + changeset: fn changeset, key -> + Ecto.Changeset.validate_number(changeset, key, + greater_than: 0, + less_than_or_equal_to: 65_535 + ) + end + ) + + @doc """ + Enable or disable IPv4 support for WireGuard. + """ + defconfig(:wireguard_ipv4_enabled, :boolean, default: true) + defconfig(:wireguard_ipv4_masquerade, :boolean, default: true) + + defconfig(:wireguard_ipv4_network, Types.CIDR, + default: "10.3.2.0/24", + changeset: &FzHttp.Validator.validate_ip_type_inclusion(&1, &2, [:ipv4]) + ) + + defconfig(:wireguard_ipv4_address, Types.IP, + default: "10.3.2.1", + changeset: &FzHttp.Validator.validate_ip_type_inclusion(&1, &2, [:ipv4]) + ) + + @doc """ + Enable or disable IPv6 support for WireGuard. + """ + defconfig(:wireguard_ipv6_enabled, :boolean, default: true) + defconfig(:wireguard_ipv6_masquerade, :boolean, default: true) + + defconfig(:wireguard_ipv6_network, Types.CIDR, + default: "fd00::3:2:0/120", + changeset: &FzHttp.Validator.validate_ip_type_inclusion(&1, &2, [:ipv6]) + ) + + defconfig(:wireguard_ipv6_address, Types.IP, + default: "fd00::3:2:1", + changeset: &FzHttp.Validator.validate_ip_type_inclusion(&1, &2, [:ipv6]) + ) + + defconfig(:wireguard_private_key_path, :string, + default: "/var/firezone/private_key", + changeset: &FzHttp.Validator.validate_file(&1, &2) + ) + + defconfig(:wireguard_interface_name, :string, default: "wg-firezone") + + defconfig(:gateway_egress_interface, :string, + legacy_keys: [{:env, "EGRESS_INTERFACE", "0.8"}], + default: "eth0" + ) + + defconfig(:gateway_nft_path, :string, default: "nft") + + ############################################## + ## HTTP Client Settings + ############################################## + + defconfig(:http_client_ssl_opts, :map, + default: %{}, + dump: &Dumper.dump_ssl_opts/1 + ) + + ############################################## + ## Outbound Email Settings + ############################################## + + @doc """ + From address to use for sending outbound emails. If not set, sending email will be disabled (default). + """ + defconfig(:outbound_email_from, :string, + default: fn -> + external_uri = URI.parse(compile_config!(:external_url)) + "firezone@#{external_uri.host}" + end, + sensitive: true, + changeset: fn changeset, key -> + changeset + |> FzHttp.Validator.trim_change(key) + |> FzHttp.Validator.validate_email(key) + end + ) + + @doc """ + Method to use for sending outbound email. + """ + defconfig( + :outbound_email_adapter, + {:parameterized, Ecto.Enum, + Ecto.Enum.init( + values: [ + Swoosh.Adapters.AmazonSES, + Swoosh.Adapters.CustomerIO, + Swoosh.Adapters.Dyn, + Swoosh.Adapters.ExAwsAmazonSES, + Swoosh.Adapters.Gmail, + Swoosh.Adapters.MailPace, + Swoosh.Adapters.Mailgun, + Swoosh.Adapters.Mailjet, + Swoosh.Adapters.Mandrill, + Swoosh.Adapters.Postmark, + Swoosh.Adapters.ProtonBridge, + Swoosh.Adapters.SMTP, + Swoosh.Adapters.SMTP2GO, + Swoosh.Adapters.Sendgrid, + Swoosh.Adapters.Sendinblue, + Swoosh.Adapters.Sendmail, + Swoosh.Adapters.SocketLabs, + Swoosh.Adapters.SparkPost, + FzHttpWeb.Mailer.NoopAdapter, + # DEPRECATED: Legacy options should be removed in 0.8 + :smtp, + :mailgun, + :mandrill, + :sendgrid, + :post_mark, + :sendmail + ] + )}, + default: FzHttpWeb.Mailer.NoopAdapter, + legacy_keys: [{:env, "OUTBOUND_EMAIL_PROVIDER", "0.9"}], + dump: fn + :smtp -> Swoosh.Adapters.SMTP + :mailgun -> Swoosh.Adapters.Mailgun + :mandrill -> Swoosh.Adapters.Mandrill + :sendgrid -> Swoosh.Adapters.Sendgrid + :post_mark -> Swoosh.Adapters.Postmark + :sendmail -> Swoosh.Adapters.Sendmail + other -> other + end + ) + + @doc """ + Adapter configuration, for list of options see [Swoosh Adapters](https://github.com/swoosh/swoosh#adapters). + """ + defconfig(:outbound_email_adapter_opts, :map, + default: %{}, + sensitive: true, + legacy_keys: [{:env, "OUTBOUND_EMAIL_CONFIGS", "0.9"}], + dump: fn + # DEPRECATED: Legacy options should be removed in 0.8 + %{"smtp" => value} -> Dumper.keyword(value) + %{"mailgun" => value} -> Dumper.keyword(value) + %{"mandrill" => value} -> Dumper.keyword(value) + %{"sendgrid" => value} -> Dumper.keyword(value) + %{"post_mark" => value} -> Dumper.keyword(value) + %{"sendmail" => value} -> Dumper.keyword(value) + value -> Dumper.keyword(value) + end + ) + + ############################################## + ## Appearance + ############################################## + + @doc """ + The path to a logo image file to replace default Firezone logo. + """ + defconfig(:logo, {:embed, Logo}, + default: nil, + changeset: {Logo, :changeset, []} + ) +end diff --git a/apps/fz_common/lib/fz_common.ex b/apps/fz_http/lib/fz_http/config/dumper.ex similarity index 59% rename from apps/fz_common/lib/fz_common.ex rename to apps/fz_http/lib/fz_http/config/dumper.ex index c3ae5e42a..302df6b27 100644 --- a/apps/fz_common/lib/fz_common.ex +++ b/apps/fz_http/lib/fz_http/config/dumper.ex @@ -1,8 +1,4 @@ -defmodule FzCommon do - @moduledoc """ - Documentation for `FzCommon`. - """ - +defmodule FzHttp.Config.Dumper do @doc ~S""" Maps JSON-decoded ssl opts to pass to Erlang's ssl module. Most users don't need to override many, if any, SSL opts. Most commonly this is @@ -10,16 +6,16 @@ defmodule FzCommon do ## Examples: - iex> FzCommon.map_ssl_opts(%{"verify" => "verify_none", "versions" => ["tlsv1.3"]}) + iex> FzCommon.dump_ssl_opts(%{"verify" => "verify_none", "versions" => ["tlsv1.3"]}) [verify: :verify_none, versions: ['tlsv1.3']] - iex> FzCommon.map_ssl_opts(%{"keep_secrets" => true}) + iex> FzCommon.dump_ssl_opts(%{"keep_secrets" => true}) ** (ArgumentError) unsupported key keep_secrets in ssl opts - iex> FzCommon.map_ssl_opts(%{"cacertfile" => "/tmp/cacerts.pem"}) + iex> FzCommon.dump_ssl_opts(%{"cacertfile" => "/tmp/cacerts.pem"}) [cacertfile: '/tmp/cacerts.pem'] """ - def map_ssl_opts(decoded_json) do + def dump_ssl_opts(decoded_json) do Keyword.new(decoded_json, fn {k, v} -> {String.to_atom(k), map_values(k, v)} end) @@ -28,5 +24,13 @@ defmodule FzCommon do defp map_values("verify", v), do: String.to_atom(v) defp map_values("versions", v), do: Enum.map(v, &String.to_charlist/1) defp map_values("cacertfile", v), do: String.to_charlist(v) + defp map_values("server_name_indication", v), do: String.to_charlist(v) defp map_values(k, _v), do: raise(ArgumentError, message: "unsupported key #{k} in ssl opts") + + def keyword(enum) do + Keyword.new(enum, fn + {k, v} when is_binary(k) -> {String.to_atom(k), v} + {k, v} when is_atom(k) -> {k, v} + end) + end end diff --git a/apps/fz_http/lib/fz_http/config/errors.ex b/apps/fz_http/lib/fz_http/config/errors.ex new file mode 100644 index 000000000..9c0623110 --- /dev/null +++ b/apps/fz_http/lib/fz_http/config/errors.ex @@ -0,0 +1,131 @@ +defmodule FzHttp.Config.Errors do + alias FzHttp.Config.Definition + require Logger + + @env_doc_url "https://www.firezone.dev/docs/reference/env-vars/#environment-variable-listing" + + def raise_error!(errors) do + errors + |> format_error() + |> raise() + end + + defp format_error({{nil, ["is required"]}, metadata}) do + module = Keyword.fetch!(metadata, :module) + key = Keyword.fetch!(metadata, :key) + + [ + "Missing required configuration value for '#{key}'.", + "## How to fix?", + env_example(key), + db_example(key), + format_doc(module, key) + ] + |> Enum.reject(&is_nil/1) + |> Enum.join("\n\n") + end + + defp format_error({values_and_errors, metadata}) do + source = Keyword.fetch!(metadata, :source) + module = Keyword.fetch!(metadata, :module) + key = Keyword.fetch!(metadata, :key) + + [ + "Invalid configuration for '#{key}' retrieved from #{source(source)}.", + "Errors:", + format_errors(module, key, values_and_errors), + format_doc(module, key) + ] + |> Enum.reject(&is_nil/1) + |> Enum.join("\n\n") + end + + defp format_error(errors) do + error_messages = Enum.map(errors, &format_error(&1)) + + (["Found #{length(errors)} configuration errors:"] ++ error_messages) + |> Enum.join("\n\n\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n") + end + + defp source({:app_env, key}), do: "application environment #{key}" + defp source({:env, key}), do: "environment variable #{FzHttp.Config.Resolver.env_key(key)}" + defp source({:db, key}), do: "database configuration #{key}" + defp source(:default), do: "default value" + + defp format_errors(module, key, values_and_errors) do + {_type, {_resolve_opts, _validate_opts, _dump_opts, debug_opts}} = + Definition.fetch_spec_and_opts!(module, key) + + sensitive? = Keyword.get(debug_opts, :sensitive, false) + + values_and_errors + |> List.wrap() + |> Enum.map_join("\n", fn {value, errors} -> + " - `#{format_value(sensitive?, value)}`: #{Enum.join(errors, ", ")}" + end) + end + + defp format_value(true, _value), do: "**SENSITIVE-VALUE-REDACTED**" + defp format_value(false, value), do: inspect(value) + + defp format_doc(module, key) do + case module.fetch_doc(key) do + {:error, :module_not_found} -> + nil + + {:error, :chunk_not_found} -> + nil + + :error -> + nil + + {:ok, doc} -> + """ + ## Documentation + + #{doc} + + You can find more information on configuration here: #{@env_doc_url} + """ + end + end + + def legacy_key_used(key, legacy_key, removed_at) do + Logger.warn( + "A legacy configuration option '#{legacy_key}' is used and it will be removed in v#{removed_at}." <> + "Please use '#{key}' configuration option instead." + ) + end + + def invalid_spec(key, opts) do + raise "unknown options #{inspect(opts)} for configuration #{inspect(key)}" + end + + defp env_example(key) do + """ + ### Using environment variables + + You can set this configuration via environment variable by adding it to `.env` file: + + #{FzHttp.Config.Resolver.env_key(key)}=YOUR_VALUE + """ + end + + defp db_example(key) do + if key in FzHttp.Config.Configuration.__schema__(:fields) do + """ + ### Using database + + Or you can set this configuration in the database by either setting it via the admin panel, + or by running an SQL query: + + cd $HOME/.firezone + docker compose exec postgres psql \\ + -U postgres \\ + -h 127.0.0.1 \\ + -d firezone \\ + -c "UPDATE configurations SET #{key} = 'YOUR_VALUE'" + """ + end + end +end diff --git a/apps/fz_http/lib/fz_http/config/fetcher.ex b/apps/fz_http/lib/fz_http/config/fetcher.ex new file mode 100644 index 000000000..cdc198a44 --- /dev/null +++ b/apps/fz_http/lib/fz_http/config/fetcher.ex @@ -0,0 +1,57 @@ +defmodule FzHttp.Config.Fetcher do + alias FzHttp.Config.{Definition, Resolver, Caster, Validator} + + @spec fetch_source_and_config( + module(), + key :: atom(), + db_configurations :: map(), + env_configurations :: map() + ) :: + {:ok, Resolver.source(), term()} | {:error, {[String.t()], metadata: term()}} + def fetch_source_and_config(module, key, %{} = db_configurations, %{} = env_configurations) + when is_atom(module) and is_atom(key) do + {type, {resolve_opts, validate_opts, dump_opts, _debug_opts}} = + Definition.fetch_spec_and_opts!(module, key) + + with {:ok, {source, value}} <- + resolve_value(module, key, env_configurations, db_configurations, resolve_opts), + {:ok, value} <- cast_value(module, key, source, value, type), + {:ok, value} <- validate_value(module, key, source, value, type, validate_opts) do + if dump_cb = Keyword.get(dump_opts, :dump) do + {:ok, source, dump_cb.(value)} + else + {:ok, source, value} + end + end + end + + defp resolve_value(module, key, env_configurations, db_configurations, opts) do + with :error <- Resolver.resolve(key, env_configurations, db_configurations, opts) do + {:error, {{nil, ["is required"]}, module: module, key: key, source: :not_found}} + end + end + + defp cast_value(module, key, source, value, type) do + case Caster.cast(value, type) do + {:ok, value} -> + {:ok, value} + + {:error, %Jason.DecodeError{} = decode_error} -> + reason = Jason.DecodeError.message(decode_error) + {:error, {{value, [reason]}, module: module, key: key, source: source}} + + {:error, reason} -> + {:error, {{value, [reason]}, module: module, key: key, source: source}} + end + end + + defp validate_value(module, key, source, value, type, opts) do + case Validator.validate(key, value, type, opts) do + {:ok, value} -> + {:ok, value} + + {:error, values_and_errors} -> + {:error, {values_and_errors, module: module, key: key, source: source}} + end + end +end diff --git a/apps/fz_http/lib/fz_http/config/logo.ex b/apps/fz_http/lib/fz_http/config/logo.ex new file mode 100644 index 000000000..5e0d8efe1 --- /dev/null +++ b/apps/fz_http/lib/fz_http/config/logo.ex @@ -0,0 +1,48 @@ +defmodule FzHttp.Config.Logo do + @moduledoc """ + Embedded Schema for logo + """ + use FzHttp, :schema + import FzHttp.Validator + import Ecto.Changeset + + @whitelisted_file_extensions ~w[.jpg .jpeg .png .gif .webp .avif .svg .tiff] + + # Singleton per configuration + @primary_key false + embedded_schema do + field :url, :string + field :file, :string + field :data, :string + field :type, :string + end + + def __types__, do: ~w[Default File URL Upload] + + def type(nil), do: "Default" + def type(%{file: path}) when not is_nil(path), do: "File" + def type(%{url: url}) when not is_nil(url), do: "URL" + def type(%{data: data}) when not is_nil(data), do: "Upload" + + def changeset(logo \\ %__MODULE__{}, attrs) do + logo + |> cast(attrs, [:url, :data, :file, :type]) + |> validate_file(:file, extensions: @whitelisted_file_extensions) + |> move_file_to_static + end + + defp move_file_to_static(changeset) do + case fetch_change(changeset, :file) do + {:ok, file} -> + directory = Path.join(Application.app_dir(:fz_http), "priv/static/uploads/logo") + file_name = Path.basename(file) + file_path = Path.join(directory, file_name) + File.mkdir_p!(directory) + File.cp!(file, file_path) + put_change(changeset, :file, file_name) + + :error -> + changeset + end + end +end diff --git a/apps/fz_http/lib/fz_http/config/resolver.ex b/apps/fz_http/lib/fz_http/config/resolver.ex new file mode 100644 index 000000000..90eaf65ed --- /dev/null +++ b/apps/fz_http/lib/fz_http/config/resolver.ex @@ -0,0 +1,144 @@ +defmodule FzHttp.Config.Resolver do + alias FzHttp.Config.Errors + + @type source :: {:env, atom()} | {:db, atom()} | :default + + @spec resolve( + key :: atom(), + env_configurations :: map(), + db_configurations :: map(), + opts :: [{:legacy_keys, [FzHttp.Config.Definition.legacy_key()]}] + ) :: + {:ok, {source :: source(), value :: term()}} | :error + def resolve(key, env_configurations, db_configurations, opts) do + with :error <- resolve_process_env_value(key), + :error <- resolve_env_value(env_configurations, key, opts), + :error <- resolve_db_value(db_configurations, key), + :error <- resolve_default_value(opts) do + :error + end + end + + @dialyzer {:nowarn_function, fetch_process_env: 1, resolve_process_env_value: 1} + defp resolve_process_env_value(key) do + pdict_key = {__MODULE__, key} + + case fetch_process_env(pdict_key) do + {:ok, {:env, value}} -> + {:ok, {{:env, env_key(key)}, value}} + + :error -> + :error + end + end + + if Mix.env() == :test do + def fetch_process_env(pdict_key) do + with :error <- fetch_process_value(pdict_key), + :error <- fetch_process_value(get_last_pid_from_pdict_list(:"$ancestors"), pdict_key), + :error <- fetch_process_value(get_last_pid_from_pdict_list(:"$callers"), pdict_key) do + :error + 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 + with {:dictionary, pdict} <- :erlang.process_info(pid, :dictionary), + {^key, value} <- List.keyfind(pdict, key, 0) do + value + else + _other -> + :error + end + end + + defp get_last_pid_from_pdict_list(stack) do + if values = Process.get(stack) do + List.last(values) + end + end + else + def fetch_process_env(_pdict_key), do: :error + end + + defp resolve_env_value(env_configurations, key, opts) do + legacy_keys = Keyword.get(opts, :legacy_keys, []) + + with :error <- fetch_env(env_configurations, key), + :error <- fetch_legacy_env(env_configurations, key, legacy_keys) do + :error + else + {:ok, {_source, nil}} -> :error + {:ok, source_and_value} -> {:ok, source_and_value} + end + end + + defp fetch_env(env_configurations, key) do + key = env_key(key) + + case Map.fetch(env_configurations, key) do + {:ok, value} -> {:ok, {{:env, key}, value}} + :error -> :error + end + end + + def env_key(key) do + key + |> to_string() + |> String.upcase() + end + + defp fetch_legacy_env(env_configurations, key, legacy_keys) do + Enum.find_value(legacy_keys, :error, fn {:env, legacy_key, removed_at} -> + case fetch_env(env_configurations, legacy_key) do + {:ok, value} -> + maybe_warn_on_legacy_key(key, legacy_key, removed_at) + {:ok, value} + + :error -> + nil + end + end) + end + + defp maybe_warn_on_legacy_key(_key, _legacy_key, nil) do + :ok + end + + defp maybe_warn_on_legacy_key(key, legacy_key, removed_at) do + Errors.legacy_key_used(key, legacy_key, removed_at) + end + + defp resolve_db_value(db_configurations, key) do + case Map.fetch(db_configurations, key) do + :error -> :error + {:ok, nil} -> :error + {:ok, value} -> {:ok, {{:db, key}, value}} + end + end + + defp resolve_default_value(opts) do + with {:ok, value} <- Keyword.fetch(opts, :default) do + {:ok, {:default, maybe_apply_default_value_callback(value)}} + end + end + + defp maybe_apply_default_value_callback(cb) when is_function(cb, 0), do: cb.() + defp maybe_apply_default_value_callback(value), do: value +end diff --git a/apps/fz_http/lib/fz_http/config/validator.ex b/apps/fz_http/lib/fz_http/config/validator.ex new file mode 100644 index 000000000..bc2b68ea4 --- /dev/null +++ b/apps/fz_http/lib/fz_http/config/validator.ex @@ -0,0 +1,169 @@ +defmodule FzHttp.Config.Validator do + import Ecto.Changeset + + def validate(_key, nil, _type, _opts) do + {:ok, nil} + end + + def validate(key, values, {:array, _separator, type, array_opts}, opts) do + validate_array(key, values, type, array_opts, opts) + end + + def validate(key, values, {:array, separator, type}, opts) do + validate(key, values, {:array, separator, type, []}, opts) + end + + def validate(key, values, {:json_array, type, array_opts}, opts) do + validate_array(key, values, type, array_opts, opts) + end + + def validate(key, values, {:json_array, type}, opts) do + validate(key, values, {:json_array, type, []}, opts) + end + + def validate(key, value, {:one_of, types}, opts) do + types + |> Enum.reduce_while({:error, []}, fn type, {:error, errors} -> + case validate(key, value, type, opts) do + {:ok, value} -> {:halt, {:ok, value}} + {:error, {_value, type_errors}} -> {:cont, {:error, type_errors ++ errors}} + end + end) + |> case do + {:ok, value} -> + {:ok, value} + + {:error, errors} -> + errors = + errors + |> Enum.uniq() + |> Enum.map(fn + "is invalid" -> "must be one of: " <> Enum.join(types, ", ") + error -> error + end) + |> Enum.reverse() + + {:error, {value, errors}} + end + end + + def validate(key, value, {:embed, type}, opts) do + {callback_module, callback_fun, callback_args} = + Keyword.get(opts, :changeset, {type, :changeset, []}) + + args = [Map.delete(value, :__struct__)] ++ callback_args + changeset = apply(callback_module, callback_fun, args) + + if changeset.valid? do + {:ok, apply_changes(changeset)} + else + {:error, {Map.get(changeset.changes, key, value), embedded_errors(changeset)}} + end + end + + def validate(key, value, type, opts) do + callback = Keyword.get(opts, :changeset, fn changeset, _key -> changeset end) + + changeset = + {%{}, %{key => type}} + |> cast(%{key => value}, [key]) + |> apply_validations(callback, type, key) + + if changeset.valid? do + {:ok, Map.get(changeset.changes, key)} + else + {:error, {Map.get(changeset.changes, key, value), errors(changeset)}} + end + end + + defp validate_array(_key, nil, _type, _array_opts, _opts) do + {:ok, nil} + end + + defp validate_array(key, values, type, array_opts, opts) when is_list(values) do + {validate_unique, array_opts} = Keyword.pop(array_opts, :validate_unique, false) + {validate_length, []} = Keyword.pop(array_opts, :validate_length, []) + + values + |> Enum.map(&validate(key, &1, type, opts)) + |> Enum.reduce({true, [], []}, fn + {:ok, value}, {valid?, values, errors} -> + if validate_unique == true and value in values do + {false, values, [{value, ["should not contain duplicates"]}] ++ errors} + else + {valid?, [value] ++ values, errors} + end + + {:error, {value, error}}, {_valid?, values, errors} -> + {false, values, [{value, error}] ++ errors} + end) + |> case do + {true, values, []} -> + min = Keyword.get(validate_length, :min) + max = Keyword.get(validate_length, :max) + is = Keyword.get(validate_length, :is) + + values + |> Enum.reverse() + |> validate_array_length(min, max, is) + + {false, _values, values_and_errors} -> + {:error, values_and_errors} + end + end + + defp validate_array(_key, values, _type, _array_opts, _opts) do + {:error, {values, ["must be an array"]}} + end + + defp validate_array_length(values, min, _max, _is) + when not is_nil(min) and length(values) < min do + {:error, {values, ["should be at least #{min} item(s)"]}} + end + + defp validate_array_length(values, _min, max, _is) + when not is_nil(max) and length(values) > max do + {:error, {values, ["should be at most #{max} item(s)"]}} + end + + defp validate_array_length(values, _min, _max, is) + when not is_nil(is) and length(values) != is do + {:error, {values, ["should be #{is} item(s)"]}} + end + + defp validate_array_length(values, _min, _max, _is) do + {:ok, values} + end + + defp apply_validations(changeset, callback, _type, key) when is_function(callback, 2) do + callback.(changeset, key) + end + + defp apply_validations(changeset, callback, type, key) when is_function(callback, 3) do + callback.(type, changeset, key) + end + + defp traverse_errors(changeset) do + changeset + |> Ecto.Changeset.traverse_errors(fn {message, opts} -> + Regex.replace(~r"%{(\w+)}", message, fn _, key -> + opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() + end) + end) + end + + defp embedded_errors(changeset) do + changeset + |> traverse_errors() + |> Enum.map(fn {key, error} -> + "#{key} #{error}" + end) + end + + defp errors(changeset) do + changeset + |> traverse_errors() + |> Map.values() + |> List.flatten() + end +end diff --git a/apps/fz_http/lib/fz_http/configurations.ex b/apps/fz_http/lib/fz_http/configurations.ex deleted file mode 100644 index d3cd79717..000000000 --- a/apps/fz_http/lib/fz_http/configurations.ex +++ /dev/null @@ -1,95 +0,0 @@ -defmodule FzHttp.Configurations do - @moduledoc """ - The Conf context for app configurations. - """ - - import Ecto.Query, warn: false - - alias FzHttp.{Repo, Configurations.Configuration} - - def get!(key) do - Map.get(get_configuration!(), key) - end - - def fetch_oidc_provider_config(provider_id) do - get!(:openid_connect_providers) - |> Enum.find(&(&1.id == provider_id)) - |> case do - nil -> - {:error, :not_found} - - provider -> - external_url = FzHttp.Config.fetch_env!(:fz_http, :external_url) - - {:ok, - %{ - discovery_document_uri: provider.discovery_document_uri, - client_id: provider.client_id, - client_secret: provider.client_secret, - redirect_uri: - provider.redirect_uri || "#{external_url}/auth/oidc/#{provider.id}/callback/", - response_type: provider.response_type, - scope: provider.scope - }} - end - end - - def put!(key, val) do - configuration = - get_configuration!() - |> Configuration.changeset(%{key => val}) - |> Repo.update!() - - FzHttp.SAML.StartProxy.refresh(configuration.saml_identity_providers) - - configuration - end - - def get_configuration! do - Repo.one!(Configuration) - 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 - Configuration.changeset(config, %{}) - end - - def update_configuration(%Configuration{} = config \\ get_configuration!(), attrs) do - case Repo.update(Configuration.changeset(config, attrs)) do - {:ok, configuration} -> - FzHttp.SAML.StartProxy.refresh(configuration.saml_identity_providers) - - {:ok, configuration} - - error -> - error - end - end - - def logo_types, do: ~w(Default URL Upload) - - def logo_type(nil), do: "Default" - 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 deleted file mode 100644 index be521ca50..000000000 --- a/apps/fz_http/lib/fz_http/configurations/configuration.ex +++ /dev/null @@ -1,107 +0,0 @@ -defmodule FzHttp.Configurations.Configuration do - @moduledoc """ - App global configuration, singleton resource - """ - use FzHttp, :schema - import Ecto.Changeset - - alias FzHttp.{ - Configurations.Logo, - Validator - } - - @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, []} - ) - |> Validator.trim_change(:default_client_dns) - |> Validator.trim_change(:default_client_allowed_ips) - |> Validator.trim_change(:default_client_endpoint) - |> Validator.validate_no_duplicates(:default_client_dns) - |> Validator.validate_list_of_ips_or_cidrs(:default_client_allowed_ips) - |> Validator.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/configurations/logo.ex b/apps/fz_http/lib/fz_http/configurations/logo.ex deleted file mode 100644 index 637b27c1f..000000000 --- a/apps/fz_http/lib/fz_http/configurations/logo.ex +++ /dev/null @@ -1,24 +0,0 @@ -defmodule FzHttp.Configurations.Logo do - @moduledoc """ - Embedded Schema for logo - """ - use FzHttp, :schema - import Ecto.Changeset - - # Singleton per configuration - @primary_key false - embedded_schema do - field :url, :string - field :data, :string - field :type, :string - end - - def changeset(logo, attrs) do - logo - |> cast(attrs, [ - :url, - :data, - :type - ]) - end -end diff --git a/apps/fz_http/lib/fz_http/configurations/mailer.ex b/apps/fz_http/lib/fz_http/configurations/mailer.ex deleted file mode 100644 index 3cb15a6c0..000000000 --- a/apps/fz_http/lib/fz_http/configurations/mailer.ex +++ /dev/null @@ -1,34 +0,0 @@ -defmodule FzHttp.Configurations.Mailer do - @moduledoc """ - A non-persisted schema to validate email configs on boot. - XXX: Consider persisting this to make outbound email configurable via API. - """ - use Ecto.Schema - import Ecto.Changeset - - embedded_schema do - field :from, :string - field :provider, :string - field :configs, :map - end - - def changeset(attrs) do - %__MODULE__{} - |> cast(attrs, [:from, :provider, :configs]) - |> validate_required([:from, :provider, :configs]) - |> validate_format(:from, ~r/@/) - |> validate_provider_in_configs() - end - - defp validate_provider_in_configs( - %Ecto.Changeset{ - changes: %{provider: provider, configs: configs} - } = changeset - ) - when not is_map_key(configs, provider) do - changeset - |> add_error(:provider, "must exist in configs") - end - - defp validate_provider_in_configs(changeset), do: changeset -end 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 69608d261..3d96b9d0d 100644 --- a/apps/fz_http/lib/fz_http/connectivity_check_service.ex +++ b/apps/fz_http/lib/fz_http/connectivity_check_service.ex @@ -91,6 +91,6 @@ defmodule FzHttp.ConnectivityCheckService do end defp http_client_options do - Application.fetch_env!(:fz_http, :http_client_options) + FzHttp.Config.fetch_env!(:fz_http, :http_client_options) end end diff --git a/apps/fz_http/lib/fz_http/devices.ex b/apps/fz_http/lib/fz_http/devices.ex index 62ff3f6df..a70c92170 100644 --- a/apps/fz_http/lib/fz_http/devices.ex +++ b/apps/fz_http/lib/fz_http/devices.ex @@ -1,10 +1,9 @@ defmodule FzHttp.Devices do import Ecto.Changeset import Ecto.Query, warn: false - alias EctoNetwork.INET alias FzHttp.{ - Configurations, + Config, Devices.Device, Devices.DeviceSetting, Repo, @@ -140,21 +139,33 @@ defmodule FzHttp.Devices do change_device(%Device{}, attrs) end - def allowed_ips(device), do: config(device, :allowed_ips) - def endpoint(device), do: config(device, :endpoint) - def dns(device), do: config(device, :dns) - def mtu(device), do: config(device, :mtu) - def persistent_keepalive(device), do: config(device, :persistent_keepalive) + def allowed_ips(device, defaults), do: config(device, defaults, :allowed_ips) + def endpoint(device, defaults), do: config(device, defaults, :endpoint) + def dns(device, defaults), do: config(device, defaults, :dns) + def mtu(device, defaults), do: config(device, defaults, :mtu) + def persistent_keepalive(device, defaults), do: config(device, defaults, :persistent_keepalive) - def config(device, key) do + # XXX: This is an A* query which is executed for every config key, + # we can load all configs in a batch instead + def config(device, defaults, key) do if Map.get(device, String.to_atom("use_default_#{key}")) do - Map.get(Configurations.get_configuration!(), String.to_atom("default_client_#{key}")) + Map.fetch!(defaults, String.to_atom("default_client_#{key}")) else Map.get(device, key) end end - def defaults(changeset) do + def defaults do + Config.fetch_configs!([ + :default_client_allowed_ips, + :default_client_endpoint, + :default_client_dns, + :default_client_mtu, + :default_client_persistent_keepalive + ]) + end + + def use_default_fields(changeset) do ~w( use_default_allowed_ips use_default_dns @@ -169,6 +180,7 @@ defmodule FzHttp.Devices do def as_config(device) do server_public_key = Application.get_env(:fz_vpn, :wireguard_public_key) + defaults = defaults() if is_nil(server_public_key) do Logger.error( @@ -180,21 +192,21 @@ defmodule FzHttp.Devices do [Interface] PrivateKey = REPLACE_ME Address = #{inet(device)} - #{mtu_config(device)} - #{dns_config(device)} + #{mtu_config(device, defaults)} + #{dns_config(device, defaults)} [Peer] #{psk_config(device)} PublicKey = #{server_public_key} - #{allowed_ips_config(device)} - #{endpoint_config(device)} - #{persistent_keepalive_config(device)} + #{allowed_ips_config(device, defaults)} + #{endpoint_config(device, defaults)} + #{persistent_keepalive_config(device, defaults)} """ end def decode(nil), do: nil def decode(inet) when is_binary(inet), do: inet - def decode(inet), do: INET.decode(inet) + def decode(inet), do: FzHttp.Types.INET.to_string(inet) @hash_range 2 ** 16 def new_name(name \\ FzCommon.NameGenerator.generate()) do @@ -219,8 +231,8 @@ defmodule FzHttp.Devices do end end - defp mtu_config(device) do - m = mtu(device) + defp mtu_config(device, defaults) do + m = mtu(device, defaults) if field_empty?(m) do "" @@ -229,18 +241,18 @@ defmodule FzHttp.Devices do end end - defp allowed_ips_config(device) do - a = allowed_ips(device) + defp allowed_ips_config(device, defaults) do + allowed_ips = allowed_ips(device, defaults) - if field_empty?(a) do + if field_empty?(allowed_ips) do "" else - "AllowedIPs = #{a}" + "AllowedIPs = #{Enum.join(allowed_ips, ",")}" end end - defp persistent_keepalive_config(device) do - pk = persistent_keepalive(device) + defp persistent_keepalive_config(device, defaults) do + pk = persistent_keepalive(device, defaults) if field_empty?(pk) do "" @@ -249,18 +261,18 @@ defmodule FzHttp.Devices do end end - defp dns_config(device) when is_struct(device) do - dns = dns(device) + defp dns_config(device, defaults) when is_struct(device) do + dns = dns(device, defaults) if field_empty?(dns) do "" else - "DNS = #{dns}" + "DNS = #{Enum.join(dns, ",")}" end end - defp endpoint_config(device) do - ep = endpoint(device) + defp endpoint_config(device, defaults) do + ep = endpoint(device, defaults) if field_empty?(ep) do "" @@ -272,7 +284,7 @@ defmodule FzHttp.Devices do # Finds a port in IPv6-formatted address, e.g. [2001::1]:51820 @capture_port ~r/\[.*]:(?[\d]+)/ defp maybe_add_port(endpoint) do - wireguard_port = Application.fetch_env!(:fz_vpn, :wireguard_port) + wireguard_port = Config.fetch_env!(:fz_vpn, :wireguard_port) colon_count = endpoint |> String.graphemes() |> Enum.count(&(&1 == ":")) if colon_count == 1 or !is_nil(Regex.named_captures(@capture_port, endpoint)) do @@ -284,8 +296,8 @@ defmodule FzHttp.Devices do end defp field_empty?(nil), do: true - defp field_empty?(0), do: true + defp field_empty?([]), do: true defp field_empty?(field) when is_binary(field) do len = @@ -299,10 +311,10 @@ defmodule FzHttp.Devices do defp field_empty?(_), do: false defp ipv4? do - FzHttp.Config.fetch_env!(:fz_http, :wireguard_ipv4_enabled) + Config.fetch_env!(:fz_http, :wireguard_ipv4_enabled) end defp ipv6? do - FzHttp.Config.fetch_env!(:fz_http, :wireguard_ipv6_enabled) + 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 6b74f2874..0555da857 100644 --- a/apps/fz_http/lib/fz_http/devices/device.ex +++ b/apps/fz_http/lib/fz_http/devices/device.ex @@ -4,9 +4,9 @@ defmodule FzHttp.Devices.Device do """ use FzHttp, :schema import Ecto.Changeset + import FzHttp.Config, only: [config_changeset: 3] alias FzHttp.Validator alias FzHttp.Devices - require Logger @description_max_length 2048 @@ -28,11 +28,11 @@ defmodule FzHttp.Devices.Device do 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 - field :ipv6, EctoNetwork.INET + field :allowed_ips, {:array, FzHttp.Types.INET}, default: [] + field :dns, {:array, :string}, default: [] + field :remote_ip, FzHttp.Types.IP + field :ipv4, FzHttp.Types.IP + field :ipv6, FzHttp.Types.IP field :latest_handshake, :utc_datetime_usec field :key_regenerated_at, :utc_datetime_usec, read_after_writes: true @@ -88,11 +88,15 @@ defmodule FzHttp.Devices.Device do defp changeset(changeset) do changeset - |> Validator.trim_change(:allowed_ips) |> Validator.trim_change(:dns) |> Validator.trim_change(:endpoint) |> Validator.trim_change(:name) |> Validator.trim_change(:description) + |> config_changeset(:allowed_ips, :default_client_allowed_ips) + |> config_changeset(:dns, :default_client_dns) + |> config_changeset(:endpoint, :default_client_endpoint) + |> config_changeset(:persistent_keepalive, :default_client_persistent_keepalive) + |> config_changeset(:mtu, :default_client_mtu) |> Validator.validate_base64(:public_key) |> Validator.validate_base64(:preshared_key) |> validate_length(:public_key, is: @key_length) @@ -108,16 +112,6 @@ defmodule FzHttp.Devices.Device do persistent_keepalive mtu ]a) - |> Validator.validate_list_of_ips_or_cidrs(:allowed_ips) - |> Validator.validate_no_duplicates(:dns) - |> validate_number(:persistent_keepalive, - greater_than_or_equal_to: 0, - less_than_or_equal_to: 120 - ) - |> validate_number(:mtu, - greater_than_or_equal_to: 576, - less_than_or_equal_to: 1500 - ) |> prepare_changes(fn changeset -> changeset |> maybe_put_default_ip(:ipv4) @@ -146,13 +140,13 @@ defmodule FzHttp.Devices.Device do defp put_default_ip(changeset, field) do cidr_string = wireguard_network(field) - {:ok, cidr_inet} = EctoNetwork.INET.cast(cidr_string) + {:ok, cidr_inet} = FzHttp.Types.CIDR.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() + |> FzHttp.Types.IP.cast() Devices.Device.Query.next_available_address(cidr_inet, offset, [gateway_address]) |> FzHttp.Repo.one() @@ -174,12 +168,12 @@ defmodule FzHttp.Devices.Device do defp ipv4_address do FzHttp.Config.fetch_env!(:fz_http, :wireguard_ipv4_address) - |> EctoNetwork.INET.cast() + |> FzHttp.Types.IP.cast() end defp ipv6_address do FzHttp.Config.fetch_env!(:fz_http, :wireguard_ipv6_address) - |> EctoNetwork.INET.cast() + |> FzHttp.Types.IP.cast() end defp validate_max_devices(changeset) do diff --git a/apps/fz_http/lib/fz_http/devices/device/query.ex b/apps/fz_http/lib/fz_http/devices/device/query.ex index 4bd5d2e83..253df6ec5 100644 --- a/apps/fz_http/lib/fz_http/devices/device/query.ex +++ b/apps/fz_http/lib/fz_http/devices/device/query.ex @@ -72,7 +72,7 @@ defmodule FzHttp.Devices.Device.Query do # 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 + # This way is chosen because IPv6 cannot 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. 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 70d78de6a..7498c41b6 100644 --- a/apps/fz_http/lib/fz_http/devices/stats_updater.ex +++ b/apps/fz_http/lib/fz_http/devices/stats_updater.ex @@ -31,20 +31,11 @@ defmodule FzHttp.Devices.StatsUpdater do end end - # XXX: Come up with a better way to update devices in Sandbox mode defp device_to_update(public_key) do - if FzHttp.Config.fetch_env!(:fz_http, :sandbox) do - Repo.one( - from Device, - order_by: fragment("RANDOM()"), - limit: 1 - ) - else - Repo.one( - from d in Device, - where: d.public_key == ^public_key - ) - end + Repo.one( + from d in Device, + where: d.public_key == ^public_key + ) end defp latest_handshake(epoch) do diff --git a/apps/fz_http/lib/fz_http/mfa/method/changeset.ex b/apps/fz_http/lib/fz_http/mfa/method/changeset.ex index 55d019c4d..a41b08796 100644 --- a/apps/fz_http/lib/fz_http/mfa/method/changeset.ex +++ b/apps/fz_http/lib/fz_http/mfa/method/changeset.ex @@ -56,10 +56,10 @@ defmodule FzHttp.MFA.Method.Changeset do {:changes, %{}} -> changeset |> add_error(:payload, "is invalid") - |> add_error(:code, "can not be verified") + |> add_error(:code, "cannot be verified") :error -> - add_error(changeset, :code, "can not be verified") + add_error(changeset, :code, "cannot be verified") false -> add_error(changeset, :code, "is invalid") diff --git a/apps/fz_http/lib/fz_http/oidc/refresher.ex b/apps/fz_http/lib/fz_http/oidc/refresher.ex index 97ed3ac9c..aa8c989e3 100644 --- a/apps/fz_http/lib/fz_http/oidc/refresher.ex +++ b/apps/fz_http/lib/fz_http/oidc/refresher.ex @@ -4,7 +4,7 @@ defmodule FzHttp.OIDC.Refresher do """ use GenServer, restart: :temporary import Ecto.{Changeset, Query} - alias FzHttp.{Configurations, OIDC, OIDC.Connection, Repo, Users} + alias FzHttp.{Auth, OIDC, OIDC.Connection, Repo, Users} require Logger def start_link(init_opts) do @@ -34,7 +34,7 @@ defmodule FzHttp.OIDC.Refresher do Logger.info("Refreshing user\##{user_id} @ #{provider_id}...") refresh_response = - with {:ok, config} <- Configurations.fetch_oidc_provider_config(provider_id), + with {:ok, config} <- Auth.fetch_oidc_provider_config(provider_id), {:ok, tokens} <- OpenIDConnect.fetch_tokens(config, %{ grant_type: "refresh_token", @@ -68,6 +68,6 @@ defmodule FzHttp.OIDC.Refresher do end defp enabled? do - FzHttp.Configurations.get!(:disable_vpn_on_oidc_error) + FzHttp.Config.fetch_config!(:disable_vpn_on_oidc_error) end end diff --git a/apps/fz_http/lib/fz_http/rules/rule.ex b/apps/fz_http/lib/fz_http/rules/rule.ex index dbd7fcff4..1bc216e4c 100644 --- a/apps/fz_http/lib/fz_http/rules/rule.ex +++ b/apps/fz_http/lib/fz_http/rules/rule.ex @@ -1,7 +1,4 @@ defmodule FzHttp.Rules.Rule do - @moduledoc """ - Not really sure what to write here. I'll update this later. - """ use FzHttp, :schema import Ecto.Changeset @@ -10,7 +7,7 @@ defmodule FzHttp.Rules.Rule do @port_type_msg "port_type must be specified with port_range" schema "rules" do - field :destination, EctoNetwork.INET, read_after_writes: true + field :destination, FzHttp.Types.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 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 a1c56fbdd..57136e3d0 100644 --- a/apps/fz_http/lib/fz_http/saml/start_proxy.ex +++ b/apps/fz_http/lib/fz_http/saml/start_proxy.ex @@ -54,7 +54,7 @@ defmodule FzHttp.SAML.StartProxy do Keyword.put(samly_configs, :identity_providers, identity_providers) end - def refresh(providers \\ FzHttp.Configurations.get!(:saml_identity_providers)) do + def refresh(providers \\ FzHttp.Config.fetch_config!(:saml_identity_providers)) do samly_configs = FzHttp.Config.fetch_env!(:samly, Samly.Provider) |> set_service_provider() diff --git a/apps/fz_http/lib/fz_http/server.ex b/apps/fz_http/lib/fz_http/server.ex index 52be47f8f..0e99db64b 100644 --- a/apps/fz_http/lib/fz_http/server.ex +++ b/apps/fz_http/lib/fz_http/server.ex @@ -2,16 +2,12 @@ defmodule FzHttp.Server do @moduledoc """ Functions for other processes to interact with the FzHttp application """ - use GenServer - alias FzHttp.{Devices, Devices.StatsUpdater, Rules, Users} - @process_opts Application.compile_env(:fz_http, :server_process_opts, []) - def start_link(_) do # We're not storing state, simply providing an API - GenServer.start_link(__MODULE__, nil, @process_opts) + GenServer.start_link(__MODULE__, nil, name: {:global, :fz_http_server}) end @impl GenServer diff --git a/apps/fz_http/lib/fz_http/telemetry.ex b/apps/fz_http/lib/fz_http/telemetry.ex index 8b98ecbbc..0e4ca6afe 100644 --- a/apps/fz_http/lib/fz_http/telemetry.ex +++ b/apps/fz_http/lib/fz_http/telemetry.ex @@ -94,6 +94,25 @@ defmodule FzHttp.Telemetry do # How far back to count handshakes as an active device @active_device_window 86_400 def ping_data do + %{ + openid_connect_providers: {_, openid_connect_providers}, + saml_identity_providers: {_, saml_identity_providers}, + allow_unprivileged_device_management: allow_unprivileged_device_management, + allow_unprivileged_device_configuration: {_, allow_unprivileged_device_configuration}, + local_auth_enabled: {_, local_auth_enabled}, + disable_vpn_on_oidc_error: {_, disable_vpn_on_oidc_error}, + logo: {_, logo} + } = + FzHttp.Config.fetch_source_and_configs!([ + :openid_connect_providers, + :saml_identity_providers, + :allow_unprivileged_device_management, + :allow_unprivileged_device_configuration, + :local_auth_enabled, + :disable_vpn_on_oidc_error, + :logo + ]) + common_fields() ++ [ devices_active_within_24h: Devices.count_active_within(@active_device_window), @@ -104,18 +123,16 @@ defmodule FzHttp.Telemetry do max_devices_for_users: Devices.max_count_by_user_id(), users_with_mfa: MFA.count_users_with_method(), users_with_mfa_totp: MFA.count_users_with_totp_method(), - 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), + openid_providers: length(openid_connect_providers), + saml_providers: length(saml_identity_providers), + unprivileged_device_management: allow_unprivileged_device_management, + unprivileged_device_configuration: allow_unprivileged_device_configuration, + local_authentication: local_auth_enabled, + disable_vpn_on_oidc_error: disable_vpn_on_oidc_error, outbound_email: FzHttpWeb.Mailer.active?(), external_database: external_database?(Map.new(FzHttp.Config.fetch_env!(:fz_http, FzHttp.Repo))), - logo_type: FzHttp.Configurations.logo_type(FzHttp.Configurations.get!(:logo)) + logo_type: FzHttp.Config.Logo.type(logo) ] end diff --git a/apps/fz_http/lib/fz_http/types/cidr.ex b/apps/fz_http/lib/fz_http/types/cidr.ex new file mode 100644 index 000000000..81c752d9a --- /dev/null +++ b/apps/fz_http/lib/fz_http/types/cidr.ex @@ -0,0 +1,68 @@ +defmodule FzHttp.Types.CIDR do + @moduledoc """ + Ecto type implementation for CIDR's based on `Postgrex.INET` type, it required netmask to be always set. + """ + @behaviour Ecto.Type + + def type, do: :inet + + def embed_as(_), do: :self + + def equal?(left, right), do: left == right + + def cast(%Postgrex.INET{} = struct), do: {:ok, struct} + + def cast(binary) when is_binary(binary) do + with {:ok, {binary_address, binary_netmask}} <- parse_binary(binary), + {:ok, address} <- cast_address(binary_address), + {:ok, netmask} <- cast_netmask(binary_netmask), + :ok <- validate_netmask(address, netmask) do + {:ok, %Postgrex.INET{address: address, netmask: netmask}} + else + _error -> {:error, message: "is invalid"} + end + end + + def cast(_), do: :error + + defp parse_binary(binary) do + binary = String.trim(binary) + + with [binary_address, binary_netmask] <- String.split(binary, "/", parts: 2) do + {:ok, {binary_address, binary_netmask}} + else + _other -> :error + end + end + + defp cast_address(address) do + address + |> String.to_charlist() + |> :inet.parse_address() + end + + defp cast_netmask(binary) when is_binary(binary) do + case Integer.parse(binary) do + {netmask, ""} -> {:ok, netmask} + _other -> :error + end + end + + defp validate_netmask(address, netmask) + when tuple_size(address) == 4 and 0 <= netmask and netmask <= 32, + do: :ok + + defp validate_netmask(address, netmask) + when tuple_size(address) == 8 and 0 <= netmask and netmask <= 128, + do: :ok + + defp validate_netmask(_address, _netmask), do: :error + + def dump(%Postgrex.INET{} = inet), do: {:ok, inet} + def dump(_), do: :error + + def load(%Postgrex.INET{} = inet), do: {:ok, inet} + def load(_), do: :error + + def to_string(%Postgrex.INET{} = inet), do: FzHttp.Types.INET.to_string(inet) +end diff --git a/apps/fz_http/lib/fz_http/types/inet.ex b/apps/fz_http/lib/fz_http/types/inet.ex new file mode 100644 index 000000000..269875c7e --- /dev/null +++ b/apps/fz_http/lib/fz_http/types/inet.ex @@ -0,0 +1,71 @@ +defmodule FzHttp.Types.INET do + @moduledoc """ + INET is an implementation for native PostgreSQL `inet` type which can hold + either a CIDR (IP with a netmask) or just an IP address (with empty netmask). + """ + @behaviour Ecto.Type + + def type, do: :inet + + def embed_as(_), do: :self + + def equal?(left, right), do: left == right + + def cast(%Postgrex.INET{} = struct), do: {:ok, struct} + def cast(tuple) when is_tuple(tuple), do: cast(%Postgrex.INET{address: tuple}) + + def cast(binary) when is_binary(binary) do + with {:ok, {binary_address, binary_netmask}} <- parse_binary(binary), + {:ok, address} <- cast_address(binary_address), + {:ok, netmask} <- cast_netmask(binary_netmask) do + {:ok, %Postgrex.INET{address: address, netmask: netmask}} + else + _error -> {:error, message: "is invalid"} + end + end + + def cast(_), do: :error + + defp parse_binary(binary) do + binary = String.trim(binary) + + case String.split(binary, "/", parts: 2) do + [binary_address, binary_netmask] -> {:ok, {binary_address, binary_netmask}} + [binary_address] -> {:ok, {binary_address, nil}} + _other -> :error + end + end + + defp cast_address(address) do + address + |> String.to_charlist() + |> :inet.parse_address() + end + + defp cast_netmask(nil), do: {:ok, nil} + + defp cast_netmask(binary) when is_binary(binary) do + case Integer.parse(binary) do + {netmask, ""} -> {:ok, netmask} + _other -> :error + end + end + + def dump(%Postgrex.INET{} = inet), do: {:ok, inet} + def dump(_), do: :error + + def load(%Postgrex.INET{} = inet), do: {:ok, inet} + def load(_), do: :error + + def to_string(%Postgrex.INET{address: address, netmask: nil}) do + "#{:inet.ntoa(address)}" + end + + def to_string(%Postgrex.INET{address: address, netmask: netmask}) do + "#{:inet.ntoa(address)}/#{netmask}" + end + + def to_string(inet) when is_binary(inet) do + inet + end +end diff --git a/apps/fz_http/lib/fz_http/types/ip.ex b/apps/fz_http/lib/fz_http/types/ip.ex new file mode 100644 index 000000000..5f16eaca4 --- /dev/null +++ b/apps/fz_http/lib/fz_http/types/ip.ex @@ -0,0 +1,34 @@ +defmodule FzHttp.Types.IP do + @moduledoc """ + Ecto type implementation for IP's based on `Postgrex.INET` type, + it always ignores netmask by setting it to `nil`. + """ + @behaviour Ecto.Type + + def type, do: :inet + + def embed_as(_), do: :self + + def equal?(left, right), do: left == right + + def cast(%Postgrex.INET{} = inet), do: {:ok, inet} + + def cast(binary) when is_binary(binary) do + with {:ok, address} <- FzHttp.Types.IPPort.cast_address(binary) do + {:ok, %Postgrex.INET{address: address, netmask: nil}} + else + {:error, _reason} -> {:error, message: "is invalid"} + end + end + + def cast(_), do: :error + + def dump(%Postgrex.INET{} = inet), do: {:ok, inet} + def dump(_), do: :error + + def load(%Postgrex.INET{} = inet), do: {:ok, inet} + def load(_), do: :error + + def to_string(ip) when is_binary(ip), do: ip + def to_string(%Postgrex.INET{} = inet), do: FzHttp.Types.INET.to_string(inet) +end diff --git a/apps/fz_http/lib/fz_http/types/ip_port.ex b/apps/fz_http/lib/fz_http/types/ip_port.ex new file mode 100644 index 000000000..e7a821944 --- /dev/null +++ b/apps/fz_http/lib/fz_http/types/ip_port.ex @@ -0,0 +1,87 @@ +defmodule FzHttp.Types.IPPort do + @behaviour Ecto.Type + + defstruct [:type, :address, :port] + + def type, do: :string + + def embed_as(_), do: :self + + def equal?(left, right), do: left == right + + def cast(%__MODULE__{} = ip_port), do: ip_port + + def cast(binary) when is_binary(binary) do + binary = String.trim(binary) + + with {:ok, {binary_address, binary_port}} <- parse_binary(binary), + {:ok, address} <- cast_address(binary_address), + {:ok, port} <- cast_port(binary_port) do + {:ok, %__MODULE__{type: type(address), address: address, port: port}} + else + _error -> {:error, message: "is invalid"} + end + end + + def cast(_), do: :error + + defp parse_binary("[" <> binary) do + with [binary_address, binary_port] <- String.split(binary, "]:", parts: 2) do + {:ok, {binary_address, binary_port}} + else + [binary_address] -> {:ok, {binary_address, nil}} + end + end + + defp parse_binary(binary) do + with [binary_address, binary_port] <- String.split(binary, ":") do + {:ok, {binary_address, binary_port}} + else + _other -> {:ok, {binary, nil}} + end + end + + def cast_address(address) do + address + |> String.to_charlist() + |> :inet.parse_address() + end + + defp cast_port(nil), do: {:ok, nil} + + defp cast_port(binary) when is_binary(binary) do + case Integer.parse(binary) do + {port, ""} when 0 < port and port <= 65_535 -> {:ok, port} + _other -> :error + end + end + + defp type(address) when tuple_size(address) == 4, do: :ipv4 + defp type(address) when tuple_size(address) == 8, do: :ipv6 + + def dump(%__MODULE__{} = ip) do + {:ok, __MODULE__.to_string(ip)} + end + + def dump(_), do: :error + + def load(%__MODULE__{} = ip) do + {:ok, ip} + end + + def load(_), do: :error + + def to_string(%__MODULE__{address: ip, port: nil}) do + ip |> :inet.ntoa() |> List.to_string() + end + + def to_string(%__MODULE__{type: :ipv4, address: ip, port: port}) do + ip = ip |> :inet.ntoa() |> List.to_string() + "#{ip}:#{port}" + end + + def to_string(%__MODULE__{type: :ipv6, address: ip, port: port}) do + ip = ip |> :inet.ntoa() |> List.to_string() + "[#{ip}]:#{port}" + end +end diff --git a/apps/fz_http/lib/fz_http/types/protocols.ex b/apps/fz_http/lib/fz_http/types/protocols.ex new file mode 100644 index 000000000..f4eca4adb --- /dev/null +++ b/apps/fz_http/lib/fz_http/types/protocols.ex @@ -0,0 +1,7 @@ +defimpl String.Chars, for: Postgrex.INET do + def to_string(%Postgrex.INET{} = inet), do: FzHttp.Types.INET.to_string(inet) +end + +defimpl Phoenix.HTML.Safe, for: Postgrex.INET do + def to_iodata(%Postgrex.INET{} = inet), do: FzHttp.Types.INET.to_string(inet) +end diff --git a/apps/fz_http/lib/fz_http/users.ex b/apps/fz_http/lib/fz_http/users.ex index 264c47652..c79f122b1 100644 --- a/apps/fz_http/lib/fz_http/users.ex +++ b/apps/fz_http/lib/fz_http/users.ex @@ -1,5 +1,5 @@ defmodule FzHttp.Users do - alias FzHttp.{Repo, Validator, Configurations} + alias FzHttp.{Repo, Validator, Config} alias FzHttp.Telemetry alias FzHttp.Users.User require Ecto.Query @@ -162,7 +162,7 @@ defmodule FzHttp.Users do end def vpn_session_expires_at(user) do - DateTime.add(user.last_signed_in_at, Configurations.vpn_duration()) + DateTime.add(user.last_signed_in_at, Config.fetch_config!(:vpn_session_duration)) end def vpn_session_expired?(user) do @@ -170,7 +170,7 @@ defmodule FzHttp.Users do is_nil(user.last_signed_in_at) -> false - not Configurations.vpn_sessions_expire?() -> + not Config.vpn_sessions_expire?() -> false true -> diff --git a/apps/fz_http/lib/fz_http/validator.ex b/apps/fz_http/lib/fz_http/validator.ex index ce66d0322..5ef131ec2 100644 --- a/apps/fz_http/lib/fz_http/validator.ex +++ b/apps/fz_http/lib/fz_http/validator.ex @@ -3,7 +3,6 @@ defmodule FzHttp.Validator do A set of changeset helpers and schema extensions to simplify our changesets and make validation more reliable. """ import Ecto.Changeset - alias FzCommon.FzNet def changed?(changeset, field) do Map.has_key?(changeset.changes, field) @@ -17,58 +16,130 @@ defmodule FzHttp.Validator do validate_format(changeset, field, ~r/@/, message: "is invalid email address") end - def validate_uri(changeset, fields) when is_list(fields) do - Enum.reduce(fields, changeset, fn field, accumulated_changeset -> - validate_uri(accumulated_changeset, field) - end) - end + def validate_uri(changeset, field, opts \\ []) when is_atom(field) do + valid_schemes = Keyword.get(opts, :schemes, ~w[http https]) - def validate_uri(changeset, field) when is_atom(field) do validate_change(changeset, field, fn _current_field, value -> case URI.new(value) do + {:ok, %URI{} = uri} -> + cond do + uri.host == nil -> + [{field, "does not contain host"}] + + uri.scheme == nil -> + [{field, "does not contain a scheme"}] + + uri.scheme not in valid_schemes -> + [{field, "only #{Enum.join(valid_schemes, ", ")} schemes are supported"}] + + true -> + [] + end + {:error, part} -> [{field, "is invalid. Error at #{part}"}] - - _ -> - [] end end) end + def normalize_url(changeset, field) do + with {:ok, value} <- fetch_change(changeset, field) do + uri = URI.parse(value) + scheme = uri.scheme || "https" + port = URI.default_port(scheme) + path = uri.path || "/" + put_change(changeset, field, %{uri | scheme: scheme, port: port, path: path}) + else + :error -> + changeset + end + end + def validate_no_duplicates(changeset, field) when is_atom(field) do - validate_change(changeset, field, fn _current_field, value -> - values = split_comma_list(value) - dupes = Enum.uniq(values -- Enum.uniq(values)) - - error_if( - dupes, - &(&1 != []), - &{field, "is invalid: duplicates are not allowed: #{Enum.join(&1, ", ")}"} - ) + validate_change(changeset, field, fn _current_field, list when is_list(list) -> + list + |> Enum.reduce_while({true, MapSet.new()}, fn value, {true, acc} -> + if MapSet.member?(acc, value) do + {:halt, {false, acc}} + else + {:cont, {true, MapSet.put(acc, value)}} + end + end) + |> case do + {true, _map_set} -> [] + {false, _map_set} -> [{field, "should not contain duplicates"}] + end end) end - def validate_list_of_ips(changeset, field) when is_atom(field) do + def validate_fqdn(changeset, field, opts \\ []) do + allow_port = Keyword.get(opts, :allow_port, false) + validate_change(changeset, field, fn _current_field, value -> - value - |> split_comma_list() - |> Enum.find(&(not FzNet.valid_ip?(&1))) - |> error_if( - &(!is_nil(&1)), - &{field, "is invalid: #{&1} is not a valid IPv4 / IPv6 address"} - ) + {fqdn, port} = split_port(value) + fqdn_validation_errors = fqdn_validation_errors(field, fqdn) + port_validation_errors = port_validation_errors(field, port, allow_port) + fqdn_validation_errors ++ port_validation_errors end) end - def validate_list_of_ips_or_cidrs(changeset, field) when is_atom(field) do + defp fqdn_validation_errors(field, fqdn) do + if Regex.match?(~r/^([a-zA-Z0-9._-])+$/i, fqdn) do + [] + else + [{field, "#{fqdn} is not a valid FQDN"}] + end + end + + defp split_port(value) do + case String.split(value, ":", parts: 2) do + [prefix, port] -> + case Integer.parse(port) do + {port, ""} -> + {prefix, port} + + _ -> + {value, nil} + end + + [value] -> + {value, nil} + end + end + + defp port_validation_errors(_field, nil, _allow?), + do: [] + + defp port_validation_errors(field, _port, false), + do: [{field, "setting port is not allowed"}] + + defp port_validation_errors(_field, port, _allow?) when 0 < port and port <= 65_535, + do: [] + + defp port_validation_errors(field, _port, _allow?), + do: [{field, "port is not a number between 0 and 65535"}] + + def validate_ip_type_inclusion(changeset, field, types) do + validate_change(changeset, field, fn _current_field, %{address: address} -> + type = if tuple_size(address) == 4, do: :ipv4, else: :ipv6 + + if type in types do + [] + else + [{field, "is not a supported IP type"}] + end + end) + end + + def validate_cidr(changeset, field, _opts \\ []) do validate_change(changeset, field, fn _current_field, value -> - value - |> split_comma_list() - |> Enum.find(&(not (FzNet.valid_ip?(&1) or FzNet.valid_cidr?(&1)))) - |> error_if( - &(!is_nil(&1)), - &{field, "is invalid: #{&1} is not a valid IPv4 / IPv6 address or CIDR range"} - ) + case FzHttp.Types.CIDR.cast(value) do + {:ok, _cidr} -> + [] + + {:error, _reason} -> + [{field, "is not a valid CIDR range"}] + end end) end @@ -88,27 +159,32 @@ defmodule FzHttp.Validator do end def validate_omitted(changeset, field) when is_atom(field) do - validate_change(changeset, field, fn _current_field, value -> - if is_nil(value) do - [] - else - [{field, "must not be present"}] - end + validate_change(changeset, field, fn + _field, nil -> [] + _field, [] -> [] + field, _value -> [{field, "must not be present"}] end) end - defp split_comma_list(text) do - text - |> String.split(",") - |> Enum.map(&String.trim/1) - end + def validate_file(changeset, field, opts \\ []) do + validate_change(changeset, field, fn _current_field, value -> + extensions = Keyword.get(opts, :extensions, []) - defp error_if(value, is_error, error) do - if is_error.(value) do - [error.(value)] - else - [] - end + cond do + not File.exists?(value) -> + [{field, "file does not exist"}] + + extensions != [] and Path.extname(value) not in extensions -> + [ + {field, + "file extension is not supported, got #{Path.extname(value)}, " <> + "expected one of #{inspect(extensions)}"} + ] + + true -> + [] + end + end) end @doc """ @@ -137,7 +213,7 @@ defmodule FzHttp.Validator do end) else {:changes, _hash} -> - add_error(changeset, value_field, "can not be verified", validation: :hash) + add_error(changeset, value_field, "can't be verified", validation: :hash) :error -> add_error(changeset, value_field, "is already verified", validation: :hash) @@ -197,7 +273,11 @@ defmodule FzHttp.Validator do defp maybe_apply(value), do: value def trim_change(changeset, field) do - update_change(changeset, field, &if(!is_nil(&1), do: String.trim(&1))) + update_change(changeset, field, fn + nil -> nil + changes when is_list(changes) -> Enum.map(changes, &String.trim/1) + change -> String.trim(change) + end) end @doc """ diff --git a/apps/fz_http/lib/fz_http_web.ex b/apps/fz_http/lib/fz_http_web.ex index 2ba99d826..d2dc83785 100644 --- a/apps/fz_http/lib/fz_http_web.ex +++ b/apps/fz_http/lib/fz_http_web.ex @@ -68,7 +68,7 @@ defmodule FzHttpWeb do def live_view_without_layout do quote do - use Phoenix.LiveView, layout: nil + use Phoenix.LiveView import FzHttpWeb.LiveHelpers alias Phoenix.LiveView.JS @@ -131,7 +131,7 @@ defmodule FzHttpWeb do end end - def static_paths, do: ~w(dist fonts images robots.txt) + def static_paths, do: ~w(dist fonts images uploads robots.txt) def verified_routes do quote do diff --git a/apps/fz_http/lib/fz_http_web/auth/html/authentication.ex b/apps/fz_http/lib/fz_http_web/auth/html/authentication.ex index e84e83732..9a911ca9c 100644 --- a/apps/fz_http/lib/fz_http_web/auth/html/authentication.ex +++ b/apps/fz_http/lib/fz_http_web/auth/html/authentication.ex @@ -4,7 +4,7 @@ defmodule FzHttpWeb.Auth.HTML.Authentication do """ use Guardian, otp_app: :fz_http use FzHttpWeb, :controller - alias FzHttp.Configurations + alias FzHttp.Auth alias FzHttp.Telemetry alias FzHttp.Users alias FzHttp.Users.User @@ -66,7 +66,7 @@ defmodule FzHttpWeb.Auth.HTML.Authentication do def sign_out(conn) do with provider_id when not is_nil(provider_id) <- Plug.Conn.get_session(conn, "login_method"), token when not is_nil(token) <- Plug.Conn.get_session(conn, "id_token"), - {:ok, config} <- Configurations.fetch_oidc_provider_config(provider_id), + {:ok, config} <- Auth.fetch_oidc_provider_config(provider_id), {:ok, end_session_uri} <- OpenIDConnect.end_session_uri(config, %{ id_token_hint: token, 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 ec923b55a..5fa444f63 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 @@ -4,7 +4,7 @@ defmodule FzHttpWeb.AuthController do """ use FzHttpWeb, :controller alias FzHttp.Users - alias FzHttp.Configurations + alias FzHttp.Auth alias FzHttpWeb.Auth.HTML.Authentication alias FzHttpWeb.OAuth.PKCE alias FzHttpWeb.OIDC.State @@ -64,7 +64,7 @@ defmodule FzHttpWeb.AuthController do token_params = Map.merge(params, PKCE.token_params(conn)) with :ok <- State.verify_state(conn, state), - {:ok, config} <- Configurations.fetch_oidc_provider_config(provider_id), + {:ok, config} <- Auth.fetch_oidc_provider_config(provider_id), {:ok, tokens} <- OpenIDConnect.fetch_tokens(config, token_params), {:ok, claims} <- OpenIDConnect.verify(config, tokens["id_token"]) do case UserFromAuth.find_or_create(provider_id, claims) do @@ -169,7 +169,7 @@ defmodule FzHttpWeb.AuthController do code_challenge: PKCE.code_challenge(verifier) } - with {:ok, config} <- Configurations.fetch_oidc_provider_config(provider_id), + with {:ok, config} <- Auth.fetch_oidc_provider_config(provider_id), {:ok, uri} <- OpenIDConnect.authorization_uri(config, params) do conn |> PKCE.put_cookie(verifier) @@ -180,7 +180,7 @@ defmodule FzHttpWeb.AuthController do {:error, :not_found} {:error, reason} -> - Logger.error("Can not redirect user to OIDC auth uri", reason: inspect(reason)) + Logger.error("Cannot redirect user to OIDC auth uri", reason: inspect(reason)) conn |> put_flash(:error, "Error while processing OpenID request.") 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 index 2084ed303..9fc9860ec 100644 --- 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 @@ -6,23 +6,22 @@ defmodule FzHttpWeb.JSON.ConfigurationController do Updates here can be applied at runtime with little to no downtime of affected services. """ use FzHttpWeb, :controller - - alias FzHttp.{Configurations.Configuration, Configurations} + alias FzHttp.Config action_fallback(FzHttpWeb.JSON.FallbackController) @doc api_doc: [summary: "Get Configuration"] def show(conn, _params) do - configuration = Configurations.get_configuration!() + configuration = Config.fetch_db_config!() render(conn, "show.json", configuration: configuration) end @doc api_doc: [summary: "Update Configuration"] def update(conn, %{"configuration" => params}) do - configuration = Configurations.get_configuration!() + configuration = Config.fetch_db_config!() - with {:ok, %Configuration{} = configuration} <- - Configurations.update_configuration(configuration, params) do + with {:ok, %Config.Configuration{} = configuration} <- + Config.update_config(configuration, params) do render(conn, "show.json", configuration: configuration) 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 index a671fae56..d328e5f45 100644 --- 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 @@ -13,23 +13,27 @@ defmodule FzHttpWeb.JSON.DeviceController do @doc api_doc: [summary: "List all Devices"] def index(conn, _params) do devices = Devices.list_devices() - render(conn, "index.json", devices: devices) + defaults = Devices.defaults() + render(conn, "index.json", devices: devices, defaults: defaults) end @doc api_doc: [summary: "Create a Device"] def create(conn, %{"device" => device_params}) do with {:ok, device} <- Devices.create_device(device_params) do + defaults = Devices.defaults() + conn |> put_status(:created) |> put_resp_header("location", ~p"/v0/devices/#{device}") - |> render("show.json", device: device) + |> render("show.json", device: device, defaults: defaults) end end @doc api_doc: [summary: "Get Device by ID"] def show(conn, %{"id" => id}) do device = Devices.get_device!(id) - render(conn, "show.json", device: device) + defaults = Devices.defaults() + render(conn, "show.json", device: device, defaults: defaults) end @doc api_doc: [summary: "Update a Device"] @@ -37,7 +41,8 @@ defmodule FzHttpWeb.JSON.DeviceController do device = Devices.get_device!(id) with {:ok, device} <- Devices.update_device(device, device_params) do - render(conn, "show.json", device: device) + defaults = Devices.defaults() + render(conn, "show.json", device: device, defaults: defaults) 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 index b68f1ce54..5c055b1aa 100644 --- 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 @@ -11,6 +11,11 @@ defmodule FzHttpWeb.JSON.UserController do If `auto_create_users` is `false`, then you need to provision users with `password` attribute, otherwise they will have no means to log in. + + ## Disabling users + + Even though API returns `disabled_at` attribute, currently, it's not possible to disable users via API, + since this field is only for internal use by automatic user disabling mechanism on OIDC/SAML errors. """ use FzHttpWeb, :controller alias FzHttp.Users 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 5bf9a99f7..41f40758e 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 @@ -5,12 +5,23 @@ defmodule FzHttpWeb.RootController do use FzHttpWeb, :controller def index(conn, _params) do + %{ + local_auth_enabled: {_, local_auth_enabled}, + openid_connect_providers: {_, openid_connect_providers}, + saml_identity_providers: {_, saml_identity_providers} + } = + FzHttp.Config.fetch_source_and_configs!([ + :local_auth_enabled, + :openid_connect_providers, + :saml_identity_providers + ]) + conn |> render( "auth.html", - 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) + local_enabled: local_auth_enabled, + openid_connect_providers: openid_connect_providers, + saml_identity_providers: saml_identity_providers ) end end diff --git a/apps/fz_http/lib/fz_http_web/error_helpers.ex b/apps/fz_http/lib/fz_http_web/error_helpers.ex index 59876b686..c6391e719 100644 --- a/apps/fz_http/lib/fz_http_web/error_helpers.ex +++ b/apps/fz_http/lib/fz_http_web/error_helpers.ex @@ -7,8 +7,12 @@ defmodule FzHttpWeb.ErrorHelpers do def aggregated_errors(%Ecto.Changeset{} = changeset) do traverse_errors(changeset, fn {msg, opts} -> - Enum.reduce(opts, msg, fn {key, value}, acc -> - String.replace(acc, "%{#{key}}", to_string(value)) + Enum.reduce(opts, msg, fn + {key, {:array, value}}, acc -> + String.replace(acc, "%{#{key}}", to_string(value)) + + {key, value}, acc -> + String.replace(acc, "%{#{key}}", to_string(value)) end) end) |> Enum.reduce("", fn {key, value}, acc -> @@ -23,12 +27,14 @@ defmodule FzHttpWeb.ErrorHelpers do def error_tag(form, field) do values = Keyword.get_values(form.errors, field) - Enum.map(values, fn error -> + values + |> Enum.map(fn error -> content_tag(:span, translate_error(error), class: "help-block" # XXX: data: [phx_error_for: input_id(form, field)] ) end) + |> Enum.intersperse(", ") end @doc """ 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 133208e4e..82ce9dca6 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 @@ -44,16 +44,18 @@ defmodule FzHttpWeb.DeviceLive.Admin.Show do end defp assigns(device) do + defaults = Devices.defaults() + [ device: device, user: Users.fetch_user_by_id!(device.user_id), page_title: device.name, - allowed_ips: Devices.allowed_ips(device), - dns: Devices.dns(device), - endpoint: Devices.endpoint(device), + allowed_ips: Devices.allowed_ips(device, defaults), + dns: Devices.dns(device, defaults), + endpoint: Devices.endpoint(device, defaults), port: FzHttp.Config.fetch_env!(:fz_vpn, :wireguard_port), - mtu: Devices.mtu(device), - persistent_keepalive: Devices.persistent_keepalive(device), + mtu: Devices.mtu(device, defaults), + persistent_keepalive: Devices.persistent_keepalive(device, defaults), config: Devices.as_config(device) ] end diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/new_form_component.ex b/apps/fz_http/lib/fz_http_web/live/device_live/new_form_component.ex index bce96ed69..247d79877 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 @@ -15,46 +15,50 @@ 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) + config = + FzHttp.Config.fetch_source_and_configs!(~w( + default_client_mtu + default_client_endpoint + default_client_persistent_keepalive + default_client_dns + default_client_allowed_ips + )a) + |> Enum.into(%{}, fn {k, {_s, v}} -> {k, v} end) + {:ok, socket |> assign(assigns) |> assign(:changeset, changeset) - |> assign(Map.take(FzHttp.Configurations.get_configuration!(), @default_fields)) - |> assign(Devices.defaults(changeset))} + |> assign(config) + |> assign(Devices.use_default_fields(changeset))} end @impl Phoenix.LiveComponent def handle_event("change", %{"device" => device_params}, socket) do - changeset = Devices.new_device(device_params) + changeset = + device_params + |> Map.update("dns", nil, &binary_to_list/1) + |> Map.update("allowed_ips", nil, &binary_to_list/1) + |> Devices.new_device() {:noreply, socket |> assign(:changeset, changeset) - |> assign(Devices.defaults(changeset))} + |> assign(Devices.use_default_fields(changeset))} end @impl Phoenix.LiveComponent def handle_event("save", %{"device" => device_params}, socket) do - result = - device_params - |> Map.put("user_id", socket.assigns.target_user_id) - |> create_device(socket) - - case result do - :not_authorized -> - {:noreply, not_authorized(socket)} - + device_params + |> Map.put("user_id", socket.assigns.target_user_id) + |> Map.update("dns", nil, &binary_to_list/1) + |> Map.update("allowed_ips", nil, &binary_to_list/1) + |> create_device(socket) + |> case do {:ok, device} -> send_update(FzHttpWeb.ModalComponent, id: :modal, hide_footer_content: true) @@ -63,6 +67,9 @@ defmodule FzHttpWeb.DeviceLive.NewFormComponent do |> assign(:device, device) |> assign(:config, Devices.as_encoded_config(device))} + :not_authorized -> + {:noreply, not_authorized(socket)} + {:error, changeset} -> {:noreply, socket @@ -81,7 +88,7 @@ defmodule FzHttpWeb.DeviceLive.NewFormComponent do defp authorized_to_create?(socket) do has_role?(socket, :admin) || - (FzHttp.Configurations.get!(:allow_unprivileged_device_management) && + (FzHttp.Config.fetch_config!(:allow_unprivileged_device_management) && socket.assigns.current_user.id == socket.assigns.target_user_id) end @@ -96,4 +103,10 @@ defmodule FzHttpWeb.DeviceLive.NewFormComponent do end |> Devices.new_device() end + + defp binary_to_list(binary) when is_binary(binary), + do: binary |> String.trim() |> String.split(",") + + defp binary_to_list(list) when is_list(list), + do: list 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 5891bd191..cd535fd28 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 @@ -105,7 +105,7 @@

- Default: <%= @default_client_allowed_ips %> + Default: <%= Enum.join(@default_client_allowed_ips, ", ") %>

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

@@ -133,7 +134,7 @@

- Default: <%= @default_client_dns %> + Default: <%= Enum.join(@default_client_dns, ", ") %>

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

diff --git a/apps/fz_http/lib/fz_http_web/live/device_live/unprivileged/show_live.ex b/apps/fz_http/lib/fz_http_web/live/device_live/unprivileged/show_live.ex index ca01cadf1..065475b88 100644 --- a/apps/fz_http/lib/fz_http_web/live/device_live/unprivileged/show_live.ex +++ b/apps/fz_http/lib/fz_http_web/live/device_live/unprivileged/show_live.ex @@ -43,7 +43,7 @@ defmodule FzHttpWeb.DeviceLive.Unprivileged.Show do def delete_device(device, socket) do if socket.assigns.current_user.id == device.user_id && (has_role?(socket.assigns.current_user, :admin) || - FzHttp.Configurations.get!(:allow_unprivileged_device_management)) do + FzHttp.Config.fetch_config!(:allow_unprivileged_device_management)) do Devices.delete_device(device) else {:not_authorized} @@ -51,16 +51,18 @@ defmodule FzHttpWeb.DeviceLive.Unprivileged.Show do end defp assigns(device) do + defaults = Devices.defaults() + [ device: device, user: Users.fetch_user_by_id!(device.user_id), page_title: device.name, - allowed_ips: Devices.allowed_ips(device), + allowed_ips: Devices.allowed_ips(device, defaults), port: FzHttp.Config.fetch_env!(:fz_vpn, :wireguard_port), - dns: Devices.dns(device), - endpoint: Devices.endpoint(device), - mtu: Devices.mtu(device), - persistent_keepalive: Devices.persistent_keepalive(device), + dns: Devices.dns(device, defaults), + endpoint: Devices.endpoint(device, defaults), + mtu: Devices.mtu(device, defaults), + persistent_keepalive: Devices.persistent_keepalive(device, defaults), config: Devices.as_config(device) ] end diff --git a/apps/fz_http/lib/fz_http_web/live/hooks/live_auth.ex b/apps/fz_http/lib/fz_http_web/live/hooks/live_auth.ex index 42782120e..a7be2509c 100644 --- a/apps/fz_http/lib/fz_http_web/live/hooks/live_auth.ex +++ b/apps/fz_http/lib/fz_http_web/live/hooks/live_auth.ex @@ -24,7 +24,7 @@ defmodule FzHttpWeb.LiveAuth do %{role: :unprivileged} = user, %{assigns: %{live_action: :new}, view: FzHttpWeb.DeviceLive.Unprivileged.Index} = socket ) do - if FzHttp.Configurations.get!(:allow_unprivileged_device_management) do + if FzHttp.Config.fetch_config!(:allow_unprivileged_device_management) do {:cont, assign_new(socket, :current_user, fn -> user end)} else {:halt, not_authorized(socket)} diff --git a/apps/fz_http/lib/fz_http_web/live/logo_component.ex b/apps/fz_http/lib/fz_http_web/live/logo_component.ex index 8fc98282b..105e5dee2 100644 --- a/apps/fz_http/lib/fz_http_web/live/logo_component.ex +++ b/apps/fz_http/lib/fz_http_web/live/logo_component.ex @@ -3,6 +3,7 @@ defmodule FzHttpWeb.LogoComponent do Logo component displays default, url and data logo """ use FzHttpWeb, :live_component + import FzHttpWeb.Endpoint, only: [static_path: 1] def render(%{url: url} = assigns) when is_binary(url) do ~H""" @@ -10,6 +11,12 @@ defmodule FzHttpWeb.LogoComponent do """ end + def render(%{file: file} = assigns) when is_binary(file) do + ~H""" + @file)} alt="Firezone App Logo" /> + """ + end + def render(%{data: data, type: type} = assigns) when is_binary(data) and is_binary(type) do ~H""" @data} alt="Firezone App Logo" /> 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 index 1808ba4a4..b4b5a6bba 100644 --- 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 @@ -3,30 +3,68 @@ defmodule FzHttpWeb.SettingLive.ClientDefaultsFormComponent do Handles updating client defaults form. """ use FzHttpWeb, :live_component + alias FzHttp.Config - alias FzHttp.Configurations + @configs ~w[ + default_client_allowed_ips + default_client_dns + default_client_endpoint + default_client_persistent_keepalive + default_client_mtu + ]a @impl Phoenix.LiveComponent def update(assigns, socket) do - {:ok, - socket - |> assign(assigns)} + socket = + socket + |> assign(assigns) + |> assign(:configs, FzHttp.Config.fetch_source_and_configs!(@configs)) + + {:ok, socket} end @impl Phoenix.LiveComponent def handle_event("save", %{"configuration" => configuration_params}, socket) do - configuration = Configurations.get_configuration!() + configuration_params = + configuration_params + |> Map.update("default_client_dns", nil, &binary_to_list/1) + |> Map.update("default_client_allowed_ips", nil, &binary_to_list/1) - case Configurations.update_configuration(configuration, configuration_params) do - {:ok, configuration} -> - {:noreply, - socket - |> assign(:changeset, Configurations.change_configuration(configuration))} + configuration = Config.fetch_db_config!() - {:error, changeset} -> - {:noreply, - socket - |> assign(:changeset, changeset)} - end + socket = + case Config.update_config(configuration, configuration_params) do + {:ok, configuration} -> + socket + |> assign(:changeset, Config.change_config(configuration)) + + {:error, changeset} -> + socket + |> assign(:changeset, changeset) + end + + {:noreply, socket} + end + + defp binary_to_list(binary) when is_binary(binary), + do: binary |> String.trim() |> String.split(",") + + defp binary_to_list(list) when is_list(list), + do: list + + def config_has_override?({{source, _source_key}, _key}) do + source not in [:db] + end + + def config_has_override?({_source, _key}) do + false + end + + def config_value({_source, value}) do + value + end + + def config_override_source({{:env, source_key}, _value}) do + "environment variable #{source_key}" end end diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/client_defaults_form_component.html.heex b/apps/fz_http/lib/fz_http_web/live/setting_live/client_defaults_form_component.html.heex index 88d40081e..3842c513f 100644 --- a/apps/fz_http/lib/fz_http_web/live/setting_live/client_defaults_form_component.html.heex +++ b/apps/fz_http/lib/fz_http_web/live/setting_live/client_defaults_form_component.html.heex @@ -1,6 +1,7 @@

<.form :let={f} for={@changeset} id={@id} phx-target={@myself} phx-submit="save">
+ <% default_client_allowed_ips = Map.fetch!(@configs, :default_client_allowed_ips) %> <%= label(f, :default_client_allowed_ips, "Allowed IPs", class: "label") %>
@@ -8,7 +9,14 @@ f, :default_client_allowed_ips, placeholder: "0.0.0.0/0, ::/0", - class: "textarea #{input_error_class(f, :default_client_allowed_ips)}" + class: "textarea #{input_error_class(f, :default_client_allowed_ips)}", + disabled: config_has_override?(default_client_allowed_ips), + value: + if config_has_override?(default_client_allowed_ips) do + "Set in #{config_override_source(default_client_allowed_ips)}: #{Enum.join(config_value(default_client_allowed_ips), ", ")}" + else + list_value(f, :default_client_allowed_ips) + end ) %>
@@ -25,6 +33,7 @@
+ <% default_client_dns = Map.fetch!(@configs, :default_client_dns) %> <%= label(f, :default_client_dns, "DNS Servers", class: "label") %>
@@ -32,7 +41,14 @@ f, :default_client_dns, placeholder: "1.1.1.1, 1.0.0.1", - class: "input #{input_error_class(f, :default_client_dns)}" + class: "input #{input_error_class(f, :default_client_dns)}", + disabled: config_has_override?(default_client_dns), + value: + if config_has_override?(default_client_dns) do + "Set in #{config_override_source(default_client_dns)}: #{Enum.join(config_value(default_client_dns), ", ")}" + else + list_value(f, :default_client_dns) + end ) %>
@@ -47,6 +63,7 @@
+ <% default_client_endpoint = Map.fetch!(@configs, :default_client_endpoint) %> <%= label(f, :default_client_endpoint, "Endpoint", class: "label") %>
@@ -54,7 +71,14 @@ f, :default_client_endpoint, placeholder: "firezone.example.com", - class: "input #{input_error_class(f, :default_client_endpoint)}" + class: "input #{input_error_class(f, :default_client_endpoint)}", + disabled: config_has_override?(default_client_endpoint), + value: + if config_has_override?(default_client_endpoint) do + "Set in #{config_override_source(default_client_endpoint)}: #{config_value(default_client_endpoint)}" + else + input_value(f, :default_client_endpoint) + end ) %>

@@ -67,6 +91,8 @@

+ <% default_client_persistent_keepalive = + Map.fetch!(@configs, :default_client_persistent_keepalive) %> <%= label(f, :default_client_persistent_keepalive, "Persistent Keepalive", class: "label") %>
@@ -74,7 +100,14 @@ f, :default_client_persistent_keepalive, placeholder: "25", - class: "input #{input_error_class(f, :default_client_persistent_keepalive)}" + class: "input #{input_error_class(f, :default_client_persistent_keepalive)}", + disabled: config_has_override?(default_client_persistent_keepalive), + value: + if config_has_override?(default_client_persistent_keepalive) do + "Set in #{config_override_source(default_client_persistent_keepalive)}: #{config_value(default_client_persistent_keepalive)}" + else + input_value(f, :default_client_persistent_keepalive) + end ) %>

<%= error_tag(f, :default_client_persistent_keepalive) %> @@ -87,6 +120,7 @@

+ <% default_client_mtu = Map.fetch!(@configs, :default_client_mtu) %> <%= label(f, :default_client_mtu, "MTU", class: "label") %>
@@ -94,7 +128,14 @@ f, :default_client_mtu, placeholder: "1280", - class: "input #{input_error_class(f, :default_client_mtu)}" + class: "input #{input_error_class(f, :default_client_mtu)}", + disabled: config_has_override?(default_client_mtu), + value: + if config_has_override?(default_client_mtu) do + "Set in #{config_override_source(default_client_mtu)}: #{config_value(default_client_mtu)}" + else + input_value(f, :default_client_mtu) + end ) %>

diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/client_defaults_live.ex b/apps/fz_http/lib/fz_http_web/live/setting_live/client_defaults_live.ex index 7124d8090..43b4e02bf 100644 --- a/apps/fz_http/lib/fz_http_web/live/setting_live/client_defaults_live.ex +++ b/apps/fz_http/lib/fz_http_web/live/setting_live/client_defaults_live.ex @@ -3,22 +3,19 @@ defmodule FzHttpWeb.SettingLive.ClientDefaults do Manages the defaults view. """ use FzHttpWeb, :live_view - - alias FzHttp.Configurations + alias FzHttp.Config @page_title "Client Defaults" @page_subtitle "Configure default values for generating WireGuard client configurations." @impl Phoenix.LiveView def mount(_params, _session, socket) do - {:ok, - socket - |> assign(:changeset, changeset()) - |> assign(:page_subtitle, @page_subtitle) - |> assign(:page_title, @page_title)} - end + socket = + socket + |> assign(:changeset, Config.change_config()) + |> assign(:page_subtitle, @page_subtitle) + |> assign(:page_title, @page_title) - defp changeset do - Configurations.get_configuration!() |> Configurations.change_configuration() + {:ok, socket} 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 2166b2b7d..30ceb9d80 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 @@ -13,25 +13,33 @@ Use a logo at least 300px wide with a 7:2 ratio for best results. GIF, JPEG, PNG, SVG, TIFF, WebP and AVIF images are supported.

+ <%= if has_override?(@logo_source) do %> +

+ The logo was overridden by the LOGO + environment variable; you cannot change it. +

+ <% end %>
-
-
- <%= for type <- FzHttp.Configurations.logo_types do %> - - <% end %> + <%= unless has_override?(@logo_source) do %> +
+
+ <%= for type <- FzHttp.Config.Logo.__types__() do %> + + <% end %> +
-
+ <% end %>
<%= if @logo_type == "Default" do %> @@ -41,44 +49,46 @@ <% end %>
- <%= if @logo_type == "Default" do %> -
- - -
- <% end %> + <%= unless has_override?(@logo_source) do %> + <%= if @logo_type == "Default" do %> +
+ + +
+ <% end %> - <%= if @logo_type == "URL" do %> -
-
-
- + <%= if @logo_type == "URL" do %> + +
+
+ +
+
+ +
-
- -
-
- - <% end %> + + <% end %> - <%= if @logo_type == "Upload" do %> -
- <%= for entry <- @uploads.logo.entries do %> - <%= for err <- upload_errors(@uploads.logo, entry) do %> -

<%= error_to_string(err) %>

+ <%= if @logo_type == "Upload" do %> + + <%= for entry <- @uploads.logo.entries do %> + <%= for err <- upload_errors(@uploads.logo, entry) do %> +

<%= error_to_string(err) %>

+ <% end %> <% end %> - <% end %> - <%= live_file_input(@uploads.logo, class: "button", required: true) %> + <%= live_file_input(@uploads.logo, class: "button", required: true) %> - -
+ + + <% end %> <% end %>
diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/customization_live.ex b/apps/fz_http/lib/fz_http_web/live/setting_live/customization_live.ex index 805e1663b..ffc0fd93c 100644 --- a/apps/fz_http/lib/fz_http_web/live/setting_live/customization_live.ex +++ b/apps/fz_http/lib/fz_http_web/live/setting_live/customization_live.ex @@ -3,6 +3,7 @@ defmodule FzHttpWeb.SettingLive.Customization do Manages the app customizations. """ use FzHttpWeb, :live_view + alias FzHttp.Config @max_logo_size 1024 ** 2 @page_title "Customization" @@ -10,21 +11,27 @@ defmodule FzHttpWeb.SettingLive.Customization do @impl Phoenix.LiveView def mount(_params, _session, socket) do - logo = FzHttp.Configurations.get!(:logo) - logo_type = FzHttp.Configurations.logo_type(logo) + {source, logo} = FzHttp.Config.fetch_source_and_config!(:logo) + logo_type = FzHttp.Config.Logo.type(logo) - {:ok, - socket - |> assign(:page_title, @page_title) - |> assign(:page_subtitle, @page_subtitle) - |> assign(:logo, logo) - |> assign(:logo_type, logo_type) - |> allow_upload(:logo, - accept: ~w(.jpg .jpeg .png .gif .webp .avif .svg .tiff), - max_file_size: @max_logo_size - )} + socket = + socket + |> assign(:page_title, @page_title) + |> assign(:page_subtitle, @page_subtitle) + |> assign(:logo, logo) + |> assign(:logo_source, source) + |> assign(:logo_type, logo_type) + |> allow_upload(:logo, + accept: ~w(.jpg .jpeg .png .gif .webp .avif .svg .tiff), + max_file_size: @max_logo_size + ) + + {:ok, socket} end + def has_override?({source, _source_key}), do: source not in [:db] + def has_override?(_source), do: false + @impl Phoenix.LiveView def handle_event("choose", %{"type" => type}, socket) do {:noreply, assign(socket, :logo_type, type)} @@ -37,15 +44,13 @@ defmodule FzHttpWeb.SettingLive.Customization do @impl Phoenix.LiveView def handle_event("save", %{"default" => "true"}, socket) do - {:ok, config} = FzHttp.Configurations.update_configuration(%{logo: nil}) - + config = Config.put_config!(:logo, nil) {:noreply, assign(socket, :logo, config.logo)} end @impl Phoenix.LiveView def handle_event("save", %{"url" => url}, socket) do - {:ok, config} = FzHttp.Configurations.update_configuration(%{logo: %{"url" => url}}) - + config = Config.put_config!(:logo, %{"url" => url}) {:noreply, assign(socket, :logo, config.logo)} end @@ -56,13 +61,7 @@ defmodule FzHttpWeb.SettingLive.Customization do config = consume_uploaded_entry(socket, entry, fn %{path: path} -> data = path |> File.read!() |> Base.encode64() - - # enforce OK, error from update_configuration instead of consume_uploaded_entry - {:ok, config} = - FzHttp.Configurations.update_configuration(%{ - logo: %{"data" => data, "type" => entry.client_type} - }) - + config = FzHttp.Config.put_config!(:logo, %{"data" => data, "type" => entry.client_type}) {:ok, config} end) diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/oidc_form_component.ex b/apps/fz_http/lib/fz_http_web/live/setting_live/oidc_form_component.ex index cb24d5af0..9a9a62200 100644 --- a/apps/fz_http/lib/fz_http_web/live/setting_live/oidc_form_component.ex +++ b/apps/fz_http/lib/fz_http_web/live/setting_live/oidc_form_component.ex @@ -3,7 +3,6 @@ defmodule FzHttpWeb.SettingLive.OIDCFormComponent do Form for OIDC configs """ use FzHttpWeb, :live_component - alias FzHttp.Configurations def render(assigns) do ~H""" @@ -61,7 +60,9 @@ defmodule FzHttpWeb.SettingLive.OIDCFormComponent do <%= error_tag(f, :scope) %>

- Space-delimited list of OpenID scopes. + Space-delimited list of OpenID scopes. openid + and email + are required in order for Firezone to work.

@@ -73,6 +74,7 @@ defmodule FzHttpWeb.SettingLive.OIDCFormComponent do
<%= text_input(f, :response_type, disabled: true, + placeholder: "code", class: "input #{input_error_class(f, :response_type)}" ) %>
@@ -130,7 +132,11 @@ defmodule FzHttpWeb.SettingLive.OIDCFormComponent do
<%= text_input(f, :redirect_uri, - placeholder: "#{@external_url}/auth/oidc/#{@provider_id || "{CONFIG_ID}"}/callback/", + placeholder: + Path.join( + @external_url, + "auth/oidc/#{input_value(f, :id) || "{CONFIG_ID}"}/callback/" + ), class: "input #{input_error_class(f, :redirect_uri)}" ) %>
@@ -139,7 +145,14 @@ defmodule FzHttpWeb.SettingLive.OIDCFormComponent do

Optionally override the Redirect URI. Must match the redirect URI set in your IdP. - In most cases you shouldn't change this. + In most cases you shouldn't change this. By default + + <%= Path.join( + @external_url, + "auth/oidc/#{input_value(f, :id) || "{CONFIG_ID}"}/callback/" + ) %> + + is used.

@@ -172,10 +185,9 @@ defmodule FzHttpWeb.SettingLive.OIDCFormComponent do def update(assigns, socket) do changeset = - assigns.providers - |> Map.get(assigns.provider_id, %{}) - |> Map.put(:id, assigns.provider_id) - |> Configurations.Configuration.OpenIDConnectProvider.create_changeset() + assigns.provider + |> Map.delete(:__struct__) + |> FzHttp.Config.Configuration.OpenIDConnectProvider.create_changeset() socket = socket @@ -187,28 +199,18 @@ defmodule FzHttpWeb.SettingLive.OIDCFormComponent do end def handle_event("save", %{"open_id_connect_provider" => params}, socket) do - create_changeset = Configurations.Configuration.OpenIDConnectProvider.create_changeset(params) + changeset = FzHttp.Config.Configuration.OpenIDConnectProvider.create_changeset(params) - if create_changeset.valid? do - new_attrs = - create_changeset - |> Ecto.Changeset.apply_changes() - |> Map.from_struct() + if changeset.valid? do + attrs = Ecto.Changeset.apply_changes(changeset) - new_attrs = - socket.assigns.providers - |> Map.get(socket.assigns.provider_id, %{}) - |> Map.merge(new_attrs) + openid_connect_providers = + FzHttp.Config.fetch_config!(:openid_connect_providers) + |> Enum.reject(&(&1.id == socket.assigns.provider.id)) + |> Kernel.++([attrs]) + |> Enum.map(&Map.from_struct/1) - providers = - socket.assigns.providers - |> Map.delete(socket.assigns.provider_id) - |> Map.values() - - {:ok, _changeset} = - FzHttp.Configurations.update_configuration(%{ - openid_connect_providers: providers ++ [new_attrs] - }) + FzHttp.Config.put_config!(:openid_connect_providers, openid_connect_providers) socket = socket @@ -217,7 +219,7 @@ defmodule FzHttpWeb.SettingLive.OIDCFormComponent do {:noreply, socket} else - socket = assign(socket, :changeset, render_changeset_errors(create_changeset)) + socket = assign(socket, :changeset, render_changeset_errors(changeset)) {:noreply, socket} end end diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/saml_form_component.ex b/apps/fz_http/lib/fz_http_web/live/setting_live/saml_form_component.ex index 748237407..a9bdc0b6b 100644 --- a/apps/fz_http/lib/fz_http_web/live/setting_live/saml_form_component.ex +++ b/apps/fz_http/lib/fz_http_web/live/setting_live/saml_form_component.ex @@ -3,7 +3,6 @@ defmodule FzHttpWeb.SettingLive.SAMLFormComponent do Form for SAML configs """ use FzHttpWeb, :live_component - alias FzHttp.Configurations def render(assigns) do ~H""" @@ -194,10 +193,9 @@ defmodule FzHttpWeb.SettingLive.SAMLFormComponent do def update(assigns, socket) do changeset = - assigns.providers - |> Map.get(assigns.provider_id, %{}) - |> Map.merge(%{id: assigns.provider_id}) - |> Configurations.Configuration.SAMLIdentityProvider.create_changeset() + assigns.provider + |> Map.delete(:__struct__) + |> FzHttp.Config.Configuration.SAMLIdentityProvider.create_changeset() socket = socket @@ -208,28 +206,18 @@ defmodule FzHttpWeb.SettingLive.SAMLFormComponent do end def handle_event("save", %{"saml_identity_provider" => params}, socket) do - create_changeset = Configurations.Configuration.SAMLIdentityProvider.create_changeset(params) + changeset = FzHttp.Config.Configuration.SAMLIdentityProvider.create_changeset(params) - if create_changeset.valid? do - new_attrs = - create_changeset - |> Ecto.Changeset.apply_changes() - |> Map.from_struct() + if changeset.valid? do + attrs = Ecto.Changeset.apply_changes(changeset) - new_attrs = - socket.assigns.providers - |> Map.get(socket.assigns.provider_id, %{}) - |> Map.merge(new_attrs) + saml_identity_providers = + FzHttp.Config.fetch_config!(:saml_identity_providers) + |> Enum.reject(&(&1.id == socket.assigns.provider.id)) + |> Kernel.++([attrs]) + |> Enum.map(&Map.from_struct/1) - providers = - socket.assigns.providers - |> Map.delete(socket.assigns.provider_id) - |> Map.values() - - {:ok, _changeset} = - FzHttp.Configurations.update_configuration(%{ - saml_identity_providers: providers ++ [new_attrs] - }) + FzHttp.Config.put_config!(:saml_identity_providers, saml_identity_providers) socket = socket @@ -238,7 +226,10 @@ defmodule FzHttpWeb.SettingLive.SAMLFormComponent do {:noreply, socket} else - socket = assign(socket, :changeset, render_changeset_errors(create_changeset)) + socket = + socket + |> assign(:changeset, render_changeset_errors(changeset)) + {:noreply, socket} end end diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/security.html.heex b/apps/fz_http/lib/fz_http_web/live/setting_live/security.html.heex index d893cce82..3d7160392 100644 --- a/apps/fz_http/lib/fz_http_web/live/setting_live/security.html.heex +++ b/apps/fz_http/lib/fz_http_web/live/setting_live/security.html.heex @@ -1,10 +1,12 @@ +<% openid_connect_providers = Map.fetch!(@configs, :openid_connect_providers) %> +<% saml_identity_providers = Map.fetch!(@configs, :saml_identity_providers) %> + <%= if @live_action == :edit_oidc do %> <%= live_modal( FzHttpWeb.SettingLive.OIDCFormComponent, return_to: ~p"/settings/security", title: "OIDC Configuration", - providers: @oidc_configs, - provider_id: @id, + provider: get_provider(config_value(openid_connect_providers), @id) || %{id: @id}, id: "oidc-form-component", form: "oidc-form" ) %> @@ -15,8 +17,7 @@ FzHttpWeb.SettingLive.SAMLFormComponent, return_to: ~p"/settings/security", title: "SAML Configuration", - providers: @saml_configs, - provider_id: @id, + provider: get_provider(config_value(saml_identity_providers), @id) || %{id: @id}, id: "saml-form-component", form: "saml-form" ) %> @@ -33,6 +34,7 @@

Authentication

+ <% vpn_session_duration = Map.fetch!(@configs, :vpn_session_duration) %> <.form :let={f} for={@configuration_changeset} @@ -46,31 +48,48 @@

- <%= select(f, :vpn_session_duration, @session_duration_options, class: "input") %> + <%= select(f, :vpn_session_duration, session_duration_options(vpn_session_duration), + class: "input", + disabled: config_has_override?(vpn_session_duration), + selected: config_value(vpn_session_duration) + ) %>

<%= submit("Save", - disabled: !@form_changed, + disabled: !(@form_changed and !config_has_override?(vpn_session_duration)), phx_disable_with: "Saving...", class: "button is-primary" ) %>

- Optionally require users to periodically authenticate to the Firezone - web UI in order to keep their VPN sessions active. + <%= if config_has_override?(vpn_session_duration) do %> + This field was overridden using <%= config_override_source(vpn_session_duration) %>; you cannot change it. + <% else %> + Optionally require users to periodically authenticate to the Firezone + web UI in order to keep their VPN sessions active. + <% end %>

-
+
+ <% local_auth_enabled = Map.fetch!(@configs, :local_auth_enabled) %> Local Auth
-

Enable or disable authentication with email and password.

+

+ Enable or disable authentication with email and password. + <%= if config_has_override?(local_auth_enabled) do %> +
+ + This value is overridden using <%= config_override_source(local_auth_enabled) %>; you cannot change it. + + <% end %> +

@@ -88,12 +108,24 @@
-
+
+ <% allow_unprivileged_device_management = + Map.fetch!(@configs, :allow_unprivileged_device_management) %> Allow unprivileged device management
-

Enable or disable management of devices on unprivileged accounts.

+

+ Enable or disable management of devices on unprivileged accounts. + <%= if config_has_override?(allow_unprivileged_device_management) do %> +
+ + This value is overridden using <%= config_override_source( + allow_unprivileged_device_management + ) %>; you cannot change it. + + <% end %> +

@@ -113,13 +144,23 @@
-
+
+ <% allow_unprivileged_device_configuration = + Map.fetch!(@configs, :allow_unprivileged_device_configuration) %> Allow unprivileged device configuration

Enable or disable configuration of device network settings for unprivileged users. + <%= if config_has_override?(allow_unprivileged_device_configuration) do %> +
+ + This value is overridden using <%= config_override_source( + allow_unprivileged_device_configuration + ) %>; you cannot change it. + + <% end %>

@@ -129,10 +170,9 @@ phx-click="toggle" name="allow_unprivileged_device_configuration" phx-value-config="allow_unprivileged_device_configuration" - checked={FzHttp.Configurations.get!(:allow_unprivileged_device_configuration)} - value={ - if(!FzHttp.Configurations.get!(:allow_unprivileged_device_configuration), do: "on") - } + disabled={config_has_override?(allow_unprivileged_device_configuration)} + checked={config_value(allow_unprivileged_device_configuration)} + value={config_toggle_status(allow_unprivileged_device_configuration)} /> @@ -150,12 +190,19 @@

-
+
+ <% disable_vpn_on_oidc_error = Map.fetch!(@configs, :disable_vpn_on_oidc_error) %> Auto disable VPN

Enable or disable auto disabling VPN connection on OIDC refresh error.

+ <%= if config_has_override?(disable_vpn_on_oidc_error) do %> +
+ + This value is overridden using <%= config_override_source(disable_vpn_on_oidc_error) %>; you cannot change it. + + <% end %>
@@ -175,6 +223,16 @@ + <%= if config_has_override?(openid_connect_providers) do %> + +

+ You cannot add new change providers because this value is overridden using <%= config_override_source( + openid_connect_providers + ) %>. +

+
+ <% end %> + @@ -183,85 +241,109 @@ - + <%= unless config_has_override?(openid_connect_providers) do %> + + <% end %> - <%= for {k, v} <- @oidc_configs do %> + <%= for provider <- config_value(openid_connect_providers) do %> - - - - - - + + + + + <%= unless config_has_override?(openid_connect_providers) do %> + + Edit + <% end %> + + + <% end %> <% end %>
Client ID Discovery URI Scope
<%= k %><%= v.label %><%= v.client_id %><%= v.discovery_document_uri %><%= v.scope %> - <%= live_patch(to: ~p"/settings/security/oidc/#{k}/edit", + <%= provider.id %><%= provider.label %><%= provider.client_id %><%= provider.discovery_document_uri %><%= provider.scope %> + <%= live_patch(to: ~p"/settings/security/oidc/#{provider.id}/edit", class: "button") do %> - Edit - <% end %> - -
- <%= live_patch( + <%= unless config_has_override?(openid_connect_providers) do %> + <%= live_patch( to: ~p"/settings/security/oidc/#{rand_string(8)}/edit", class: "button mb-4") do %> - Add OpenID Connect Provider + Add OpenID Connect Provider + <% end %> <% end %> + <%= if config_has_override?(saml_identity_providers) do %> +

+ + You cannot add new change providers because this value is overridden using <%= config_override_source( + saml_identity_providers + ) %>. + +

+ <% end %> + - + <%= unless config_has_override?(saml_identity_providers) do %> + + <% end %> - <%= for {k, v} <- @saml_configs do %> + <%= for provider <- config_value(saml_identity_providers) do %> - - + + - + Edit + <% end %> + + + <% end %> <% end %>
Config ID label Metadata
<%= k %><%= v.label %><%= provider.id %><%= provider.label %> -
<%= v.metadata %>
+
<%= provider.metadata %>
- <%= live_patch(to: ~p"/settings/security/saml/#{k}/edit", + <%= unless config_has_override?(saml_identity_providers) do %> + + <%= live_patch(to: ~p"/settings/security/saml/#{provider.id}/edit", class: "button") do %> - Edit - <% end %> - -
- <%= live_patch( + <%= unless config_has_override?(saml_identity_providers) do %> + <%= live_patch( to: ~p"/settings/security/saml/#{rand_string(8)}/edit", class: "button mb-4") do %> - Add SAML Identity Provider + Add SAML Identity Provider + <% end %> <% end %> diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/security_live.ex b/apps/fz_http/lib/fz_http_web/live/setting_live/security_live.ex index 9f692b643..75a4acdbe 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 @@ -3,38 +3,36 @@ defmodule FzHttpWeb.SettingLive.Security do Manages security LiveView """ use FzHttpWeb, :live_view - - import Ecto.Changeset import FzCommon.FzCrypto, only: [rand_string: 1] - - alias FzHttp.Configurations + alias FzHttp.Config @page_title "Security Settings" @page_subtitle "Configure security-related settings." + @hour 3_600 + @day 24 * @hour + + @configs ~w[ + local_auth_enabled + disable_vpn_on_oidc_error + allow_unprivileged_device_management + allow_unprivileged_device_configuration + vpn_session_duration + openid_connect_providers + saml_identity_providers + ]a + @impl Phoenix.LiveView def mount(_params, _session, socket) do - config_changeset = Configurations.change_configuration() + socket = + socket + |> assign(:page_title, @page_title) + |> assign(:page_subtitle, @page_subtitle) + |> assign(:form_changed, false) + |> assign(:configuration_changeset, configuration_changeset()) + |> assign(:configs, FzHttp.Config.fetch_source_and_configs!(@configs)) - {:ok, - socket - |> assign(:form_changed, false) - |> assign(:session_duration_options, session_duration_options()) - |> assign(:configuration_changeset, configuration_changeset()) - |> assign(:config_changeset, config_changeset) - |> 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)} + {:ok, socket} end @impl Phoenix.LiveView @@ -43,104 +41,98 @@ defmodule FzHttpWeb.SettingLive.Security do end @impl Phoenix.LiveView - def handle_event("change", _params, socket) do - {:noreply, - socket - |> assign(:form_changed, true)} + def handle_event("change", %{"configuration" => attrs}, socket) do + changeset = configuration_changeset(attrs) + {:noreply, assign(socket, :form_changed, changeset.changes != %{})} end @impl Phoenix.LiveView - def handle_event( - "save_configuration", - %{"configuration" => %{"vpn_session_duration" => vpn_session_duration}}, - socket - ) do - configuration = Configurations.get_configuration!() + def handle_event("save_configuration", %{"configuration" => attrs}, socket) do + configuration = Config.fetch_db_config!() - case Configurations.update_configuration(configuration, %{ - vpn_session_duration: vpn_session_duration - }) do - {:ok, configuration} -> - {:noreply, - socket - |> assign(:form_changed, false) - |> assign(:configuration_changeset, Configurations.change_configuration(configuration))} + socket = + case Config.update_config(configuration, attrs) do + {:ok, configuration} -> + socket + |> assign(:form_changed, false) + |> assign(:configuration_changeset, Config.change_config(configuration)) - {:error, configuration_changeset} -> - {:noreply, - socket - |> assign(:configuration_changeset, configuration_changeset)} - end - end + {:error, configuration_changeset} -> + socket + |> assign(:configuration_changeset, configuration_changeset) + end - @impl Phoenix.LiveView - def handle_event("toggle", %{"config" => config} = params, socket) do - toggle_value = !!params["value"] - {:ok, _conf} = Configurations.update_configuration(%{config => toggle_value}) {:noreply, socket} end - @types %{"oidc" => :openid_connect_providers, "saml" => :saml_identity_providers} + @impl Phoenix.LiveView + def handle_event("toggle", %{"config" => key} = params, socket) do + Config.put_config!(key, !!params["value"]) + configs = FzHttp.Config.fetch_source_and_configs!(@configs) + {:noreply, assign(socket, :configs, configs)} + end @impl Phoenix.LiveView def handle_event("delete", %{"type" => type, "key" => key}, socket) do - field_key = Map.fetch!(@types, type) + field_key = String.to_existing_atom(type) providers = - socket.assigns.config_changeset.data + Config.fetch_db_config!() |> Map.fetch!(field_key) |> Enum.reject(&(&1.id == key)) |> Enum.map(&Map.from_struct/1) - {:ok, configuration} = Configurations.update_configuration(%{field_key => providers}) + Config.put_config!(field_key, providers) + configs = FzHttp.Config.fetch_source_and_configs!(@configs) - socket = - socket - |> assign(:config_changeset, Configurations.change_configuration(configuration)) - |> assign(:oidc_configs, map_providers(configuration.openid_connect_providers)) - |> assign(:saml_configs, map_providers(configuration.saml_identity_providers)) - - {:noreply, socket} + {:noreply, assign(socket, :configs, configs)} end - @hour 3_600 - @day 24 * @hour + def config_has_override?({{source, _source_key}, _key}), do: source not in [:db] + def config_has_override?({_source, _key}), do: false - def session_duration_options do - [ - Never: 0, - Once: FzHttp.Configurations.Configuration.max_vpn_session_duration(), - "Every Hour": @hour, - "Every Day": @day, - "Every Week": 7 * @day, - "Every 30 Days": 30 * @day, - "Every 90 Days": 90 * @day + def config_value({_source, value}) do + value + end + + def get_provider(providers, id) do + Enum.find(providers, &(&1.id == id)) + end + + def config_toggle_status({_source, value}) do + if(!value, do: "on") + end + + def config_override_source({{:env, source_key}, _value}) do + "environment variable #{source_key}" + end + + def session_duration_options(vpn_session_duration) do + options = [ + {"Never", 0}, + {"Once", FzHttp.Config.Configuration.Changeset.max_vpn_session_duration()}, + {"Every Hour", @hour}, + {"Every Day", @day}, + {"Every Week", 7 * @day}, + {"Every 30 Days", 30 * @day}, + {"Every 90 Days", 90 * @day} ] + + values = Enum.map(options, fn {_, value} -> value end) + + if config_value(vpn_session_duration) in values do + options + else + options ++ + [ + {"Every #{config_value(vpn_session_duration)} seconds", + config_value(vpn_session_duration)} + ] + end end - defp configuration_changeset do - Configurations.get_configuration!() - |> Configurations.change_configuration() - end - - @fields ~w( - local_auth_enabled - disable_vpn_on_oidc_error - allow_unprivileged_device_management - allow_unprivileged_device_configuration - openid_connect_providers - )a - @override_title """ - This value is currently overriding the value set in your configuration file. - """ - defp field_titles(changeset) do - @fields - |> Map.new(fn key -> - if is_nil(get_field(changeset, key)) do - {key, ""} - else - {key, @override_title} - end - end) + defp configuration_changeset(attrs \\ %{}) do + Config.fetch_db_config!() + |> Config.change_config(attrs) end end 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 index a01e9f8f9..33b5eeb4b 100644 --- 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 @@ -49,7 +49,7 @@ defmodule FzHttpWeb.SettingLive.ShowApiTokenComponent do
# List all users
     curl -H 'Content-Type: application/json' \
          -H 'Authorization: Bearer <%= @secret %>' \
-         <%= Application.fetch_env!(:fz_http, :external_url) %>/v0/users
+ <%= FzHttp.Config.fetch_env!(:fz_http, :external_url) %>/v0/users
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 d3a7121ef..b4aaf4d05 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 @@ -23,7 +23,7 @@ defmodule FzHttpWeb.SettingLive.Unprivileged.Account do socket = socket - |> assign(:local_auth_enabled, FzHttp.Configurations.get!(:local_auth_enabled)) + |> assign(:local_auth_enabled, FzHttp.Config.fetch_config!(:local_auth_enabled)) |> assign(:changeset, Users.change_user(socket.assigns.current_user)) |> assign(:methods, methods) |> assign(:page_title, @page_title) diff --git a/apps/fz_http/lib/fz_http_web/live_helpers.ex b/apps/fz_http/lib/fz_http_web/live_helpers.ex index 3c9a31e1d..c1bb6a55e 100644 --- a/apps/fz_http/lib/fz_http_web/live_helpers.ex +++ b/apps/fz_http/lib/fz_http_web/live_helpers.ex @@ -5,7 +5,7 @@ defmodule FzHttpWeb.LiveHelpers do https://bernheisel.com/blog/phoenix-liveview-and-views """ use Phoenix.Component - alias FzHttp.{Configurations, Users} + alias FzHttp.{Config, Users} def live_modal(component, opts) do path = Keyword.fetch!(opts, :return_to) @@ -40,7 +40,7 @@ defmodule FzHttpWeb.LiveHelpers do end def vpn_sessions_expire? do - Configurations.vpn_sessions_expire?() + Config.vpn_sessions_expire?() end def vpn_expires_at(user) do @@ -71,4 +71,11 @@ defmodule FzHttpWeb.LiveHelpers do def render_changeset_errors(%Ecto.Changeset{} = changeset) do %{changeset | action: :validate} end + + def list_value(form, field) do + case Phoenix.HTML.Form.input_value(form, field) do + value when is_list(value) -> Enum.join(value, ", ") + value -> value + end + end end diff --git a/apps/fz_http/lib/fz_http_web/mailer.ex b/apps/fz_http/lib/fz_http_web/mailer.ex index 0bfaff3f6..d5ec94620 100644 --- a/apps/fz_http/lib/fz_http_web/mailer.ex +++ b/apps/fz_http/lib/fz_http_web/mailer.ex @@ -2,19 +2,8 @@ defmodule FzHttpWeb.Mailer do @moduledoc """ Outbound Email Sender. """ - use Swoosh.Mailer, otp_app: :fz_http - - alias Swoosh.{Adapters, Email} - - @provider_mapping %{ - "smtp" => Adapters.SMTP, - "mailgun" => Adapters.Mailgun, - "mandrill" => Adapters.Mandrill, - "sendgrid" => Adapters.Sendgrid, - "post_mark" => Adapters.Postmark, - "sendmail" => Adapters.Sendmail - } + alias Swoosh.Email def active? do mailer_config = FzHttp.Config.fetch_env!(:fz_http, FzHttpWeb.Mailer) @@ -30,15 +19,4 @@ defmodule FzHttpWeb.Mailer do Email.new() |> Email.from(from_email) end - - def from_configuration(%FzHttp.Configurations.Mailer{} = mailer) do - from_email = mailer.from - config = Map.fetch!(mailer.configs, mailer.provider) - adapter = Map.fetch!(@provider_mapping, mailer.provider) - - [ - from_email: from_email, - adapter: adapter - ] ++ Enum.map(config, fn {k, v} -> {String.to_atom(k), v} end) - end end diff --git a/apps/fz_http/lib/fz_http_web/plug/require_local_authentication.ex b/apps/fz_http/lib/fz_http_web/plug/require_local_authentication.ex index 22cf31087..dd86530b1 100644 --- a/apps/fz_http/lib/fz_http_web/plug/require_local_authentication.ex +++ b/apps/fz_http/lib/fz_http_web/plug/require_local_authentication.ex @@ -4,7 +4,7 @@ defmodule FzHttpWeb.Plug.RequireLocalAuthentication do def init(opts), do: opts def call(conn, _opts) do - if FzHttp.Configurations.get!(:local_auth_enabled) do + if FzHttp.Config.fetch_config!(:local_auth_enabled) do conn else conn diff --git a/apps/fz_http/lib/fz_http_web/proxy_headers.ex b/apps/fz_http/lib/fz_http_web/proxy_headers.ex index d9b5cc0bf..350237c69 100644 --- a/apps/fz_http/lib/fz_http_web/proxy_headers.ex +++ b/apps/fz_http/lib/fz_http_web/proxy_headers.ex @@ -3,9 +3,8 @@ defmodule FzHttpWeb.ProxyHeaders do Loads proxy-related headers when it corresponds using runtime config """ alias FzHttpWeb.HeaderHelpers - @behaviour Plug - require Logger + @behaviour Plug def init(opts), do: opts diff --git a/apps/fz_http/lib/fz_http_web/templates/layout/root.html.heex b/apps/fz_http/lib/fz_http_web/templates/layout/root.html.heex index 676ca130c..544ea5446 100644 --- a/apps/fz_http/lib/fz_http_web/templates/layout/root.html.heex +++ b/apps/fz_http/lib/fz_http_web/templates/layout/root.html.heex @@ -20,9 +20,7 @@
- <%= FzHttpWeb.LogoComponent.render( - FzHttp.Configurations.get_configuration!().logo - ) %> + <%= FzHttpWeb.LogoComponent.render(FzHttp.Config.fetch_config!(:logo)) %>
<%= @inner_content %> diff --git a/apps/fz_http/lib/fz_http_web/templates/layout/unprivileged.html.heex b/apps/fz_http/lib/fz_http_web/templates/layout/unprivileged.html.heex index ede74884d..c1464527b 100644 --- a/apps/fz_http/lib/fz_http_web/templates/layout/unprivileged.html.heex +++ b/apps/fz_http/lib/fz_http_web/templates/layout/unprivileged.html.heex @@ -26,9 +26,7 @@
- <%= FzHttpWeb.LogoComponent.render( - FzHttp.Configurations.get_configuration!().logo - ) %> + <%= FzHttpWeb.LogoComponent.render(FzHttp.Config.fetch_config!(:logo)) %>
diff --git a/apps/fz_http/lib/fz_http_web/templates/shared/device_details.html.heex b/apps/fz_http/lib/fz_http_web/templates/shared/device_details.html.heex index 007509911..da775ba17 100644 --- a/apps/fz_http/lib/fz_http_web/templates/shared/device_details.html.heex +++ b/apps/fz_http/lib/fz_http_web/templates/shared/device_details.html.heex @@ -51,22 +51,22 @@ Received - <%= FzCommon.FzInteger.to_human_bytes(@device.rx_bytes) %> + <%= to_human_bytes(@device.rx_bytes) %> Sent - <%= FzCommon.FzInteger.to_human_bytes(@device.tx_bytes) %> + <%= to_human_bytes(@device.tx_bytes) %> Allowed IPs - <%= @allowed_ips || "None" %> + <%= list_to_string(@allowed_ips) || "None" %> DNS Servers - <%= @dns || "None" %> + <%= list_to_string(@dns) || "None" %> 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 feecc6b50..e0511de72 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 @@ -43,8 +43,8 @@ … - <%= FzCommon.FzInteger.to_human_bytes(device.rx_bytes) %> received
- <%= FzCommon.FzInteger.to_human_bytes(device.tx_bytes) %> sent + <%= to_human_bytes(device.rx_bytes) %> received
+ <%= to_human_bytes(device.tx_bytes) %> sent <%= device.public_key %> nil + binary -> binary + end + end + + def to_human_bytes(nil), do: to_human_bytes(0) + + def to_human_bytes(bytes) when is_integer(bytes) do + FileSize.from_bytes(bytes, scale: :iec) + |> FileSize.format(@byte_size_opts) + end end diff --git a/apps/fz_http/mix.exs b/apps/fz_http/mix.exs index af4320ac2..f3f5c00f5 100644 --- a/apps/fz_http/mix.exs +++ b/apps/fz_http/mix.exs @@ -36,13 +36,12 @@ defmodule FzHttp.MixProject do extra_applications: [ :logger, :runtime_tools - ], - registered: [:fz_http_server] + ] ] end # Specifies which paths to compile per environment. - defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(:test), do: ["test/support", "lib"] defp elixirc_paths(_), do: ["lib"] defp deps do @@ -62,13 +61,12 @@ defmodule FzHttp.MixProject do {:phoenix_live_reload, "~> 1.3", only: :dev}, {:phoenix_swoosh, "~> 1.0"}, {:gettext, "~> 0.18"}, + {:file_size, "~> 3.0.1"}, # Ecto-related deps {:postgrex, "~> 0.16"}, {:decimal, "~> 2.0"}, {:ecto_sql, "~> 3.7"}, - {:ecto_network, - github: "firezone/ecto_network", ref: "7dfe65bcb6506fb0ed6050871b433f3f8b1c10cb"}, {:cloak, "~> 1.1"}, {:cloak_ecto, "~> 1.2"}, @@ -104,6 +102,7 @@ defmodule FzHttp.MixProject do defp aliases do [ + "assets.build": ["cmd cd assets && yarn install --frozen-lockfile && node esbuild.js prod"], "ecto.seed": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], "ecto.setup": ["ecto.create", "ecto.migrate"], "ecto.reset": ["ecto.drop", "ecto.setup"], 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 index 21269f861..37f67392d 100644 --- 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 @@ -1,19 +1,16 @@ 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")) + local_auth_enabled = 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")) + disable_vpn_on_oidc_error = 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")) + 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")) + to_boolean(System.get_env("ALLOW_UNPRIVILEGED_DEVICE_CONFIGURATION", "true")) execute(""" UPDATE configurations @@ -53,4 +50,20 @@ defmodule FzHttp.Repo.Migrations.MoveCacheFallbacksToConfigurations do modify(:saml_identity_providers, :map, default: %{}, null: false) end end + + def to_boolean(str) when is_binary(str) do + as_bool(String.downcase(str)) + end + + defp as_bool("true") do + true + end + + defp as_bool("false") do + false + end + + defp as_bool(unknown) do + raise "Unknown boolean: string #{unknown} not one of ['true', 'false']." + end end diff --git a/apps/fz_http/priv/repo/migrations/20230206172556_change_dns_and_allowed_ips_to_inet_array.exs b/apps/fz_http/priv/repo/migrations/20230206172556_change_dns_and_allowed_ips_to_inet_array.exs new file mode 100644 index 000000000..92cda0481 --- /dev/null +++ b/apps/fz_http/priv/repo/migrations/20230206172556_change_dns_and_allowed_ips_to_inet_array.exs @@ -0,0 +1,46 @@ +defmodule FzHttp.Repo.Migrations.ChangeDnsAndAllowedIpsToInetArray do + use Ecto.Migration + + def change do + rename(table(:configurations), :default_client_dns, to: :default_client_dns_string) + + rename(table(:configurations), :default_client_allowed_ips, + to: :default_client_allowed_ips_string + ) + + rename(table(:devices), :dns, to: :dns_string) + rename(table(:devices), :allowed_ips, to: :allowed_ips_string) + + alter table(:configurations) do + add(:default_client_dns, {:array, :string}, default: []) + add(:default_client_allowed_ips, {:array, :inet}, default: []) + end + + alter table(:devices) do + add(:dns, {:array, :string}, default: []) + add(:allowed_ips, {:array, :inet}, default: []) + end + + execute(""" + UPDATE configurations + SET default_client_dns = string_to_array(default_client_dns_string, ','), + default_client_allowed_ips = string_to_array(default_client_allowed_ips_string, ',')::inet[] + """) + + execute(""" + UPDATE devices + SET dns = string_to_array(dns_string, ','), + allowed_ips = string_to_array(allowed_ips_string, ',')::inet[] + """) + + alter table(:configurations) do + remove(:default_client_dns_string, :string) + remove(:default_client_allowed_ips_string, :string) + end + + alter table(:devices) do + remove(:dns_string, :string) + remove(:allowed_ips_string, :string) + end + end +end diff --git a/apps/fz_http/priv/repo/seeds.exs b/apps/fz_http/priv/repo/seeds.exs index 251715a08..32e829c8c 100644 --- a/apps/fz_http/priv/repo/seeds.exs +++ b/apps/fz_http/priv/repo/seeds.exs @@ -32,8 +32,12 @@ alias FzHttp.{ preshared_key: "27eCDMVRVFfMVS5Rfnn9n7as4M6MemGY/oghmdrwX2E=", public_key: "4Fo+SBnDJ6hi8qzPt3nWLwgjCVwvpjHL35qJeatKwEc=", remote_ip: %Postgrex.INET{address: {127, 5, 0, 1}}, - dns: "8.8.8.8,8.8.4.4", - allowed_ips: "0.0.0.0/0,::/0,1.1.1.1", + dns: ["8.8.8.8", "8.8.4.4"], + allowed_ips: [ + %Postgrex.INET{address: {0, 0, 0, 0}, netmask: 0}, + %Postgrex.INET{address: {0, 0, 0, 0, 0, 0, 0, 0}, netmask: 0}, + %Postgrex.INET{address: {1, 1, 1, 1}} + ], use_default_allowed_ips: false, use_default_dns: false, rx_bytes: 123_917_823, @@ -81,9 +85,12 @@ MFA.create_method( public_key: "pSLWbPiQ2mKh26IG1dMFQQWuAstFJXV91dNk+olzEjA=", mtu: 1280, persistent_keepalive: 25, - allowed_ips: "0.0.0.0,::/0", + allowed_ips: [ + %Postgrex.INET{address: {0, 0, 0, 0}, netmask: 0}, + %Postgrex.INET{address: {0, 0, 0, 0, 0, 0, 0, 0}, netmask: 0} + ], endpoint: "elixir", - dns: "127.0.0.11", + dns: ["127.0.0.11"], use_default_allowed_ips: false, use_default_dns: false, use_default_endpoint: false, @@ -208,10 +215,18 @@ Rules.create_rule(%{ destination: "1.2.3.4" }) -FzHttp.Configurations.put!(:default_client_dns, "4.3.2.1,1.2.3.4") -FzHttp.Configurations.put!(:default_client_allowed_ips, "10.0.0.1/20,::/0,1.1.1.1") +FzHttp.Config.put_config!(:default_client_dns, ["4.3.2.1", "1.2.3.4"]) -FzHttp.Configurations.put!( +FzHttp.Config.put_config!( + :default_client_allowed_ips, + [ + %Postgrex.INET{address: {10, 0, 0, 1}, netmask: 20}, + %Postgrex.INET{address: {0, 0, 0, 0, 0, 0, 0, 0}, netmask: 0}, + %Postgrex.INET{address: {1, 1, 1, 1}} + ] +) + +FzHttp.Config.put_config!( :openid_connect_providers, [ %{ diff --git a/apps/fz_http/test/fz_http/api_tokens_test.exs b/apps/fz_http/test/fz_http/api_tokens_test.exs index f39548755..2b5e560f1 100644 --- a/apps/fz_http/test/fz_http/api_tokens_test.exs +++ b/apps/fz_http/test/fz_http/api_tokens_test.exs @@ -1,5 +1,5 @@ defmodule FzHttp.ApiTokensTest do - use FzHttp.DataCase + use FzHttp.DataCase, async: true alias FzHttp.ApiTokensFixtures alias FzHttp.UsersFixtures alias FzHttp.ApiTokens diff --git a/apps/fz_http/test/fz_http/auth_test.exs b/apps/fz_http/test/fz_http/auth_test.exs new file mode 100644 index 000000000..86a8f2a5f --- /dev/null +++ b/apps/fz_http/test/fz_http/auth_test.exs @@ -0,0 +1,83 @@ +defmodule FzHttp.AuthTest do + use FzHttp.DataCase + import FzHttp.Auth + alias FzHttp.ConfigFixtures + + describe "fetch_oidc_provider_config/1" do + test "returns error when provider does not exist" do + assert fetch_oidc_provider_config(Ecto.UUID.generate()) == {:error, :not_found} + assert fetch_oidc_provider_config("foo") == {:error, :not_found} + end + + test "returns openid connect provider" do + {_bypass, [attrs]} = ConfigFixtures.start_openid_providers(["google"]) + + assert fetch_oidc_provider_config(attrs["id"]) == + {:ok, + %{ + client_id: attrs["client_id"], + client_secret: attrs["client_secret"], + discovery_document_uri: attrs["discovery_document_uri"], + redirect_uri: attrs["redirect_uri"], + response_type: attrs["response_type"], + scope: attrs["scope"] + }} + end + + test "puts default redirect_uri" do + FzHttp.Config.put_env_override(:external_url, "http://foo.bar.com/") + + {_bypass, [attrs]} = + ConfigFixtures.start_openid_providers(["google"], %{"redirect_uri" => nil}) + + assert fetch_oidc_provider_config(attrs["id"]) == + {:ok, + %{ + client_id: attrs["client_id"], + client_secret: attrs["client_secret"], + discovery_document_uri: attrs["discovery_document_uri"], + redirect_uri: "http://foo.bar.com//auth/oidc/google/callback/", + response_type: attrs["response_type"], + scope: attrs["scope"] + }} + end + end + + describe "auto_create_users?/2" do + test "raises if provider_id not found" do + assert_raise(RuntimeError, "Unknown provider foobar", fn -> + auto_create_users?(:openid_connect_providers, "foobar") + end) + end + + test "returns true if auto_create_users is true" do + ConfigFixtures.configuration(%{ + saml_identity_providers: [ + %{ + "id" => "test", + "metadata" => ConfigFixtures.saml_metadata(), + "auto_create_users" => true, + "label" => "SAML" + } + ] + }) + + assert auto_create_users?(:saml_identity_providers, "test") + end + + test "returns false if auto_create_users is false" do + ConfigFixtures.configuration(%{ + saml_identity_providers: [ + %{ + "id" => "test", + "metadata" => ConfigFixtures.saml_metadata(), + "auto_create_users" => false, + "label" => "SAML" + } + ] + }) + + refute auto_create_users?(:saml_identity_providers, "test") + end + end +end diff --git a/apps/fz_http/test/fz_http/config/caster_test.exs b/apps/fz_http/test/fz_http/config/caster_test.exs new file mode 100644 index 000000000..27c731d69 --- /dev/null +++ b/apps/fz_http/test/fz_http/config/caster_test.exs @@ -0,0 +1,59 @@ +defmodule FzHttp.Config.CasterTest do + use ExUnit.Case, async: true + import FzHttp.Config.Caster + + describe "cast/2" do + test "casts a binary to an array of integers" do + assert cast("1,2,3", {:array, ",", :integer}) == {:ok, [1, 2, 3]} + end + + test "casts a binary to an embed" do + assert cast(~s|{"foo": "bar"}|, :embed) == {:ok, %{"foo" => "bar"}} + end + + test "casts a binary to an array of embeds" do + assert cast(~s|[{"foo": "bar"}]|, {:json_array, :embed}) == {:ok, [%{"foo" => "bar"}]} + end + + test "casts a binary to a map" do + assert cast(~s|{"foo": "bar"}|, :map) == {:ok, %{"foo" => "bar"}} + end + + test "casts a binary to boolean" do + assert cast("true", :boolean) == {:ok, true} + assert cast("false", :boolean) == {:ok, false} + assert cast("", :boolean) == {:ok, nil} + end + + test "casts a binary to integer" do + assert cast("1", :integer) == {:ok, 1} + assert cast("12345", :integer) == {:ok, 12_345} + end + + test "keeps original non-binary value even if doesn't match the type" do + assert cast(1, :integer) == {:ok, 1} + assert cast(1, :boolean) == {:ok, 1} + assert cast(1, {:array, ",", :integer}) == {:ok, 1} + assert cast(1, :embed) == {:ok, 1} + assert cast(1, {:json_array, :embed}) == {:ok, 1} + assert cast(1, :map) == {:ok, 1} + end + + test "raises when integer is not valid" do + assert cast("invalid integer", :integer) == {:error, "cannot be cast to an integer"} + + assert cast("123invalid integer", :integer) == + {:error, + "cannot be cast to an integer, " <> + "got a reminder invalid integer after an integer value 123"} + end + + test "raises when JSON is not valid" do + assert cast("invalid json", :embed) == + {:error, %Jason.DecodeError{position: 0, token: nil, data: "invalid json"}} + + assert cast("invalid json", {:json_array, :embed}) == + {:error, %Jason.DecodeError{position: 0, token: nil, data: "invalid json"}} + end + end +end diff --git a/apps/fz_http/test/fz_http/config/definition_test.exs b/apps/fz_http/test/fz_http/config/definition_test.exs new file mode 100644 index 000000000..37522c9c1 --- /dev/null +++ b/apps/fz_http/test/fz_http/config/definition_test.exs @@ -0,0 +1,93 @@ +defmodule FzHttp.Config.DefinitionTest do + use ExUnit.Case, async: true + import FzHttp.Config.Definition + + defmodule InvalidDefinitions do + use FzHttp.Config.Definition + + defconfig(:required, Types.IP, foo: :bar) + end + + defmodule Definitions do + use FzHttp.Config.Definition + + defconfig(:required, Types.IP) + + defconfig(:optional, Types.IP, default: "0.0.0.0") + + defconfig(:with_legacy_key, :string, legacy_keys: [{:env, "FOO", "100.0"}]) + + defconfig(:with_validation, :integer, + changeset: fn changeset, key -> + Ecto.Changeset.validate_number(changeset, key, + greater_than_or_equal_to: 0, + less_than_or_equal_to: 2 + ) + end + ) + + defconfig(:sensitive, :string, sensitive: true) + + defconfig(:with_dump, :map, + dump: fn value -> + for {k, v} <- value, do: {k, v} + end + ) + end + + describe "__using__/1" do + test "inserts a function which returns list of defined configs" do + assert Definitions.configs() == [ + {Definitions, :with_dump}, + {Definitions, :sensitive}, + {Definitions, :with_validation}, + {Definitions, :with_legacy_key}, + {Definitions, :optional}, + {Definitions, :required} + ] + end + + test "inserts a function which returns spec of a given config definition" do + assert Definitions.required() == {Types.IP, []} + assert Definitions.optional() == {Types.IP, default: "0.0.0.0"} + assert Definitions.with_legacy_key() == {:string, legacy_keys: [{:env, "FOO", "100.0"}]} + assert {:integer, changeset: _cb} = Definitions.with_validation() + + assert InvalidDefinitions.required() == {Types.IP, [foo: :bar]} + end + + test "inserts a function which returns definition doc" do + assert fetch_doc(FzHttp.Config.Definitions, :default_admin_email) == + {:ok, "Primary administrator email.\n"} + + assert fetch_doc(Foo, :bar) == + {:error, :module_not_found} + end + end + + describe "fetch_spec_and_opts!/2" do + test "returns spec and opts for a given config definition" do + assert fetch_spec_and_opts!(Definitions, :required) == {Types.IP, {[], [], [], []}} + + assert fetch_spec_and_opts!(Definitions, :optional) == + {Types.IP, {[default: "0.0.0.0"], [], [], []}} + + assert fetch_spec_and_opts!(Definitions, :with_legacy_key) == + {:string, {[legacy_keys: [{:env, "FOO", "100.0"}]], [], [], []}} + + assert {:integer, {[], [{:changeset, _cb}], [], []}} = + fetch_spec_and_opts!(Definitions, :with_validation) + + assert {:map, {[], [], [{:dump, _cb}], []}} = fetch_spec_and_opts!(Definitions, :with_dump) + + assert {:string, {[], [], [], [sensitive: true]}} = + fetch_spec_and_opts!(Definitions, :sensitive) + end + + test "raises on invalid opts" do + assert_raise RuntimeError, "unknown options [foo: :bar] for configuration :required", fn -> + fetch_spec_and_opts!(InvalidDefinitions, :required) + end + end + end +end diff --git a/apps/fz_http/test/fz_http/config/fetcher_test.exs b/apps/fz_http/test/fz_http/config/fetcher_test.exs new file mode 100644 index 000000000..4b6b5e7dd --- /dev/null +++ b/apps/fz_http/test/fz_http/config/fetcher_test.exs @@ -0,0 +1,217 @@ +defmodule FzHttp.Config.FetcherTest do + use ExUnit.Case, async: true + import FzHttp.Config.Fetcher + + defmodule Test do + use FzHttp.Config.Definition + alias FzHttp.Types + + defconfig(:required, Types.IP) + + defconfig(:optional, Types.IP, default: "0.0.0.0") + + defconfig(:optional_generated, Types.IP, + legacy_keys: [{:env, "OGID", "1.0"}], + default: fn -> "1.1.1.1" end + ) + + defconfig(:one_of, {:one_of, [:string, :integer]}, + changeset: fn + :integer, changeset, key -> + Ecto.Changeset.validate_number(changeset, key, + greater_than_or_equal_to: 0, + less_than_or_equal_to: 2 + ) + + :string, changeset, key -> + Ecto.Changeset.validate_inclusion(changeset, key, ~w[a b]) + end + ) + + defconfig(:integer, :integer) + + defconfig(:invalid_with_validation, :integer, + default: -1, + changeset: fn changeset, key -> + Ecto.Changeset.validate_number(changeset, key, + greater_than_or_equal_to: 0, + less_than_or_equal_to: 2 + ) + end + ) + + defconfig(:array, {:array, ",", :integer}, + default: [1, 2, 3], + changeset: fn changeset, key -> + Ecto.Changeset.validate_number(changeset, key, + greater_than_or_equal_to: 0, + less_than_or_equal_to: 2 + ) + end + ) + + defconfig(:json_array, {:json_array, :map}) + + defconfig(:json, :map, + dump: fn value -> + for {k, v} <- value, do: {String.to_atom(k), v} + end + ) + + defconfig(:boolean, :boolean) + + defconfig(:sensitive, :map, default: %{}, sensitive: true) + end + + describe "fetch_source_and_config/4" do + test "returns error when required config is not set" do + assert fetch_source_and_config(Test, :required, %{}, %{}) == + {:error, + {{nil, ["is required"]}, [module: Test, key: :required, source: :not_found]}} + end + + test "does not allow to explicitly set required config to nil" do + assert fetch_source_and_config(Test, :required, %{required: nil}, %{}) == + {:error, + {{nil, ["is required"]}, [module: Test, key: :required, source: :not_found]}} + + assert fetch_source_and_config(Test, :required, %{}, %{"REQUIRED" => nil}) == + {:error, + {{nil, ["is required"]}, [module: Test, key: :required, source: :not_found]}} + end + + test "returns default value when config is not set" do + assert fetch_source_and_config(Test, :optional, %{}, %{}) == + {:ok, :default, %Postgrex.INET{address: {0, 0, 0, 0}, netmask: nil}} + + assert fetch_source_and_config(Test, :optional_generated, %{}, %{}) == + {:ok, :default, %Postgrex.INET{address: {1, 1, 1, 1}, netmask: nil}} + end + + test "returns error when resolved value is invalid" do + assert fetch_source_and_config(Test, :invalid_with_validation, %{}, %{}) == + {:error, + {{-1, ["must be greater than or equal to 0"]}, + [ + module: __MODULE__.Test, + key: :invalid_with_validation, + source: :default + ]}} + + assert fetch_source_and_config(Test, :required, %{required: "a.b.c.d"}, %{}) == + {:error, + {{"a.b.c.d", ["is invalid"]}, + [ + module: __MODULE__.Test, + key: :required, + source: {:db, :required} + ]}} + + assert fetch_source_and_config(Test, :one_of, %{one_of: :atom}, %{}) == + {:error, + {{:atom, ["must be one of: string, integer"]}, + [ + module: __MODULE__.Test, + key: :one_of, + source: {:db, :one_of} + ]}} + + assert fetch_source_and_config(Test, :array, %{}, %{}) == + {:error, + {[{3, ["must be less than or equal to 2"]}], + [module: __MODULE__.Test, key: :array, source: :default]}} + end + + test "casts binary to appropriate data type" do + assert fetch_source_and_config(Test, :array, %{}, %{"ARRAY" => "0,1,2"}) == + {:ok, {:env, "ARRAY"}, [0, 1, 2]} + + json = Jason.encode!(%{foo: :bar}) + + assert fetch_source_and_config(Test, :json, %{}, %{"JSON" => json}) == + {:ok, {:env, "JSON"}, foo: "bar"} + + json = Jason.encode!([%{foo: :bar}]) + + assert fetch_source_and_config(Test, :json_array, %{}, %{"JSON_ARRAY" => json}) == + {:ok, {:env, "JSON_ARRAY"}, [%{"foo" => "bar"}]} + + assert fetch_source_and_config(Test, :optional, %{}, %{"OPTIONAL" => "127.0.0.1"}) == + {:ok, {:env, "OPTIONAL"}, %Postgrex.INET{address: {127, 0, 0, 1}, netmask: nil}} + + assert fetch_source_and_config(Test, :boolean, %{}, %{"BOOLEAN" => "true"}) == + {:ok, {:env, "BOOLEAN"}, true} + end + + test "applies dump function" do + json = Jason.encode!(%{foo: :bar}) + + assert fetch_source_and_config(Test, :json, %{}, %{"JSON" => json}) == + {:ok, {:env, "JSON"}, foo: "bar"} + end + + test "does not apply dump function on invalid values" do + assert fetch_source_and_config(Test, :json, %{}, %{"JSON" => "foo"}) == + {:error, + {{"foo", ["unexpected byte at position 0: 0x66 (\"f\")"]}, + [module: __MODULE__.Test, key: :json, source: {:env, "JSON"}]}} + end + + test "returns error when type can't be casted" do + assert fetch_source_and_config(Test, :integer, %{}, %{"INTEGER" => "X"}) == + {:error, + {{"X", ["cannot be cast to an integer"]}, + [ + module: __MODULE__.Test, + key: :integer, + source: {:env, "INTEGER"} + ]}} + + assert fetch_source_and_config(Test, :integer, %{}, %{"INTEGER" => "123a"}) == + {:error, + {{"123a", + ["cannot be cast to an integer, got a reminder a after an integer value 123"]}, + [ + module: __MODULE__.Test, + key: :integer, + source: {:env, "INTEGER"} + ]}} + + json = Jason.encode!(%{foo: :bar}) + + assert fetch_source_and_config(Test, :json, %{}, %{"JSON" => json}) == + {:ok, {:env, "JSON"}, foo: "bar"} + + json = Jason.encode!([%{foo: :bar}]) + + assert fetch_source_and_config(Test, :json_array, %{}, %{"JSON_ARRAY" => json}) == + {:ok, {:env, "JSON_ARRAY"}, [%{"foo" => "bar"}]} + end + + test "returns value for a given config using resolver precedence" do + key = :optional_generated + + # Generated default value + assert fetch_source_and_config(Test, key, %{}, %{}) == + {:ok, :default, %Postgrex.INET{address: {1, 1, 1, 1}}} + + # DB value overrides default + db = %{optional_generated: "2.2.2.2"} + + assert fetch_source_and_config(Test, key, db, %{}) == + {:ok, {:db, key}, %Postgrex.INET{address: {2, 2, 2, 2}}} + + # Legacy env overrides DB + env = %{"OGID" => "3.3.3.3"} + + assert fetch_source_and_config(Test, key, db, env) == + {:ok, {:env, "OGID"}, %Postgrex.INET{address: {3, 3, 3, 3}}} + + # Env overrides legacy env + env = Map.merge(env, %{"OPTIONAL_GENERATED" => "4.4.4.4"}) + + assert fetch_source_and_config(Test, key, db, env) == + {:ok, {:env, "OPTIONAL_GENERATED"}, %Postgrex.INET{address: {4, 4, 4, 4}}} + end + end +end diff --git a/apps/fz_http/test/fz_http/config/resolver_test.exs b/apps/fz_http/test/fz_http/config/resolver_test.exs new file mode 100644 index 000000000..857837f29 --- /dev/null +++ b/apps/fz_http/test/fz_http/config/resolver_test.exs @@ -0,0 +1,72 @@ +defmodule FzHttp.Config.ResolverTest do + use ExUnit.Case, async: true + import FzHttp.Config.Resolver + + describe "resolve/4" do + test "returns nil when variable is not found" do + env_configurations = %{} + db_configurations = %{} + + assert resolve(:foo, env_configurations, db_configurations, []) == :error + end + + test "returns default value when variable is not found" do + env_configurations = %{} + db_configurations = %{} + opts = [default: :foo] + + assert resolve(:foo, env_configurations, db_configurations, opts) == {:ok, {:default, :foo}} + end + + test "returns variable from system environment" do + env_configurations = %{"FOO" => "bar"} + db_configurations = %{} + + assert resolve(:foo, env_configurations, db_configurations, []) == + {:ok, {{:env, "FOO"}, "bar"}} + end + + test "returns variable from system environment with legacy key" do + env_configurations = %{"FOO" => "bar"} + db_configurations = %{} + opts = [legacy_keys: [{:env, "FOO", "1.0"}]] + + assert resolve(:bar, env_configurations, db_configurations, opts) == + {:ok, {{:env, "FOO"}, "bar"}} + end + + test "returns variable from database" do + env_configurations = %{} + db_configurations = %FzHttp.Config.Configuration{default_client_dns: "1.2.3.4"} + + assert resolve(:default_client_dns, env_configurations, db_configurations, []) == + {:ok, {{:db, :default_client_dns}, "1.2.3.4"}} + end + + test "precedence" do + key = :my_key + env = %{"FOO" => "3.3.2.2"} + db = %{} + + # `nil` by default + opts = [] + assert resolve(key, env, db, opts) == :error + + # `default` opt overrides `nil` + opts = [default: "8.8.4.4"] + assert resolve(key, env, db, opts) == {:ok, {:default, "8.8.4.4"}} + + # DB value overrides default + db = %{my_key: "1.2.3.4"} + assert resolve(key, env, db, opts) == {:ok, {{:db, key}, "1.2.3.4"}} + + # Legacy env overrides DB + opts = [legacy_keys: [{:env, "FOO", "1.0"}]] + assert resolve(key, env, db, opts) == {:ok, {{:env, "FOO"}, "3.3.2.2"}} + + # Env overrides legacy env + env = Map.merge(env, %{"MY_KEY" => "2.7.2.8"}) + assert resolve(key, env, db, opts) == {:ok, {{:env, "MY_KEY"}, "2.7.2.8"}} + end + end +end diff --git a/apps/fz_http/test/fz_http/config/validator_test.exs b/apps/fz_http/test/fz_http/config/validator_test.exs new file mode 100644 index 000000000..24ca5b88f --- /dev/null +++ b/apps/fz_http/test/fz_http/config/validator_test.exs @@ -0,0 +1,92 @@ +defmodule FzHttp.Config.ValidatorTest do + use ExUnit.Case, async: true + import FzHttp.Config.Validator + alias FzHttp.Types + + describe "validate/4" do + test "validates an array of integers" do + assert validate(:key, "1,2,3", {:array, "x", :integer}, []) == + {:error, {"1,2,3", ["must be an array"]}} + + assert validate(:key, "1,2,3", {:json_array, :integer}, []) == + {:error, {"1,2,3", ["must be an array"]}} + + assert validate(:key, ~w"1 2 3", {:array, ",", :integer}, []) == {:ok, [1, 2, 3]} + + assert validate(:key, ~w"1 2 3", {:array, :integer}, []) == {:ok, [1, 2, 3]} + end + + test "validates arrays" do + type = {:array, "x", :integer, validate_unique: true, validate_length: [min: 1, max: 3]} + + assert validate(:key, [], type, []) == + {:error, {[], ["should be at least 1 item(s)"]}} + + assert validate(:key, [1, 2, 3, 4], type, []) == + {:error, {[1, 2, 3, 4], ["should be at most 3 item(s)"]}} + + assert validate(:key, [1, 2, 1], type, []) == + {:error, [{1, ["should not contain duplicates"]}]} + end + + test "validates one of types" do + type = {:one_of, [:integer, :boolean]} + assert validate(:key, 1, type, []) == {:ok, 1} + assert validate(:key, true, type, []) == {:ok, true} + + assert validate(:key, "invalid", type, []) == + {:error, {"invalid", ["must be one of: integer, boolean"]}} + + type = {:one_of, [Types.IP, Types.CIDR]} + + assert validate(:key, "1.1.1.1", type, []) == + {:ok, %Postgrex.INET{address: {1, 1, 1, 1}, netmask: nil}} + + assert validate(:key, "127.0.0.1/24", type, []) == + {:ok, %Postgrex.INET{address: {127, 0, 0, 1}, netmask: 24}} + + assert validate(:key, "invalid", type, []) == + {:error, + {"invalid", ["must be one of: Elixir.FzHttp.Types.IP, Elixir.FzHttp.Types.CIDR"]}} + + type = {:json_array, {:one_of, [:integer, :boolean]}} + + assert validate(:key, [1, true, "invalid"], type, []) == + {:error, [{"invalid", ["must be one of: integer, boolean"]}]} + end + + test "validates embeds" do + type = {:json_array, {:embed, FzHttp.Config.Configuration.SAMLIdentityProvider}} + + opts = [ + changeset: {FzHttp.Config.Configuration.SAMLIdentityProvider, :create_changeset, []} + ] + + attrs = FzHttp.ConfigFixtures.saml_identity_providers_attrs() + + assert validate(:key, [attrs], type, opts) == + {:ok, + [ + %FzHttp.Config.Configuration.SAMLIdentityProvider{ + auto_create_users: attrs["auto_create_users"], + base_url: "http://localhost:13000/auth/saml", + id: attrs["id"], + label: attrs["label"], + metadata: attrs["metadata"] + } + ]} + + assert validate(:key, [%{"id" => "saml"}], type, opts) == + {:error, + [ + {%{"id" => "saml"}, + [ + "auto_create_users can't be blank", + "id is reserved", + "label can't be blank", + "metadata can't be blank" + ]} + ]} + end + end +end diff --git a/apps/fz_http/test/fz_http/config_test.exs b/apps/fz_http/test/fz_http/config_test.exs new file mode 100644 index 000000000..4b3f8bedc --- /dev/null +++ b/apps/fz_http/test/fz_http/config_test.exs @@ -0,0 +1,540 @@ +defmodule FzHttp.ConfigTest do + use FzHttp.DataCase, async: true + import FzHttp.Config + + defmodule Test do + use FzHttp.Config.Definition + alias FzHttp.Types + + defconfig(:required, Types.IP) + + defconfig(:optional_generated, Types.IP, + legacy_keys: [{:env, "OGID", "1.0"}], + default: fn -> "1.1.1.1" end + ) + + defconfig(:one_of, {:one_of, [:string, :integer]}, + changeset: fn + :integer, changeset, key -> + Ecto.Changeset.validate_number(changeset, key, + greater_than_or_equal_to: 0, + less_than_or_equal_to: 2 + ) + + :string, changeset, key -> + Ecto.Changeset.validate_inclusion(changeset, key, ~w[a b]) + end + ) + + defconfig(:integer, :integer) + + defconfig(:invalid_with_validation, :integer, + default: -1, + changeset: fn changeset, key -> + Ecto.Changeset.validate_number(changeset, key, + greater_than_or_equal_to: 0, + less_than_or_equal_to: 2 + ) + end + ) + + defconfig(:array, {:array, ",", :integer}, + default: [1, 2, 3], + changeset: fn changeset, key -> + Ecto.Changeset.validate_number(changeset, key, + greater_than_or_equal_to: 0, + less_than_or_equal_to: 2 + ) + end + ) + + defconfig(:json_array, {:json_array, :map}) + + defconfig(:json, :map, + dump: fn value -> + for {k, v} <- value, do: {String.to_atom(k), v} + end + ) + + defconfig(:boolean, :boolean) + + defconfig(:sensitive, :map, default: %{}, sensitive: true) + end + + describe "fetch_source_and_config!/1" do + test "returns source and config value" do + assert fetch_source_and_config!(:default_client_mtu) == + {{:db, :default_client_mtu}, 1280} + end + + test "raises an error when value is missing" do + message = """ + Missing required configuration value for 'external_url'. + + ## How to fix? + + ### Using environment variables + + You can set this configuration via environment variable by adding it to `.env` file: + + EXTERNAL_URL=YOUR_VALUE + + + ## Documentation + + The external URL the web UI will be accessible at. + + Must be a valid and public FQDN for ACME SSL issuance to function. + + You can add a path suffix if you want to serve firezone from a non-root path, + eg: `https://firezone.mycorp.com/vpn`. + + + You can find more information on configuration here: https://www.firezone.dev/docs/reference/env-vars/#environment-variable-listing + """ + + assert_raise RuntimeError, message, fn -> + fetch_source_and_config!(:external_url) + end + end + end + + describe "fetch_source_and_configs!/1" do + test "returns source and config values" do + assert fetch_source_and_configs!([:default_client_mtu, :default_client_dns]) == + %{ + default_client_dns: + {{:db, :default_client_dns}, + [ + %Postgrex.INET{address: {1, 1, 1, 1}, netmask: nil}, + %Postgrex.INET{address: {1, 0, 0, 1}, netmask: nil} + ]}, + default_client_mtu: {{:db, :default_client_mtu}, 1280} + } + end + + test "raises an error when value is missing" do + message = """ + Missing required configuration value for 'external_url'. + + ## How to fix? + + ### Using environment variables + + You can set this configuration via environment variable by adding it to `.env` file: + + EXTERNAL_URL=YOUR_VALUE + + + ## Documentation + + The external URL the web UI will be accessible at. + + Must be a valid and public FQDN for ACME SSL issuance to function. + + You can add a path suffix if you want to serve firezone from a non-root path, + eg: `https://firezone.mycorp.com/vpn`. + + + You can find more information on configuration here: https://www.firezone.dev/docs/reference/env-vars/#environment-variable-listing + """ + + assert_raise RuntimeError, message, fn -> + fetch_source_and_configs!([:external_url]) + end + end + end + + describe "fetch_config/1" do + test "returns config value" do + assert fetch_config(:default_client_mtu) == + {:ok, 1280} + end + + test "returns error when value is missing" do + assert fetch_config(:external_url) == + {:error, + {{nil, ["is required"]}, + [module: FzHttp.Config.Definitions, key: :external_url, source: :not_found]}} + end + end + + describe "fetch_config!/1" do + test "returns config value" do + assert fetch_config!(:default_client_mtu) == + 1280 + end + + test "raises when value is missing" do + assert_raise RuntimeError, fn -> + fetch_config!(:external_url) + end + end + end + + describe "fetch_configs!/1" do + test "returns source and config values" do + assert fetch_configs!([:default_client_mtu, :default_client_dns]) == + %{ + default_client_dns: [ + %Postgrex.INET{address: {1, 1, 1, 1}, netmask: nil}, + %Postgrex.INET{address: {1, 0, 0, 1}, netmask: nil} + ], + default_client_mtu: 1280 + } + end + + test "raises an error when value is missing" do + message = """ + Missing required configuration value for 'external_url'. + + ## How to fix? + + ### Using environment variables + + You can set this configuration via environment variable by adding it to `.env` file: + + EXTERNAL_URL=YOUR_VALUE + + + ## Documentation + + The external URL the web UI will be accessible at. + + Must be a valid and public FQDN for ACME SSL issuance to function. + + You can add a path suffix if you want to serve firezone from a non-root path, + eg: `https://firezone.mycorp.com/vpn`. + + + You can find more information on configuration here: https://www.firezone.dev/docs/reference/env-vars/#environment-variable-listing + """ + + assert_raise RuntimeError, message, fn -> + fetch_configs!([:external_url]) + end + end + end + + describe "compile_config!/1" do + test "returns config value" do + assert compile_config!(Test, :optional_generated) == + %Postgrex.INET{address: {1, 1, 1, 1}, netmask: nil} + end + + test "raises an error when value is missing" do + message = """ + Missing required configuration value for 'required'. + + ## How to fix? + + ### Using environment variables + + You can set this configuration via environment variable by adding it to `.env` file: + + REQUIRED=YOUR_VALUE + """ + + assert_raise RuntimeError, message, fn -> + compile_config!(Test, :required) + end + end + + test "raises an error when value cannot be casted" do + message = """ + Invalid configuration for 'integer' retrieved from environment variable INTEGER. + + Errors: + + - `"123a"`: cannot be cast to an integer, got a reminder a after an integer value 123\ + """ + + assert_raise RuntimeError, message, fn -> + compile_config!(Test, :integer, %{"INTEGER" => "123a"}) + end + end + + test "raises an error when value is invalid" do + message = """ + Invalid configuration for 'required' retrieved from environment variable REQUIRED. + + Errors: + + - `\"a.b.c.d\"`: is invalid\ + """ + + assert_raise RuntimeError, message, fn -> + compile_config!(Test, :required, %{"REQUIRED" => "a.b.c.d"}) + end + + message = """ + Invalid configuration for 'one_of' retrieved from environment variable ONE_OF. + + Errors: + + - `"X"`: must be one of: string, integer\ + """ + + assert_raise RuntimeError, message, fn -> + compile_config!(Test, :one_of, %{"ONE_OF" => "X"}) + end + + message = """ + Invalid configuration for 'array' retrieved from environment variable ARRAY. + + Errors: + + - `-2`: must be greater than or equal to 0 + - `-100`: must be greater than or equal to 0 + - `-1`: must be greater than or equal to 0\ + """ + + assert_raise RuntimeError, message, fn -> + compile_config!(Test, :array, %{"ARRAY" => "1,-1,0,2,-100,-2"}) + end + end + + test "does not print sensitive values" do + message = """ + Invalid configuration for 'sensitive' retrieved from environment variable SENSITIVE. + + Errors: + + - `**SENSITIVE-VALUE-REDACTED**`: unexpected byte at position 0: 0x66 ("f")\ + """ + + assert_raise RuntimeError, message, fn -> + compile_config!(Test, :sensitive, %{"SENSITIVE" => "foo"}) + end + end + end + + describe "validate_runtime_config!/0" do + test "raises error on invalid values" do + message = """ + Found 8 configuration errors: + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Missing required configuration value for 'boolean'. + + ## How to fix? + + ### Using environment variables + + You can set this configuration via environment variable by adding it to `.env` file: + + BOOLEAN=YOUR_VALUE + + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Missing required configuration value for 'json'. + + ## How to fix? + + ### Using environment variables + + You can set this configuration via environment variable by adding it to `.env` file: + + JSON=YOUR_VALUE + + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Missing required configuration value for 'json_array'. + + ## How to fix? + + ### Using environment variables + + You can set this configuration via environment variable by adding it to `.env` file: + + JSON_ARRAY=YOUR_VALUE + + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Invalid configuration for 'array' retrieved from default value. + + Errors: + + - `3`: must be less than or equal to 2 + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Invalid configuration for 'invalid_with_validation' retrieved from default value. + + Errors: + + - `-1`: must be greater than or equal to 0 + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Missing required configuration value for 'integer'. + + ## How to fix? + + ### Using environment variables + + You can set this configuration via environment variable by adding it to `.env` file: + + INTEGER=YOUR_VALUE + + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Missing required configuration value for 'one_of'. + + ## How to fix? + + ### Using environment variables + + You can set this configuration via environment variable by adding it to `.env` file: + + ONE_OF=YOUR_VALUE + + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Missing required configuration value for 'required'. + + ## How to fix? + + ### Using environment variables + + You can set this configuration via environment variable by adding it to `.env` file: + + REQUIRED=YOUR_VALUE + """ + + assert_raise RuntimeError, message, fn -> + validate_runtime_config!(Test, %{}, %{}) + end + end + + test "returns :ok when config is valid" do + env_config = %{ + "BOOLEAN" => "true", + "ARRAY" => "1", + "JSON" => "{\"foo\":\"bar\"}", + "JSON_ARRAY" => "[{\"foo\":\"bar\"}]", + "INTEGER" => "123", + "ONE_OF" => "a", + "REQUIRED" => "1.1.1.1", + "INVALID_WITH_VALIDATION" => "2" + } + + assert validate_runtime_config!(Test, %{}, env_config) == :ok + end + end + + describe "fetch_db_config!" do + test "returns config from db table" do + assert fetch_db_config!() == Repo.one(FzHttp.Config.Configuration) + end + end + + describe "change_config/2" do + test "returns config changeset" do + assert %Ecto.Changeset{} = change_config() + end + end + + describe "update_config/2" do + test "returns error when changeset is invalid" do + config = Repo.one(FzHttp.Config.Configuration) + + attrs = %{ + local_auth_enabled: 1, + allow_unprivileged_device_management: 1, + allow_unprivileged_device_configuration: 1, + disable_vpn_on_oidc_error: 1, + default_client_persistent_keepalive: -1, + default_client_mtu: -1, + default_client_endpoint: "123", + default_client_dns: ["!!!"], + default_client_allowed_ips: ["!"], + vpn_session_duration: -1 + } + + assert {:error, changeset} = update_config(config, attrs) + + assert errors_on(changeset) == %{ + default_client_mtu: ["must be greater than or equal to 576"], + allow_unprivileged_device_configuration: ["is invalid"], + allow_unprivileged_device_management: ["is invalid"], + default_client_allowed_ips: ["is invalid"], + default_client_dns: [ + "!!! is not a valid FQDN", + "must be one of: Elixir.FzHttp.Types.IP, string" + ], + default_client_persistent_keepalive: ["must be greater than or equal to 0"], + disable_vpn_on_oidc_error: ["is invalid"], + local_auth_enabled: ["is invalid"], + vpn_session_duration: ["must be greater than or equal to 0"] + } + end + + test "returns error when trying to change overridden value" do + put_system_env_override(:local_auth_enabled, false) + + config = Repo.one(FzHttp.Config.Configuration) + + attrs = %{ + local_auth_enabled: false + } + + assert {:error, changeset} = update_config(config, attrs) + + assert errors_on(changeset) == + %{ + local_auth_enabled: [ + "cannot be changed; it is overridden by LOCAL_AUTH_ENABLED environment variable" + ] + } + end + + test "trims binary fields" do + config = Repo.one(FzHttp.Config.Configuration) + + attrs = %{ + default_client_dns: [" foobar.com", "google.com "], + default_client_endpoint: " 127.0.0.1 " + } + + assert {:ok, config} = update_config(config, attrs) + assert config.default_client_dns == ["foobar.com", "google.com"] + assert config.default_client_endpoint == "127.0.0.1" + end + + test "changes database config value" do + config = Repo.one(FzHttp.Config.Configuration) + attrs = %{default_client_dns: ["foobar.com", "google.com"]} + assert {:ok, config} = update_config(config, attrs) + assert config.default_client_dns == attrs.default_client_dns + end + end + + describe "put_config!/2" do + test "updates config field in a database" do + assert config = put_config!(:default_client_endpoint, " 127.0.0.1") + assert config.default_client_endpoint == "127.0.0.1" + assert Repo.one(FzHttp.Config.Configuration).default_client_endpoint == "127.0.0.1" + end + + test "raises when config field is not valid" do + assert_raise RuntimeError, fn -> + put_config!(:default_client_endpoint, "!!!") + end + end + end +end diff --git a/apps/fz_http/test/fz_http/configurations/mailer_test.exs b/apps/fz_http/test/fz_http/configurations/mailer_test.exs deleted file mode 100644 index b0d3a0f88..000000000 --- a/apps/fz_http/test/fz_http/configurations/mailer_test.exs +++ /dev/null @@ -1,39 +0,0 @@ -defmodule FzHttp.Configurations.MailerTest do - use ExUnit.Case, async: true - - alias FzHttp.Configurations.Mailer - - describe "changeset/1" do - test "adds errors for required fields" do - changeset = Mailer.changeset(%{}) - - assert changeset.errors[:from] == {"can't be blank", [validation: :required]} - assert changeset.errors[:provider] == {"can't be blank", [validation: :required]} - assert changeset.errors[:configs] == {"can't be blank", [validation: :required]} - end - - test "adds error for invalid from address" do - changeset = Mailer.changeset(%{"from" => "invalid"}) - - assert changeset.errors[:from] == {"has invalid format", [validation: :format]} - end - - test "adds error when provider is not in configs" do - changeset = - Mailer.changeset(%{"from" => "foobar@localhost", "provider" => "smtp", "configs" => %{}}) - - assert changeset.errors[:provider] == {"must exist in configs", []} - end - - test "doesn't add errors when attrs is valid" do - changeset = - Mailer.changeset(%{ - "from" => "foobar@localhost", - "provider" => "smtp", - "configs" => %{"smtp" => %{}} - }) - - assert changeset.errors == [] - 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 deleted file mode 100644 index 94dada538..000000000 --- a/apps/fz_http/test/fz_http/configurations_test.exs +++ /dev/null @@ -1,151 +0,0 @@ -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 "auto_create_users?/2" do - import FzHttp.ConfigurationsFixtures - import FzHttp.SAMLIdentityProviderFixtures - - test "raises if provider_id not found" do - assert_raise(RuntimeError, "Unknown provider foobar", fn -> - Configurations.auto_create_users?(:openid_connect_providers, "foobar") - end) - end - - test "returns true for found provider_id" do - configuration(%{ - saml_identity_providers: [ - %{ - "id" => "test", - "metadata" => metadata(), - "auto_create_users" => true, - "label" => "SAML" - } - ] - }) - - assert Configurations.auto_create_users?(:saml_identity_providers, "test") - end - - test "returns false for found provider_id" do - configuration(%{ - saml_identity_providers: [ - %{ - "id" => "test", - "metadata" => metadata(), - "auto_create_users" => false, - "label" => "SAML" - } - ] - }) - - refute Configurations.auto_create_users?(:saml_identity_providers, "test") - 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/devices/device/query_test.exs b/apps/fz_http/test/fz_http/devices/device/query_test.exs index 23606b0ce..4325a8681 100644 --- a/apps/fz_http/test/fz_http/devices/device/query_test.exs +++ b/apps/fz_http/test/fz_http/devices/device/query_test.exs @@ -5,8 +5,8 @@ defmodule FzHttp.Devices.Device.QueryTest do 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") + cidr = string_to_cidr("10.3.2.0/29") + gateway_ip = string_to_ip("10.3.2.0") offset = 3 queryable = next_available_address(cidr, offset, [gateway_ip]) @@ -15,8 +15,8 @@ defmodule FzHttp.Devices.Device.QueryTest do 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") + cidr = string_to_cidr("10.3.3.0/29") + gateway_ip = string_to_ip("10.3.3.3") offset = 3 queryable = next_available_address(cidr, offset, [gateway_ip]) @@ -25,8 +25,8 @@ defmodule FzHttp.Devices.Device.QueryTest do 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") + cidr = string_to_cidr("10.3.4.0/29") + gateway_ip = string_to_ip("10.3.4.0") offset = 3 queryable = next_available_address(cidr, offset, [gateway_ip]) @@ -40,8 +40,8 @@ defmodule FzHttp.Devices.Device.QueryTest do 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") + cidr = string_to_cidr("10.3.5.0/29") + gateway_ip = string_to_ip("10.3.5.0") offset = 5 queryable = next_available_address(cidr, offset, [gateway_ip]) @@ -57,8 +57,8 @@ defmodule FzHttp.Devices.Device.QueryTest do 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") + cidr = string_to_cidr("10.3.6.0/30") + gateway_ip = string_to_ip("10.3.6.1") offset = 1 DevicesFixtures.device(%{ipv4: "10.3.6.2"}) @@ -74,8 +74,8 @@ defmodule FzHttp.Devices.Device.QueryTest do 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") + cidr = string_to_cidr("10.3.7.0/29") + gateway_ip = string_to_ip("10.3.7.3") offset = 3 queryable = next_available_address(cidr, offset, [gateway_ip]) @@ -103,8 +103,8 @@ defmodule FzHttp.Devices.Device.QueryTest do 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") + cidr = string_to_cidr("fd00::3:3:0/120") + gateway_ip = string_to_ip("fd00::3:3:3") offset = 3 queryable = next_available_address(cidr, offset, [gateway_ip]) @@ -113,8 +113,8 @@ defmodule FzHttp.Devices.Device.QueryTest do end test "selects available IPv6 at end of CIDR range" do - cidr = string_to_inet("fd00::/106") - gateway_ip = string_to_inet("fd00::3:3:3") + cidr = string_to_cidr("fd00::/106") + gateway_ip = string_to_ip("fd00::3:3:3") offset = 4_194_304 queryable = next_available_address(cidr, offset, [gateway_ip]) @@ -123,8 +123,8 @@ defmodule FzHttp.Devices.Device.QueryTest do end test "works when offset is out of IPv6 CIDR range" do - cidr = string_to_inet("fd00::/106") - gateway_ip = string_to_inet("fd00::3:3:3") + cidr = string_to_cidr("fd00::/106") + gateway_ip = string_to_ip("fd00::3:3:3") offset = 4_194_305 queryable = next_available_address(cidr, offset, [gateway_ip]) @@ -133,8 +133,8 @@ defmodule FzHttp.Devices.Device.QueryTest do end test "works when netmask allows a large number of devices" do - cidr = string_to_inet("fd00::/70") - gateway_ip = string_to_inet("fd00::3:3:3") + cidr = string_to_cidr("fd00::/70") + gateway_ip = string_to_ip("fd00::3:3:3") offset = 9_223_372_036_854_775_807 queryable = next_available_address(cidr, offset, [gateway_ip]) @@ -145,8 +145,8 @@ defmodule FzHttp.Devices.Device.QueryTest do 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") + cidr = string_to_cidr("fd00::3:2:0/126") + gateway_ip = string_to_ip("fd00::3:2:1") offset = 3 DevicesFixtures.device(%{ipv6: "fd00::3:2:2"}) @@ -156,8 +156,13 @@ defmodule FzHttp.Devices.Device.QueryTest do end end - defp string_to_inet(string) do - {:ok, inet} = EctoNetwork.INET.cast(string) + defp string_to_cidr(string) do + {:ok, inet} = FzHttp.Types.CIDR.cast(string) + inet + end + + defp string_to_ip(string) do + {:ok, inet} = FzHttp.Types.IP.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 1a5c5a090..cbe375f60 100644 --- a/apps/fz_http/test/fz_http/devices_test.exs +++ b/apps/fz_http/test/fz_http/devices_test.exs @@ -133,22 +133,26 @@ defmodule FzHttp.DevicesTest do @attrs %{ name: "Go hard or go home.", - allowed_ips: "0.0.0.0", + allowed_ips: [%Postgrex.INET{address: {0, 0, 0, 0}, netmask: nil}], use_default_allowed_ips: false } @valid_dns_attrs %{ use_default_dns: false, - dns: "1.1.1.1, 1.0.0.1, 2606:4700:4700::1111, 2606:4700:4700::1001" + dns: ["1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"] } @duplicate_dns_attrs %{ - dns: "8.8.8.8, 1.1.1.1, 1.1.1.1, ::1, ::1, ::1, ::1, ::1, 8.8.8.8" + dns: ["8.8.8.8", "1.1.1.1", "1.1.1.1", "::1", "::1", "::1", "::1", "::1", "8.8.8.8"] } @valid_allowed_ips_attrs %{ use_default_allowed_ips: false, - allowed_ips: "0.0.0.0/0, ::/0, ::0/0, 192.168.1.0/24" + allowed_ips: [ + %Postgrex.INET{address: {0, 0, 0, 0}, netmask: 0}, + %Postgrex.INET{address: {0, 0, 0, 0, 0, 0, 0, 0}, netmask: 0}, + %Postgrex.INET{address: {192, 168, 1, 0}, netmask: 24} + ] } @valid_endpoint_ipv4_attrs %{ @@ -156,11 +160,6 @@ defmodule FzHttp.DevicesTest do endpoint: "5.5.5.5" } - @valid_endpoint_ipv6_attrs %{ - use_default_endpoint: false, - endpoint: "fd00::1" - } - @valid_endpoint_host_attrs %{ use_default_endpoint: false, endpoint: "valid-endpoint.example.com" @@ -172,12 +171,12 @@ defmodule FzHttp.DevicesTest do } @invalid_allowed_ips_attrs %{ - allowed_ips: "1.1.1.1, 11, foobar" + allowed_ips: ["1.1.1.1", "11", "foobar"] } @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_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} @@ -199,8 +198,23 @@ defmodule FzHttp.DevicesTest do end test "updates device with valid ipv6 endpoint", %{device: device} do - {:ok, test_device} = Devices.update_device(device, @valid_endpoint_ipv6_attrs) - assert @valid_endpoint_ipv6_attrs = test_device + attrs = %{ + use_default_endpoint: false, + endpoint: "fd00::1" + } + + {:ok, test_device} = Devices.update_device(device, attrs) + assert test_device.use_default_endpoint == attrs.use_default_endpoint + assert test_device.endpoint == attrs.endpoint + + attrs = %{ + use_default_endpoint: false, + endpoint: "[fd00::1]:8080" + } + + {:ok, test_device} = Devices.update_device(device, attrs) + assert test_device.use_default_endpoint == attrs.use_default_endpoint + assert test_device.endpoint == attrs.endpoint end test "updates device with valid host endpoint", %{device: device} do @@ -224,7 +238,7 @@ defmodule FzHttp.DevicesTest do end end - @tag attrs: %{use_default_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 @@ -240,11 +254,7 @@ defmodule FzHttp.DevicesTest do test "prevents assigning duplicate DNS servers", %{device: device} do {:error, changeset} = Devices.update_device(device, @duplicate_dns_attrs) - - assert changeset.errors[:dns] == { - "is invalid: duplicates are not allowed: 1.1.1.1, ::1, 8.8.8.8", - [] - } + assert "should not contain duplicates" in errors_on(changeset).dns end test "updates device with valid allowed_ips", %{device: device} do @@ -255,10 +265,8 @@ defmodule FzHttp.DevicesTest do test "prevents updating device with invalid allowed_ips", %{device: device} do {:error, changeset} = Devices.update_device(device, @invalid_allowed_ips_attrs) - assert changeset.errors[:allowed_ips] == { - "is invalid: 11 is not a valid IPv4 / IPv6 address or CIDR range", - [] - } + assert changeset.errors[:allowed_ips] == + {"is invalid", [{:type, {:array, FzHttp.Types.INET}}, {:validation, :cast}]} 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 273ffa85c..5b73adbfd 100644 --- a/apps/fz_http/test/fz_http/events_test.exs +++ b/apps/fz_http/test/fz_http/events_test.exs @@ -6,6 +6,8 @@ defmodule FzHttp.EventsTest do alias FzHttp.{Devices, Events} + @moduletag :acceptance + # XXX: Not needed with start_supervised! setup do on_exit(fn -> diff --git a/apps/fz_http/test/fz_http/oidc/refresher_test.exs b/apps/fz_http/test/fz_http/oidc/refresher_test.exs index f0a9624ed..bc7d644d9 100644 --- a/apps/fz_http/test/fz_http/oidc/refresher_test.exs +++ b/apps/fz_http/test/fz_http/oidc/refresher_test.exs @@ -5,7 +5,7 @@ defmodule FzHttp.OIDC.RefresherTest do setup :create_user setup %{user: user} do - {bypass, [provider_attrs]} = FzHttp.ConfigurationsFixtures.start_openid_providers(["google"]) + {bypass, [provider_attrs]} = FzHttp.ConfigFixtures.start_openid_providers(["google"]) conn = Repo.insert!(%FzHttp.OIDC.Connection{ @@ -19,7 +19,7 @@ defmodule FzHttp.OIDC.RefresherTest do describe "refresh failed" do test "disable user", %{user: user, conn: conn, bypass: bypass} do - FzHttp.ConfigurationsFixtures.expect_refresh_token_failure(bypass) + FzHttp.ConfigFixtures.expect_refresh_token_failure(bypass) assert Refresher.refresh(user.id) == {:stop, :shutdown, user.id} user = Repo.reload(user) @@ -32,7 +32,7 @@ defmodule FzHttp.OIDC.RefresherTest do describe "refresh succeeded" do test "does not change user", %{user: user, conn: conn, bypass: bypass} do - FzHttp.ConfigurationsFixtures.expect_refresh_token(bypass) + FzHttp.ConfigFixtures.expect_refresh_token(bypass) assert Refresher.refresh(user.id) == {:stop, :shutdown, user.id} user = Repo.reload(user) diff --git a/apps/fz_http/test/fz_http/release_test.exs b/apps/fz_http/test/fz_http/release_test.exs index cec554001..2c6f6ff44 100644 --- a/apps/fz_http/test/fz_http/release_test.exs +++ b/apps/fz_http/test/fz_http/release_test.exs @@ -25,7 +25,7 @@ defmodule FzHttp.ReleaseTest do Release.create_admin_user() assert {:ok, %User{}} = - Users.fetch_user_by_email(Application.fetch_env!(:fz_http, :admin_email)) + Users.fetch_user_by_email(FzHttp.Config.fetch_env!(:fz_http, :admin_email)) end test "reset admin password when user exists" do 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 f19efda10..f470f4b2d 100644 --- a/apps/fz_http/test/fz_http/repo/notifier_test.exs +++ b/apps/fz_http/test/fz_http/repo/notifier_test.exs @@ -4,6 +4,8 @@ defmodule FzHttp.Repo.NotifierTest do alias FzHttp.Repo.Notifier alias FzHttp.Events + @moduletag :acceptance + setup do on_exit(fn -> :sys.replace_state(Events.vpn_pid(), fn _state -> %{} end) diff --git a/apps/fz_http/test/fz_http/rules_test.exs b/apps/fz_http/test/fz_http/rules_test.exs index a007ac2d6..4e32a7c5a 100644 --- a/apps/fz_http/test/fz_http/rules_test.exs +++ b/apps/fz_http/test/fz_http/rules_test.exs @@ -75,7 +75,7 @@ defmodule FzHttp.RulesTest do {:error, changeset} = Rules.create_rule(%{destination: "10.0 0.0/24"}) assert changeset.errors[:destination] == - {"is invalid", [type: EctoNetwork.INET, validation: :cast]} + {"is invalid", [type: FzHttp.Types.INET, validation: :cast]} end test "prevents invalid port_range: no port_type" do diff --git a/apps/fz_http/test/fz_http/telemetry_test.exs b/apps/fz_http/test/fz_http/telemetry_test.exs index bc82fbd6c..7fa8c002e 100644 --- a/apps/fz_http/test/fz_http/telemetry_test.exs +++ b/apps/fz_http/test/fz_http/telemetry_test.exs @@ -41,7 +41,7 @@ defmodule FzHttp.TelemetryTest do describe "auth" do test "count openid providers" do - FzHttp.ConfigurationsFixtures.start_openid_providers([ + FzHttp.ConfigFixtures.start_openid_providers([ "google", "okta", "auth0", @@ -57,7 +57,7 @@ defmodule FzHttp.TelemetryTest do end test "disable vpn on oidc error enabled" do - FzHttp.Configurations.put!(:disable_vpn_on_oidc_error, true) + FzHttp.Config.put_config!(:disable_vpn_on_oidc_error, true) ping_data = Telemetry.ping_data() @@ -65,7 +65,7 @@ defmodule FzHttp.TelemetryTest do end test "disable vpn on oidc error disabled" do - FzHttp.Configurations.put!(:disable_vpn_on_oidc_error, false) + FzHttp.Config.put_config!(:disable_vpn_on_oidc_error, false) ping_data = Telemetry.ping_data() @@ -73,7 +73,7 @@ defmodule FzHttp.TelemetryTest do end test "local authentication enabled" do - FzHttp.Configurations.put!(:local_auth_enabled, true) + FzHttp.Config.put_config!(:local_auth_enabled, true) ping_data = Telemetry.ping_data() @@ -81,7 +81,7 @@ defmodule FzHttp.TelemetryTest do end test "local authentication disabled" do - FzHttp.Configurations.put!(:local_auth_enabled, false) + FzHttp.Config.put_config!(:local_auth_enabled, false) ping_data = Telemetry.ping_data() @@ -89,7 +89,7 @@ defmodule FzHttp.TelemetryTest do end test "unprivileged device management enabled" do - FzHttp.Configurations.put!(:allow_unprivileged_device_management, true) + FzHttp.Config.put_config!(:allow_unprivileged_device_management, true) ping_data = Telemetry.ping_data() @@ -97,7 +97,7 @@ defmodule FzHttp.TelemetryTest do end test "unprivileged device configuration enabled" do - FzHttp.Configurations.put!(:allow_unprivileged_device_configuration, true) + FzHttp.Config.put_config!(:allow_unprivileged_device_configuration, true) ping_data = Telemetry.ping_data() @@ -105,7 +105,7 @@ defmodule FzHttp.TelemetryTest do end test "unprivileged device configuration disabled" do - FzHttp.Configurations.put!(:allow_unprivileged_device_configuration, false) + FzHttp.Config.put_config!(:allow_unprivileged_device_configuration, false) ping_data = Telemetry.ping_data() diff --git a/apps/fz_http/test/fz_http/users_test.exs b/apps/fz_http/test/fz_http/users_test.exs index d974d3963..db3f873c2 100644 --- a/apps/fz_http/test/fz_http/users_test.exs +++ b/apps/fz_http/test/fz_http/users_test.exs @@ -2,7 +2,7 @@ defmodule FzHttp.UsersTest do use FzHttp.DataCase, async: true alias FzHttp.UsersFixtures alias FzHttp.DevicesFixtures - alias FzHttp.Configurations + alias FzHttp.Config alias FzHttp.Users describe "count/0" do @@ -524,7 +524,7 @@ defmodule FzHttp.UsersTest do describe "vpn_session_expires_at/1" do test "returns expiration datetime of VPN session" do now = DateTime.utc_now() - Configurations.put!(:vpn_session_duration, 30) + Config.put_config!(:vpn_session_duration, 30) user = UsersFixtures.create_user() @@ -537,13 +537,13 @@ defmodule FzHttp.UsersTest do describe "vpn_session_expired?/1" do test "returns false when user did not sign in" do - Configurations.put!(:vpn_session_duration, 30) + Config.put_config!(:vpn_session_duration, 30) user = UsersFixtures.create_user() assert Users.vpn_session_expired?(user) == false end test "returns false when VPN session is not expired" do - Configurations.put!(:vpn_session_duration, 30) + Config.put_config!(:vpn_session_duration, 30) user = UsersFixtures.create_user() user = @@ -555,7 +555,7 @@ defmodule FzHttp.UsersTest do end test "returns true when VPN session is expired" do - Configurations.put!(:vpn_session_duration, 30) + Config.put_config!(:vpn_session_duration, 30) user = UsersFixtures.create_user() user = @@ -567,7 +567,7 @@ defmodule FzHttp.UsersTest do end test "returns false when VPN session never expires" do - Configurations.put!(:vpn_session_duration, 0) + Config.put_config!(:vpn_session_duration, 0) user = UsersFixtures.create_user() user = diff --git a/apps/fz_http/test/fz_http_web/acceptance/admin_test.exs b/apps/fz_http/test/fz_http_web/acceptance/admin_test.exs index fb03607b2..b1954c764 100644 --- a/apps/fz_http/test/fz_http_web/acceptance/admin_test.exs +++ b/apps/fz_http/test/fz_http_web/acceptance/admin_test.exs @@ -145,21 +145,20 @@ defmodule FzHttpWeb.Acceptance.AdminTest do |> toggle("toggle_disabled_at") end) - # XXX: We need to show some kind of message when status changed - Process.sleep(200) - - assert updated_user = Repo.get(FzHttp.Users.User, user.id) - assert updated_user.disabled_at + wait_for(fn -> + assert updated_user = Repo.get(FzHttp.Users.User, user.id) + refute is_nil(updated_user.disabled_at) + end) accept_confirm(session, fn session -> session |> toggle("toggle_disabled_at") end) - Process.sleep(200) - - assert updated_user = Repo.get(FzHttp.Users.User, user.id) - refute updated_user.disabled_at + wait_for(fn -> + assert updated_user = Repo.get(FzHttp.Users.User, user.id) + assert is_nil(updated_user.disabled_at) + end) end feature "delete user", %{session: session, user: user} do @@ -232,8 +231,8 @@ defmodule FzHttpWeb.Acceptance.AdminTest do assert device = Repo.one(FzHttp.Devices.Device) assert device.name == "big-leg-007" assert device.description == "Dummy description" - assert device.allowed_ips == "127.0.0.1" - assert device.dns == "1.1.1.1,2.2.2.2" + assert device.allowed_ips == [%Postgrex.INET{address: {127, 0, 0, 1}, netmask: nil}] + assert device.dns == ["1.1.1.1", "2.2.2.2"] assert device.endpoint == "example.com:51820" assert device.mtu == 1400 assert device.persistent_keepalive == 10 @@ -292,8 +291,9 @@ defmodule FzHttpWeb.Acceptance.AdminTest do # XXX: We need to show a confirmation dialog on delete, # and message once record was saved or deleted. - Process.sleep(200) - assert is_nil(Repo.one(FzHttp.Rules.Rule)) + wait_for(fn -> + assert is_nil(Repo.one(FzHttp.Rules.Rule)) + end) end end @@ -314,12 +314,16 @@ defmodule FzHttpWeb.Acceptance.AdminTest do |> visit(~p"/settings/client_defaults") |> assert_el(Query.text("Client Defaults", count: 2)) - assert configuration = FzHttp.Configurations.get_configuration!() + assert configuration = FzHttp.Config.fetch_db_config!() assert configuration.default_client_persistent_keepalive == 10 assert configuration.default_client_mtu == 1234 assert configuration.default_client_endpoint == "example.com:8123" - assert configuration.default_client_dns == "1.1.1.1,2.2.2.2" - assert configuration.default_client_allowed_ips == "192.0.0.0/0,::/0" + assert configuration.default_client_dns == ["1.1.1.1", "2.2.2.2"] + + assert configuration.default_client_allowed_ips == [ + %Postgrex.INET{address: {192, 0, 0, 0}, netmask: 0}, + %Postgrex.INET{address: {0, 0, 0, 0, 0, 0, 0, 0}, netmask: 0} + ] end end @@ -333,7 +337,7 @@ defmodule FzHttpWeb.Acceptance.AdminTest do |> click(Query.button("Save")) |> assert_el(Query.css("img[src=\"https://http.cat/200\"]")) - assert configuration = FzHttp.Configurations.get_configuration!() + assert configuration = FzHttp.Config.fetch_db_config!() assert configuration.logo.url == "https://http.cat/200" end end @@ -342,7 +346,7 @@ defmodule FzHttpWeb.Acceptance.AdminTest do feature "change security settings", %{ session: session } do - assert configuration = FzHttp.Configurations.get_configuration!() + assert configuration = FzHttp.Config.fetch_db_config!() assert configuration.local_auth_enabled == true assert configuration.allow_unprivileged_device_management == true assert configuration.allow_unprivileged_device_configuration == true @@ -357,7 +361,7 @@ defmodule FzHttpWeb.Acceptance.AdminTest do |> toggle("disable_vpn_on_oidc_error") |> assert_el(Query.text("Security Settings")) - assert configuration = FzHttp.Configurations.get_configuration!() + assert configuration = FzHttp.Config.fetch_db_config!() assert configuration.local_auth_enabled == false assert configuration.allow_unprivileged_device_management == false assert configuration.allow_unprivileged_device_configuration == false @@ -365,7 +369,7 @@ defmodule FzHttpWeb.Acceptance.AdminTest do end feature "change required authentication timeout", %{session: session} do - assert configuration = FzHttp.Configurations.get_configuration!() + assert configuration = FzHttp.Config.fetch_db_config!() assert configuration.vpn_session_duration == 0 session @@ -378,14 +382,14 @@ defmodule FzHttpWeb.Acceptance.AdminTest do |> assert_el(Query.text("Security Settings")) # XXX: We need to show a flash that settings are saved - Process.sleep(200) - - assert configuration = FzHttp.Configurations.get_configuration!() - assert configuration.vpn_session_duration == 604_800 + wait_for(fn -> + assert configuration = FzHttp.Config.fetch_db_config!() + assert configuration.vpn_session_duration == 604_800 + end) end feature "manage OpenIDConnect providers", %{session: session} do - {_bypass, uri} = FzHttp.ConfigurationsFixtures.discovery_document_server() + {_bypass, uri} = FzHttp.ConfigFixtures.discovery_document_server() # Create session = @@ -415,10 +419,10 @@ defmodule FzHttpWeb.Acceptance.AdminTest do |> assert_el(Query.text("oidc-foo-bar")) |> assert_el(Query.text("Firebook")) - assert [open_id_connect_provider] = FzHttp.Configurations.get!(:openid_connect_providers) + assert [open_id_connect_provider] = FzHttp.Config.fetch_config!(:openid_connect_providers) assert open_id_connect_provider == - %FzHttp.Configurations.Configuration.OpenIDConnectProvider{ + %FzHttp.Config.Configuration.OpenIDConnectProvider{ id: "oidc-foo-bar", label: "Firebook", scope: "openid email eyes_color", @@ -440,7 +444,7 @@ defmodule FzHttpWeb.Acceptance.AdminTest do |> assert_el(Query.text("Updated successfully.")) |> assert_el(Query.text("Metabook")) - assert [open_id_connect_provider] = FzHttp.Configurations.get!(:openid_connect_providers) + assert [open_id_connect_provider] = FzHttp.Config.fetch_config!(:openid_connect_providers) assert open_id_connect_provider.label == "Metabook" # Delete @@ -450,11 +454,11 @@ defmodule FzHttpWeb.Acceptance.AdminTest do assert_el(session, Query.text("Updated successfully.")) - assert FzHttp.Configurations.get!(:openid_connect_providers) == [] + assert FzHttp.Config.fetch_config!(:openid_connect_providers) == [] end feature "manage SAML providers", %{session: session} do - saml_metadata = FzHttp.SAMLIdentityProviderFixtures.metadata() + saml_metadata = FzHttp.ConfigFixtures.saml_metadata() # Create session = @@ -471,7 +475,7 @@ defmodule FzHttpWeb.Acceptance.AdminTest do |> fill_in(Query.fillable_field("saml_identity_provider[id]"), with: "foo-bar-buz") |> fill_in(Query.fillable_field("saml_identity_provider[label]"), with: "Sneaky ID") |> fill_in(Query.fillable_field("saml_identity_provider[base_url]"), - with: "http://localhost:4002/autX/saml#foo" + with: "http://localhost:13000/autX/saml#foo" ) |> fill_in(Query.fillable_field("saml_identity_provider[metadata]"), with: saml_metadata @@ -481,13 +485,13 @@ defmodule FzHttpWeb.Acceptance.AdminTest do |> assert_el(Query.text("foo-bar-buz")) |> assert_el(Query.text("Sneaky ID")) - assert [saml_identity_provider] = FzHttp.Configurations.get!(:saml_identity_providers) + assert [saml_identity_provider] = FzHttp.Config.fetch_config!(:saml_identity_providers) assert saml_identity_provider == - %FzHttp.Configurations.Configuration.SAMLIdentityProvider{ + %FzHttp.Config.Configuration.SAMLIdentityProvider{ id: "foo-bar-buz", label: "Sneaky ID", - base_url: "http://localhost:4002/autX/saml#foo", + base_url: "http://localhost:13000/autX/saml#foo", metadata: saml_metadata, sign_requests: false, sign_metadata: false, @@ -506,7 +510,7 @@ defmodule FzHttpWeb.Acceptance.AdminTest do |> assert_el(Query.text("Updated successfully.")) |> assert_el(Query.text("Sneaky XID")) - assert [saml_identity_provider] = FzHttp.Configurations.get!(:saml_identity_providers) + assert [saml_identity_provider] = FzHttp.Config.fetch_config!(:saml_identity_providers) assert saml_identity_provider.label == "Sneaky XID" # Delete @@ -516,7 +520,7 @@ defmodule FzHttpWeb.Acceptance.AdminTest do assert_el(session, Query.text("Updated successfully.")) - assert FzHttp.Configurations.get!(:saml_identity_providers) == [] + assert FzHttp.Config.fetch_config!(:saml_identity_providers) == [] end end diff --git a/apps/fz_http/test/fz_http_web/acceptance/authentication_test.exs b/apps/fz_http/test/fz_http_web/acceptance/authentication_test.exs index 14f4a1998..1604f050c 100644 --- a/apps/fz_http/test/fz_http_web/acceptance/authentication_test.exs +++ b/apps/fz_http/test/fz_http_web/acceptance/authentication_test.exs @@ -52,7 +52,7 @@ defmodule FzHttpWeb.Acceptance.AuthenticationTest do |> Auth.assert_authenticated(user) end - feature "can not reset password using invalid email", %{session: session} do + feature "can't reset password using invalid email", %{session: session} do UsersFixtures.create_user_with_role(:unprivileged) session @@ -116,7 +116,7 @@ defmodule FzHttpWeb.Acceptance.AuthenticationTest do end feature "does not create new users when auto_create_users is false", %{session: session} do - FzHttp.Configurations.put!(:local_auth_enabled, false) + FzHttp.Config.put_config!(:local_auth_enabled, false) :ok = SimpleSAML.setup_saml_provider(%{"auto_create_users" => false}) session @@ -204,7 +204,7 @@ defmodule FzHttpWeb.Acceptance.AuthenticationTest do :ok = Vault.setup_oidc_provider(@endpoint.url, %{"auto_create_users" => false}) :ok = Vault.upsert_user(oidc_login, user_attrs.email, oidc_password) - FzHttp.Configurations.put!(:local_auth_enabled, false) + FzHttp.Config.put_config!(:local_auth_enabled, false) session = visit(session, ~p"/") assert find(session, Query.css(".input", count: 0)) diff --git a/apps/fz_http/test/fz_http_web/acceptance/unprivileged_user_test.exs b/apps/fz_http/test/fz_http_web/acceptance/unprivileged_user_test.exs index 5803e444d..71f1ccca7 100644 --- a/apps/fz_http/test/fz_http_web/acceptance/unprivileged_user_test.exs +++ b/apps/fz_http/test/fz_http_web/acceptance/unprivileged_user_test.exs @@ -19,7 +19,7 @@ defmodule FzHttpWeb.Acceptance.UnprivilegedUserTest do feature "allows user to add and configure a device", %{ session: session } do - FzHttp.Configurations.put!(:allow_unprivileged_device_configuration, true) + FzHttp.Config.put_config!(:allow_unprivileged_device_configuration, true) session |> visit(~p"/user_devices") @@ -55,8 +55,8 @@ defmodule FzHttpWeb.Acceptance.UnprivilegedUserTest do assert device = Repo.one(FzHttp.Devices.Device) assert device.name == "big-head-007" assert device.description == "Dummy description" - assert device.allowed_ips == "127.0.0.1" - assert device.dns == "1.1.1.1,2.2.2.2" + assert device.allowed_ips == [%Postgrex.INET{address: {127, 0, 0, 1}, netmask: nil}] + assert device.dns == ["1.1.1.1", "2.2.2.2"] assert device.endpoint == "example.com:51820" assert device.mtu == 1400 assert device.persistent_keepalive == 10 @@ -67,7 +67,7 @@ defmodule FzHttpWeb.Acceptance.UnprivilegedUserTest do feature "allows user to add a device, download config and close the modal", %{ session: session } do - FzHttp.Configurations.put!(:allow_unprivileged_device_configuration, false) + FzHttp.Config.put_config!(:allow_unprivileged_device_configuration, false) session |> visit(~p"/user_devices") @@ -92,7 +92,7 @@ defmodule FzHttpWeb.Acceptance.UnprivilegedUserTest do end feature "does not allow adding devices", %{session: session} do - FzHttp.Configurations.put!(:allow_unprivileged_device_management, false) + FzHttp.Config.put_config!(:allow_unprivileged_device_management, false) session |> visit(~p"/user_devices") 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 c12d5717c..1a3e4ca4f 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,11 +1,11 @@ defmodule FzHttpWeb.AuthControllerTest do use FzHttpWeb.ConnCase, async: true - alias FzHttp.ConfigurationsFixtures + alias FzHttp.ConfigFixtures alias FzHttp.Repo setup do {bypass, _openid_connect_providers_attrs} = - ConfigurationsFixtures.start_openid_providers([ + ConfigFixtures.start_openid_providers([ "google", "okta", "auth0", @@ -15,9 +15,9 @@ defmodule FzHttpWeb.AuthControllerTest do "vault" ]) - FzHttp.Configurations.put!( + FzHttp.Config.put_config!( :saml_identity_providers, - [FzHttp.SAMLIdentityProviderFixtures.saml_attrs() |> Map.put("label", "SAML")] + [FzHttp.ConfigFixtures.saml_identity_providers_attrs(%{"label" => "SAML"})] ) %{bypass: bypass} @@ -68,14 +68,14 @@ defmodule FzHttpWeb.AuthControllerTest do test "GET /auth/identity omits forgot password link when local_auth disabled", %{ unauthed_conn: conn } do - FzHttp.Configurations.put!(:local_auth_enabled, false) + FzHttp.Config.put_config!(:local_auth_enabled, false) test_conn = get(conn, ~p"/auth/identity") assert text_response(test_conn, 404) == "Local auth disabled" end test "when local_auth is disabled responds with 404", %{unauthed_conn: conn} do - FzHttp.Configurations.put!(:local_auth_enabled, false) + FzHttp.Config.put_config!(:local_auth_enabled, false) test_conn = post(conn, ~p"/auth/identity/callback", %{}) assert text_response(test_conn, 404) == "Local auth disabled" @@ -127,7 +127,7 @@ defmodule FzHttpWeb.AuthControllerTest do "password" => "password1234" } - FzHttp.Configurations.put!(:local_auth_enabled, false) + FzHttp.Config.put_config!(:local_auth_enabled, false) test_conn = post(conn, ~p"/auth/identity/callback", params) assert text_response(test_conn, 404) == "Local auth disabled" @@ -136,7 +136,7 @@ defmodule FzHttpWeb.AuthControllerTest do describe "GET /auth/reset_password" do test "protects route when local_auth is disabled", %{unauthed_conn: conn} do - FzHttp.Configurations.put!(:local_auth_enabled, false) + FzHttp.Config.put_config!(:local_auth_enabled, false) test_conn = get(conn, ~p"/auth/reset_password") assert text_response(test_conn, 404) == "Local auth disabled" @@ -158,7 +158,7 @@ defmodule FzHttpWeb.AuthControllerTest do setup %{unauthed_conn: conn} = context do signed_state = Plug.Crypto.sign( - Application.fetch_env!(:fz_http, FzHttpWeb.Endpoint)[:secret_key_base], + FzHttp.Config.fetch_env!(:fz_http, FzHttpWeb.Endpoint)[:secret_key_base], @key <> "_cookie", @state, key: Plug.Keys, @@ -173,7 +173,7 @@ defmodule FzHttpWeb.AuthControllerTest do user: user, bypass: bypass } do - jwk = ConfigurationsFixtures.jwks_attrs() + jwk = ConfigFixtures.jwks_attrs() claims = %{"email" => user.email, "sub" => user.id} @@ -183,7 +183,7 @@ defmodule FzHttpWeb.AuthControllerTest do |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) |> JOSE.JWS.compact() - ConfigurationsFixtures.expect_refresh_token(bypass, %{"id_token" => token}) + ConfigFixtures.expect_refresh_token(bypass, %{"id_token" => token}) test_conn = get(conn, ~p"/auth/oidc/google/callback", @params) assert redirected_to(test_conn) == ~p"/users" @@ -192,7 +192,7 @@ defmodule FzHttpWeb.AuthControllerTest do end test "when a user returns with an invalid claim", %{unauthed_conn: conn, bypass: bypass} do - jwk = ConfigurationsFixtures.jwks_attrs() + jwk = ConfigFixtures.jwks_attrs() claims = %{"email" => "foo@example.com", "sub" => Ecto.UUID.generate()} @@ -202,7 +202,7 @@ defmodule FzHttpWeb.AuthControllerTest do |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) |> JOSE.JWS.compact() - ConfigurationsFixtures.expect_refresh_token(bypass, %{"id_token" => token}) + ConfigFixtures.expect_refresh_token(bypass, %{"id_token" => token}) test_conn = get(conn, ~p"/auth/oidc/google/callback", @params) @@ -307,7 +307,7 @@ defmodule FzHttpWeb.AuthControllerTest do end test "prevents signing in when local_auth_disabled", %{unauthed_conn: conn, user: user} do - FzHttp.Configurations.put!(:local_auth_enabled, false) + FzHttp.Config.put_config!(:local_auth_enabled, false) test_conn = get(conn, ~p"/auth/magic/#{user.id}/#{user.sign_in_token}") assert text_response(test_conn, 404) == "Local auth disabled" @@ -320,7 +320,7 @@ defmodule FzHttpWeb.AuthControllerTest do query = URI.encode_query(%{ "id_token_hint" => "abc", - "post_logout_redirect_uri" => FzHttp.Config.fetch_env!(:fz_http, :external_url) <> "/", + "post_logout_redirect_uri" => FzHttp.Config.fetch_env!(:fz_http, :external_url), "client_id" => "okta-client-id" }) 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 index b8ae6d204..b76906f10 100644 --- 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 @@ -1,6 +1,6 @@ defmodule FzHttpWeb.JSON.ConfigurationControllerTest do use FzHttpWeb.ApiCase, async: true - alias FzHttp.SAMLIdentityProviderFixtures + alias FzHttp.ConfigFixtures describe "GET /v0/configuration" do test "renders configuration" do @@ -12,7 +12,7 @@ defmodule FzHttpWeb.JSON.ConfigurationControllerTest do end test "renders logotype" do - FzHttp.Configurations.put!(:logo, %{"url" => "https://example.com/logo.png"}) + FzHttp.Config.put_config!(:logo, %{"url" => "https://example.com/logo.png"}) conn = get(authed_conn(), ~p"/v0/configuration") @@ -41,8 +41,8 @@ defmodule FzHttpWeb.JSON.ConfigurationControllerTest do %{ "id" => "google", "label" => "google", - "scope" => "test-scope", - "response_type" => "response-type", + "scope" => "email openid", + "response_type" => "code", "client_id" => "test-id", "client_secret" => "test-secret", "discovery_document_uri" => @@ -56,7 +56,7 @@ defmodule FzHttpWeb.JSON.ConfigurationControllerTest do "id" => "okta", "label" => "okta", "base_url" => "https://saml", - "metadata" => SAMLIdentityProviderFixtures.metadata(), + "metadata" => ConfigFixtures.saml_metadata(), "sign_requests" => false, "sign_metadata" => false, "signed_assertion_in_resp" => false, @@ -69,8 +69,8 @@ defmodule FzHttpWeb.JSON.ConfigurationControllerTest do "default_client_persistent_keepalive" => 1, "default_client_mtu" => 1100, "default_client_endpoint" => "new-endpoint", - "default_client_dns" => "1.1.1.1", - "default_client_allowed_ips" => "1.1.1.1,2.2.2.2" + "default_client_dns" => ["1.1.1.1"], + "default_client_allowed_ips" => ["1.1.1.1", "2.2.2.2"] } conn = @@ -91,8 +91,8 @@ defmodule FzHttpWeb.JSON.ConfigurationControllerTest do %{ "id" => "google", "label" => "google-label", - "scope" => "test-scope-2", - "response_type" => "response-type-2", + "scope" => "email openid", + "response_type" => "code", "client_id" => "test-id-2", "client_secret" => "test-secret-2", "discovery_document_uri" => @@ -106,7 +106,7 @@ defmodule FzHttpWeb.JSON.ConfigurationControllerTest do "id" => "okta", "label" => "okta-label", "base_url" => "https://saml-old", - "metadata" => SAMLIdentityProviderFixtures.metadata(), + "metadata" => ConfigFixtures.saml_metadata(), "sign_requests" => true, "sign_metadata" => true, "signed_assertion_in_resp" => true, @@ -119,8 +119,8 @@ defmodule FzHttpWeb.JSON.ConfigurationControllerTest do "default_client_persistent_keepalive" => 25, "default_client_mtu" => 1200, "default_client_endpoint" => "old-endpoint", - "default_client_dns" => "4.4.4.4", - "default_client_allowed_ips" => "8.8.8.8" + "default_client_dns" => ["4.4.4.4"], + "default_client_allowed_ips" => ["8.8.8.8"] } conn = put(authed_conn(), ~p"/v0/configuration", configuration: attrs) @@ -139,6 +139,24 @@ defmodule FzHttpWeb.JSON.ConfigurationControllerTest do assert json_response(conn, 422)["errors"] == %{"local_auth_enabled" => ["is invalid"]} end + test "renders error when trying to override a value with environment override" do + FzHttp.Config.put_system_env_override(:local_auth_enabled, true) + + attrs = %{ + "local_auth_enabled" => false + } + + conn = put(authed_conn(), ~p"/v0/configuration", configuration: attrs) + + assert json_response(conn, 422) == %{ + "errors" => %{ + "local_auth_enabled" => [ + "cannot be changed; it is overridden by LOCAL_AUTH_ENABLED environment variable" + ] + } + } + end + test "renders 401 for missing authorization header" do conn = put(unauthed_conn(), ~p"/v0/configuration") assert json_response(conn, 401)["errors"] == %{"auth" => "unauthenticated"} 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 index e9b0e46c9..fb6d302c5 100644 --- 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 @@ -16,8 +16,8 @@ defmodule FzHttpWeb.JSON.DeviceControllerTest do "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", + "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" } 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 70f546fee..f5c7704bb 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 @@ -28,7 +28,7 @@ defmodule FzHttpWeb.DeviceLive.Unprivileged.IndexTest do describe "authenticated device management disabled" do setup do - FzHttp.Configurations.put!(:allow_unprivileged_device_management, false) + FzHttp.Config.put_config!(:allow_unprivileged_device_management, false) :ok end @@ -49,7 +49,7 @@ defmodule FzHttpWeb.DeviceLive.Unprivileged.IndexTest do describe "authenticated device configuration disabled" do setup do - FzHttp.Configurations.put!(:allow_unprivileged_device_configuration, false) + FzHttp.Config.put_config!(:allow_unprivileged_device_configuration, false) :ok 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 3aca4ea09..62eb72a42 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 @@ -43,7 +43,7 @@ defmodule FzHttpWeb.DeviceLive.Unprivileged.ShowTest do } do {:ok, device: device} = create_device(user_id: user.id) - FzHttp.Configurations.put!(:allow_unprivileged_device_management, false) + FzHttp.Config.put_config!(: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 index b4545b923..8b5178c18 100644 --- 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 @@ -1,14 +1,14 @@ defmodule FzHttpWeb.SettingLive.ClientDefaultsTest do use FzHttpWeb.ConnCase, async: true - alias FzHttp.Configurations + alias FzHttp.Config describe "authenticated/client_defaults" do @valid_allowed_ips %{ - "configuration" => %{"default_client_allowed_ips" => "1.1.1.1"} + "configuration" => %{"default_client_allowed_ips" => ["1.1.1.1"]} } @valid_dns %{ - "configuration" => %{"default_client_dns" => "1.1.1.1"} + "configuration" => %{"default_client_dns" => ["1.1.1.1"]} } @valid_endpoint %{ "configuration" => %{"default_client_endpoint" => "1.1.1.1"} @@ -20,9 +20,6 @@ defmodule FzHttpWeb.SettingLive.ClientDefaultsTest do "configuration" => %{"default_client_mtu" => "1000"} } - @invalid_allowed_ips %{ - "configuration" => %{"default_client_allowed_ips" => "foobar"} - } @invalid_persistent_keepalive %{ "configuration" => %{"default_client_persistent_keepalive" => "-1"} } @@ -38,8 +35,13 @@ defmodule FzHttpWeb.SettingLive.ClientDefaultsTest do 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 + for allowed_ips <- Config.fetch_config!(:default_client_allowed_ips) do + assert html =~ to_string(allowed_ips) + end + + for dns <- Config.fetch_config!(:default_client_dns) do + assert html =~ to_string(dns) + end assert html =~ """ id="client_defaults_form_component_default_client_endpoint"\ @@ -120,14 +122,24 @@ defmodule FzHttpWeb.SettingLive.ClientDefaultsTest do test_view = view |> element("#client_defaults_form_component") - |> render_submit(@invalid_allowed_ips) + |> render_submit(%{ + "configuration" => %{"default_client_allowed_ips" => "foobar"} + }) assert test_view =~ "is invalid" - assert test_view =~ """ - \ - """ + assert Floki.find( + test_view, + "#client_defaults_form_component_default_client_allowed_ips" + ) == [ + {"textarea", + [ + {"class", "textarea is-danger"}, + {"id", "client_defaults_form_component_default_client_allowed_ips"}, + {"name", "configuration[default_client_allowed_ips]"}, + {"placeholder", "0.0.0.0/0, ::/0"} + ], ["\nfoobar"]} + ] end test "prevents invalid persistent_keepalive", %{view: view} do 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 af7f22b3f..bd720cbdb 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 @@ -3,7 +3,7 @@ defmodule FzHttpWeb.SettingLive.CustomizationTest do describe "logo" do setup %{admin_conn: conn} = context do - FzHttp.Configurations.put!(:logo, context[:logo]) + FzHttp.Config.put_config!(:logo, context[:logo]) path = ~p"/settings/customization" {:ok, view, html} = live(conn, path) @@ -50,7 +50,7 @@ defmodule FzHttpWeb.SettingLive.CustomizationTest do view |> element("input[value=Default]") |> render_click() view |> element("form") |> render_submit() - assert FzHttp.Configurations.get!(:logo) == nil + assert FzHttp.Config.fetch_config!(:logo) == nil end test "change to url", %{view: view, html: html} do @@ -58,7 +58,7 @@ defmodule FzHttpWeb.SettingLive.CustomizationTest do view |> element("input[value=URL]") |> render_click() view |> render_submit("save", %{"url" => "new"}) - assert %{url: "new"} = FzHttp.Configurations.get!(:logo) + assert %{url: "new"} = FzHttp.Config.fetch_config!(:logo) end test "change to upload", %{view: view, html: html} do @@ -80,7 +80,7 @@ defmodule FzHttpWeb.SettingLive.CustomizationTest do view |> render_submit("save", %{}) data = Base.encode64("new") - assert %{data: ^data, type: "image/jpeg"} = FzHttp.Configurations.get!(:logo) + assert %{data: ^data, type: "image/jpeg"} = FzHttp.Config.fetch_config!(: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 3db2af86b..1fe450e1f 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,7 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do use FzHttpWeb.ConnCase, async: true - alias FzHttp.Configurations + alias FzHttp.ConfigFixtures alias FzHttpWeb.SettingLive.Security - import FzHttp.SAMLIdentityProviderFixtures - import FzHttp.ConfigurationsFixtures describe "authenticated mount" do test "loads the active sessions table", %{admin_conn: conn} do @@ -18,8 +16,7 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do {:ok, _view, html} = live(conn, path) assert html =~ ~s|| - Configurations.get_configuration!() - |> Configurations.update_configuration(%{vpn_session_duration: 3_600}) + FzHttp.Config.put_config!(:vpn_session_duration, 3_600) {:ok, _view, html} = live(conn, path) assert html =~ ~s|| @@ -36,24 +33,33 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do end describe "session_duration_options/0" do - @expected_durations [ - Never: 0, - Once: 2_147_483_647, - "Every Hour": 3_600, - "Every Day": 86_400, - "Every Week": 604_800, - "Every 30 Days": 2_592_000, - "Every 90 Days": 7_776_000 - ] - test "displays the correct session duration integers" do - assert Security.session_duration_options() == @expected_durations + assert Security.session_duration_options({{:db, :foo}, 3_600}) == [ + {"Never", 0}, + {"Once", 2_147_483_647}, + {"Every Hour", 3_600}, + {"Every Day", 86_400}, + {"Every Week", 604_800}, + {"Every 30 Days", 2_592_000}, + {"Every 90 Days", 7_776_000} + ] + + assert Security.session_duration_options({{:env, "FOO"}, 1_234}) == [ + {"Never", 0}, + {"Once", 2_147_483_647}, + {"Every Hour", 3_600}, + {"Every Day", 86_400}, + {"Every Week", 604_800}, + {"Every 30 Days", 2_592_000}, + {"Every 90 Days", 7_776_000}, + {"Every 1234 seconds", 1_234} + ] end end describe "toggles" do setup %{conf_key: key, conf_val: val} do - FzHttp.Configurations.put!(key, val) + FzHttp.Config.put_config!(key, val) {:ok, path: ~p"/settings/security"} end @@ -70,31 +76,31 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do assert html =~ "checked" view |> element("input[phx-value-config=#{unquote(key)}]") |> render_click() - assert FzHttp.Configurations.get!(unquote(key)) == false + assert FzHttp.Config.fetch_config!(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 + local_auth_enabled: false, + allow_unprivileged_device_management: false, + allow_unprivileged_device_configuration: false, + disable_vpn_on_oidc_error: false ] do @tag conf_key: key, conf_val: val - test "toggle #{key} when value in db is nil", %{admin_conn: conn, path: path} do + test "toggle #{key} when value in db is false", %{admin_conn: conn, path: path} do {:ok, view, _html} = live(conn, path) html = view |> element("input[phx-value-config=#{unquote(key)}]") |> render() refute html =~ "checked" view |> element("input[phx-value-config=#{unquote(key)}]") |> render_click() - assert FzHttp.Configurations.get!(unquote(key)) == true + assert FzHttp.Config.fetch_config!(unquote(key)) == true end end end describe "oidc configuration" do setup %{admin_conn: conn} do - configuration(%{ + ConfigFixtures.configuration(%{ openid_connect_providers: [ %{ "id" => "test", @@ -158,7 +164,7 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do assert {:error, {:redirect, _}} = return - assert %FzHttp.Configurations.Configuration.OpenIDConnectProvider{ + assert %FzHttp.Config.Configuration.OpenIDConnectProvider{ id: "test", label: "updated", scope: "openid email profile", @@ -169,7 +175,7 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do "https://common.auth0.com/.well-known/openid-configuration", redirect_uri: nil, auto_create_users: false - } in FzHttp.Configurations.get!(:openid_connect_providers) + } in FzHttp.Config.fetch_config!(:openid_connect_providers) end test "delete", %{view: view} do @@ -177,24 +183,26 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do |> element("button[phx-value-key=\"test\"]", "Delete") |> render_click() - openid_connect_providers = FzHttp.Configurations.get!(:openid_connect_providers) + openid_connect_providers = FzHttp.Config.fetch_config!(:openid_connect_providers) assert Enum.map(openid_connect_providers, & &1.id) == ["test2"] view |> element("button[phx-value-key=\"test2\"]", "Delete") |> render_click() - assert FzHttp.Configurations.get!(:openid_connect_providers) == [] + assert FzHttp.Config.fetch_config!(:openid_connect_providers) == [] end end describe "saml configuration" do setup %{admin_conn: conn} do # Security views use the DB config, not cached config, so update DB here for testing - saml_attrs1 = saml_attrs() - saml_attrs2 = saml_attrs() |> Map.put("id", "test2") |> Map.put("label", "test2") + saml_attrs1 = ConfigFixtures.saml_identity_providers_attrs() - configuration(%{ + saml_attrs2 = + ConfigFixtures.saml_identity_providers_attrs(%{"id" => "test2", "label" => "test2"}) + + ConfigFixtures.configuration(%{ openid_connect_providers: [], saml_identity_providers: [saml_attrs1, saml_attrs2] }) @@ -225,7 +233,7 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do assert html =~ "{:fatal, {:expected_element_start_tag," assert html =~ "can't be blank" - attrs = saml_attrs() + attrs = ConfigFixtures.saml_identity_providers_attrs() return = view @@ -240,14 +248,14 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do assert {:error, {:redirect, _}} = return - saml_identity_providers = FzHttp.Configurations.get!(:saml_identity_providers) + saml_identity_providers = FzHttp.Config.fetch_config!(:saml_identity_providers) assert length(saml_identity_providers) == 3 - assert %FzHttp.Configurations.Configuration.SAMLIdentityProvider{ + assert %FzHttp.Config.Configuration.SAMLIdentityProvider{ auto_create_users: false, # XXX this field would be nil if we don't "guess" the url when we load the record in StartProxy - base_url: "#{FzHttp.Config.fetch_env!(:fz_http, :external_url)}/auth/saml", + base_url: "#{FzHttp.Config.fetch_env!(:fz_http, :external_url)}auth/saml", id: "FAKEID", label: "FOO", metadata: attrs["metadata"], @@ -265,7 +273,7 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do |> render_click() assert html =~ ~s|| - assert html =~ ~s|entityID="http://localhost:8080/realms/firezone| + assert html =~ ~s|entityID="| assert html =~ ~s| Enum.find(fn saml_identity_provider -> saml_identity_provider.id == "new_id" end) @@ -294,7 +302,7 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do end test "validate", %{view: view} do - attrs = saml_attrs() + attrs = ConfigFixtures.saml_identity_providers_attrs() view |> element("a[href=\"/settings/security/saml/test/edit\"]", "Edit") @@ -308,9 +316,9 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do # stays on the modal assert html =~ ~s|| - assert %FzHttp.Configurations.Configuration.SAMLIdentityProvider{ + assert %FzHttp.Config.Configuration.SAMLIdentityProvider{ auto_create_users: true, - base_url: "#{FzHttp.Config.fetch_env!(:fz_http, :external_url)}/auth/saml", + base_url: "#{FzHttp.Config.fetch_env!(:fz_http, :external_url)}auth/saml", id: attrs["id"], label: attrs["label"], metadata: attrs["metadata"], @@ -318,7 +326,7 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do sign_requests: true, signed_assertion_in_resp: true, signed_envelopes_in_resp: true - } in FzHttp.Configurations.get!(:saml_identity_providers) + } in FzHttp.Config.fetch_config!(:saml_identity_providers) end test "delete", %{view: view} do @@ -326,14 +334,14 @@ defmodule FzHttpWeb.SettingLive.SecurityTest do |> element("button[phx-value-key=\"test\"]", "Delete") |> render_click() - saml_identity_providers = FzHttp.Configurations.get!(:saml_identity_providers) + saml_identity_providers = FzHttp.Config.fetch_config!(:saml_identity_providers) assert Enum.map(saml_identity_providers, & &1.id) == ["test2"] view |> element("button", "Delete") |> render_click() - assert FzHttp.Configurations.get!(:saml_identity_providers) == [] + assert FzHttp.Config.fetch_config!(:saml_identity_providers) == [] 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 57901e191..dd6f44547 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 @@ -37,7 +37,7 @@ defmodule FzHttpWeb.UserLive.ShowTest do "description" => "new_description" } } - @allowed_ips "2.2.2.2" + @allowed_ips ["2.2.2.2"] @allowed_ips_change %{ "device" => %{ "public_key" => @test_pubkey, @@ -52,7 +52,7 @@ defmodule FzHttpWeb.UserLive.ShowTest do "allowed_ips" => @allowed_ips } } - @dns "8.8.8.8, 8.8.4.4" + @dns ["8.8.8.8", "8.8.4.4"] @dns_change %{ "device" => %{ "public_key" => @test_pubkey, @@ -261,7 +261,10 @@ defmodule FzHttpWeb.UserLive.ShowTest do {:ok, _view, html} = live(conn, path) path = ~p"/devices/#{device_id(html)}" {:ok, _view, html} = live(conn, path) - assert html =~ @allowed_ips + + for allowed_ip <- @allowed_ips do + assert html =~ allowed_ip + end end test "allows dns changes", %{admin_conn: conn, admin_user: user} do @@ -279,7 +282,10 @@ defmodule FzHttpWeb.UserLive.ShowTest do {:ok, _view, html} = live(conn, path) path = ~p"/devices/#{device_id(html)}" {:ok, _view, html} = live(conn, path) - assert html =~ @dns + + for dns <- @dns do + assert html =~ dns + end end test "allows endpoint changes", %{admin_conn: conn, admin_user: user} do 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 d3d330b0d..657413b7b 100644 --- a/apps/fz_http/test/fz_http_web/mailer_test.exs +++ b/apps/fz_http/test/fz_http_web/mailer_test.exs @@ -8,24 +8,6 @@ defmodule FzHttpWeb.MailerTest do assert Mailer.default_email().from == {"", "test@firez.one"} end - test "from_configuration/1" do - attrs = %{ - "from" => "foo@localhost", - "provider" => "smtp", - "configs" => %{"smtp" => %{"config_key" => "config_value"}} - } - - mailer = - FzHttp.Configurations.Mailer.changeset(attrs) - |> Ecto.Changeset.apply_changes() - - assert Mailer.from_configuration(mailer) == [ - from_email: "foo@localhost", - adapter: Swoosh.Adapters.SMTP, - config_key: "config_value" - ] - end - describe "with templates" do defmodule SampleEmail do use Phoenix.Swoosh, 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 01f203b29..6eb3ce809 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 @@ -26,7 +26,7 @@ defmodule FzHttpWeb.UserFromAuthTest do describe "find_or_create/2 via OIDC with auto create enabled" do test "sign in creates user", %{email: email} do - FzHttp.ConfigurationsFixtures.start_openid_providers(["google"], %{ + FzHttp.ConfigFixtures.start_openid_providers(["google"], %{ "auto_create_users" => true }) @@ -43,12 +43,12 @@ defmodule FzHttpWeb.UserFromAuthTest do describe "find_or_create/2 via OIDC with auto create disabled" do test "sign in returns error", %{email: email} do {_bypass, [openid_connect_provider_attrs]} = - FzHttp.ConfigurationsFixtures.start_openid_providers(["google"]) + FzHttp.ConfigFixtures.start_openid_providers(["google"]) openid_connect_provider_attrs = Map.put(openid_connect_provider_attrs, "auto_create_users", false) - FzHttp.Configurations.put!( + FzHttp.Config.put_config!( :openid_connect_providers, [openid_connect_provider_attrs] ) @@ -64,9 +64,9 @@ defmodule FzHttpWeb.UserFromAuthTest do end describe "find_or_create/2 via SAML with auto create enabled" do - @tag config: [FzHttp.SAMLIdentityProviderFixtures.saml_attrs()] + @tag config: [FzHttp.ConfigFixtures.saml_identity_providers_attrs()] test "sign in creates user", %{config: config, email: email} do - FzHttp.Configurations.put!(:saml_identity_providers, config) + FzHttp.Config.put_config!(:saml_identity_providers, config) assert {:ok, result} = UserFromAuth.find_or_create(:saml, "test", %{"email" => email, "sub" => :noop}) @@ -77,10 +77,10 @@ defmodule FzHttpWeb.UserFromAuthTest do describe "find_or_create/2 via SAML with auto create disabled" do @tag config: [ - FzHttp.SAMLIdentityProviderFixtures.saml_attrs() |> Map.put("auto_create_users", false) + FzHttp.ConfigFixtures.saml_identity_providers_attrs(%{"auto_create_users" => false}) ] test "sign in returns error", %{email: email, config: config} do - FzHttp.Configurations.put!(:saml_identity_providers, config) + FzHttp.Config.put_config!(:saml_identity_providers, config) assert {:error, "user not found and auto_create_users disabled"} = UserFromAuth.find_or_create(:saml, "test", %{"email" => email, "sub" => :noop}) diff --git a/apps/fz_http/test/fz_http_web/views/shared_view_test.exs b/apps/fz_http/test/fz_http_web/views/shared_view_test.exs new file mode 100644 index 000000000..19c826b4a --- /dev/null +++ b/apps/fz_http/test/fz_http_web/views/shared_view_test.exs @@ -0,0 +1,19 @@ +defmodule FzHttpWeb.SharedViewTest do + use ExUnit.Case, async: true + import FzHttpWeb.SharedView + + describe "to_human_bytes/1" do + test "handles expected cases" do + for {bytes, str} <- [ + {nil, "0.00 B"}, + {1_023, "1023.00 B"}, + {1_023_999_999_999_999_999_999, "888.18 EiB"}, + {1_000, "1000.00 B"}, + {1_115, "1.09 KiB"}, + {987_654_321_123_456_789_987, "856.65 EiB"} + ] do + assert to_human_bytes(bytes) == str + end + end + end +end diff --git a/apps/fz_http/test/support/acceptance_case.ex b/apps/fz_http/test/support/acceptance_case.ex index e6dc42dc2..4386a09f5 100644 --- a/apps/fz_http/test/support/acceptance_case.ex +++ b/apps/fz_http/test/support/acceptance_case.ex @@ -144,6 +144,29 @@ defmodule FzHttpWeb.AcceptanceCase do end end + def wait_for(assertion_callback, started_at \\ nil) do + now = :erlang.monotonic_time(:milli_seconds) + started_at = started_at || now + + try do + assertion_callback.() + rescue + e in [ExUnit.AssertionError] -> + time_spent = now - started_at + max_wait_seconds = fetch_max_wait_seconds!() + + if time_spent > :timer.seconds(max_wait_seconds) do + reraise(e, __STACKTRACE__) + else + floor(time_spent / 10) + |> max(100) + |> :timer.sleep() + + wait_for(assertion_callback, started_at) + end + end + end + def fill_form(session, %{} = fields) do # Wait for form to be rendered {form_el, _opts} = Enum.at(fields, 0) @@ -246,7 +269,7 @@ defmodule FzHttpWeb.AcceptanceCase do IO.puts( IO.ANSI.red() <> "Warning! This test runs in browser-debug mode, " <> - "it sleep the test process on failure for 50 seconds." <> IO.ANSI.reset() + "it will sleep the test process for infinity." <> IO.ANSI.reset() ) IO.puts("") @@ -254,7 +277,7 @@ defmodule FzHttpWeb.AcceptanceCase do IO.puts("Exception was rescued:") IO.puts(Exception.format(:error, e, __STACKTRACE__)) IO.puts(IO.ANSI.reset()) - Process.sleep(:timer.seconds(50)) + Process.sleep(:infinity) Wallaby.screenshot_on_failure?() -> unquote(__MODULE__).take_screenshot(unquote(message)) diff --git a/apps/fz_http/test/support/acceptance_case/simple_saml.ex b/apps/fz_http/test/support/acceptance_case/simple_saml.ex index 1621f9364..1747582e1 100644 --- a/apps/fz_http/test/support/acceptance_case/simple_saml.ex +++ b/apps/fz_http/test/support/acceptance_case/simple_saml.ex @@ -10,7 +10,7 @@ defmodule FzHttpWeb.AcceptanceCase.SimpleSAML do def setup_saml_provider(attrs_overrides \\ %{}) do metadata = fetch_metadata!(@endpoint) - FzHttp.Configurations.put!(:saml_identity_providers, [ + FzHttp.Config.put_config!(:saml_identity_providers, [ %{ "id" => "mysamlidp", "label" => "test-saml-idp", diff --git a/apps/fz_http/test/support/acceptance_case/vault.ex b/apps/fz_http/test/support/acceptance_case/vault.ex index 493f85b93..33b09dbc9 100644 --- a/apps/fz_http/test/support/acceptance_case/vault.ex +++ b/apps/fz_http/test/support/acceptance_case/vault.ex @@ -50,7 +50,7 @@ defmodule FzHttpWeb.AcceptanceCase.Vault do {:ok, {200, params}} = request(:get, "identity/oidc/client/firezone") - FzHttp.Configurations.put!( + FzHttp.Config.put_config!( :openid_connect_providers, [ %{ diff --git a/apps/fz_http/test/support/conn_case.ex b/apps/fz_http/test/support/conn_case.ex index bd1004fad..1a9390e19 100644 --- a/apps/fz_http/test/support/conn_case.ex +++ b/apps/fz_http/test/support/conn_case.ex @@ -40,6 +40,11 @@ defmodule FzHttpWeb.ConnCase do end end + # def assert_element(html, selector) do + # elements = Floki.find(html, selector) + + # end + def new_conn do Phoenix.ConnTest.build_conn() end diff --git a/apps/fz_http/test/support/api_doc_formatter.ex b/apps/fz_http/test/support/docs_generator.ex similarity index 55% rename from apps/fz_http/test/support/api_doc_formatter.ex rename to apps/fz_http/test/support/docs_generator.ex index 729c6e11b..377bd2fea 100644 --- a/apps/fz_http/test/support/api_doc_formatter.ex +++ b/apps/fz_http/test/support/docs_generator.ex @@ -1,9 +1,170 @@ -defmodule Firezone.DocusaurusWriter do +defmodule DocsGenerator do + alias FzHttp.Config.Definition + @keep_req_headers ["authorization"] @keep_resp_headers ["content-type", "location"] def write(conns, path) do + write_config_doc!(FzHttp.Config.Definitions, "../../www/docs/reference/env-vars.mdx") File.mkdir_p!(path) + write_api_doc!(conns, path) + end + + def write_config_doc!(module, file_path) do + file = File.open!(file_path, [:write, :utf8]) + + w!(file, "---") + + w!( + file, + docusaurus_header( + title: "Environment Variables", + sidebar_position: 1 + ) + ) + + w!(file, "---") + + with {:ok, doc} <- Definition.fetch_doc(module) do + w!(file, doc) + end + + w!(file, "## Environment Variable Listing") + w!(file, "We recommend setting these in your Docker ENV file (`$HOME/.firezone/.env` by") + w!(file, "default). Required fields in **bold**.") + + keys = + Enum.flat_map(module.doc_sections(), fn + {header, description, keys} -> + w_env_vars!(file, module, header, description, keys) + keys + + {header, keys} -> + w_env_vars!(file, module, header, nil, keys) + keys + end) + + all_keys = module.configs() |> Enum.map(&elem(&1, 1)) + w_env_vars!(file, module, "Other", nil, all_keys -- keys) + end + + defp w_env_vars!(_file, _module, _header, _description, []), do: :ok + + defp w_env_vars!(file, module, header, description, keys) do + w!(file, "") + w!(file, "### #{header}") + if description, do: w!(file, description) + + w!(file, "") + w!(file, "| Env Key | Description | Format | Default |") + w!(file, "| ------ | --------------- | ------ | ------- |") + + for key <- keys do + with {:ok, doc} <- Definition.fetch_doc(module, key) do + {type, {resolve_opts, _validate_opts, _dump_opts, _debug_opts}} = + Definition.fetch_spec_and_opts!(module, key) + + default = Keyword.get(resolve_opts, :default) + required? = if Keyword.has_key?(resolve_opts, :default), do: false, else: true + + key = FzHttp.Config.Resolver.env_key(key) + key = if required?, do: "**#{key}**", else: key + + doc = doc_env(doc) + + {type, default} = type_and_default(type, default) + + w!(file, "| #{key} | #{doc} | #{type} | #{default} |") + end + end + end + + defp doc_env(doc) do + doc + |> String.trim() + |> String.replace("\n * `", "
- `") + |> String.replace("```json", "```") + |> String.replace("\n\n", "

") + |> String.replace("\n", " ") + end + + defp type_and_default(type, default) when is_function(default), + do: type_and_default(type, "generated") + + defp type_and_default(type, nil), + do: type_and_default(type, "") + + defp type_and_default(type, []), + do: type_and_default(type, "[]") + + defp type_and_default({:parameterized, Ecto.Enum, opts}, default) do + values = + opts.mappings + |> Keyword.keys() + # DEPRECATION 0.8: We remove legacy keys here to prevent people from using it in new installs + |> Kernel.--([:smtp, :mailgun, :mandrill, :sendgrid, :post_mark, :sendmail]) + |> Enum.map(&to_string/1) + |> Enum.map(&String.trim_leading(&1, "Elixir.")) + |> Enum.map_join(", ", &"`#{&1}`") + + default = + default + |> Atom.to_string() + |> String.trim_leading("Elixir.") + + {"One of #{values}", "`#{default}`"} + end + + defp type_and_default(FzHttp.Types.CIDR, default), + do: {"CIDR", default} + + defp type_and_default(FzHttp.Types.IP, default), + do: {"IP", default} + + defp type_and_default(FzHttp.Types.IPPort, default), + do: {"IP with port", default} + + defp type_and_default(:integer, default), + do: {"integer", default} + + defp type_and_default(:string, default), + do: {"string", default} + + defp type_and_default(:boolean, default), + do: {"boolean", default} + + defp type_and_default(:map, default), + do: {"JSON-encoded map", "`" <> Jason.encode!(default) <> "`"} + + defp type_and_default(:embed, default), + do: {"JSON-encoded map", "`" <> Jason.encode!(default) <> "`"} + + defp type_and_default({:one_of, types}, default) do + types = + types + |> Enum.map(&type_and_default(&1, default)) + |> Enum.map(&elem(&1, 0)) + |> Enum.map(&to_string/1) + |> Enum.map_join(", ", &"`#{&1}`") + + {"one of #{types}", default} + end + + defp type_and_default({:json_array, _}, default), + do: {"JSON-encoded list", "`" <> Jason.encode!(default) <> "`"} + + defp type_and_default({:array, separator, type}, default) do + {type, default} = type_and_default(type, default) + {"a list of #{type} separated by `#{separator}`", default} + end + + defp type_and_default(type, default) when not is_binary(default), + do: type_and_default(type, inspect(default)) + + defp type_and_default(type, default), + do: {inspect(type), "`" <> default <> "`"} + + defp write_api_doc!(conns, path) do routes = Phoenix.Router.routes(List.first(conns).private.phoenix_router) conns diff --git a/apps/fz_http/test/support/fixtures/configurations_fixtures.ex b/apps/fz_http/test/support/fixtures/config_fixtures.ex similarity index 91% rename from apps/fz_http/test/support/fixtures/configurations_fixtures.ex rename to apps/fz_http/test/support/fixtures/config_fixtures.ex index ecfa9863a..c7e76e49c 100644 --- a/apps/fz_http/test/support/fixtures/configurations_fixtures.ex +++ b/apps/fz_http/test/support/fixtures/config_fixtures.ex @@ -1,19 +1,14 @@ -defmodule FzHttp.ConfigurationsFixtures do +defmodule FzHttp.ConfigFixtures do @moduledoc """ Allows for easily updating configuration in tests. """ + alias FzHttp.Repo + alias FzHttp.Config - alias FzHttp.{ - Configurations, - Configurations.Configuration, - Repo - } - - @doc "Configurations table holds a singleton record." - def configuration(%Configuration{} = conf \\ Configurations.get_configuration!(), attrs) do + def configuration(%Config.Configuration{} = conf \\ Config.fetch_db_config!(), attrs) do {:ok, configuration} = conf - |> Configuration.changeset(attrs) + |> Config.Configuration.Changeset.changeset(attrs) |> Repo.update() configuration @@ -32,11 +27,25 @@ defmodule FzHttp.ConfigurationsFixtures do |> Map.merge(overrides) end) - Configurations.put!(:openid_connect_providers, openid_connect_providers_attrs) + Config.put_config!(:openid_connect_providers, openid_connect_providers_attrs) {bypass, openid_connect_providers_attrs} end + def openid_connect_provider_attrs(overrides \\ %{}) do + Enum.into(overrides, %{ + "id" => "google", + "discovery_document_uri" => "https://firezone.example.com/.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", + "auto_create_users" => false + }) + end + defp openid_connect_providers_attrs(discovery_document_url) do [ %{ @@ -267,10 +276,13 @@ defmodule FzHttp.ConfigurationsFixtures do |> Plug.Parsers.call(opts) end - def saml_identity_providers_attrs do - [ - %{"id" => "test", "label" => "SAML"} - ] + def saml_identity_providers_attrs(attrs \\ %{}) do + Enum.into(attrs, %{ + "metadata" => saml_metadata(), + "label" => "test", + "id" => "test", + "auto_create_users" => true + }) end def saml_metadata do diff --git a/apps/fz_http/test/support/fixtures/openid_connect_provider_fixtures.ex b/apps/fz_http/test/support/fixtures/openid_connect_provider_fixtures.ex deleted file mode 100644 index 23e25a31f..000000000 --- a/apps/fz_http/test/support/fixtures/openid_connect_provider_fixtures.ex +++ /dev/null @@ -1,19 +0,0 @@ -defmodule FzHttp.OpenIDConnectProviderFixtures do - @moduledoc """ - Fixtures for OIDC configs. - """ - - def oidc_attrs do - %{ - "discovery_document_uri" => "https://okta/.well-known/openid-configuration", - "client_id" => "okta-client-id", - "client_secret" => "okta-client-secret", - "redirect_uri" => "https://localhost", - "id" => "okta", - "label" => "Okta", - "scope" => "openid profile email", - "response_type" => "code", - "auto_create_users" => true - } - end -end diff --git a/apps/fz_http/test/support/fixtures/saml_identity_provider_fixtures.ex b/apps/fz_http/test/support/fixtures/saml_identity_provider_fixtures.ex deleted file mode 100644 index a62349c27..000000000 --- a/apps/fz_http/test/support/fixtures/saml_identity_provider_fixtures.ex +++ /dev/null @@ -1,43 +0,0 @@ -defmodule FzHttp.SAMLIdentityProviderFixtures do - @moduledoc """ - Fixtures for SAML configs. - """ - - @metadata """ - - - - - - pdSMtx2s3RVVhxg_qJOjHhlZhwZk6JiBMiSm5PEgjkA - - MIICnzCCAYcCBgGD18ZU8TANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhmaXJlem9uZTAeFw0yMjEwMTQxODMyMjJaFw0zMjEwMTQxODM0MDJaMBMxETAPBgNVBAMMCGZpcmV6b25lMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAur5Cb0jrDJbMwr96WWE+z9CjDg0A/uRkaB4loRqkmu3A2fQGsS6CP7F7lQWMJmpzvBgkNtB69toO2sgx1u1fhpIJBZ0uSHF5gnzQAivgVxInvkMKRTRSkpMbhObiDHZnEGI2+Ly+8iV8IvprdrbDgm52u4conam0H1PewUKkHulrVQ+ImFuEWAjKCRSqpUG2F1eRkA0YpqB09x0CZAOOoucwTsBYj/ZAz3dUXhYIENAF7v0ykvzGOCAyOZIn1uYQc7jvWpwoI8qQdL45phj2FLoFlght3tlZV8IG5hsXrE6rg7Ufqvv8xyGltrOMKj/jEFEunagZOUjkypDp36b8cwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBEZKLLr66GB3NxqXGTMl0PvTDNB9GdyShQHaJYjeeUQnEXixjlAVrOq/txEBKjhGUcqyFELoNuwcxxV1iHA5oXhCoqYmnp9T/ftmXPDT3c49PBABHgLJaFOKYTpVx1YjP7mA44X1ijLZmgboIeeFNerVNHIzR9BsxcloQlB0r9QfC14rsuXo6QD3QnaVI8wDgWXQHqpcwLFqvehXdNvMFniRvX2qBNU8E0FPoMaZ1C3n2nssLcVZ+C4ghq6YoAG+wLGY7XE8+v5rnYGDpGpfgr2wdefn6tryFq3PyGqA8ThjARESRRQG9kI/RlNX7qCnP/8/7JQ4wLdfz5C25uhakP - - - - - - - - urn:oasis:names:tc:SAML:2.0:nameid-format:persistent - urn:oasis:names:tc:SAML:2.0:nameid-format:transient - urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified - urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress - - - - - - - """ - def metadata, do: @metadata - - def saml_attrs do - %{ - "metadata" => @metadata, - "label" => "test", - "id" => "test", - "auto_create_users" => true - } - end -end diff --git a/apps/fz_http/test/test_helper.exs b/apps/fz_http/test/test_helper.exs index 2cf491681..ec27e1228 100644 --- a/apps/fz_http/test/test_helper.exs +++ b/apps/fz_http/test/test_helper.exs @@ -2,7 +2,7 @@ Path.join(File.cwd!(), "screenshots") |> File.rm_rf!() Bureaucrat.start( - writer: Firezone.DocusaurusWriter, + writer: DocsGenerator, default_path: "../../www/docs/reference/rest-api" ) diff --git a/apps/fz_vpn/lib/fz_vpn/server.ex b/apps/fz_vpn/lib/fz_vpn/server.ex index 9b2916d23..ea4f6d174 100644 --- a/apps/fz_vpn/lib/fz_vpn/server.ex +++ b/apps/fz_vpn/lib/fz_vpn/server.ex @@ -9,11 +9,10 @@ defmodule FzVpn.Server do alias FzVpn.Interface alias FzVpn.Keypair - @process_opts Application.compile_env(:fz_vpn, :server_process_opts, []) @init_timeout 10_000 def start_link(_) do - GenServer.start_link(__MODULE__, %{}, @process_opts) + GenServer.start_link(__MODULE__, %{}, name: {:global, :fz_vpn_server}) end @impl GenServer diff --git a/apps/fz_wall/lib/fz_wall/cli/live.ex b/apps/fz_wall/lib/fz_wall/cli/live.ex index 708465001..5c60d2cce 100644 --- a/apps/fz_wall/lib/fz_wall/cli/live.ex +++ b/apps/fz_wall/lib/fz_wall/cli/live.ex @@ -5,11 +5,8 @@ defmodule FzWall.CLI.Live do Rules operate on the nftables forward chain to deny outgoing packets to specified IP addresses, ports, and protocols from Firezone device IPs. """ - import FzWall.CLI.Helpers.Sets import FzWall.CLI.Helpers.Nft - import FzCommon.FzNet, only: [ip_type: 1] - require Logger @doc """ Setup @@ -149,10 +146,9 @@ defmodule FzWall.CLI.Live do end defp proto(ip) do - case ip_type("#{ip}") do - "IPv4" -> :ip - "IPv6" -> :ip6 - "unknown" -> raise "Unknown protocol." + case FzHttp.Types.IP.cast(ip) do + {:ok, %{address: address}} when tuple_size(address) == 4 -> :ip + {:ok, %{address: address}} when tuple_size(address) == 6 -> :ip6 end end diff --git a/apps/fz_wall/lib/fz_wall/server.ex b/apps/fz_wall/lib/fz_wall/server.ex index 94cc8fa97..97f0fa0e6 100644 --- a/apps/fz_wall/lib/fz_wall/server.ex +++ b/apps/fz_wall/lib/fz_wall/server.ex @@ -2,15 +2,13 @@ defmodule FzWall.Server do @moduledoc """ Functions for applying firewall rules. """ - use GenServer import FzWall.CLI - @process_opts Application.compile_env(:fz_wall, :server_process_opts, []) @init_timeout 1_000 def start_link(_) do - GenServer.start_link(__MODULE__, %{}, @process_opts) + GenServer.start_link(__MODULE__, %{}, name: {:global, :fz_wall_server}) end @impl GenServer diff --git a/config/config.exs b/config/config.exs index 8271cbfb4..d39bb2b6a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,5 +1,125 @@ +# Couple rules: +# +# 1. This file should contain all supported application environment variables, +# even if they are overridden in `runtime.exs`, because it's the main source of +# truth and self-documentation. +# +# 2. The configurations here should be as close to `dev` environment as possible, +# to prevent having too many overrides in other files. import Config +config :fz_http, supervision_tree_mode: :full + +config :fz_http, ecto_repos: [FzHttp.Repo] +config :fz_http, sql_sandbox: false + +config :fz_http, FzHttp.Repo, + hostname: "localhost", + username: "postgres", + password: "postgres", + database: "firezone_dev", + show_sensitive_data_on_connection_error: true, + pool_size: :erlang.system_info(:logical_processors_available) * 2, + queue_target: 500, + queue_interval: 1000, + migration_timestamps: [type: :timestamptz] + +config :fz_http, + external_url: "http://localhost:13000/", + path_prefix: "/" + +config :fz_http, FzHttpWeb.Endpoint, + url: [ + scheme: "http", + host: "localhost", + port: 13000, + path: nil + ], + render_errors: [view: FzHttpWeb.ErrorView, accepts: ~w(html json)], + pubsub_server: FzHttp.PubSub, + secret_key_base: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5SD", + live_view: [ + signing_salt: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDejX" + ] + +config :fz_http, + wireguard_ipv4_enabled: true, + wireguard_ipv4_network: "100.64.0.0/10", + wireguard_ipv4_address: "100.64.0.1", + wireguard_ipv6_enabled: true, + wireguard_ipv6_network: "fd00::/106", + wireguard_ipv6_address: "fd00::1" + +config :fz_http, + saml_entity_id: "urn:firezone.dev:firezone-app", + 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__) + +config :fz_http, + external_trusted_proxies: [], + private_clients: ["172.28.0.0/16"] + +config :fz_http, + telemetry_id: "firezone-dev", + telemetry_module: FzCommon.MockTelemetry + +config :fz_http, + cookie_secure: false, + cookie_signing_salt: "WjllcThpb2Y=", + cookie_encryption_salt: "M0EzM0R6NEMyaw==" + +config :fz_http, + http_client: HTTPoison, + http_client_options: [], + connectivity_checks_enabled: true, + connectivity_checks_interval: 43_200, + connectivity_checks_url: "https://ping-dev.firez.one/" + +config :fz_http, + admin_email: "firezone@localhost", + default_admin_password: "firezone1234" + +config :fz_http, + max_devices_per_user: 10 + +############################### +##### FZ Firewall configs ##### +############################### + +config :fz_wall, cli: FzWall.CLI.Sandbox + +config :fz_wall, + wireguard_ipv4_masquerade: true, + wireguard_ipv6_masquerade: true, + wireguard_interface_name: "wg-firezone", + nft_path: "nft", + egress_interface: "dummy" + +config :fz_wall, + port_based_rules_supported: true + +############################### +##### FZ VPN configs ########## +############################### + +# This will be changed per-env +config :fz_vpn, + wireguard_private_key_path: "priv/wg_dev_private_key", + stats_push_service_enabled: true, + wireguard_interface_name: "wg-firezone", + wireguard_port: 51_820, + wg_adapter: FzVpn.Interface.WGAdapter.Live, + supervised_children: [FzVpn.Server, FzVpn.StatsPushService] + +############################### +##### Third-party configs ##### +############################### + +config :logger, :console, + level: String.to_atom(System.get_env("LOG_LEVEL", "info")), + format: "$time $metadata[$level] $message\n", + metadata: :all + # Use Jason for JSON parsing in Phoenix config :phoenix, :json_library, Jason @@ -8,6 +128,11 @@ config :posthog, api_url: "https://t.firez.one", api_key: "phc_ubuPhiqqjMdedpmbWpG2Ak3axqv5eMVhFDNBaXl9UZK" +config :ueberauth, Ueberauth, + providers: [ + identity: {Ueberauth.Strategy.Identity, callback_methods: ["POST"], uid_field: :email} + ] + # Guardian configuration config :fz_http, FzHttpWeb.Auth.HTML.Authentication, issuer: "fz_http", @@ -19,68 +144,6 @@ config :fz_http, FzHttpWeb.Auth.JSON.Authentication, # 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, - http_client_options: [], - external_trusted_proxies: [], - private_clients: [], - sandbox: true, - telemetry_id: "543aae08-5a2b-428d-b704-2956dd3f5a57", - wireguard_ipv4_enabled: true, - wireguard_ipv4_network: "100.64.0.0/10", - wireguard_ipv4_address: "100.64.0.1", - wireguard_ipv6_enabled: true, - wireguard_ipv6_network: "fd00::/106", - wireguard_ipv6_address: "fd00::1", - max_devices_per_user: 10, - telemetry_module: FzCommon.Telemetry, - supervision_tree_mode: :full, - http_client: HTTPoison, - connectivity_checks_enabled: true, - connectivity_checks_interval: 43_200, - connectivity_checks_url: "https://ping-dev.firez.one/", - cookie_secure: true, - cookie_signing_salt: "Z9eq8iof", - cookie_encryption_salt: "3A33Dz4C2k", - ecto_repos: [FzHttp.Repo], - admin_email: "firezone@localhost", - default_admin_password: "firezone1234", - server_process_opts: [name: {:global, :fz_http_server}], - saml_entity_id: "urn:firezone.dev:firezone-app", - 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__) - -config :fz_wall, - cli: FzWall.CLI.Sandbox, - wireguard_ipv4_masquerade: true, - wireguard_ipv6_masquerade: true, - server_process_opts: [name: {:global, :fz_wall_server}], - egress_interface: "dummy", - wireguard_interface_name: "wg-firezone", - port_based_rules_supported: true - -# This will be changed per-env -config :fz_vpn, - wireguard_private_key_path: "priv/wg_dev_private_key", - stats_push_service_enabled: true, - wireguard_interface_name: "wg-firezone", - wireguard_port: 51_820, - wg_adapter: FzVpn.Interface.WGAdapter.Live, - server_process_opts: [name: {:global, :fz_vpn_server}], - supervised_children: [FzVpn.Server, FzVpn.StatsPushService] - -config :fz_http, FzHttpWeb.Endpoint, - render_errors: [view: FzHttpWeb.ErrorView, accepts: ~w(html json)], - pubsub_server: FzHttp.PubSub - -# 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] - # Configures the vault config :fz_http, FzHttp.Vault, ciphers: [ @@ -98,7 +161,9 @@ config :fz_http, FzHttp.Vault, } ] -config :fz_http, FzHttpWeb.Mailer, adapter: FzHttpWeb.Mailer.NoopAdapter +config :fz_http, FzHttpWeb.Mailer, + adapter: FzHttpWeb.Mailer.NoopAdapter, + from_email: "test@firez.one" config :samly, Samly.State, store: Samly.State.Session diff --git a/config/dev.exs b/config/dev.exs index 89dcf703f..21202f76d 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1,26 +1,5 @@ import Config -# Configure your database -if url = System.get_env("DATABASE_URL") do - config :fz_http, FzHttp.Repo, - url: url, - show_sensitive_data_on_connection_error: true, - pool_size: 10 -else - config :fz_http, FzHttp.Repo, - username: "postgres", - password: "postgres", - database: "firezone_dev", - ssl: false, - ssl_opts: [], - parameters: [], - hostname: "localhost", - show_sensitive_data_on_connection_error: true, - pool_size: 10 -end - -# For development, we disable any cache and enable -# debugging and code reloading. config :fz_http, FzHttpWeb.Endpoint, http: [port: 13000], debug_errors: true, @@ -28,8 +7,20 @@ config :fz_http, FzHttpWeb.Endpoint, check_origin: ["//127.0.0.1", "//localhost"], watchers: [ node: ["esbuild.js", "dev", cd: Path.expand("../apps/fz_http/assets", __DIR__)] + ], + live_reload: [ + patterns: [ + ~r"apps/fz_http/priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", + ~r"apps/fz_http/priv/gettext/.*(po)$", + ~r"apps/fz_http/lib/fz_http_web/(live|views)/.*(ex)$", + ~r"apps/fz_http/lib/fz_http_web/templates/.*(eex)$" + ] ] +############################### +##### FZ Firewall configs ##### +############################### + get_egress_interface = fn -> egress_interface_cmd = case :os.type() do @@ -52,65 +43,17 @@ config :fz_wall, egress_interface: egress_interface, cli: fz_wall_cli_module -{fz_vpn_mod, _} = - Code.eval_string(System.get_env("FZ_VPN_WG_ADAPTER", "FzVpn.Interface.WGAdapter.Live")) +############################### +##### FZ VPN configs ########## +############################### config :fz_vpn, - supervised_children: [FzVpn.Interface.WGAdapter.Sandbox, FzVpn.Server, FzVpn.StatsPushService], - wireguard_private_key_path: "priv/wg_dev_private_key", - wg_adapter: fz_vpn_mod + wg_adapter: FzVpn.Interface.WGAdapter.Sandbox, + supervised_children: [FzVpn.Interface.WGAdapter.Sandbox, FzVpn.Server, FzVpn.StatsPushService] -# Auth -local_auth_enabled = System.get_env("LOCAL_AUTH_ENABLED") == "true" - -config :ueberauth, Ueberauth, - providers: [ - identity: - {Ueberauth.Strategy.Identity, - [ - callback_methods: ["POST"], - uid_field: :email - ]} - ] - -# ## SSL Support -# -# In order to use HTTPS in development, a self-signed -# certificate can be generated by running the following -# Mix task: -# -# mix phx.gen.cert -# -# Note that this task requires Erlang/OTP 20 or later. -# Run `mix help phx.gen.cert` for more information. -# -# The `http:` config above can be replaced with: -# -# https: [ -# port: 4001, -# cipher_suite: :strong, -# keyfile: "priv/cert/selfsigned_key.pem", -# certfile: "priv/cert/selfsigned.pem" -# ], -# -# If desired, both `http:` and `https:` keys can be -# configured to run both http and https servers on -# different ports. - -# Watch static and templates for browser reloading. -config :fz_http, FzHttpWeb.Endpoint, - secret_key_base: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5SD", - live_view: [ - signing_salt: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDejX" - ], - live_reload: [ - patterns: [ - ~r"apps/fz_http/priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", - ~r"apps/fz_http/priv/gettext/.*(po)$", - ~r"apps/fz_http/lib/fz_http_web/(live|views)/.*(ex)$", - ~r"apps/fz_http/lib/fz_http_web/templates/.*(eex)$" - ] - ] +############################### +##### Third-party configs ##### +############################### # Do not include metadata nor timestamps in development logs config :logger, :console, format: "[$level] $message\n" @@ -122,9 +65,4 @@ config :phoenix, :stacktrace_depth, 20 # Initialize plugs at runtime for faster development compilation config :phoenix, :plug_init_mode, :runtime -config :fz_http, - private_clients: ["172.28.0.0/16"], - cookie_secure: false, - telemetry_module: FzCommon.MockTelemetry - -config :fz_http, FzHttpWeb.Mailer, adapter: Swoosh.Adapters.Local, from_email: "dev@firez.one" +config :fz_http, FzHttpWeb.Mailer, adapter: Swoosh.Adapters.Local diff --git a/config/prod.exs b/config/prod.exs index 4e5030d56..01571d71b 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -1,56 +1,31 @@ import Config -# For production, don't forget to configure the url host -# to something meaningful, Phoenix uses this information -# when generating URLs. -# -# Note we also include the path to a cache manifest -# containing the digested version of static files. This -# manifest is generated by the `mix phx.digest` task, -# which you should run after static files are built and -# before starting your production server. +config :fz_http, FzHttpWeb.Endpoint, + cache_static_manifest: "priv/static/cache_manifest.json", + server: true + +# This will be overridden on releases + +config :fz_http, FzHttp.Repo, + pool_size: 10, + show_sensitive_data_on_connection_error: false + +config :fz_http, + http_client: HTTPoison, + connectivity_checks_url: "https://ping.firez.one/" + +############################### +##### FZ VPN configs ########## +############################### config :fz_wall, nft_path: "nft", cli: FzWall.CLI.Sandbox -config :fz_http, FzHttpWeb.Endpoint, - cache_static_manifest: "priv/static/cache_manifest.json", - # changed by release config - secret_key_base: "dummy", - # changed by release config - live_view: [signing_salt: "dummy"], - server: true +############################### +##### Third-party configs ##### +############################### -# This will be overridden on releases -if url = System.get_env("DATABASE_URL") do - config :fz_http, FzHttp.Repo, - url: url, - pool_size: 10 -else - config :fz_http, FzHttp.Repo, - username: "postgres", - password: "postgres", - database: "firezone", - hostname: "localhost", - pool_size: 10 -end - -# Do not print debug messages in production config :logger, level: :info -config :fz_http, - sandbox: false, - connectivity_checks_url: "https://ping.firez.one/" - -config :ueberauth, Ueberauth, - providers: [ - {:identity, - {Ueberauth.Strategy.Identity, - [ - callback_methods: ["POST"], - uid_field: :email - ]}} - ] - config :swoosh, local: false diff --git a/config/runtime.exs b/config/runtime.exs index 0ec550581..8dc5ff157 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1,163 +1,130 @@ -# In this file, we load configuration and secrets -# from environment variables. You can also hardcode secrets, -# although such is generally not recommended and you have to -# remember to add this file to your .gitignore. - import Config -alias FzCommon.{CLI, FzInteger, FzString, FzKernelVersion, FzNet} - -require Logger - -# external_url is important, so fail fast here if we can't parse -{:ok, external_url} = - if config_env() == :prod do - System.fetch_env!("EXTERNAL_URL") - |> FzNet.to_complete_url() - else - System.get_env("EXTERNAL_URL", "http://localhost:4002") - |> FzNet.to_complete_url() - end - -%{host: host, path: path, port: port, scheme: scheme} = URI.parse(external_url) - -config :fz_http, - external_url: external_url, - path_prefix: path - -config :fz_http, FzHttpWeb.Endpoint, - url: [host: host, scheme: scheme, port: port, path: path], - check_origin: ["//127.0.0.1", "//localhost", "//#{host}"] - -config :fz_wall, - port_based_rules_supported: FzKernelVersion.is_version_greater_than?({5, 6, 8}) - -# Formerly releases.exs - Only evaluated in production if config_env() == :prod do - # For releases, require that all these are set - admin_email = System.fetch_env!("ADMIN_EMAIL") - default_admin_password = System.fetch_env!("DEFAULT_ADMIN_PASSWORD") - guardian_secret_key = System.fetch_env!("GUARDIAN_SECRET_KEY") - encryption_key = System.fetch_env!("DATABASE_ENCRYPTION_KEY") - secret_key_base = System.fetch_env!("SECRET_KEY_BASE") - live_view_signing_salt = System.fetch_env!("LIVE_VIEW_SIGNING_SALT") - cookie_signing_salt = System.fetch_env!("COOKIE_SIGNING_SALT") - cookie_encryption_salt = System.fetch_env!("COOKIE_ENCRYPTION_SALT") + import FzHttp.Config, only: [compile_config!: 1] - # OPTIONAL + config :fz_http, FzHttp.Repo, + database: compile_config!(:database_name), + username: compile_config!(:database_user), + hostname: compile_config!(:database_host), + port: compile_config!(:database_port), + password: compile_config!(:database_password), + pool_size: compile_config!(:database_pool_size), + ssl: compile_config!(:database_ssl_enabled), + ssl_opts: compile_config!(:database_ssl_opts), + parameters: compile_config!(:database_parameters) - # telemetry env var name was renamed; use newer one if exists - telemetry_id = System.get_env("TID", System.get_env("TELEMETRY_ID", "unknown")) - telemetry_enabled = FzString.to_boolean(System.get_env("TELEMETRY_ENABLED", "true")) + external_url = compile_config!(:external_url) - wireguard_private_key_path = - System.get_env("WIREGUARD_PRIVATE_KEY_PATH", "/var/firezone/private_key") + %{ + scheme: external_url_scheme, + host: external_url_host, + port: external_url_port, + path: external_url_path + } = URI.parse(external_url) - saml_entity_id = System.get_env("SAML_ENTITY_ID", "urn:firezone.dev:firezone-app") - saml_keyfile_path = System.get_env("SAML_KEYFILE_PATH", "/var/firezone/saml.key") - saml_certfile_path = System.get_env("SAML_CERTFILE_PATH", "/var/firezone/saml.crt") - database_name = System.get_env("DATABASE_NAME", "firezone") - database_user = System.get_env("DATABASE_USER", "postgres") - database_host = System.get_env("DATABASE_HOST", "postgres") - database_port = String.to_integer(System.get_env("DATABASE_PORT", "5432")) - database_pool = String.to_integer(System.get_env("DATABASE_POOL", "10")) - database_ssl = FzString.to_boolean(System.get_env("DATABASE_SSL", "false")) - database_ssl_opts = Jason.decode!(System.get_env("DATABASE_SSL_OPTS", "{}")) - database_parameters = Jason.decode!(System.get_env("DATABASE_PARAMETERS", "{}")) - http_client_ssl_opts = Jason.decode!(System.get_env("HTTP_CLIENT_SSL_OPTS", "{}")) - phoenix_listen_address = System.get_env("PHOENIX_LISTEN_ADDRESS", "0.0.0.0") - phoenix_port = String.to_integer(System.get_env("PHOENIX_PORT", "13000")) - external_trusted_proxies = Jason.decode!(System.get_env("EXTERNAL_TRUSTED_PROXIES", "[]")) - private_clients = Jason.decode!(System.get_env("PRIVATE_CLIENTS", "[]")) - wireguard_interface_name = System.get_env("WIREGUARD_INTERFACE_NAME", "wg-firezone") - wireguard_port = String.to_integer(System.get_env("WIREGUARD_PORT", "51820")) - 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")) + config :fz_http, + external_url: external_url, + path_prefix: external_url_path - wireguard_ipv4_masquerade = - FzString.to_boolean(System.get_env("WIREGUARD_IPV4_MASQUERADE", "true")) + config :fz_http, FzHttpWeb.Endpoint, + server: true, + http: [ + ip: compile_config!(:phoenix_listen_address).address, + port: compile_config!(:phoenix_http_port) + ], + url: [ + scheme: external_url_scheme, + host: external_url_host, + port: external_url_port, + path: external_url_path + ], + secret_key_base: compile_config!(:secret_key_base), + live_view: [ + signing_salt: compile_config!(:live_view_signing_salt) + ], + check_origin: ["//127.0.0.1", "//localhost", "//#{external_url_host}"] - wireguard_ipv6_masquerade = - FzString.to_boolean(System.get_env("WIREGUARD_IPV6_MASQUERADE", "true")) + config :fz_http, + wireguard_ipv4_enabled: compile_config!(:wireguard_ipv4_enabled), + wireguard_ipv4_network: compile_config!(:wireguard_ipv4_network), + wireguard_ipv4_address: compile_config!(:wireguard_ipv4_address), + wireguard_ipv6_enabled: compile_config!(:wireguard_ipv6_enabled), + wireguard_ipv6_network: compile_config!(:wireguard_ipv6_network), + wireguard_ipv6_address: compile_config!(:wireguard_ipv6_address) - # 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_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") + config :fz_http, + saml_entity_id: compile_config!(:saml_entity_id), + saml_certfile_path: compile_config!(:saml_certfile_path), + saml_keyfile_path: compile_config!(:saml_keyfile_path) - cookie_secure = FzString.to_boolean(System.get_env("SECURE_COOKIES", "true")) + config :fz_http, + external_trusted_proxies: compile_config!(:phoenix_external_trusted_proxies), + private_clients: compile_config!(:phoenix_private_clients) - # Outbound Email - outbound_config_env = System.get_env("OUTBOUND_EMAIL_CONFIGS", "{}") - - with {:ok, configs} <- Jason.decode(outbound_config_env) do - changeset = - FzHttp.Configurations.Mailer.changeset(%{ - "from" => System.get_env("OUTBOUND_EMAIL_FROM"), - "provider" => System.get_env("OUTBOUND_EMAIL_PROVIDER", "sendmail"), - "configs" => configs - }) - - if changeset.valid? do - mailer = Ecto.Changeset.apply_changes(changeset) - config :fz_http, FzHttpWeb.Mailer, FzHttpWeb.Mailer.from_configuration(mailer) - else - Logger.warn( - "Outbound email not configured. Disabling! Details: #{inspect(changeset.errors)}" + config :fz_http, + telemetry_id: compile_config!(:telemetry_id), + telemetry_module: + if(compile_config!(:telemetry_enabled) == true, + do: FzCommon.Telemetry, + else: FzCommon.MockTelemetry ) - end - else - {:error, error} -> - raise "OUTBOUND_EMAIL_CONFIGS not a valid JSON-encoded string. Error: #{error}" - end - max_devices_per_user = - System.get_env("MAX_DEVICES_PER_USER", "10") - |> String.to_integer() - |> FzInteger.clamp(0, 100) + config :fz_http, + cookie_secure: compile_config!(:phoenix_secure_cookies), + cookie_signing_salt: compile_config!(:cookie_signing_salt), + cookie_encryption_salt: compile_config!(:cookie_encryption_salt) - telemetry_module = - if telemetry_enabled do - FzCommon.Telemetry - else - FzCommon.MockTelemetry - end + config :fz_http, + http_client_options: compile_config!(:http_client_ssl_opts), + connectivity_checks_enabled: compile_config!(:connectivity_checks_enabled), + connectivity_checks_interval: compile_config!(:connectivity_checks_interval) - connectivity_checks_enabled = - FzString.to_boolean(System.get_env("CONNECTIVITY_CHECKS_ENABLED", "true")) && - System.get_env("CI") != "true" + config :fz_http, + admin_email: compile_config!(:default_admin_email), + default_admin_password: compile_config!(:default_admin_password) - connectivity_checks_interval = - System.get_env("CONNECTIVITY_CHECKS_INTERVAL", "43200") - |> String.to_integer() - |> FzInteger.clamp(60, 86_400) + config :fz_http, + max_devices_per_user: compile_config!(:max_devices_per_user) - # Password is not needed if using bundled PostgreSQL, so use nil if it's not set. - database_password = System.get_env("DATABASE_PASSWORD") + ############################### + ##### FZ Firewall configs ##### + ############################### - parameters = Keyword.new(database_parameters, fn {k, v} -> {String.to_atom(k), v} end) + config :fz_wall, cli: FzWall.CLI.Live - # Database configuration - connect_opts = [ - database: database_name, - username: database_user, - hostname: database_host, - port: database_port, - pool_size: database_pool, - ssl: database_ssl, - ssl_opts: FzCommon.map_ssl_opts(database_ssl_opts), - parameters: parameters, - queue_target: 500 - ] + config :fz_wall, + wireguard_ipv4_masquerade: compile_config!(:wireguard_ipv4_masquerade), + wireguard_ipv6_masquerade: compile_config!(:wireguard_ipv6_masquerade), + wireguard_interface_name: compile_config!(:wireguard_interface_name), + nft_path: compile_config!(:gateway_nft_path), + egress_interface: compile_config!(:gateway_egress_interface) - if database_password do - config(:fz_http, FzHttp.Repo, connect_opts ++ [password: database_password]) - else - config(:fz_http, FzHttp.Repo, connect_opts) - end + config :fz_wall, + port_based_rules_supported: + :os.version() + |> Tuple.to_list() + |> Enum.join(".") + |> Version.match?("> 5.6.8") + + ############################### + ##### FZ VPN configs ########## + ############################### + + config :fz_vpn, + wireguard_private_key_path: compile_config!(:wireguard_private_key_path), + wireguard_interface_name: compile_config!(:wireguard_interface_name), + wireguard_port: compile_config!(:wireguard_port) + + ############################### + ##### Third-party configs ##### + ############################### + + config :fz_http, FzHttpWeb.Auth.HTML.Authentication, + secret_key: compile_config!(:guardian_secret_key) + + config :fz_http, FzHttpWeb.Auth.JSON.Authentication, + secret_key: compile_config!(:guardian_secret_key) config :fz_http, FzHttp.Vault, ciphers: [ @@ -169,90 +136,28 @@ if config_env() == :prod do # https://github.com/danielberkompas/cloak/issues/93 # # In Cloak 2.0, this will be the default iv length for AES.GCM. - tag: "AES.GCM.V1", key: Base.decode64!(encryption_key), iv_length: 12 + tag: "AES.GCM.V1", + key: Base.decode64!(compile_config!(:database_encryption_key)), + iv_length: 12 } ] - listen_ip = - phoenix_listen_address - |> String.split(".") - |> Enum.map(&String.to_integer/1) - |> List.to_tuple() + config :openid_connect, + finch_transport_opts: compile_config!(:http_client_ssl_opts) - config :fz_http, FzHttpWeb.Endpoint, - http: [ip: listen_ip, port: phoenix_port], - server: true, - secret_key_base: secret_key_base, - live_view: [ - signing_salt: live_view_signing_salt + config :ueberauth, Ueberauth, + providers: [ + identity: + {Ueberauth.Strategy.Identity, + callback_methods: ["POST"], + callback_url: "#{external_url}/auth/identity/callback", + uid_field: :email} ] - config :fz_wall, - wireguard_ipv4_masquerade: wireguard_ipv4_masquerade, - wireguard_ipv6_masquerade: wireguard_ipv6_masquerade, - nft_path: nft_path, - egress_interface: egress_interface, - wireguard_interface_name: wireguard_interface_name, - cli: FzWall.CLI.Live - - config :fz_vpn, - wireguard_private_key_path: wireguard_private_key_path, - wireguard_interface_name: wireguard_interface_name, - wireguard_port: wireguard_port - - # Guardian configuration - # 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 - config :fz_http, - http_client_options: [ssl: FzCommon.map_ssl_opts(http_client_ssl_opts)], - saml_entity_id: saml_entity_id, - saml_certfile_path: saml_certfile_path, - saml_keyfile_path: saml_keyfile_path, - external_trusted_proxies: external_trusted_proxies, - private_clients: private_clients, - cookie_signing_salt: cookie_signing_salt, - cookie_encryption_salt: cookie_encryption_salt, - cookie_secure: cookie_secure, - max_devices_per_user: max_devices_per_user, - wireguard_ipv4_enabled: wireguard_ipv4_enabled, - wireguard_ipv4_network: wireguard_ipv4_network, - wireguard_ipv4_address: wireguard_ipv4_address, - wireguard_ipv6_enabled: wireguard_ipv6_enabled, - wireguard_ipv6_network: wireguard_ipv6_network, - wireguard_ipv6_address: wireguard_ipv6_address, - telemetry_module: telemetry_module, - telemetry_id: telemetry_id, - connectivity_checks_enabled: connectivity_checks_enabled, - connectivity_checks_interval: connectivity_checks_interval, - admin_email: admin_email, - default_admin_password: default_admin_password - - # Configure OpenID Connect - config :openid_connect, - finch_transport_opts: FzCommon.map_ssl_opts(http_client_ssl_opts) - - # Configure strategies - identity_strategy = - {:identity, - {Ueberauth.Strategy.Identity, - [ - callback_methods: ["POST"], - callback_url: "#{external_url}/auth/identity/callback", - uid_field: :email - ]}} - - # 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 + FzHttpWeb.Mailer, + [ + adapter: compile_config!(:outbound_email_adapter), + from_email: compile_config!(:outbound_email_from) + ] ++ compile_config!(:outbound_email_adapter_opts) end diff --git a/config/test.exs b/config/test.exs index bbae70e30..5f4686c34 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,67 +1,44 @@ import Config -defmodule DBConfig do - def config(db_url) when is_nil(db_url) do - [ - username: "postgres", - password: "postgres", - database: "firezone_test", - hostname: System.get_env("POSTGRES_HOST", "localhost"), - pool: Ecto.Adapters.SQL.Sandbox, - pool_size: 64, - queue_target: 1000 - ] +config :fz_http, supervision_tree_mode: :test + +partition_suffix = + if partition = System.get_env("MIX_TEST_PARTITION") do + "_p#{partition}" + else + "" end - def config(db_url) do - [ - url: db_url, - pool: Ecto.Adapters.SQL.Sandbox, - pool_size: 64, - queue_target: 1000 - ] - end -end +config :fz_http, sql_sandbox: true -# Configure your database -db_url = System.get_env("DATABASE_URL") -config :fz_http, FzHttp.Repo, DBConfig.config(db_url) +config :fz_http, FzHttp.Repo, + database: "firezone_test#{partition_suffix}", + pool: Ecto.Adapters.SQL.Sandbox, + queue_target: 1000 -# We don't run a server during test. If one is required, -# you can enable the server option below. config :fz_http, FzHttpWeb.Endpoint, - http: [port: 4002], - secret_key_base: "t5hsQU868q6aaI9jsCrso9Qhi7A9Lvy5/NjCnJ8t8f652jtRjcBpYJkm96E8Q5Ko", - live_view: [ - signing_salt: "mgC0uvbIgQM7GT5liNSbzJJhvjFjhb7t" - ], + http: [port: 13000], server: true config :fz_http, - mock_events_module_errors: false, - telemetry_module: FzCommon.MockTelemetry, - supervision_tree_mode: :test, - connectivity_checks_interval: 43_200, - sql_sandbox: true, http_client: FzHttp.Mocks.HttpClient -# Print only warnings and errors during test -config :logger, level: :warn - -config :ueberauth, Ueberauth, - providers: [ - identity: {Ueberauth.Strategy.Identity, [callback_methods: ["POST"], uid_field: :email]} - ] - -config :fz_http, FzHttpWeb.Mailer, - adapter: FzHttpWeb.MailerTestAdapter, - from_email: "test@firez.one" +############################### +##### FZ VPN configs ########## +############################### config :fz_vpn, # XXX: Bump test coverage by adding a stubbed out module for FzVpn.StatsPushService supervised_children: [FzVpn.Interface.WGAdapter.Sandbox, FzVpn.Server], wg_adapter: FzVpn.Interface.WGAdapter.Sandbox +############################### +##### Third-party configs ##### +############################### +config :fz_http, FzHttpWeb.Mailer, adapter: FzHttpWeb.MailerTestAdapter + +config :logger, level: :warn + config :argon2_elixir, t_cost: 1, m_cost: 8 config :bureaucrat, :json_library, Jason diff --git a/docker-compose.desktop.yml b/docker-compose.desktop.yml index 5da815f9e..c363f403e 100644 --- a/docker-compose.desktop.yml +++ b/docker-compose.desktop.yml @@ -48,7 +48,7 @@ services: - ${WIREGUARD_PORT:-51820}:${WIREGUARD_PORT:-51820}/udp env_file: # This should contain a list of env vars for configuring Firezone. - # See https://docs.firezone.dev/reference/env-vars for more info. + # See https://www.firezone.dev/docs/reference/env-vars for more info. - ${FZ_INSTALL_DIR:-.}/.env volumes: # IMPORTANT: Persists WireGuard private key and other data. If diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 119bb0684..a3320c531 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -45,7 +45,7 @@ services: - ${WIREGUARD_PORT:-51820}:${WIREGUARD_PORT:-51820}/udp env_file: # This should contain a list of env vars for configuring Firezone. - # See https://docs.firezone.dev/reference/env-vars for more info. + # See https://www.firezone.dev/docs/reference/env-vars for more info. - ${FZ_INSTALL_DIR:-.}/.env volumes: # IMPORTANT: Persists WireGuard private key and other data. If diff --git a/docker-compose.yml b/docker-compose.yml index 82014be17..983d4c791 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -89,8 +89,8 @@ services: image: vihangk1/docker-test-saml-idp:latest environment: SIMPLESAMLPHP_SP_ENTITY_ID: 'urn:firezone.dev:firezone-app' - SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE: 'http://localhost:4002/auth/saml/sp/consume/mysamlidp' - SIMPLESAMLPHP_SP_SINGLE_LOGOUT_SERVICE: 'http://localhost:4002/auth/saml/sp/logout/mysamlidp' + SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE: 'http://localhost:13000/auth/saml/sp/consume/mysamlidp' + SIMPLESAMLPHP_SP_SINGLE_LOGOUT_SERVICE: 'http://localhost:13000/auth/saml/sp/logout/mysamlidp' SIMPLESAMLPHP_SP_NAME_ID_FORMAT: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' SIMPLESAMLPHP_SP_NAME_ID_ATTRIBUTE: 'email' SIMPLESAMLPHP_IDP_AUTH: 'example-userpass' diff --git a/mix.lock b/mix.lock index ff736f486..5a31a56a9 100644 --- a/mix.lock +++ b/mix.lock @@ -20,7 +20,6 @@ "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.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.3", "c37fdae1b52d2cc51069713a58c2314877c1ad40800a57efb213f77b078a460d", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "24ada3e3996adbed1fa024ca14995ef2ba3d0d17b678b0f3f2b1f66e6ce2b274"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, @@ -31,7 +30,7 @@ "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"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "finch": {:hex, :finch, "0.14.0", "619bfdee18fc135190bf590356c4bf5d5f71f916adb12aec94caa3fa9267a4bc", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5459acaf18c4fdb47a8c22fb3baff5d8173106217c8e56c5ba0b93e66501a8dd"}, - "floki": {:hex, :floki, "0.34.1", "b1f9c413d91140230788b173906065f6f8906bbbf5b3f0d3c626301aeeef44c5", [:mix], [], "hexpm", "cc9b62312a45c1239ca8f65e05377ef8c646f3d7712e5727a9b47c43c946e885"}, + "floki": {:hex, :floki, "0.34.0", "002d0cc194b48794d74711731db004fafeb328fe676976f160685262d43706a8", [:mix], [], "hexpm", "9c3a9f43f40dde00332a589bd9d389b90c1f518aef500364d00636acc5ebc99c"}, "gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"}, "gettext": {:hex, :gettext, "0.22.0", "a25d71ec21b1848957d9207b81fd61cb25161688d282d58bdafef74c2270bdc4", [:mix], [{:expo, "~> 0.3.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "cb0675141576f73720c8e49b4f0fd3f2c69f0cd8c218202724d4aebab8c70ace"}, "guardian": {:hex, :guardian, "2.3.1", "2b2d78dc399a7df182d739ddc0e566d88723299bfac20be36255e2d052fd215d", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bbe241f9ca1b09fad916ad42d6049d2600bbc688aba5b3c4a6c82592a54274c3"}, @@ -61,13 +60,13 @@ "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "phoenix": {:hex, :phoenix, "1.7.0-rc.2", "8faaff6f699aad2fe6a003c627da65d0864c868a4c10973ff90abfd7286c1f27", [: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", "71abde2f67330c55b625dcc0e42bf76662dbadc7553c4f545c2f3759f40f7487"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"}, - "phoenix_html": {:hex, :phoenix_html, "3.3.0", "bf451c71ebdaac8d2f40d3b703435e819ccfbb9ff243140ca3bd10c155f134cc", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "272c5c1533499f0132309936c619186480bafcc2246588f99a69ce85095556ef"}, + "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "0.18.13", "956aed3ffe24b9ecd5e4bde10fdc9673b77f43adf4d1172a6812abad35dbc94c", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, 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]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9ecbe76c79102565a14e9fe54aac4b086991dbd5e8da7da4d4d6442f4e79147f"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "0.18.11", "c50eac83dae6b5488859180422dfb27b2c609de87f4aa5b9c926ecd0501cd44f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, 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]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "76c99a0ffb47cd95bf06a917e74f282a603f3e77b00375f3c2dd95110971b102"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.1.0", "f8e4780705c9f254cc853f7a40e25f7198ba4d91102bcfad2226669b69766b35", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "aa82f10afd9a4b6080fdf3274dbb9432b25b210d42b4b6b55308f6e59cd87c3d"}, - "phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"}, + "phoenix_template": {:hex, :phoenix_template, "1.0.0", "c57bc5044f25f007dc86ab21895688c098a9f846a8dda6bc40e2d0ddc146e38f", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "1b066f99a26fd22064c12b2600a9a6e56700f591bf7b20b418054ea38b4d4357"}, "phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"}, "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"}, diff --git a/www/docs/reference/env-vars.mdx b/www/docs/reference/env-vars.mdx index 76736b777..b99a7f9d8 100644 --- a/www/docs/reference/env-vars.mdx +++ b/www/docs/reference/env-vars.mdx @@ -2,63 +2,143 @@ title: Environment Variables sidebar_position: 1 --- +Most day-to-day config of Firezone can be done via the Firezone Web UI, +but for zero-touch deployments we allow to override most of configuration options +using environment variables. -# Docker environment variables +Read more about configuring Firezone in our [configure guide](/deploy/configure). -Most day-to-day config of Firezone can (and should) be done via the -Firezone Web UI. +## Errors -For Docker-based deployments, deployment-related or infrastructure-related -config of Firezone is done through environment variables passed to the -Firezone image upon launch. +Firezone will not boot if the configuration is invalid, providing a detailed error message +and a link to the documentation for the configuration key with samples how to set it. -Read more about configuring Firezone in our [configure guide](/docs/deploy/configure). +## Naming + +If environment variables are used, the configuration key must be uppercased. +The database variables are the same as the configuration keys. + +## Precedence + +The configuration precedence is as follows: + +1. Environment variables +2. Database values +3. Default values + +It means that if environment variable is set, it will be used, regardless of the database value, +and UI to edit database value will be disabled. ## Environment Variable Listing - 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`. | -| **`DATABASE_PASSWORD`** | Password used to connect to the DB. | String | Randomly generated upon install with `docker run firezone/firezone bin/gen-env`. | -| **`DATABASE_ENCRYPTION_KEY`** | The base64-encoded symmetric encryption key used to encrypt and decrypt sensitive fields. | base64-encoded String | Randomly generated upon install with `docker run firezone/firezone bin/gen-env`. | -| **`GUARDIAN_SECRET_KEY`** | Secret key used for signing JWTs. | base64-encoded String | Randomly generated upon install with `docker run firezone/firezone bin/gen-env`. | -| **`COOKIE_ENCRYPTION_SALT`** | Encryption salt for cookies issued by the Phoenix web application. | base64-encoded String | Randomly generated upon install with `docker run firezone/firezone bin/gen-env`. | -| **`COOKIE_SIGNING_SALT`** | Signing salt for cookies issued by the Phoenix web application. | base64-encoded String | Randomly generated upon install with `docker run firezone/firezone bin/gen-env`. | -| **`LIVE_VIEW_SIGNING_SALT`** | Signing salt for Phoenix LiveView connection tokens. | base64-encoded String | Randomly generated upon install with `docker run firezone/firezone bin/gen-env`. | -| **`SECRET_KEY_BASE`** | Primary secret key base for the Phoenix application. | base64-encoded String | Randomly generated upon install with `docker run firezone/firezone bin/gen-env`. | -| `RESET_ADMIN_ON_BOOT` | Set this variable to `true` to create or reset the admin with email specified by `ADMIM_EMAIL` to password `DEFAULT_ADMIN_PASSWORD` on boot. Note: This **will not** change the status of local authentication. | Boolean | | -| `LOCAL_AUTH_ENABLED` | Enable or disable the local authentication method for all users. | Boolean | `true` | -| `SAML_ENTITY_ID` | SAML Entity ID. | String | `urn:firezone.dev:firezone-app` | -| `SAML_KEYFILE_PATH` | Path to the SAML keyfile inside the container. | String | `/var/firezone/saml.key` | -| `SAML_CERTFILE_PATH` | Path to the SAML certificate file inside the container. | String | `/var/firezone/saml.crt` | -| `DATABASE_HOST` | Database host. | IP or hostname | `postgres` | -| `DATABASE_PORT` | Database port. | Integer | `5432` | -| `DATABASE_NAME` | Name of database. | String | `firezone` | -| `DATABASE_USER` | Database user. | String | `postgres` | -| `DATABASE_POOL` | Size of the Firezone connection pool. | Integer | `10` | -| `DATABASE_SSL` | Whether to connect to the database over SSL | Boolean | `false` | -| `DATABASE_SSL_OPTS` | Map of options to send to the `:ssl_opts` option when connecting over SSL. See [Ecto.Adapters.Postgres documentation](https://hexdocs.pm/ecto_sql/Ecto.Adapters.Postgres.html#module-connection-options) | JSON-encoded String | `{}` | -| `DATABASE_PARAMETERS` | Map of parameters to send to the `:parameters` option when connecting to the database. See [Ecto.Adapters.Postgres documentation](https://hexdocs.pm/ecto_sql/Ecto.Adapters.Postgres.html#module-connection-options). | JSON-encoded String | `{}` | -| `HTTP_CLIENT_SSL_OPTS` | Map of options to use for outbound SSL connections for OIDC document retrieval and Connectivity Checks. | JSON-encoded String, e.g. `{"verify": "verify_none", "cacertfile": "/etc/ssl/cacerts.pem"}`. See [Erlang's SSL options](https://www.erlang.org/doc/man/ssl.html#type-client_option) for a full list of client options. | | -| `CONNECTIVITY_CHECKS_ENABLED` | Enable / disable periodic checking for egress connectivity. Determines the instance's public IP to populate `Endpoint` fields. | Boolean | `true` | -| `CONNECTIVITY_CHECKS_INTERVAL` | Periodicity in seconds to check for egress connectivity. | Integer | `3600` | -| `EXTERNAL_TRUSTED_PROXIES` | List of trusted reverse proxies. | JSON-encoded array | `[]` | -| `MAX_DEVICES_PER_USER` | Maximum number of devices to allow per user. | Integer | `10` | -| `OUTBOUND_EMAIL_FROM` | From address to use for sending outbound emails. If not set, sending email will be disabled (default). | String | | -| `OUTBOUND_EMAIL_PROVIDER` | Method to use for sending outbound email. If not set, will default to `sendmail`. See the list of [Swoosh Adapters](https://github.com/swoosh/swoosh#adapters). | String | | -| `OUTBOUND_EMAIL_CONFIGS` | Email provider-specific config. | JSON-encoded String 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_ENABLED` | Enable / disable tunnel-side IPv4 connectivity. | Boolean | `true` | -| `WIREGUARD_IPV4_MASQUERADE` | Enable / disable IPv4 masquerade. | String | `true` | -| `WIREGUARD_IPV6_ENABLED` | Enable / disable tunnel IPv6 addresses. | Boolean | `true` | -| `WIREGUARD_IPV6_MASQUERADE` | Enable / disable IPv6 masquerade. | Boolean | `true` | -| `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` | -| `TELEMETRY_ENABLED` | Enable / disable product telemetry. Read more about [what that means here](/docs/reference/telemetry). | Boolean | `true` | +### WebServer + +| Env Key | Description | Format | Default | +| ------ | --------------- | ------ | ------- | +| **EXTERNAL_URL** | The external URL the web UI will be accessible at.

Must be a valid and public FQDN for ACME SSL issuance to function.

You can add a path suffix if you want to serve firezone from a non-root path, eg: `https://firezone.mycorp.com/vpn`. | string | | +| PHOENIX_SECURE_COOKIES | Enable or disable requiring secure cookies. Required for HTTPS. | boolean | true | +| PHOENIX_HTTP_PORT | Internal port to listen on for the Phoenix web server. | integer | 13000 | +| PHOENIX_EXTERNAL_TRUSTED_PROXIES | List of trusted reverse proxies.

This is used to determine the correct IP address of the client when the application is behind a reverse proxy by skipping a trusted proxy IP from a list of possible source IPs. | a list of one of `IP`, `CIDR` separated by `,` | [] | +| PHOENIX_PRIVATE_CLIENTS | List of trusted clients.

This is used to determine the correct IP address of the client when the application is behind a reverse proxy by picking a trusted client IP from a list of possible source IPs. | a list of one of `IP`, `CIDR` separated by `,` | [] | + +### Database + +| Env Key | Description | Format | Default | +| ------ | --------------- | ------ | ------- | +| DATABASE_HOST | PostgreSQL host. | string | postgres | +| DATABASE_PORT | PostgreSQL port. | integer | 5432 | +| DATABASE_NAME | Name of the PostgreSQL database. | string | firezone | +| DATABASE_USER | User that will be used to access the PostgreSQL database. | string | postgres | +| **DATABASE_PASSWORD** | Password that will be used to access the PostgreSQL database. | string | | +| DATABASE_POOL_SIZE | Size of the connection pool to the PostgreSQL database. | integer | generated | +| DATABASE_SSL_ENABLED | Whether to connect to the database over SSL.

If this field is set to `true`, the `database_ssl_opts` config must be set too with at least `cacertfile` option present. | boolean | false | +| DATABASE_SSL_OPTS | SSL options for connecting to the PostgreSQL database.

Typically, to enabled SSL you want following options:
- `cacertfile` - path to the CA certificate file;
- `verify` - set to `verify_peer` to verify the server certificate;
- `fail_if_no_peer_cert` - set to `true` to require the server to present a certificate;
- `server_name_indication` - specify the hostname to be used in TLS Server Name Indication extension.

See [Ecto.Adapters.Postgres documentation](https://hexdocs.pm/ecto_sql/Ecto.Adapters.Postgres.html#module-connection-options). For list of all supported options, see the [`ssl`](http://erlang.org/doc/man/ssl.html#type-tls_client_option) module documentation. | JSON-encoded map | `{}` | + +### Admin Setup +Options responsible for initial admin provisioning and resetting the admin password. + +For more details see [troubleshooting guide](/administer/troubleshoot/#admin-login-isnt-working). + + +| Env Key | Description | Format | Default | +| ------ | --------------- | ------ | ------- | +| RESET_ADMIN_ON_BOOT | Set this variable to `true` to create or reset the admin password every time Firezone starts. By default, the admin password is only set when Firezone is installed.

Note: This **will not** change the status of local authentication. | boolean | false | +| DEFAULT_ADMIN_EMAIL | Primary administrator email. | string | | +| DEFAULT_ADMIN_PASSWORD | Default password that will be used for creating or resetting the primary administrator account. | string | | + +### Secrets and Encryption +Your secrets should be generated during installation automatically and persisted to `.env` file. + +All secrets should be a **base64-encoded string**. + + +| Env Key | Description | Format | Default | +| ------ | --------------- | ------ | ------- | +| **GUARDIAN_SECRET_KEY** | Secret key used for signing JWTs. | string | | +| **DATABASE_ENCRYPTION_KEY** | Secret key used for encrypting sensitive data in the database. | string | | +| **SECRET_KEY_BASE** | Primary secret key base for the Phoenix application. | string | | +| **LIVE_VIEW_SIGNING_SALT** | Signing salt for Phoenix LiveView connection tokens. | string | | +| **COOKIE_SIGNING_SALT** | Encryption salt for cookies issued by the Phoenix web application. | string | | +| **COOKIE_ENCRYPTION_SALT** | Signing salt for cookies issued by the Phoenix web application. | string | | + +### Devices + +| Env Key | Description | Format | Default | +| ------ | --------------- | ------ | ------- | +| ALLOW_UNPRIVILEGED_DEVICE_MANAGEMENT | Enable or disable management of devices on unprivileged accounts. | boolean | true | +| ALLOW_UNPRIVILEGED_DEVICE_CONFIGURATION | Enable or disable configuration of device network settings for unprivileged users. | boolean | true | +| VPN_SESSION_DURATION | Optionally require users to periodically authenticate to the Firezone web UI in order to keep their VPN sessions active. | integer | 0 | +| DEFAULT_CLIENT_PERSISTENT_KEEPALIVE | Interval for WireGuard [persistent keepalive](https://www.wireguard.com/quickstart/#nat-and-firewall-traversal-persistence).

If you experience NAT or firewall traversal problems, you can enable this to send a keepalive packet every 25 seconds. Otherwise, keep it disabled with a 0 default value. | integer | 25 | +| DEFAULT_CLIENT_MTU | WireGuard interface MTU for devices. 1280 is a safe bet for most networks. Leave this blank to omit this field from generated configs. | integer | 1280 | +| DEFAULT_CLIENT_ENDPOINT | IPv4, IPv6 address, or FQDN that devices will be configured to connect to. Defaults to this server's FQDN. | one of `IP with port`, `string` | generated | +| DEFAULT_CLIENT_DNS | Comma-separated list of DNS servers to use for devices.

It can be either an IP address or a FQDN if you intend to use a DNS-over-TLS server.

Leave this blank to omit the `DNS` section from generated configs. | {:array, ",", {:one_of, [FzHttp.Types.IP, :string]}, [validate_unique: true]} | `[]` | +| DEFAULT_CLIENT_ALLOWED_IPS | Configures the default AllowedIPs setting for devices.

AllowedIPs determines which destination IPs get routed through Firezone.

Specify a comma-separated list of IPs or CIDRs here to achieve split tunneling, or use `0.0.0.0/0, ::/0` to route all device traffic through this Firezone server. | {:array, ",", {:one_of, [FzHttp.Types.CIDR, FzHttp.Types.IP]}, [validate_unique: true]} | `0.0.0.0/0, ::/0` | + +### Authorization + +| Env Key | Description | Format | Default | +| ------ | --------------- | ------ | ------- | +| LOCAL_AUTH_ENABLED | Enable or disable the local authentication method for all users. | boolean | true | +| DISABLE_VPN_ON_OIDC_ERROR | Enable or disable auto disabling VPN connection on OIDC refresh error. | boolean | false | +| SAML_ENTITY_ID | Entity ID for SAML authentication. | string | urn:firezone.dev:firezone-app | +| SAML_KEYFILE_PATH | Path to the SAML keyfile inside the container. | string | /var/firezone/saml.key | +| SAML_CERTFILE_PATH | Path to the SAML certificate file inside the container. | string | /var/firezone/saml.crt | +| OPENID_CONNECT_PROVIDERS | List of OpenID Connect identity providers configurations.

For example:

``` [ { "auto_create_users": false, "id": "google", "label": "google", "client_id": "test-id", "client_secret": "test-secret", "discovery_document_uri": "https://accounts.google.com/.well-known/openid-configuration", "redirect_uri": "https://invalid", "response_type": "response-type", "scope": "oauth email profile" } ] ```

For more details see https://docs.firezone.dev/authenticate/oidc/. | JSON-encoded list | `"[]"` | +| SAML_IDENTITY_PROVIDERS | List of SAML identity providers configurations.

For example:

``` [ { "auto_create_users": false, "base_url": "https://saml", "id": "okta", "label": "okta", "metadata": "...", "sign_metadata": false, "sign_requests": false, "signed_assertion_in_resp": false, "signed_envelopes_in_resp": false } ] ```

For more details see https://docs.firezone.dev/authenticate/saml/. | JSON-encoded list | `"[]"` | + +### WireGuard + +| Env Key | Description | Format | Default | +| ------ | --------------- | ------ | ------- | +| WIREGUARD_PORT | A port on which WireGuard will listen for incoming connections. | integer | 51820 | +| WIREGUARD_IPV4_ENABLED | Enable or disable IPv4 support for WireGuard. | boolean | true | +| WIREGUARD_IPV6_ENABLED | Enable or disable IPv6 support for WireGuard. | boolean | true | + +### Outbound Emails + +| Env Key | Description | Format | Default | +| ------ | --------------- | ------ | ------- | +| OUTBOUND_EMAIL_FROM | From address to use for sending outbound emails. If not set, sending email will be disabled (default). | string | generated | +| OUTBOUND_EMAIL_ADAPTER | Method to use for sending outbound email. | One of `Swoosh.Adapters.AmazonSES`, `Swoosh.Adapters.CustomerIO`, `Swoosh.Adapters.Dyn`, `Swoosh.Adapters.ExAwsAmazonSES`, `Swoosh.Adapters.Gmail`, `Swoosh.Adapters.MailPace`, `Swoosh.Adapters.Mailgun`, `Swoosh.Adapters.Mailjet`, `Swoosh.Adapters.Mandrill`, `Swoosh.Adapters.Postmark`, `Swoosh.Adapters.ProtonBridge`, `Swoosh.Adapters.SMTP`, `Swoosh.Adapters.SMTP2GO`, `Swoosh.Adapters.Sendgrid`, `Swoosh.Adapters.Sendinblue`, `Swoosh.Adapters.Sendmail`, `Swoosh.Adapters.SocketLabs`, `Swoosh.Adapters.SparkPost`, `FzHttpWeb.Mailer.NoopAdapter` | `FzHttpWeb.Mailer.NoopAdapter` | +| OUTBOUND_EMAIL_ADAPTER_OPTS | Adapter configuration, for list of options see [Swoosh Adapters](https://github.com/swoosh/swoosh#adapters). | JSON-encoded map | `{}` | + +### Connectivity Checks + +| Env Key | Description | Format | Default | +| ------ | --------------- | ------ | ------- | +| CONNECTIVITY_CHECKS_ENABLED | Enable / disable periodic checking for egress connectivity. Determines the instance's public IP to populate `Endpoint` fields. | boolean | true | +| CONNECTIVITY_CHECKS_INTERVAL | Periodicity in seconds to check for egress connectivity. | integer | 43200 | + +### Telemetry + +| Env Key | Description | Format | Default | +| ------ | --------------- | ------ | ------- | +| TELEMETRY_ENABLED | Enable or disable the Firezone telemetry collection.

For more details see https://docs.firezone.dev/reference/telemetry/. | boolean | true | + +### Other + +| Env Key | Description | Format | Default | +| ------ | --------------- | ------ | ------- | +| LOGO | The path to a logo image file to replace default Firezone logo. | {:embed, FzHttp.Config.Logo} | `` | diff --git a/www/docs/reference/rest-api/configurations.mdx b/www/docs/reference/rest-api/configurations.mdx index afd7392bc..c1f521610 100644 --- a/www/docs/reference/rest-api/configurations.mdx +++ b/www/docs/reference/rest-api/configurations.mdx @@ -6,6 +6,8 @@ group: Configuration This endpoint allows an administrator to manage Configurations. +Updates here can be applied at runtime with little to no downtime of affected services. + ## API Documentation ### GET /v0/configuration @@ -25,19 +27,25 @@ Content-Type: application/json; charset=utf-8 "data": { "allow_unprivileged_device_configuration": true, "allow_unprivileged_device_management": true, - "default_client_allowed_ips": "0.0.0.0/0,::/0", - "default_client_dns": "1.1.1.1,1.0.0.1", + "default_client_allowed_ips": [ + "0.0.0.0/0", + "::/0" + ], + "default_client_dns": [ + "1.1.1.1", + "1.0.0.1" + ], "default_client_endpoint": "localhost:51820", "default_client_mtu": 1280, "default_client_persistent_keepalive": 25, "disable_vpn_on_oidc_error": false, - "id": "8f17e873-de8a-4264-8567-39e450870306", - "inserted_at": "2023-01-13T06:00:43.178729Z", + "id": "1c5b3594-1309-4779-b01d-cd21bee561b8", + "inserted_at": "2023-02-16T17:31:21.614660Z", "local_auth_enabled": true, - "logo": null, + "logo": {}, "openid_connect_providers": [], "saml_identity_providers": [], - "updated_at": "2023-01-13T06:00:43.178729Z", + "updated_at": "2023-02-16T17:31:21.614660Z", "vpn_session_duration": 0 } } @@ -57,8 +65,13 @@ $ curl -i \ "configuration": { "allow_unprivileged_device_configuration": false, "allow_unprivileged_device_management": false, - "default_client_allowed_ips": "1.1.1.1,2.2.2.2", - "default_client_dns": "1.1.1.1", + "default_client_allowed_ips": [ + "1.1.1.1", + "2.2.2.2" + ], + "default_client_dns": [ + "1.1.1.1" + ], "default_client_endpoint": "new-endpoint", "default_client_mtu": 1100, "default_client_persistent_keepalive": 1, @@ -73,8 +86,8 @@ $ curl -i \ "id": "google", "label": "google", "redirect_uri": "https://invalid", - "response_type": "response-type", - "scope": "test-scope" + "response_type": "code", + "scope": "email openid" } ], "saml_identity_providers": [ @@ -83,7 +96,7 @@ $ curl -i \ "base_url": "https://saml", "id": "okta", "label": "okta", - "metadata": "\n\n \n \n \n pdSMtx2s3RVVhxg_qJOjHhlZhwZk6JiBMiSm5PEgjkA\n \n MIICnzCCAYcCBgGD18ZU8TANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhmaXJlem9uZTAeFw0yMjEwMTQxODMyMjJaFw0zMjEwMTQxODM0MDJaMBMxETAPBgNVBAMMCGZpcmV6b25lMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAur5Cb0jrDJbMwr96WWE+z9CjDg0A/uRkaB4loRqkmu3A2fQGsS6CP7F7lQWMJmpzvBgkNtB69toO2sgx1u1fhpIJBZ0uSHF5gnzQAivgVxInvkMKRTRSkpMbhObiDHZnEGI2+Ly+8iV8IvprdrbDgm52u4conam0H1PewUKkHulrVQ+ImFuEWAjKCRSqpUG2F1eRkA0YpqB09x0CZAOOoucwTsBYj/ZAz3dUXhYIENAF7v0ykvzGOCAyOZIn1uYQc7jvWpwoI8qQdL45phj2FLoFlght3tlZV8IG5hsXrE6rg7Ufqvv8xyGltrOMKj/jEFEunagZOUjkypDp36b8cwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBEZKLLr66GB3NxqXGTMl0PvTDNB9GdyShQHaJYjeeUQnEXixjlAVrOq/txEBKjhGUcqyFELoNuwcxxV1iHA5oXhCoqYmnp9T/ftmXPDT3c49PBABHgLJaFOKYTpVx1YjP7mA44X1ijLZmgboIeeFNerVNHIzR9BsxcloQlB0r9QfC14rsuXo6QD3QnaVI8wDgWXQHqpcwLFqvehXdNvMFniRvX2qBNU8E0FPoMaZ1C3n2nssLcVZ+C4ghq6YoAG+wLGY7XE8+v5rnYGDpGpfgr2wdefn6tryFq3PyGqA8ThjARESRRQG9kI/RlNX7qCnP/8/7JQ4wLdfz5C25uhakP\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:persistent\n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\n \n \n \n \n \n\n", + "metadata": "\n\n \n \n \n \n MIIDqDCCApCgAwIBAgIGAYMaIfiKMA0GCSqGSIb3DQEBCwUAMIGUMQswCQYDVQQGEwJVUzETMBEG\nA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU\nMBIGA1UECwwLU1NPUHJvdmlkZXIxFTATBgNVBAMMDGRldi04Mzg1OTk1NTEcMBoGCSqGSIb3DQEJ\nARYNaW5mb0Bva3RhLmNvbTAeFw0yMjA5MDcyMjQ1MTdaFw0zMjA5MDcyMjQ2MTdaMIGUMQswCQYD\nVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsG\nA1UECgwET2t0YTEUMBIGA1UECwwLU1NPUHJvdmlkZXIxFTATBgNVBAMMDGRldi04Mzg1OTk1NTEc\nMBoGCSqGSIb3DQEJARYNaW5mb0Bva3RhLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\nggEBAOmj276L3kHm57hNGYTocT6NS4mffPbcvsA2UuKIWfmpV8HLTcmS+NahLtuN841OnRnTn+2p\nfjlwa1mwJhCODbF3dcVYOkGTPUC4y2nvf1Xas6M7+0O2WIfrzdX/OOUs/ROMnB/O/MpBwMR2SQh6\nQ3V+9v8g3K9yfMvcifDbl6g9fTliDzqV7I9xF5eJykl+iCAKNaQgp3cO6TaIa5u2ZKtRAdzwnuJC\nBXMyzaoNs/vfnwzuFtzWP1PSS1Roan+8AMwkYA6BCr1YRIqZ0GSkr/qexFCTZdq0UnSN78fY6CCM\nRFw5wU0WM9nEpbWzkBBWsYHeTLo5JqR/mZukfjlPDlcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA\nlUhwzCSnuqt4wlHxJONN4kxUBG8bPnjHxob6jBKK+onFDuSVWZ+7LZw67blz6xdxvlOLaQLi1fK2\nFifehbc7KbRLckcgNgg7Y8qfUKdP0/nS0JlyAvlnICQqaHTHwhIzQqTHtTZeeIJHtpWOX/OPRI0S\nbkygh2qjF8bYn3sX8bGNUQL8iiMxFnvwGrXaErPqlRqFJbWQDBXD+nYDIBw7WN3Jyb0Ydin2zrlh\ngp3Qooi0TnAir3ncw/UF/+sivCgd+6nX7HkbZtipkMbg7ZByyD9xrOQG2JXrP6PyzGCPwnGMt9pL\niiVMepeLNqKZ3UvhrR1uRN0KWu7lduIRhxldLA==\n \n \n \n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\n \n \n \n\n", "sign_metadata": false, "sign_requests": false, "signed_assertion_in_resp": false, @@ -102,16 +115,21 @@ Content-Type: application/json; charset=utf-8 "data": { "allow_unprivileged_device_configuration": false, "allow_unprivileged_device_management": false, - "default_client_allowed_ips": "1.1.1.1,2.2.2.2", - "default_client_dns": "1.1.1.1", + "default_client_allowed_ips": [ + "1.1.1.1", + "2.2.2.2" + ], + "default_client_dns": [ + "1.1.1.1" + ], "default_client_endpoint": "new-endpoint", "default_client_mtu": 1100, "default_client_persistent_keepalive": 1, "disable_vpn_on_oidc_error": true, - "id": "8f17e873-de8a-4264-8567-39e450870306", - "inserted_at": "2023-01-13T06:00:43.178729Z", + "id": "1c5b3594-1309-4779-b01d-cd21bee561b8", + "inserted_at": "2023-02-16T17:31:21.614660Z", "local_auth_enabled": false, - "logo": null, + "logo": {}, "openid_connect_providers": [ { "auto_create_users": false, @@ -121,8 +139,8 @@ Content-Type: application/json; charset=utf-8 "id": "google", "label": "google", "redirect_uri": "https://invalid", - "response_type": "response-type", - "scope": "test-scope" + "response_type": "code", + "scope": "email openid" } ], "saml_identity_providers": [ @@ -131,14 +149,14 @@ Content-Type: application/json; charset=utf-8 "base_url": "https://saml", "id": "okta", "label": "okta", - "metadata": "\n\n \n \n \n pdSMtx2s3RVVhxg_qJOjHhlZhwZk6JiBMiSm5PEgjkA\n \n MIICnzCCAYcCBgGD18ZU8TANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhmaXJlem9uZTAeFw0yMjEwMTQxODMyMjJaFw0zMjEwMTQxODM0MDJaMBMxETAPBgNVBAMMCGZpcmV6b25lMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAur5Cb0jrDJbMwr96WWE+z9CjDg0A/uRkaB4loRqkmu3A2fQGsS6CP7F7lQWMJmpzvBgkNtB69toO2sgx1u1fhpIJBZ0uSHF5gnzQAivgVxInvkMKRTRSkpMbhObiDHZnEGI2+Ly+8iV8IvprdrbDgm52u4conam0H1PewUKkHulrVQ+ImFuEWAjKCRSqpUG2F1eRkA0YpqB09x0CZAOOoucwTsBYj/ZAz3dUXhYIENAF7v0ykvzGOCAyOZIn1uYQc7jvWpwoI8qQdL45phj2FLoFlght3tlZV8IG5hsXrE6rg7Ufqvv8xyGltrOMKj/jEFEunagZOUjkypDp36b8cwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBEZKLLr66GB3NxqXGTMl0PvTDNB9GdyShQHaJYjeeUQnEXixjlAVrOq/txEBKjhGUcqyFELoNuwcxxV1iHA5oXhCoqYmnp9T/ftmXPDT3c49PBABHgLJaFOKYTpVx1YjP7mA44X1ijLZmgboIeeFNerVNHIzR9BsxcloQlB0r9QfC14rsuXo6QD3QnaVI8wDgWXQHqpcwLFqvehXdNvMFniRvX2qBNU8E0FPoMaZ1C3n2nssLcVZ+C4ghq6YoAG+wLGY7XE8+v5rnYGDpGpfgr2wdefn6tryFq3PyGqA8ThjARESRRQG9kI/RlNX7qCnP/8/7JQ4wLdfz5C25uhakP\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:persistent\n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\n \n \n \n \n \n\n", + "metadata": "\n\n \n \n \n \n MIIDqDCCApCgAwIBAgIGAYMaIfiKMA0GCSqGSIb3DQEBCwUAMIGUMQswCQYDVQQGEwJVUzETMBEG\nA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU\nMBIGA1UECwwLU1NPUHJvdmlkZXIxFTATBgNVBAMMDGRldi04Mzg1OTk1NTEcMBoGCSqGSIb3DQEJ\nARYNaW5mb0Bva3RhLmNvbTAeFw0yMjA5MDcyMjQ1MTdaFw0zMjA5MDcyMjQ2MTdaMIGUMQswCQYD\nVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsG\nA1UECgwET2t0YTEUMBIGA1UECwwLU1NPUHJvdmlkZXIxFTATBgNVBAMMDGRldi04Mzg1OTk1NTEc\nMBoGCSqGSIb3DQEJARYNaW5mb0Bva3RhLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\nggEBAOmj276L3kHm57hNGYTocT6NS4mffPbcvsA2UuKIWfmpV8HLTcmS+NahLtuN841OnRnTn+2p\nfjlwa1mwJhCODbF3dcVYOkGTPUC4y2nvf1Xas6M7+0O2WIfrzdX/OOUs/ROMnB/O/MpBwMR2SQh6\nQ3V+9v8g3K9yfMvcifDbl6g9fTliDzqV7I9xF5eJykl+iCAKNaQgp3cO6TaIa5u2ZKtRAdzwnuJC\nBXMyzaoNs/vfnwzuFtzWP1PSS1Roan+8AMwkYA6BCr1YRIqZ0GSkr/qexFCTZdq0UnSN78fY6CCM\nRFw5wU0WM9nEpbWzkBBWsYHeTLo5JqR/mZukfjlPDlcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA\nlUhwzCSnuqt4wlHxJONN4kxUBG8bPnjHxob6jBKK+onFDuSVWZ+7LZw67blz6xdxvlOLaQLi1fK2\nFifehbc7KbRLckcgNgg7Y8qfUKdP0/nS0JlyAvlnICQqaHTHwhIzQqTHtTZeeIJHtpWOX/OPRI0S\nbkygh2qjF8bYn3sX8bGNUQL8iiMxFnvwGrXaErPqlRqFJbWQDBXD+nYDIBw7WN3Jyb0Ydin2zrlh\ngp3Qooi0TnAir3ncw/UF/+sivCgd+6nX7HkbZtipkMbg7ZByyD9xrOQG2JXrP6PyzGCPwnGMt9pL\niiVMepeLNqKZ3UvhrR1uRN0KWu7lduIRhxldLA==\n \n \n \n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\n \n \n \n\n", "sign_metadata": false, "sign_requests": false, "signed_assertion_in_resp": false, "signed_envelopes_in_resp": false } ], - "updated_at": "2023-01-13T06:30:47.529652Z", + "updated_at": "2023-02-18T18:55:51.972476Z", "vpn_session_duration": 100 } } diff --git a/www/docs/reference/rest-api/devices.mdx b/www/docs/reference/rest-api/devices.mdx index 40584ba99..65279cb4b 100644 --- a/www/docs/reference/rest-api/devices.mdx +++ b/www/docs/reference/rest-api/devices.mdx @@ -24,139 +24,169 @@ Content-Type: application/json; charset=utf-8 { "data": [ { - "allowed_ips": "0.0.0.0/0,::/0", + "allowed_ips": [ + "0.0.0.0/0", + "::/0" + ], "description": "factory description", - "dns": "1.1.1.1,1.0.0.1", + "dns": [ + "1.1.1.1", + "1.0.0.1" + ], "endpoint": "localhost:51820", - "id": "9fc9b189-b740-4676-a7c2-c135b97d951d", - "inserted_at": "2023-01-13T06:30:47.092141Z", - "ipv4": "100.99.95.137", - "ipv6": "fd00::2a:3f6d", + "id": "3e768884-c957-482c-8467-08e457f4acea", + "inserted_at": "2023-02-18T18:55:51.295135Z", + "ipv4": "100.104.159.238", + "ipv6": "fd00::d:a98a", "latest_handshake": null, "mtu": 1280, - "name": "factory 3175", + "name": "factory 3555", "persistent_keepalive": 25, - "preshared_key": "Fj2iQKy5R5lLi8GIMI96FIfgWs/8saEYj4q6mLtgpg8=", - "public_key": "cVAdAno+PbyPBg5ubwxPe86QahSZ3AwhsKHfkFNw0nc=", + "preshared_key": "Ev+p0ASEswYRzLVtZX+cAhadlOPmAhH1/coj/i4Mrug=", + "public_key": "IwJeJ05UPKacEUKoQazEfzVMySiUa0prvRxZYCSteQs=", "remote_ip": null, "rx_bytes": null, "server_public_key": "is+0ov0/SZ9I+qyDD+adVoH9LreWHa85QQgpt6RUtA4=", "tx_bytes": null, - "updated_at": "2023-01-13T06:30:47.092141Z", + "updated_at": "2023-02-18T18:55:51.295135Z", "use_default_allowed_ips": true, "use_default_dns": true, "use_default_endpoint": true, "use_default_mtu": true, "use_default_persistent_keepalive": true, - "user_id": "4b195eb9-d675-4143-8de0-64e68c5a6d86" + "user_id": "e555125b-9831-470e-adbd-b4bffcdcfa8e" }, { - "allowed_ips": "0.0.0.0/0,::/0", + "allowed_ips": [ + "0.0.0.0/0", + "::/0" + ], "description": "factory description", - "dns": "1.1.1.1,1.0.0.1", + "dns": [ + "1.1.1.1", + "1.0.0.1" + ], "endpoint": "localhost:51820", - "id": "85243eb4-8c30-4bce-bb40-b4b9650e7ffa", - "inserted_at": "2023-01-13T06:30:47.101527Z", - "ipv4": "100.106.156.151", - "ipv6": "fd00::3e:876d", + "id": "c49e49ad-0a23-4857-9a70-c2b47d399f8c", + "inserted_at": "2023-02-18T18:55:51.272421Z", + "ipv4": "100.97.251.99", + "ipv6": "fd00::10:70ff", "latest_handshake": null, "mtu": 1280, - "name": "factory 2820", + "name": "factory 3395", "persistent_keepalive": 25, - "preshared_key": "NXiX1/xSyDfEl+S3O7VaaVTJKUu2kZo91pCycuZG3mk=", - "public_key": "e8e8+NBCAyHPycu2VJIRK9NQCR5Bz5Oo6aFbuOmUMhc=", + "preshared_key": "IwAp9NXPyuVbWhqT2YnMydKKxzZM/azEQlcn8uNC9UA=", + "public_key": "wyiIyos+4gsK1FovaHji5tDsNZHz9eqPuP+aXQvaKKc=", "remote_ip": null, "rx_bytes": null, "server_public_key": "is+0ov0/SZ9I+qyDD+adVoH9LreWHa85QQgpt6RUtA4=", "tx_bytes": null, - "updated_at": "2023-01-13T06:30:47.101527Z", + "updated_at": "2023-02-18T18:55:51.272421Z", "use_default_allowed_ips": true, "use_default_dns": true, "use_default_endpoint": true, "use_default_mtu": true, "use_default_persistent_keepalive": true, - "user_id": "1949803a-bb74-429c-ac99-fae4ce09ca08" + "user_id": "84ae42a2-00b6-4518-a02e-636846a2b517" }, { - "allowed_ips": "0.0.0.0/0,::/0", + "allowed_ips": [ + "0.0.0.0/0", + "::/0" + ], "description": "factory description", - "dns": "1.1.1.1,1.0.0.1", + "dns": [ + "1.1.1.1", + "1.0.0.1" + ], "endpoint": "localhost:51820", - "id": "7898b569-7c59-4596-b002-e1717c6fe5df", - "inserted_at": "2023-01-13T06:30:47.108829Z", - "ipv4": "100.120.62.86", - "ipv6": "fd00::38:7ee5", + "id": "2803d77b-5f34-4277-ab15-afc452792d53", + "inserted_at": "2023-02-18T18:55:51.280811Z", + "ipv4": "100.124.65.64", + "ipv6": "fd00::10:c793", "latest_handshake": null, "mtu": 1280, - "name": "factory 2978", + "name": "factory 3301", "persistent_keepalive": 25, - "preshared_key": "i6kyqjysbWGEWMO9FNUDMxE1OhrYJsgjuIfgnJNApyE=", - "public_key": "ulryM87WfDob8foWZtIiaW+cH+ugh4t/31vSO2YNRtA=", + "preshared_key": "p1xB8mSPSYqWeQ88zuomTe3/qP/dGNHIWMuhZV35aNw=", + "public_key": "kjnSbt1PzVNxy2Zk0WU61+euwB47PhkYGVjPH3Qu3ws=", "remote_ip": null, "rx_bytes": null, "server_public_key": "is+0ov0/SZ9I+qyDD+adVoH9LreWHa85QQgpt6RUtA4=", "tx_bytes": null, - "updated_at": "2023-01-13T06:30:47.108829Z", + "updated_at": "2023-02-18T18:55:51.280811Z", "use_default_allowed_ips": true, "use_default_dns": true, "use_default_endpoint": true, "use_default_mtu": true, "use_default_persistent_keepalive": true, - "user_id": "a1ebe3b2-3843-47ee-baf4-077740bfcd35" + "user_id": "8b93ed2e-0413-4aa4-8141-d47ab9a44489" }, { - "allowed_ips": "0.0.0.0/0,::/0", + "allowed_ips": [ + "0.0.0.0/0", + "::/0" + ], "description": "factory description", - "dns": "1.1.1.1,1.0.0.1", + "dns": [ + "1.1.1.1", + "1.0.0.1" + ], "endpoint": "localhost:51820", - "id": "3113df9e-29a4-4062-8a7d-7bf67612c61c", - "inserted_at": "2023-01-13T06:30:47.115812Z", - "ipv4": "100.64.196.12", - "ipv6": "fd00::1e:b441", + "id": "e40913be-777b-4c8c-b046-729691d3ab3d", + "inserted_at": "2023-02-18T18:55:51.288043Z", + "ipv4": "100.110.155.13", + "ipv6": "fd00::36:ad92", "latest_handshake": null, "mtu": 1280, - "name": "factory 1577", + "name": "factory 3461", "persistent_keepalive": 25, - "preshared_key": "oZsi27RP/myoPFDTdxv29KVAxb2D1PTN+ojhUHsuM9I=", - "public_key": "ZZrmY3JKOLJc9JZO+JTBJ9toM+x3hZBikcEAAxhdnWY=", + "preshared_key": "81ZGYaMfpDMGJ1NSAA6X2m82WcHrj/JYSSUxHcEGtNc=", + "public_key": "e9zG3QTAFUx+3TixOgtTc1K3xLqmz+2ePIjQC6yvfiw=", "remote_ip": null, "rx_bytes": null, "server_public_key": "is+0ov0/SZ9I+qyDD+adVoH9LreWHa85QQgpt6RUtA4=", "tx_bytes": null, - "updated_at": "2023-01-13T06:30:47.115812Z", + "updated_at": "2023-02-18T18:55:51.288043Z", "use_default_allowed_ips": true, "use_default_dns": true, "use_default_endpoint": true, "use_default_mtu": true, "use_default_persistent_keepalive": true, - "user_id": "634c7a8b-95c3-4c2f-9566-78be4d768cde" + "user_id": "dc94df4e-a0f6-4982-9964-c266eb2b9760" }, { - "allowed_ips": "0.0.0.0/0,::/0", + "allowed_ips": [ + "0.0.0.0/0", + "::/0" + ], "description": "factory description", - "dns": "1.1.1.1,1.0.0.1", + "dns": [ + "1.1.1.1", + "1.0.0.1" + ], "endpoint": "localhost:51820", - "id": "44c8dd33-ad63-47cf-93ae-ff5ffcb16f52", - "inserted_at": "2023-01-13T06:30:47.123039Z", - "ipv4": "100.93.227.124", - "ipv6": "fd00::3:b1dd", + "id": "ac9b4b14-2931-462b-8f69-0eaab1e09c79", + "inserted_at": "2023-02-18T18:55:51.306250Z", + "ipv4": "100.127.98.215", + "ipv6": "fd00::3:ba30", "latest_handshake": null, "mtu": 1280, - "name": "factory 1673", + "name": "factory 3651", "persistent_keepalive": 25, - "preshared_key": "G0XAE7HCOArcB+RH09g/HIsDJM3SfYk9WKa1WYo6hv4=", - "public_key": "RBr9GYzFGuoltoIt2iTeqSb0CHaiPAcwxxxxSYlX4tg=", + "preshared_key": "zVx8+DzRh8k3RcdadCmN+Rv7tMYBeB6NssRZKXiDPJU=", + "public_key": "itqdVYUfCY48iWiSfNR6+fidTKK2WXeWLwJybMiv3Mc=", "remote_ip": null, "rx_bytes": null, "server_public_key": "is+0ov0/SZ9I+qyDD+adVoH9LreWHa85QQgpt6RUtA4=", "tx_bytes": null, - "updated_at": "2023-01-13T06:30:47.123039Z", + "updated_at": "2023-02-18T18:55:51.306250Z", "use_default_allowed_ips": true, "use_default_dns": true, "use_default_endpoint": true, "use_default_mtu": true, "use_default_persistent_keepalive": true, - "user_id": "b61a4d7a-34a6-4d37-9871-a024fb97fe60" + "user_id": "49e2e587-f979-4aae-bad5-a513872526b5" } ] } @@ -174,9 +204,15 @@ $ curl -i \ --data-binary @- << EOF { "device": { - "allowed_ips": "0.0.0.0/0, ::/0, 1.1.1.1", + "allowed_ips": [ + "0.0.0.0/0", + "::/0", + "1.1.1.1" + ], "description": "create-description", - "dns": "9.9.9.8", + "dns": [ + "9.9.9.8" + ], "endpoint": "9.9.9.9", "ipv4": "100.64.0.2", "ipv6": "fd00::2", @@ -190,23 +226,29 @@ $ curl -i \ "use_default_endpoint": false, "use_default_mtu": false, "use_default_persistent_keepalive": false, - "user_id": "57afecab-c92c-45b8-8764-017a7cb5276b" + "user_id": "6b657ede-8018-4b00-b8a0-28c6de4337dc" } }' EOF HTTP/1.1 201 Content-Type: application/json; charset=utf-8 -Location: /v0/devices/0f71030e-b872-494f-af2b-c31730f119e0 +Location: /v0/devices/9341237e-8d5c-4b88-8a04-90f622d79b88 { "data": { - "allowed_ips": "0.0.0.0/0, ::/0, 1.1.1.1", + "allowed_ips": [ + "0.0.0.0/0", + "::/0", + "1.1.1.1" + ], "description": "create-description", - "dns": "9.9.9.8", + "dns": [ + "9.9.9.8" + ], "endpoint": "9.9.9.9", - "id": "0f71030e-b872-494f-af2b-c31730f119e0", - "inserted_at": "2023-01-13T06:30:47.204640Z", + "id": "9341237e-8d5c-4b88-8a04-90f622d79b88", + "inserted_at": "2023-02-18T18:55:51.232890Z", "ipv4": "100.64.0.2", "ipv6": "fd00::2", "latest_handshake": null, @@ -219,13 +261,13 @@ Location: /v0/devices/0f71030e-b872-494f-af2b-c31730f119e0 "rx_bytes": null, "server_public_key": "is+0ov0/SZ9I+qyDD+adVoH9LreWHa85QQgpt6RUtA4=", "tx_bytes": null, - "updated_at": "2023-01-13T06:30:47.204640Z", + "updated_at": "2023-02-18T18:55:51.232890Z", "use_default_allowed_ips": false, "use_default_dns": false, "use_default_endpoint": false, "use_default_mtu": false, "use_default_persistent_keepalive": false, - "user_id": "57afecab-c92c-45b8-8764-017a7cb5276b" + "user_id": "6b657ede-8018-4b00-b8a0-28c6de4337dc" } } ``` @@ -236,7 +278,7 @@ Location: /v0/devices/0f71030e-b872-494f-af2b-c31730f119e0 #### Example **URI Parameters:** - - `id`: `904fbe05-86a5-4edb-afb2-6b728755a210` + - `id`: `996f3a7e-8314-4caa-88ca-67b0a798810b` ```bash $ curl -i \ -X GET "https://{firezone_host}/v0/devices/{id}" \ @@ -248,31 +290,37 @@ Content-Type: application/json; charset=utf-8 { "data": { - "allowed_ips": "0.0.0.0/0,::/0", + "allowed_ips": [ + "0.0.0.0/0", + "::/0" + ], "description": "factory description", - "dns": "1.1.1.1,1.0.0.1", + "dns": [ + "1.1.1.1", + "1.0.0.1" + ], "endpoint": "localhost:51820", - "id": "904fbe05-86a5-4edb-afb2-6b728755a210", - "inserted_at": "2023-01-13T06:30:47.048145Z", - "ipv4": "100.74.213.26", - "ipv6": "fd00::5:110a", + "id": "996f3a7e-8314-4caa-88ca-67b0a798810b", + "inserted_at": "2023-02-18T18:55:49.480530Z", + "ipv4": "100.115.46.241", + "ipv6": "fd00::13:a505", "latest_handshake": null, "mtu": 1280, - "name": "factory 2403", + "name": "factory 2050", "persistent_keepalive": 25, - "preshared_key": "fue0i7cIahPPlWr6UCSerJNu3NRDrETv3YW+Xjc3qU4=", - "public_key": "6KYOCkP8off66kidQOXpwQlwKix8ELfXm/kR5cwsnug=", + "preshared_key": "zwYGMjuBBLZk4YkBlDx5LZHOf6gf35b6/2SoFZIc8a0=", + "public_key": "CZz2mwmaCyNrjAcANfOCPpozFrIOkDvCaaoeb6O3hvw=", "remote_ip": null, "rx_bytes": null, "server_public_key": "is+0ov0/SZ9I+qyDD+adVoH9LreWHa85QQgpt6RUtA4=", "tx_bytes": null, - "updated_at": "2023-01-13T06:30:47.048145Z", + "updated_at": "2023-02-18T18:55:49.480530Z", "use_default_allowed_ips": true, "use_default_dns": true, "use_default_endpoint": true, "use_default_mtu": true, "use_default_persistent_keepalive": true, - "user_id": "bc22e5c4-853c-4e0d-bf93-85111707e66f" + "user_id": "1e4c2c8b-914a-4cd7-b3f0-fd2b2c401e17" } } ``` @@ -283,7 +331,7 @@ Content-Type: application/json; charset=utf-8 #### Example **URI Parameters:** - - `id`: `ef1da923-caf4-47f3-ab6a-7ab0908d7f0e` + - `id`: `f8dccb0b-2b2a-463c-a5f0-630df7c2ea53` ```bash $ curl -i \ -X PUT "https://{firezone_host}/v0/devices/{id}" \ @@ -292,9 +340,15 @@ $ curl -i \ --data-binary @- << EOF { "device": { - "allowed_ips": "0.0.0.0/0, ::/0, 1.1.1.1", + "allowed_ips": [ + "0.0.0.0/0", + "::/0", + "1.1.1.1" + ], "description": "create-description", - "dns": "9.9.9.8", + "dns": [ + "9.9.9.8" + ], "endpoint": "9.9.9.9", "ipv4": "100.64.0.2", "ipv6": "fd00::2", @@ -317,12 +371,18 @@ Content-Type: application/json; charset=utf-8 { "data": { - "allowed_ips": "0.0.0.0/0, ::/0, 1.1.1.1", + "allowed_ips": [ + "0.0.0.0/0", + "::/0", + "1.1.1.1" + ], "description": "create-description", - "dns": "9.9.9.8", + "dns": [ + "9.9.9.8" + ], "endpoint": "9.9.9.9", - "id": "ef1da923-caf4-47f3-ab6a-7ab0908d7f0e", - "inserted_at": "2023-01-13T06:30:47.264719Z", + "id": "f8dccb0b-2b2a-463c-a5f0-630df7c2ea53", + "inserted_at": "2023-02-18T18:55:51.243689Z", "ipv4": "100.64.0.2", "ipv6": "fd00::2", "latest_handshake": null, @@ -335,13 +395,13 @@ Content-Type: application/json; charset=utf-8 "rx_bytes": null, "server_public_key": "is+0ov0/SZ9I+qyDD+adVoH9LreWHa85QQgpt6RUtA4=", "tx_bytes": null, - "updated_at": "2023-01-13T06:30:47.279141Z", + "updated_at": "2023-02-18T18:55:51.255811Z", "use_default_allowed_ips": false, "use_default_dns": false, "use_default_endpoint": false, "use_default_mtu": false, "use_default_persistent_keepalive": false, - "user_id": "ae05e328-3a67-4101-9d6b-48dcd9cdddf8" + "user_id": "1233e2d4-c9ba-4d2a-bb8b-ac23050eba78" } } ``` @@ -352,7 +412,7 @@ Content-Type: application/json; charset=utf-8 #### Example **URI Parameters:** - - `id`: `5b8cf677-ae60-4eca-b038-9abcd410904f` + - `id`: `1385934c-17f4-4129-9e66-cbba1e4c1734` ```bash $ curl -i \ -X DELETE "https://{firezone_host}/v0/devices/{id}" \ diff --git a/www/docs/reference/rest-api/rules.mdx b/www/docs/reference/rest-api/rules.mdx index a7a01dd8c..d5e2c5d0b 100644 --- a/www/docs/reference/rest-api/rules.mdx +++ b/www/docs/reference/rest-api/rules.mdx @@ -26,51 +26,51 @@ Content-Type: application/json; charset=utf-8 { "action": "drop", "destination": "10.3.2.1", - "id": "fdcf2ca8-1871-4aac-bc01-591b3e18578e", - "inserted_at": "2023-01-13T06:30:46.079209Z", + "id": "f0f6bd4e-f68b-4347-ada6-4d024787aae9", + "inserted_at": "2023-02-18T18:55:51.352217Z", "port_range": null, "port_type": null, - "updated_at": "2023-01-13T06:30:46.079209Z", + "updated_at": "2023-02-18T18:55:51.352217Z", "user_id": null }, { "action": "drop", "destination": "10.3.2.2", - "id": "2e544ab0-0ca5-432e-9a31-b5feb982f50a", - "inserted_at": "2023-01-13T06:30:46.087033Z", + "id": "0d2b5c4d-92e0-4271-adf3-9ad852d2bda7", + "inserted_at": "2023-02-18T18:55:51.353801Z", "port_range": null, "port_type": null, - "updated_at": "2023-01-13T06:30:46.087033Z", + "updated_at": "2023-02-18T18:55:51.353801Z", "user_id": null }, { "action": "drop", "destination": "10.3.2.3", - "id": "02df6455-65f1-406b-a3dd-7c223af04f9b", - "inserted_at": "2023-01-13T06:30:46.088443Z", + "id": "1b9e57f4-0510-46c5-8440-ce1fec30af66", + "inserted_at": "2023-02-18T18:55:51.354824Z", "port_range": null, "port_type": null, - "updated_at": "2023-01-13T06:30:46.088443Z", + "updated_at": "2023-02-18T18:55:51.354824Z", "user_id": null }, { "action": "drop", "destination": "10.3.2.4", - "id": "e0c4e652-e3a2-4a69-a8f3-265e07515938", - "inserted_at": "2023-01-13T06:30:46.090111Z", + "id": "99d86823-1b87-49f5-8522-c1c2ba7d42b3", + "inserted_at": "2023-02-18T18:55:51.355740Z", "port_range": null, "port_type": null, - "updated_at": "2023-01-13T06:30:46.090111Z", + "updated_at": "2023-02-18T18:55:51.355740Z", "user_id": null }, { "action": "drop", "destination": "10.3.2.5", - "id": "a7c2bfb2-3e09-48d8-b038-c69c84b777b1", - "inserted_at": "2023-01-13T06:30:46.091623Z", + "id": "57e0237f-1dc2-4f6c-849c-5c24e47efd23", + "inserted_at": "2023-02-18T18:55:51.356725Z", "port_range": null, "port_type": null, - "updated_at": "2023-01-13T06:30:46.091623Z", + "updated_at": "2023-02-18T18:55:51.356725Z", "user_id": null } ] @@ -93,25 +93,25 @@ $ curl -i \ "destination": "1.1.1.1/24", "port_range": "1 - 2", "port_type": "udp", - "user_id": "9f4d207a-90d8-4cc9-800c-accefe9f90cf" + "user_id": "d6e0fef3-8b87-496a-aa63-34178d559b71" } }' EOF HTTP/1.1 201 Content-Type: application/json; charset=utf-8 -Location: /v0/rules/cc374f84-b003-4858-8772-516ea3f098a1 +Location: /v0/rules/cac89e93-00d3-4d98-ad50-b75a60b0a464 { "data": { "action": "accept", "destination": "1.1.1.1/24", - "id": "cc374f84-b003-4858-8772-516ea3f098a1", - "inserted_at": "2023-01-13T06:30:47.193190Z", + "id": "cac89e93-00d3-4d98-ad50-b75a60b0a464", + "inserted_at": "2023-02-18T18:55:51.290304Z", "port_range": "1 - 2", "port_type": "udp", - "updated_at": "2023-01-13T06:30:47.193190Z", - "user_id": "9f4d207a-90d8-4cc9-800c-accefe9f90cf" + "updated_at": "2023-02-18T18:55:51.290304Z", + "user_id": "d6e0fef3-8b87-496a-aa63-34178d559b71" } } ``` @@ -122,7 +122,7 @@ Location: /v0/rules/cc374f84-b003-4858-8772-516ea3f098a1 #### Example **URI Parameters:** - - `id`: `49a9ae27-74f2-45dd-a324-d47a7581205c` + - `id`: `7b91d771-8c4a-45aa-8b6f-0cb5b7e486fc` ```bash $ curl -i \ -X GET "https://{firezone_host}/v0/rules/{id}" \ @@ -136,11 +136,11 @@ Content-Type: application/json; charset=utf-8 "data": { "action": "drop", "destination": "10.10.10.0/24", - "id": "49a9ae27-74f2-45dd-a324-d47a7581205c", - "inserted_at": "2023-01-13T06:30:46.993421Z", + "id": "7b91d771-8c4a-45aa-8b6f-0cb5b7e486fc", + "inserted_at": "2023-02-18T18:55:51.211234Z", "port_range": null, "port_type": null, - "updated_at": "2023-01-13T06:30:46.993421Z", + "updated_at": "2023-02-18T18:55:51.211234Z", "user_id": null } } @@ -152,7 +152,7 @@ Content-Type: application/json; charset=utf-8 #### Example **URI Parameters:** - - `id`: `b8231fa6-1df2-4b1b-8687-61a72a8031b1` + - `id`: `ca67b973-2ee6-4bc5-942c-848990eaae49` ```bash $ curl -i \ -X PUT "https://{firezone_host}/v0/rules/{id}" \ @@ -176,11 +176,11 @@ Content-Type: application/json; charset=utf-8 "data": { "action": "accept", "destination": "1.1.1.1/24", - "id": "b8231fa6-1df2-4b1b-8687-61a72a8031b1", - "inserted_at": "2023-01-13T06:30:47.244050Z", + "id": "ca67b973-2ee6-4bc5-942c-848990eaae49", + "inserted_at": "2023-02-18T18:55:51.294125Z", "port_range": "1 - 2", "port_type": "udp", - "updated_at": "2023-01-13T06:30:47.254788Z", + "updated_at": "2023-02-18T18:55:51.313846Z", "user_id": null } } @@ -192,7 +192,7 @@ Content-Type: application/json; charset=utf-8 #### Example **URI Parameters:** - - `id`: `05c0342c-984c-43df-855b-32e88ea8ee08` + - `id`: `7e8a2c10-3a34-4e94-bc10-70c1ba265f99` ```bash $ curl -i \ -X DELETE "https://{firezone_host}/v0/rules/{id}" \ diff --git a/www/docs/reference/rest-api/users.mdx b/www/docs/reference/rest-api/users.mdx index 2e8dd717c..17cae19b3 100644 --- a/www/docs/reference/rest-api/users.mdx +++ b/www/docs/reference/rest-api/users.mdx @@ -4,6 +4,7 @@ sidebar_position: 2 toc_max_heading_level: 4 --- + This endpoint allows an administrator to manage Users. ## Auto-Create Users from OpenID or SAML providers @@ -15,12 +16,17 @@ able to log-in to Firezone. If `auto_create_users` is `false`, then you need to provision users with `password` attribute, otherwise they will have no means to log in. -## API Documentation +## Disabling users +Even though API returns `disabled_at` attribute, currently, it's not possible to disable users via API, +since this field is only for internal use by automatic user disabling mechanism on OIDC/SAML errors. + +## API Documentation ### List all Users [`GET /v0/users`] -#### Example + +#### Example ```bash $ curl -i \ -X GET "https://{firezone_host}/v0/users" \ @@ -34,59 +40,59 @@ Content-Type: application/json; charset=utf-8 "data": [ { "disabled_at": null, - "email": "test-2886@test", - "id": "9f0ce70d-d9e6-4610-ad3b-e5758318c016", - "inserted_at": "2023-01-13T06:30:47.076850Z", + "email": "test-4578@test", + "id": "61598ea6-acaa-4308-b12f-2da95b312387", + "inserted_at": "2023-02-18T18:55:50.972304Z", "last_signed_in_at": null, "last_signed_in_method": null, "role": "admin", - "updated_at": "2023-01-13T06:30:47.076850Z" + "updated_at": "2023-02-18T18:55:50.972304Z" }, { "disabled_at": null, - "email": "test-2918@test", - "id": "36479416-7099-46f9-b9b9-3ad4411eef7d", - "inserted_at": "2023-01-13T06:30:47.079115Z", + "email": "test-2280@test", + "id": "9cde3f7f-db18-49b7-84de-b88675c6ab73", + "inserted_at": "2023-02-18T18:55:50.973729Z", "last_signed_in_at": null, "last_signed_in_method": null, "role": "admin", - "updated_at": "2023-01-13T06:30:47.079115Z" + "updated_at": "2023-02-18T18:55:50.973729Z" }, { "disabled_at": null, - "email": "test-3045@test", - "id": "232c2358-5132-4fc7-8e42-cd8464fcae02", - "inserted_at": "2023-01-13T06:30:47.081138Z", + "email": "test-2312@test", + "id": "820eb7eb-354e-4f6f-8fdc-acbab6e35e7b", + "inserted_at": "2023-02-18T18:55:50.975468Z", "last_signed_in_at": null, "last_signed_in_method": null, "role": "admin", - "updated_at": "2023-01-13T06:30:47.081138Z" + "updated_at": "2023-02-18T18:55:50.975468Z" }, { "disabled_at": null, - "email": "test-2950@test", - "id": "b15b274b-751e-4ca6-9c3e-3a798299ec86", - "inserted_at": "2023-01-13T06:30:47.083059Z", + "email": "test-2344@test", + "id": "a2b12d92-9498-4b88-b2fc-215b612714c4", + "inserted_at": "2023-02-18T18:55:50.976834Z", "last_signed_in_at": null, "last_signed_in_method": null, "role": "admin", - "updated_at": "2023-01-13T06:30:47.083059Z" + "updated_at": "2023-02-18T18:55:50.976834Z" } ] } ``` - ### Create a User [`POST /v0/users`] + Create a new User. This endpoint is useful in two cases: -1. When [Local Authentication](/docs/authenticate/local-auth/) is enabled (discouraged in - production deployments), it allows an administrator to provision users with their passwords; -2. When `auto_create_users` in the associated OpenID or SAML configuration is disabled, - it allows an administrator to provision users with their emails beforehand, effectively - whitelisting specific users for authentication. + 1. When [Local Authentication](/authenticate/local-auth/) is enabled (discouraged in + production deployments), it allows an administrator to provision users with their passwords; + 2. When `auto_create_users` in the associated OpenID or SAML configuration is disabled, + it allows an administrator to provision users with their emails beforehand, effectively + whitelisting specific users for authentication. If `auto_create_users` is `true` in the associated OpenID or SAML configuration, there is no need to provision users; they will be created automatically when they log in for the first time using @@ -94,15 +100,14 @@ the associated OpenID or SAML provider. #### User Attributes -| Attribute | Type | Required | Description | -| ----------------------- | ----------------------------------- | -------- | -------------------------------------------------------------- | -| `role` | `admin` or `unprivileged` (default) | No | User role. | -| `email` | `string` | Yes | Email which will be used to identify the user. | -| `password` | `string` | No | A password that can be used for login-password authentication. | -| `password_confirmation` | `string` | -> | Is required when the `password` is set. | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `role` | `admin` or `unprivileged` (default) | No | User role. | +| `email` | `string` | Yes | Email which will be used to identify the user. | +| `password` | `string` | No | A password that can be used for login-password authentication. | +| `password_confirmation` | `string` | -> | Is required when the `password` is set. | #### Example - ```bash $ curl -i \ -X POST "https://{firezone_host}/v0/users" \ @@ -121,24 +126,22 @@ EOF HTTP/1.1 201 Content-Type: application/json; charset=utf-8 -Location: /v0/users/86616e3e-13f0-4177-bc8e-1a0e588f0be8 +Location: /v0/users/1c4476d8-b3ed-4e2a-a327-43d8a8145902 { "data": { "disabled_at": null, "email": "new-user@test", - "id": "86616e3e-13f0-4177-bc8e-1a0e588f0be8", - "inserted_at": "2023-01-13T06:30:47.047550Z", + "id": "1c4476d8-b3ed-4e2a-a327-43d8a8145902", + "inserted_at": "2023-02-18T18:55:51.312737Z", "last_signed_in_at": null, "last_signed_in_method": null, "role": "unprivileged", - "updated_at": "2023-01-13T06:30:47.047550Z" + "updated_at": "2023-02-18T18:55:51.312737Z" } } ``` - #### Provision an unprivileged OpenID User - ```bash $ curl -i \ -X POST "https://{firezone_host}/v0/users" \ @@ -155,24 +158,22 @@ EOF HTTP/1.1 201 Content-Type: application/json; charset=utf-8 -Location: /v0/users/6e7962a7-c183-4afb-8569-9001bdfd0d87 +Location: /v0/users/b0c662db-fe4b-4be6-8cba-b96e6de85d3c { "data": { "disabled_at": null, "email": "new-user@test", - "id": "6e7962a7-c183-4afb-8569-9001bdfd0d87", - "inserted_at": "2023-01-13T06:30:47.282412Z", + "id": "b0c662db-fe4b-4be6-8cba-b96e6de85d3c", + "inserted_at": "2023-02-18T18:55:51.143811Z", "last_signed_in_at": null, "last_signed_in_method": null, "role": "unprivileged", - "updated_at": "2023-01-13T06:30:47.282412Z" + "updated_at": "2023-02-18T18:55:51.143811Z" } } ``` - #### Provision an admin OpenID User - ```bash $ curl -i \ -X POST "https://{firezone_host}/v0/users" \ @@ -189,24 +190,22 @@ EOF HTTP/1.1 201 Content-Type: application/json; charset=utf-8 -Location: /v0/users/dedc4dcc-0f65-4110-ad7f-9c354e36e5e5 +Location: /v0/users/859b1c5f-d25e-4e15-bdba-1cb90de3b4f1 { "data": { "disabled_at": null, "email": "new-user@test", - "id": "dedc4dcc-0f65-4110-ad7f-9c354e36e5e5", - "inserted_at": "2023-01-13T06:30:47.166645Z", + "id": "859b1c5f-d25e-4e15-bdba-1cb90de3b4f1", + "inserted_at": "2023-02-18T18:55:51.071112Z", "last_signed_in_at": null, "last_signed_in_method": null, "role": "admin", - "updated_at": "2023-01-13T06:30:47.166645Z" + "updated_at": "2023-02-18T18:55:51.071112Z" } } ``` - #### Error due to invalid parameters - ```bash $ curl -i \ -X POST "https://{firezone_host}/v0/users" \ @@ -227,21 +226,22 @@ Content-Type: application/json; charset=utf-8 { "errors": { "password": [ - "should be at least 12 character(s)", - "does not match password confirmation." + "should be at least 12 character(s)" + ], + "password_confirmation": [ + "can't be blank" ] } } ``` - ### GET /v0/users/{id} -#### An email can be used instead of ID. + +#### An email can be used instead of ID. **URI Parameters:** -- `id`: `test-1481@test` - + - `id`: `test-2757@test` ```bash $ curl -i \ -X GET "https://{firezone_host}/v0/users/{id}" \ @@ -254,27 +254,25 @@ Content-Type: application/json; charset=utf-8 { "data": { "disabled_at": null, - "email": "test-1481@test", - "id": "b19a929d-fd84-4f11-a799-23416c8efaf8", - "inserted_at": "2023-01-13T06:30:47.050304Z", + "email": "test-2757@test", + "id": "a6150251-3d5d-4eef-b918-feefbb18e986", + "inserted_at": "2023-02-18T18:55:51.102821Z", "last_signed_in_at": null, "last_signed_in_method": null, "role": "admin", - "updated_at": "2023-01-13T06:30:47.050304Z" + "updated_at": "2023-02-18T18:55:51.102821Z" } } ``` - ### Update a User [`PATCH /v0/users/{id}`] + For details please see [Create a User](#create-a-user-post-v0users) section. #### Update by email - **URI Parameters:** -- `id`: `test-3618@test` - + - `id`: `test-4452@test` ```bash $ curl -i \ -X PUT "https://{firezone_host}/v0/users/{id}" \ @@ -292,23 +290,20 @@ Content-Type: application/json; charset=utf-8 { "data": { "disabled_at": null, - "email": "test-3618@test", - "id": "54d4de57-21f3-4adc-a9a9-a3ee642da76e", - "inserted_at": "2023-01-13T06:30:47.285730Z", + "email": "test-4452@test", + "id": "18fcbb10-af71-45fd-841e-8419f5d48a76", + "inserted_at": "2023-02-18T18:55:51.316849Z", "last_signed_in_at": null, "last_signed_in_method": null, "role": "unprivileged", - "updated_at": "2023-01-13T06:30:47.285730Z" + "updated_at": "2023-02-18T18:55:51.316849Z" } } ``` - #### Update by ID - **URI Parameters:** -- `id`: `8dd4eff5-3d2f-4868-94cd-73abb6f130dc` - + - `id`: `43838f3c-47e6-4368-851b-4004d389d0e2` ```bash $ curl -i \ -X PUT "https://{firezone_host}/v0/users/{id}" \ @@ -326,25 +321,24 @@ Content-Type: application/json; charset=utf-8 { "data": { "disabled_at": null, - "email": "test-3235@test", - "id": "8dd4eff5-3d2f-4868-94cd-73abb6f130dc", - "inserted_at": "2023-01-13T06:30:47.265280Z", + "email": "test-5026@test", + "id": "43838f3c-47e6-4368-851b-4004d389d0e2", + "inserted_at": "2023-02-18T18:55:51.074271Z", "last_signed_in_at": null, "last_signed_in_method": null, "role": "unprivileged", - "updated_at": "2023-01-13T06:30:47.265280Z" + "updated_at": "2023-02-18T18:55:51.074271Z" } } ``` - ### DELETE /v0/users/{id} + + #### Example - **URI Parameters:** -- `id`: `fc0b513f-bd4b-4015-ac71-29b59c678a20` - + - `id`: `65871243-ac8a-4c46-97b7-01e710d6e05e` ```bash $ curl -i \ -X DELETE "https://{firezone_host}/v0/users/{id}" \ @@ -352,14 +346,12 @@ $ curl -i \ -H 'Authorization: Bearer {api_token}' \ HTTP/1.1 204 +Content-Type: application/json; charset=utf-8 ``` - #### An email can be used instead of ID. - **URI Parameters:** -- `id`: `test-3816@test` - + - `id`: `test-4866@test` ```bash $ curl -i \ -X DELETE "https://{firezone_host}/v0/users/{id}" \ @@ -367,4 +359,5 @@ $ curl -i \ -H 'Authorization: Bearer {api_token}' \ HTTP/1.1 204 +Content-Type: application/json; charset=utf-8 ``` diff --git a/www/docs/reference/reverse-proxy-templates/traefik.mdx b/www/docs/reference/reverse-proxy-templates/traefik.mdx index 4aa65dea5..d0c2e060c 100644 --- a/www/docs/reference/reverse-proxy-templates/traefik.mdx +++ b/www/docs/reference/reverse-proxy-templates/traefik.mdx @@ -75,7 +75,7 @@ services: - 51820:51820/udp env_file: # This should contain a list of env vars for configuring Firezone. - # See https://docs.firezone.dev/reference/env-vars for more info. + # See https://www.firezone.dev/docs/reference/env-vars for more info. - ${FZ_INSTALL_DIR:-.}/.env volumes: # IMPORTANT: Persists WireGuard private key and other data. If @@ -133,14 +133,14 @@ http: routers: test: entryPoints: - - "web" + - "web" service: test rule: "Host(`44.200.42.78`)" services: test: loadBalancer: servers: - - url: "http://firezone:13000" + - url: "http://firezone:13000" ``` Now you should be able to start the Traefik proxy with `docker compose up`. @@ -152,7 +152,7 @@ This configuration uses Firezone's auto-generated self-signed certificates. ### SSL `docker-compose.yml` ```yaml -version: '3' +version: "3" x-deploy: &default-deploy restart_policy: @@ -193,7 +193,7 @@ services: DATABASE_ENCRYPTION_KEY: ${DATABASE_ENCRYPTION_KEY:?err} # Ensure this includes the traefik service IP. - EXTERAL_TRUSTED_PROXIES: [''] + EXTERAL_TRUSTED_PROXIES: [""] networks: - app cap_add: @@ -212,13 +212,13 @@ services: image: traefik:v2.8 # Enables the web UI and tells Traefik to listen to docker command: - - "--providers.docker" - - "--providers.file.filename=rules.yml" - - "--entrypoints.web.address=:443" - - "--entrypoints.web.forwardedHeaders.insecure" - - "--log.level=DEBUG" + - "--providers.docker" + - "--providers.file.filename=rules.yml" + - "--entrypoints.web.address=:443" + - "--entrypoints.web.forwardedHeaders.insecure" + - "--log.level=DEBUG" extra_hosts: - - "host.docker.internal:host-gateway" + - "host.docker.internal:host-gateway" ports: # The HTTP port - "443:443" @@ -265,7 +265,7 @@ http: test: loadBalancer: servers: - - url: "http://firezone:13000" + - url: "http://firezone:13000" tls: stores: default: