mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
REST API (#1155)
### TODO - [x] "/v0" - [x] Double-check migration order; re-timestamp if necessary - [x] Move `sites` fields to `configurations` so they can be updated from API - [x] #1240 -- it introduces possible race conditions for API requests - [x] #1249 - [ ] #1008 - [ ] Final review Signed-off-by: Jamil <jamilbk@users.noreply.github.com> Co-authored-by: Andrew Dryga <andrew@dryga.com>
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
[codespell]
|
||||
skip = ./cover,./vendor,./omnibus,*.json,yarn.lock,seeds.exs,./**/node_modules,./deps,./priv/static,./priv/plts,./**/priv/static,./.git,./docs/build,./_build
|
||||
skip = ./erl_crash.dump,./apps/fz_http/erl_crash.dump,./cover,./vendor,./omnibus,*.json,yarn.lock,seeds.exs,./**/node_modules,./deps,./priv/static,./priv/plts,./**/priv/static,./.git,./docs/build,./_build
|
||||
ignore-words-list = keypair,keypairs,iif,statics,wee
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
{Credo.Check.Readability.LargeNumbers, []},
|
||||
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
|
||||
{Credo.Check.Readability.ModuleAttributeNames, []},
|
||||
{Credo.Check.Readability.ModuleDoc, []},
|
||||
{Credo.Check.Readability.ModuleNames, []},
|
||||
{Credo.Check.Readability.ParenthesesInCondition, []},
|
||||
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
|
||||
|
||||
@@ -61,4 +61,4 @@ repos:
|
||||
- -b
|
||||
- master
|
||||
- --pattern
|
||||
- '^(?!((chore|feat|feature|bug|fix|build|ci|docs|style|refactor|perf|test|revert)\/[a-zA-Z0-9\-\.\/]+)$).*'
|
||||
- '^(?!((chore|feat|feature|bug|fix|build|ci|docs|style|refactor|perf|test|revert)\/[@a-zA-Z0-9\-\.\/]+)$).*'
|
||||
|
||||
@@ -109,27 +109,6 @@ We use [pre-commit](https://pre-commit.com) to catch any static analysis issues
|
||||
before code is committed. Install with Homebrew: `brew install pre-commit` or
|
||||
pip: `pip install pre-commit`.
|
||||
|
||||
### The ENV file
|
||||
|
||||
For running tests and developing Firezone outside of Docker, you'll need some
|
||||
environment variables present in your shell's env.
|
||||
|
||||
See .env.sample an example of what variables you need. We recommend copying this
|
||||
file to `.env` and using a dotenv loader to apply this to your current shell
|
||||
env.
|
||||
|
||||
For example, run the following command to 'source' the environment variables
|
||||
from .env on `mix test`:
|
||||
|
||||
`env $(cat .env | grep -v \# | xargs) mix test`
|
||||
|
||||
This will initialize everything and run the test suite. If you have no
|
||||
failures, Firezone should be properly set up 🥳.
|
||||
|
||||
At this point you should be able to sign in to
|
||||
[http://localhost:4000](http://localhost:4000) with email `firezone@localhost` and
|
||||
password `firezone1234`.
|
||||
|
||||
### Bootstrapping
|
||||
|
||||
To start the local development cluster, follow these steps:
|
||||
@@ -137,13 +116,17 @@ To start the local development cluster, follow these steps:
|
||||
```
|
||||
docker compose build
|
||||
docker compose up -d postgres
|
||||
docker compose run --rm firezone mix ecto.setup
|
||||
docker compose run --rm firezone mix do ecto.setup, ecto.seed
|
||||
docker compose up
|
||||
```
|
||||
|
||||
Now you should be able to connect to `https://localhost/`
|
||||
and sign in with email `firezone@localhost` and password `firezone1234`.
|
||||
|
||||
The [`docker-compose.yml`](docker-compose.yml) file configures the Docker
|
||||
development environment. If you make any changes you feel would benefit
|
||||
all developers, feel free to open a PR to get them merged!
|
||||
|
||||
### Ensure Everything Works
|
||||
|
||||
There is a `client` container in the docker-compose configuration that
|
||||
|
||||
@@ -36,12 +36,12 @@ COPY apps/fz_vpn/mix.exs /var/app/apps/fz_vpn/mix.exs
|
||||
COPY apps/fz_wall/mix.exs /var/app/apps/fz_wall/mix.exs
|
||||
COPY mix.exs /var/app/mix.exs
|
||||
COPY mix.lock /var/app/mix.lock
|
||||
RUN mix do deps.get --only dev, deps.compile --only dev, compile --only dev
|
||||
RUN mix do deps.get --only test, deps.compile --only test, compile --only test
|
||||
RUN mix do deps.get, deps.compile, compile
|
||||
|
||||
# Copy more granular, dependency management files first to prevent
|
||||
# busting the Docker build cache unnecessarily
|
||||
COPY apps/fz_http/assets/package.json /var/app/apps/fz_http/assets/package.json
|
||||
COPY apps/fz_http/assets/local_modules /var/app/apps/fz_http/assets/local_modules
|
||||
COPY apps/fz_http/assets/package-lock.json /var/app/apps/fz_http/assets/package-lock.json
|
||||
RUN npm install --prefix apps/fz_http/assets
|
||||
|
||||
|
||||
@@ -2,19 +2,9 @@ defmodule FzCommon.FzNet do
|
||||
@moduledoc """
|
||||
Network utility functions.
|
||||
"""
|
||||
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
|
||||
@ip_regex ~r/((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/
|
||||
import FzCommon.FzRegex
|
||||
|
||||
@cidr4_regex ~r/^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))$/
|
||||
|
||||
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
|
||||
@cidr6_regex ~r/^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))$/
|
||||
|
||||
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
|
||||
@host_regex ~r/\A(([a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?)\.)*([a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?)$\Z/
|
||||
@fqdn_regex ~r/\A(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)\z/
|
||||
|
||||
# XXX: Consider using InetCidr for this
|
||||
# XXX: Consider using CIDR for this
|
||||
def ip_type(str) when is_binary(str) do
|
||||
charlist =
|
||||
str
|
||||
@@ -36,11 +26,11 @@ defmodule FzCommon.FzNet do
|
||||
end
|
||||
|
||||
def valid_cidr?(cidr) when is_binary(cidr) do
|
||||
String.match?(cidr, @cidr4_regex) or String.match?(cidr, @cidr6_regex)
|
||||
String.match?(cidr, cidr4_regex()) or String.match?(cidr, cidr6_regex())
|
||||
end
|
||||
|
||||
def valid_ip?(ip) when is_binary(ip) do
|
||||
String.match?(ip, @ip_regex)
|
||||
String.match?(ip, ip_regex())
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -50,8 +40,8 @@ defmodule FzCommon.FzNet do
|
||||
if String.contains?(inet, "/") do
|
||||
inet
|
||||
# normalize CIDR
|
||||
|> InetCidr.parse(true)
|
||||
|> InetCidr.to_string()
|
||||
|> CIDR.parse()
|
||||
|> to_string()
|
||||
else
|
||||
{:ok, addr} = inet |> String.to_charlist() |> :inet.parse_address()
|
||||
:inet.ntoa(addr) |> List.to_string()
|
||||
@@ -59,11 +49,11 @@ defmodule FzCommon.FzNet do
|
||||
end
|
||||
|
||||
def valid_fqdn?(fqdn) when is_binary(fqdn) do
|
||||
String.match?(fqdn, @fqdn_regex)
|
||||
String.match?(fqdn, fqdn_regex())
|
||||
end
|
||||
|
||||
def valid_hostname?(hostname) when is_binary(hostname) do
|
||||
String.match?(hostname, @host_regex)
|
||||
String.match?(hostname, host_regex())
|
||||
end
|
||||
|
||||
def to_complete_url(str) when is_binary(str) do
|
||||
|
||||
25
apps/fz_common/lib/fz_regex.ex
Normal file
25
apps/fz_common/lib/fz_regex.ex
Normal file
@@ -0,0 +1,25 @@
|
||||
defmodule FzCommon.FzRegex do
|
||||
@moduledoc """
|
||||
A common module to store RegExps used in FzCommon in order
|
||||
to avoid linting and editor slowdown issues in other modules.
|
||||
|
||||
What a time to be a developer, right?
|
||||
"""
|
||||
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
|
||||
@ip_regex ~r/((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/
|
||||
|
||||
@cidr4_regex ~r/^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))$/
|
||||
|
||||
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
|
||||
@cidr6_regex ~r/^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))$/
|
||||
|
||||
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
|
||||
@host_regex ~r/\A(([a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?)\.)*([a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?)$\Z/
|
||||
@fqdn_regex ~r/\A(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)\z/
|
||||
|
||||
def ip_regex, do: @ip_regex
|
||||
def cidr4_regex, do: @cidr4_regex
|
||||
def cidr6_regex, do: @cidr6_regex
|
||||
def host_regex, do: @host_regex
|
||||
def fqdn_regex, do: @fqdn_regex
|
||||
end
|
||||
@@ -34,7 +34,7 @@ defmodule FzCommon.MixProject do
|
||||
{:file_size, "~> 3.0.1"},
|
||||
{:posthog, "~> 0.1"},
|
||||
{:jason, "~> 1.2"},
|
||||
{:inet_cidr, "~> 1.0.0"}
|
||||
{:cidr, github: "firezone/cidr-elixir"}
|
||||
# {:dep_from_hexpm, "~> 0.3.0"},
|
||||
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
|
||||
# {:sibling_app_in_umbrella, in_umbrella: true}
|
||||
|
||||
@@ -127,11 +127,7 @@ defmodule FzCommon.FzNetTest do
|
||||
end
|
||||
end
|
||||
|
||||
@tag cases: [
|
||||
"<",
|
||||
"{",
|
||||
"["
|
||||
]
|
||||
@tag cases: ["<", "{", "["]
|
||||
test "returns {:error, _} for invalid URIs", %{cases: cases} do
|
||||
for subject <- cases do
|
||||
assert {:error, _} = FzNet.to_complete_url(subject)
|
||||
|
||||
@@ -8,6 +8,14 @@ nav.navbar {
|
||||
@extend .is-family-monospace;
|
||||
}
|
||||
|
||||
pre.multiline {
|
||||
white-space: pre-wrap; /* Since CSS 2.1 */
|
||||
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
|
||||
white-space: -pre-wrap; /* Opera 4-6 */
|
||||
white-space: -o-pre-wrap; /* Opera 7 */
|
||||
word-wrap: break-word; /* Internet Explorer 5.5+ */
|
||||
}
|
||||
|
||||
.is-horizontally-scrollable {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@@ -16,3 +16,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
$span.innerHTML = FormatTimestamp($span.dataset.timestamp)
|
||||
})
|
||||
})
|
||||
|
||||
document.addEventListener("firezone:clipcopy", (event) => {
|
||||
if ("clipboard" in navigator) {
|
||||
const text = event.target.textContent
|
||||
navigator.clipboard.writeText(text)
|
||||
const dispatcher = event.detail.dispatcher
|
||||
const span = dispatcher.getElementsByTagName("span")[0]
|
||||
const icon = dispatcher.getElementsByTagName("i")[0]
|
||||
|
||||
span.classList.add("has-text-success")
|
||||
icon.classList.replace("mdi-content-copy", "mdi-check-bold")
|
||||
|
||||
setTimeout(() => {
|
||||
span.classList.remove("has-text-success")
|
||||
icon.classList.replace("mdi-check-bold", "mdi-content-copy")
|
||||
}, 1000)
|
||||
} else {
|
||||
alert("Sorry, your browser does not support clipboard copy.")
|
||||
}
|
||||
})
|
||||
|
||||
63
apps/fz_http/assets/package-lock.json
generated
63
apps/fz_http/assets/package-lock.json
generated
@@ -36,31 +36,6 @@
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"../../../deps/phoenix": {
|
||||
"version": "1.7.0-rc.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"../../../deps/phoenix_html": {
|
||||
"version": "3.2.0"
|
||||
},
|
||||
"../../../deps/phoenix_live_view": {
|
||||
"version": "0.18.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"local_modules/admin-one-bulma-dashboard": {
|
||||
"version": "1.5.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bulma": "^0.9.0",
|
||||
"bulma-checkbox": "^1.1.1",
|
||||
"bulma-radio": "^1.1.1",
|
||||
"bulma-responsive-tables": "^1.2.3",
|
||||
"bulma-switch-control": "^1.1.1",
|
||||
"bulma-upload-control": "^1.2.0",
|
||||
"node-sass": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
|
||||
@@ -271,8 +246,19 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/admin-one-bulma-dashboard": {
|
||||
"resolved": "local_modules/admin-one-bulma-dashboard",
|
||||
"link": true
|
||||
"version": "1.5.5",
|
||||
"resolved": "file:local_modules/admin-one-bulma-dashboard",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bulma": "^0.9.0",
|
||||
"bulma-checkbox": "^1.1.1",
|
||||
"bulma-radio": "^1.1.1",
|
||||
"bulma-responsive-tables": "^1.2.3",
|
||||
"bulma-switch-control": "^1.1.1",
|
||||
"bulma-upload-control": "^1.2.0",
|
||||
"node-sass": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
@@ -2526,16 +2512,18 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/phoenix": {
|
||||
"resolved": "../../../deps/phoenix",
|
||||
"link": true
|
||||
"version": "1.7.0-rc.0",
|
||||
"resolved": "file:../../../deps/phoenix",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/phoenix_html": {
|
||||
"resolved": "../../../deps/phoenix_html",
|
||||
"link": true
|
||||
"version": "3.2.0",
|
||||
"resolved": "file:../../../deps/phoenix_html"
|
||||
},
|
||||
"node_modules/phoenix_live_view": {
|
||||
"resolved": "../../../deps/phoenix_live_view",
|
||||
"link": true
|
||||
"version": "0.18.3",
|
||||
"resolved": "file:../../../deps/phoenix_live_view",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
@@ -3690,7 +3678,8 @@
|
||||
"dev": true
|
||||
},
|
||||
"admin-one-bulma-dashboard": {
|
||||
"version": "file:local_modules/admin-one-bulma-dashboard",
|
||||
"version": "1.5.5",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bulma": "^0.9.0",
|
||||
"bulma-checkbox": "^1.1.1",
|
||||
@@ -5336,13 +5325,13 @@
|
||||
"dev": true
|
||||
},
|
||||
"phoenix": {
|
||||
"version": "file:../../../deps/phoenix"
|
||||
"version": "1.7.0-rc.0"
|
||||
},
|
||||
"phoenix_html": {
|
||||
"version": "file:../../../deps/phoenix_html"
|
||||
"version": "3.2.0"
|
||||
},
|
||||
"phoenix_live_view": {
|
||||
"version": "file:../../../deps/phoenix_live_view"
|
||||
"version": "0.18.3"
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.3.1",
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
defmodule FzHttp do
|
||||
@moduledoc """
|
||||
FzHttp keeps the contexts that define your domain
|
||||
and business logic.
|
||||
def schema do
|
||||
quote do
|
||||
use Ecto.Schema
|
||||
|
||||
Contexts are also responsible for managing your data, regardless
|
||||
if it comes from the database, an external API or others.
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
||||
@timestamps_opts [type: :utc_datetime_usec]
|
||||
|
||||
@type id :: binary()
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
When used, dispatch to the appropriate schema/context/changeset/query/etc.
|
||||
"""
|
||||
defmacro __using__(which) when is_atom(which) do
|
||||
apply(__MODULE__, which, [])
|
||||
end
|
||||
end
|
||||
|
||||
62
apps/fz_http/lib/fz_http/api_tokens.ex
Normal file
62
apps/fz_http/lib/fz_http/api_tokens.ex
Normal file
@@ -0,0 +1,62 @@
|
||||
defmodule FzHttp.ApiTokens do
|
||||
@moduledoc """
|
||||
The ApiTokens context.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias FzHttp.Repo
|
||||
|
||||
alias FzHttp.ApiTokens.ApiToken
|
||||
|
||||
def count_by_user_id(user_id) do
|
||||
Repo.aggregate(from(a in ApiToken, where: a.user_id == ^user_id), :count)
|
||||
end
|
||||
|
||||
def list_api_tokens do
|
||||
Repo.all(ApiToken)
|
||||
end
|
||||
|
||||
def list_api_tokens(user_id) do
|
||||
Repo.all(from a in ApiToken, where: a.user_id == ^user_id)
|
||||
end
|
||||
|
||||
def get_api_token(id), do: Repo.get(ApiToken, id)
|
||||
|
||||
def get_api_token!(id), do: Repo.get!(ApiToken, id)
|
||||
|
||||
def get_unexpired_api_token(api_token_id) do
|
||||
now = DateTime.utc_now()
|
||||
|
||||
Repo.one(
|
||||
from a in ApiToken,
|
||||
where: a.id == ^api_token_id and a.expires_at >= ^now
|
||||
)
|
||||
end
|
||||
|
||||
def new_api_token(attrs \\ %{}) do
|
||||
ApiToken.create_changeset(attrs)
|
||||
end
|
||||
|
||||
def create_user_api_token(%FzHttp.Users.User{} = user, params) do
|
||||
changeset =
|
||||
params
|
||||
|> Enum.into(%{"user_id" => user.id})
|
||||
|> ApiToken.create_changeset(count_per_user: count_by_user_id(user.id))
|
||||
|
||||
with {:ok, api_token} <- Repo.insert(changeset) do
|
||||
FzHttp.Telemetry.create_api_token()
|
||||
{:ok, api_token}
|
||||
end
|
||||
end
|
||||
|
||||
def api_token_expired?(%ApiToken{} = api_token) do
|
||||
DateTime.diff(api_token.expires_at, DateTime.utc_now()) < 0
|
||||
end
|
||||
|
||||
def delete_api_token(%ApiToken{} = api_token) do
|
||||
with {:ok, api_token} <- Repo.delete(api_token) do
|
||||
FzHttp.Telemetry.delete_api_token(api_token)
|
||||
{:ok, api_token}
|
||||
end
|
||||
end
|
||||
end
|
||||
51
apps/fz_http/lib/fz_http/api_tokens/api_token.ex
Normal file
51
apps/fz_http/lib/fz_http/api_tokens/api_token.ex
Normal file
@@ -0,0 +1,51 @@
|
||||
defmodule FzHttp.ApiTokens.ApiToken do
|
||||
use FzHttp, :schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@max_per_user 25
|
||||
|
||||
schema "api_tokens" do
|
||||
field :expires_at, :utc_datetime_usec
|
||||
|
||||
# Developer-friendly way to set expires_at
|
||||
field :expires_in, :integer, virtual: true, default: 30
|
||||
|
||||
belongs_to :user, FzHttp.Users.User
|
||||
|
||||
timestamps(updated_at: false)
|
||||
end
|
||||
|
||||
def create_changeset(attrs, opts \\ []) do
|
||||
%__MODULE__{}
|
||||
|> cast(attrs, ~w[
|
||||
user_id
|
||||
expires_in
|
||||
expires_at
|
||||
]a)
|
||||
|> validate_required([:user_id, :expires_in])
|
||||
|> validate_number(:expires_in, greater_than_or_equal_to: 1, less_than_or_equal_to: 90)
|
||||
|> resolve_expires_at()
|
||||
|> validate_required(:expires_at)
|
||||
|> assoc_constraint(:user)
|
||||
|> maybe_validate_count_per_user(@max_per_user, opts[:count_per_user])
|
||||
end
|
||||
|
||||
def max_per_user, do: @max_per_user
|
||||
|
||||
defp resolve_expires_at(changeset) do
|
||||
expires_at =
|
||||
DateTime.utc_now()
|
||||
|> DateTime.add(get_field(changeset, :expires_in), :day)
|
||||
|
||||
put_change(changeset, :expires_at, expires_at)
|
||||
end
|
||||
|
||||
defp maybe_validate_count_per_user(changeset, max, num) when is_integer(num) and num >= max do
|
||||
# XXX: This suffers from a race condition because the count happens in a separate transaction.
|
||||
# At the moment it's not a big concern. Fixing it would require locking against INSERTs or DELETEs
|
||||
# while counts are happening.
|
||||
add_error(changeset, :base, "token limit of #{@max_per_user} reached")
|
||||
end
|
||||
|
||||
defp maybe_validate_count_per_user(changeset, _, _), do: changeset
|
||||
end
|
||||
@@ -22,17 +22,15 @@ defmodule FzHttp.Application do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp children, do: children(Application.fetch_env!(:fz_http, :supervision_tree_mode))
|
||||
defp children, do: children(FzHttp.Config.fetch_env!(:fz_http, :supervision_tree_mode))
|
||||
|
||||
defp children(:full) do
|
||||
[
|
||||
{Cachex, name: :conf},
|
||||
FzHttp.Server,
|
||||
FzHttp.Repo,
|
||||
{Postgrex.Notifications, [name: FzHttp.Repo.Notifications] ++ FzHttp.Repo.config()},
|
||||
FzHttp.Repo.Notifier,
|
||||
FzHttp.Vault,
|
||||
FzHttp.Configurations.Cache,
|
||||
FzHttpWeb.Endpoint,
|
||||
{Phoenix.PubSub, name: FzHttp.PubSub},
|
||||
{FzHttp.Notifications, name: FzHttp.Notifications},
|
||||
@@ -41,25 +39,23 @@ defmodule FzHttp.Application do
|
||||
FzHttp.TelemetryPingService,
|
||||
FzHttp.VpnSessionScheduler,
|
||||
FzHttp.OIDC.StartProxy,
|
||||
FzHttp.SAML.StartProxy,
|
||||
{DynamicSupervisor, name: FzHttp.RefresherSupervisor, strategy: :one_for_one},
|
||||
FzHttp.OIDC.RefreshManager,
|
||||
FzHttp.SAML.StartProxy
|
||||
FzHttp.OIDC.RefreshManager
|
||||
]
|
||||
end
|
||||
|
||||
defp children(:test) do
|
||||
[
|
||||
{Cachex, name: :conf},
|
||||
FzHttp.Server,
|
||||
FzHttp.Repo,
|
||||
FzHttp.Vault,
|
||||
FzHttp.Configurations.Cache,
|
||||
FzHttpWeb.Endpoint,
|
||||
{FzHttp.OIDC.StartProxy, :test},
|
||||
{FzHttp.SAML.StartProxy, :test},
|
||||
{Phoenix.PubSub, name: FzHttp.PubSub},
|
||||
{FzHttp.Notifications, name: FzHttp.Notifications},
|
||||
FzHttpWeb.Presence,
|
||||
FzHttp.SAML.StartProxy
|
||||
FzHttpWeb.Presence
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
defmodule FzHttp.Configurations.Cache do
|
||||
@moduledoc """
|
||||
Manipulate cached configurations.
|
||||
"""
|
||||
|
||||
use GenServer, restart: :transient
|
||||
|
||||
alias FzHttp.Configurations, as: Conf
|
||||
|
||||
@name :conf
|
||||
|
||||
def get(key) do
|
||||
Cachex.get(@name, key)
|
||||
end
|
||||
|
||||
def get!(key) do
|
||||
Cachex.get!(@name, key)
|
||||
end
|
||||
|
||||
def put(key, value) do
|
||||
Cachex.put(@name, key, value)
|
||||
end
|
||||
|
||||
def put!(key, value) do
|
||||
Cachex.put!(@name, key, value)
|
||||
end
|
||||
|
||||
def start_link(_) do
|
||||
GenServer.start_link(__MODULE__, [])
|
||||
end
|
||||
|
||||
@no_fallback [:logo]
|
||||
|
||||
@impl true
|
||||
def init(_) do
|
||||
configurations =
|
||||
Conf.get_configuration!()
|
||||
|> Map.from_struct()
|
||||
|> Map.delete(:id)
|
||||
|
||||
for {k, v} <- configurations do
|
||||
# XXX: Remove fallbacks before 1.0?
|
||||
v =
|
||||
with nil <- v, true <- k not in @no_fallback do
|
||||
Application.fetch_env!(:fz_http, k)
|
||||
else
|
||||
_ -> v
|
||||
end
|
||||
|
||||
{:ok, _} = put(k, v)
|
||||
end
|
||||
|
||||
:ignore
|
||||
end
|
||||
end
|
||||
@@ -1,37 +0,0 @@
|
||||
defmodule FzHttp.Configurations.Configuration do
|
||||
@moduledoc """
|
||||
App global configuration, singleton resource
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias FzHttp.Configurations.Logo
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
|
||||
schema "configurations" do
|
||||
embeds_one :logo, Logo, on_replace: :update
|
||||
field :local_auth_enabled, :boolean
|
||||
field :allow_unprivileged_device_management, :boolean
|
||||
field :allow_unprivileged_device_configuration, :boolean
|
||||
field :openid_connect_providers, :map
|
||||
field :saml_identity_providers, :map
|
||||
field :disable_vpn_on_oidc_error, :boolean
|
||||
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(configuration, attrs) do
|
||||
configuration
|
||||
|> cast(attrs, [
|
||||
:local_auth_enabled,
|
||||
:allow_unprivileged_device_management,
|
||||
:allow_unprivileged_device_configuration,
|
||||
:openid_connect_providers,
|
||||
:saml_identity_providers,
|
||||
:disable_vpn_on_oidc_error
|
||||
])
|
||||
|> cast_embed(:logo)
|
||||
end
|
||||
end
|
||||
@@ -1,5 +0,0 @@
|
||||
defimpl Phoenix.HTML.Safe, for: Map do
|
||||
def to_iodata(%{} = map) do
|
||||
Jason.encode_to_iodata!(map, pretty: true)
|
||||
end
|
||||
end
|
||||
70
apps/fz_http/lib/fz_http/config.ex
Normal file
70
apps/fz_http/lib/fz_http/config.ex
Normal file
@@ -0,0 +1,70 @@
|
||||
defmodule FzHttp.Config do
|
||||
@moduledoc """
|
||||
This module provides set of helper functions that are useful when reading application runtime configuration overrides
|
||||
in test environment.
|
||||
"""
|
||||
|
||||
if Mix.env() != :test do
|
||||
def maybe_put_env_override(_key, _value), do: :ok
|
||||
def fetch_env!(app, key), do: Application.fetch_env!(app, key)
|
||||
else
|
||||
def maybe_put_env_override(key, value) do
|
||||
_ = Process.put(key, value)
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Attempts to override application env configuration from one of 3 sources (in this exact order):
|
||||
* takes it from process dictionary of a current process;
|
||||
* takes it from process dictionary of a last process in $ancestors stack.
|
||||
* takes it from process dictionary of a last process in $callers stack;
|
||||
|
||||
This function is especially useful when some options (eg. request endpoint) needs to be overridden
|
||||
in test environment (eg. to send those requests to Bypass).
|
||||
"""
|
||||
def fetch_env!(app, key) do
|
||||
application_env = Application.fetch_env!(app, key)
|
||||
|
||||
with :error <- fetch_process_value(key),
|
||||
:error <- fetch_process_value(get_last_pid_from_pdict_list(:"$ancestors"), key),
|
||||
:error <- fetch_process_value(get_last_pid_from_pdict_list(:"$callers"), key) do
|
||||
application_env
|
||||
else
|
||||
{:ok, override} -> override
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_process_value(key) do
|
||||
case Process.get(key) do
|
||||
nil -> :error
|
||||
value -> {:ok, value}
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_process_value(nil, _key) do
|
||||
:error
|
||||
end
|
||||
|
||||
defp fetch_process_value(atom, key) when is_atom(atom) do
|
||||
atom
|
||||
|> Process.whereis()
|
||||
|> fetch_process_value(key)
|
||||
end
|
||||
|
||||
defp fetch_process_value(pid, key) do
|
||||
case :erlang.process_info(pid, :dictionary) do
|
||||
{:dictionary, pdict} ->
|
||||
Keyword.fetch(pdict, key)
|
||||
|
||||
_other ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp get_last_pid_from_pdict_list(stack) do
|
||||
if values = Process.get(stack) do
|
||||
List.last(values)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,20 +4,39 @@ defmodule FzHttp.Configurations do
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
import Ecto.Changeset
|
||||
alias FzHttp.{Repo, Configurations.Configuration, Configurations.Cache}
|
||||
|
||||
defdelegate get(key), to: FzHttp.Configurations.Cache
|
||||
defdelegate get!(key), to: FzHttp.Configurations.Cache
|
||||
alias FzHttp.{Repo, Configurations.Configuration}
|
||||
|
||||
def get!(key) do
|
||||
Map.get(get_configuration!(), key)
|
||||
end
|
||||
|
||||
def put!(key, val) do
|
||||
get_configuration!()
|
||||
|> Configuration.changeset(%{key => val})
|
||||
|> Repo.update!()
|
||||
end
|
||||
|
||||
def get_configuration! do
|
||||
Repo.one!(Configuration)
|
||||
end
|
||||
|
||||
def auto_create_users?(field, provider) do
|
||||
get!(field)
|
||||
|> Map.get(provider)
|
||||
|> Map.get("auto_create_users")
|
||||
def get_provider_by_id(field, provider_id) do
|
||||
FzHttp.Configurations.get!(field)
|
||||
|> Enum.find(&(&1.id == provider_id))
|
||||
end
|
||||
|
||||
def auto_create_users?(field, provider_id) do
|
||||
FzHttp.Configurations.get!(field)
|
||||
|> Enum.find(&(&1.id == provider_id))
|
||||
|> case do
|
||||
nil -> raise RuntimeError, "Unknown provider #{provider_id}"
|
||||
provider -> provider.auto_create_users
|
||||
end
|
||||
end
|
||||
|
||||
def new_configuration(attrs \\ %{}) do
|
||||
Configuration.changeset(%Configuration{}, attrs)
|
||||
end
|
||||
|
||||
def change_configuration(%Configuration{} = config \\ get_configuration!()) do
|
||||
@@ -25,27 +44,30 @@ defmodule FzHttp.Configurations do
|
||||
end
|
||||
|
||||
def update_configuration(%Configuration{} = config \\ get_configuration!(), attrs) do
|
||||
config
|
||||
|> Configuration.changeset(attrs)
|
||||
|> prepare_changes(fn changeset ->
|
||||
for {k, v} <- changeset.changes do
|
||||
case v do
|
||||
%Ecto.Changeset{} ->
|
||||
Cache.put!(k, v.changes)
|
||||
case Repo.update(Configuration.changeset(config, attrs)) do
|
||||
{:ok, configuration} ->
|
||||
FzHttp.SAML.StartProxy.restart()
|
||||
FzHttp.OIDC.StartProxy.restart()
|
||||
|
||||
_ ->
|
||||
Cache.put!(k, v)
|
||||
end
|
||||
end
|
||||
{:ok, configuration}
|
||||
|
||||
changeset
|
||||
end)
|
||||
|> Repo.update()
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
def logo_types, do: ~w(Default URL Upload)
|
||||
|
||||
def logo_type(nil), do: "Default"
|
||||
def logo_type(%{url: _url}), do: "URL"
|
||||
def logo_type(%{data: _data}), do: "Upload"
|
||||
def logo_type(%{url: url}) when not is_nil(url), do: "URL"
|
||||
def logo_type(%{data: data}) when not is_nil(data), do: "Upload"
|
||||
|
||||
def vpn_sessions_expire? do
|
||||
freq = vpn_duration()
|
||||
freq > 0 && freq < Configuration.max_vpn_session_duration()
|
||||
end
|
||||
|
||||
def vpn_duration do
|
||||
get_configuration!().vpn_session_duration
|
||||
end
|
||||
end
|
||||
|
||||
107
apps/fz_http/lib/fz_http/configurations/configuration.ex
Normal file
107
apps/fz_http/lib/fz_http/configurations/configuration.ex
Normal file
@@ -0,0 +1,107 @@
|
||||
defmodule FzHttp.Configurations.Configuration do
|
||||
@moduledoc """
|
||||
App global configuration, singleton resource
|
||||
"""
|
||||
use FzHttp, :schema
|
||||
import Ecto.Changeset
|
||||
|
||||
alias FzHttp.{
|
||||
Configurations.Logo,
|
||||
Validators.Common
|
||||
}
|
||||
|
||||
@min_mtu 576
|
||||
@max_mtu 1500
|
||||
@min_persistent_keepalive 0
|
||||
@max_persistent_keepalive 120
|
||||
|
||||
# Postgres max int size is 4 bytes
|
||||
@max_pg_integer 2_147_483_647
|
||||
@min_vpn_session_duration 0
|
||||
@max_vpn_session_duration @max_pg_integer
|
||||
|
||||
schema "configurations" do
|
||||
field :allow_unprivileged_device_management, :boolean
|
||||
field :allow_unprivileged_device_configuration, :boolean
|
||||
|
||||
field :local_auth_enabled, :boolean
|
||||
field :disable_vpn_on_oidc_error, :boolean
|
||||
|
||||
# The defaults for these fields are set in the following migration:
|
||||
# apps/fz_http/priv/repo/migrations/20221224210654_fix_sites_nullable_fields.exs
|
||||
#
|
||||
# This will be changing in 0.8 and again when we have client apps,
|
||||
# so this works for the time being. The important thing is allowing users
|
||||
# to update these fields via the REST API since they were removed as
|
||||
# environment variables in the above migration. This is important for users
|
||||
# wishing to configure Firezone with automated Infrastructure tools like
|
||||
# Terraform.
|
||||
field :default_client_persistent_keepalive, :integer
|
||||
field :default_client_mtu, :integer
|
||||
field :default_client_endpoint, :string
|
||||
field :default_client_dns, :string
|
||||
field :default_client_allowed_ips, :string
|
||||
|
||||
# XXX: Remove when this feature is refactored into config expiration feature
|
||||
# and WireGuard keys are decoupled from devices to facilitate rotation.
|
||||
#
|
||||
# See https://github.com/firezone/firezone/issues/1236
|
||||
field :vpn_session_duration, :integer, read_after_writes: true
|
||||
|
||||
embeds_one :logo, Logo, on_replace: :delete
|
||||
|
||||
embeds_many :openid_connect_providers,
|
||||
FzHttp.Configurations.Configuration.OpenIDConnectProvider,
|
||||
on_replace: :delete
|
||||
|
||||
embeds_many :saml_identity_providers,
|
||||
FzHttp.Configurations.Configuration.SAMLIdentityProvider,
|
||||
on_replace: :delete
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(configuration, attrs) do
|
||||
configuration
|
||||
|> cast(attrs, ~w[
|
||||
local_auth_enabled
|
||||
allow_unprivileged_device_management
|
||||
allow_unprivileged_device_configuration
|
||||
disable_vpn_on_oidc_error
|
||||
default_client_persistent_keepalive
|
||||
default_client_mtu
|
||||
default_client_endpoint
|
||||
default_client_dns
|
||||
default_client_allowed_ips
|
||||
vpn_session_duration
|
||||
]a)
|
||||
|> cast_embed(:logo)
|
||||
|> cast_embed(:openid_connect_providers,
|
||||
with: {FzHttp.Configurations.Configuration.OpenIDConnectProvider, :changeset, []}
|
||||
)
|
||||
|> cast_embed(:saml_identity_providers,
|
||||
with: {FzHttp.Configurations.Configuration.SAMLIdentityProvider, :changeset, []}
|
||||
)
|
||||
|> Common.trim_change(:default_client_dns)
|
||||
|> Common.trim_change(:default_client_allowed_ips)
|
||||
|> Common.trim_change(:default_client_endpoint)
|
||||
|> Common.validate_no_duplicates(:default_client_dns)
|
||||
|> Common.validate_list_of_ips_or_cidrs(:default_client_allowed_ips)
|
||||
|> Common.validate_no_duplicates(:default_client_allowed_ips)
|
||||
|> validate_number(:default_client_mtu,
|
||||
greater_than_or_equal_to: @min_mtu,
|
||||
less_than_or_equal_to: @max_mtu
|
||||
)
|
||||
|> validate_number(:default_client_persistent_keepalive,
|
||||
greater_than_or_equal_to: @min_persistent_keepalive,
|
||||
less_than_or_equal_to: @max_persistent_keepalive
|
||||
)
|
||||
|> validate_number(:vpn_session_duration,
|
||||
greater_than_or_equal_to: @min_vpn_session_duration,
|
||||
less_than_or_equal_to: @max_vpn_session_duration
|
||||
)
|
||||
end
|
||||
|
||||
def max_vpn_session_duration, do: @max_vpn_session_duration
|
||||
end
|
||||
@@ -1,12 +1,10 @@
|
||||
defmodule FzHttp.Conf.OIDCConfig do
|
||||
defmodule FzHttp.Configurations.Configuration.OpenIDConnectProvider do
|
||||
@moduledoc """
|
||||
OIDC Config virtual schema
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
use FzHttp, :schema
|
||||
import Ecto.Changeset
|
||||
import FzHttp.Validators.OpenIDConnect
|
||||
import FzHttp.Validators.Common, only: [validate_uri: 2]
|
||||
alias FzHttp.Validators
|
||||
|
||||
@reserved_config_ids [
|
||||
"identity",
|
||||
@@ -27,8 +25,8 @@ defmodule FzHttp.Conf.OIDCConfig do
|
||||
field :auto_create_users, :boolean, default: true
|
||||
end
|
||||
|
||||
def changeset(data) do
|
||||
%__MODULE__{}
|
||||
def changeset(struct \\ %__MODULE__{}, data) do
|
||||
struct
|
||||
|> cast(
|
||||
data,
|
||||
[
|
||||
@@ -55,8 +53,8 @@ defmodule FzHttp.Conf.OIDCConfig do
|
||||
])
|
||||
# Don't allow users to enter reserved config ids
|
||||
|> validate_exclusion(:id, @reserved_config_ids)
|
||||
|> validate_discovery_document_uri()
|
||||
|> validate_uri([
|
||||
|> Validators.OpenIDConnect.validate_discovery_document_uri()
|
||||
|> Validators.Common.validate_uri([
|
||||
:redirect_uri
|
||||
])
|
||||
end
|
||||
@@ -1,11 +1,10 @@
|
||||
defmodule FzHttp.Conf.SAMLConfig do
|
||||
defmodule FzHttp.Configurations.Configuration.SAMLIdentityProvider do
|
||||
@moduledoc """
|
||||
SAML Config virtual schema
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
use FzHttp, :schema
|
||||
import Ecto.Changeset
|
||||
import FzHttp.Validators.SAML
|
||||
alias FzHttp.Validators
|
||||
|
||||
@primary_key false
|
||||
embedded_schema do
|
||||
@@ -20,8 +19,8 @@ defmodule FzHttp.Conf.SAMLConfig do
|
||||
field :auto_create_users, :boolean, default: true
|
||||
end
|
||||
|
||||
def changeset(data) do
|
||||
%__MODULE__{}
|
||||
def changeset(struct \\ %__MODULE__{}, data) do
|
||||
struct
|
||||
|> cast(data, [
|
||||
:id,
|
||||
:label,
|
||||
@@ -39,6 +38,6 @@ defmodule FzHttp.Conf.SAMLConfig do
|
||||
:metadata,
|
||||
:auto_create_users
|
||||
])
|
||||
|> validate_metadata()
|
||||
|> Validators.SAML.validate_metadata()
|
||||
end
|
||||
end
|
||||
@@ -2,10 +2,12 @@ defmodule FzHttp.Configurations.Logo do
|
||||
@moduledoc """
|
||||
Embedded Schema for logo
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
use FzHttp, :schema
|
||||
import Ecto.Changeset
|
||||
|
||||
# Singleton per configuration
|
||||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field :url, :string
|
||||
field :data, :string
|
||||
@@ -61,11 +61,11 @@ defmodule FzHttp.ConnectivityCheckService do
|
||||
end
|
||||
|
||||
defp url do
|
||||
Application.fetch_env!(:fz_http, :connectivity_checks_url) <> version()
|
||||
FzHttp.Config.fetch_env!(:fz_http, :connectivity_checks_url) <> version()
|
||||
end
|
||||
|
||||
defp http_client do
|
||||
Application.fetch_env!(:fz_http, :http_client)
|
||||
FzHttp.Config.fetch_env!(:fz_http, :http_client)
|
||||
end
|
||||
|
||||
defp version do
|
||||
@@ -73,10 +73,10 @@ defmodule FzHttp.ConnectivityCheckService do
|
||||
end
|
||||
|
||||
defp interval do
|
||||
Application.fetch_env!(:fz_http, :connectivity_checks_interval) * 1_000
|
||||
FzHttp.Config.fetch_env!(:fz_http, :connectivity_checks_interval) * 1_000
|
||||
end
|
||||
|
||||
defp enabled? do
|
||||
Application.fetch_env!(:fz_http, :connectivity_checks_enabled)
|
||||
FzHttp.Config.fetch_env!(:fz_http, :connectivity_checks_enabled)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@ defmodule FzHttp.ConnectivityChecks.ConnectivityCheck do
|
||||
@moduledoc """
|
||||
Manages the connectivity_checks table
|
||||
"""
|
||||
use Ecto.Schema
|
||||
use FzHttp, :schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@url_regex ~r<\Ahttps://ping(?:-dev)?\.firez\.one/\d+\.\d+\.\d+(?:\+git\.\d+\.[0-9a-fA-F]{7,})?\z>
|
||||
@@ -13,7 +13,7 @@ defmodule FzHttp.ConnectivityChecks.ConnectivityCheck do
|
||||
field :response_headers, :map
|
||||
field :url, :string
|
||||
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
timestamps(updated_at: false)
|
||||
end
|
||||
|
||||
@doc false
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
defmodule FzHttp.Devices do
|
||||
@moduledoc """
|
||||
The Devices context.
|
||||
"""
|
||||
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query, warn: false
|
||||
|
||||
alias EctoNetwork.INET
|
||||
alias FzHttp.{Devices.Device, Devices.DeviceSetting, Repo, Sites, Telemetry, Users, Users.User}
|
||||
|
||||
alias FzHttp.{
|
||||
Configurations,
|
||||
Devices.Device,
|
||||
Devices.DeviceSetting,
|
||||
Repo,
|
||||
Telemetry,
|
||||
Users,
|
||||
Users.User
|
||||
}
|
||||
|
||||
require Logger
|
||||
|
||||
@@ -22,7 +26,13 @@ defmodule FzHttp.Devices do
|
||||
end
|
||||
|
||||
def count do
|
||||
Repo.one(from d in Device, select: count(d.id))
|
||||
Repo.aggregate(Device, :count)
|
||||
end
|
||||
|
||||
def count(nil), do: 0
|
||||
|
||||
def count(user_id) do
|
||||
Repo.one(from d in Device, where: d.user_id == ^user_id, select: count())
|
||||
end
|
||||
|
||||
def max_count_by_user_id do
|
||||
@@ -52,14 +62,11 @@ defmodule FzHttp.Devices do
|
||||
end
|
||||
|
||||
def setting_projection(device) do
|
||||
DeviceSetting.parse(device)
|
||||
device
|
||||
|> DeviceSetting.parse()
|
||||
|> Map.from_struct()
|
||||
end
|
||||
|
||||
def count(user_id) do
|
||||
Repo.one(from d in Device, where: d.user_id == ^user_id, select: count())
|
||||
end
|
||||
|
||||
def get_device!(id), do: Repo.get!(Device, id)
|
||||
|
||||
def create_device(attrs \\ %{}) do
|
||||
@@ -113,7 +120,7 @@ defmodule FzHttp.Devices do
|
||||
end
|
||||
|
||||
def to_peer_list do
|
||||
vpn_duration = Sites.vpn_duration()
|
||||
vpn_duration = Configurations.vpn_duration()
|
||||
|
||||
Repo.all(
|
||||
from d in Device,
|
||||
@@ -132,16 +139,7 @@ defmodule FzHttp.Devices do
|
||||
end
|
||||
|
||||
def new_device(attrs \\ %{}) do
|
||||
change_device(
|
||||
%Device{},
|
||||
Map.merge(
|
||||
%{
|
||||
"name" => Device.new_name(),
|
||||
"preshared_key" => FzCommon.FzCrypto.psk()
|
||||
},
|
||||
attrs
|
||||
)
|
||||
)
|
||||
change_device(%Device{}, attrs)
|
||||
end
|
||||
|
||||
def allowed_ips(device), do: config(device, :allowed_ips)
|
||||
@@ -150,9 +148,9 @@ defmodule FzHttp.Devices do
|
||||
def mtu(device), do: config(device, :mtu)
|
||||
def persistent_keepalive(device), do: config(device, :persistent_keepalive)
|
||||
|
||||
defp config(device, key) do
|
||||
if Map.get(device, String.to_atom("use_site_#{key}")) do
|
||||
Map.get(Sites.get_site!(), key)
|
||||
def config(device, key) do
|
||||
if Map.get(device, String.to_atom("use_default_#{key}")) do
|
||||
Map.get(Configurations.get_configuration!(), String.to_atom("default_client_#{key}"))
|
||||
else
|
||||
Map.get(device, key)
|
||||
end
|
||||
@@ -160,11 +158,11 @@ defmodule FzHttp.Devices do
|
||||
|
||||
def defaults(changeset) do
|
||||
~w(
|
||||
use_site_allowed_ips
|
||||
use_site_dns
|
||||
use_site_endpoint
|
||||
use_site_mtu
|
||||
use_site_persistent_keepalive
|
||||
use_default_allowed_ips
|
||||
use_default_dns
|
||||
use_default_endpoint
|
||||
use_default_mtu
|
||||
use_default_persistent_keepalive
|
||||
)a
|
||||
|> Map.new(&{&1, get_field(changeset, &1)})
|
||||
end
|
||||
@@ -197,8 +195,24 @@ defmodule FzHttp.Devices do
|
||||
end
|
||||
|
||||
def decode(nil), do: nil
|
||||
def decode(inet) when is_binary(inet), do: inet
|
||||
def decode(inet), do: INET.decode(inet)
|
||||
|
||||
@hash_range 2 ** 16
|
||||
def new_name(name \\ FzCommon.NameGenerator.generate()) do
|
||||
hash =
|
||||
name
|
||||
|> :erlang.phash2(@hash_range)
|
||||
|> Integer.to_string(16)
|
||||
|> String.pad_leading(4, "0")
|
||||
|
||||
if String.length(name) > 15 do
|
||||
String.slice(name, 0..10) <> hash
|
||||
else
|
||||
name
|
||||
end
|
||||
end
|
||||
|
||||
defp psk_config(device) do
|
||||
if device.preshared_key do
|
||||
"PresharedKey = #{device.preshared_key}"
|
||||
@@ -287,10 +301,10 @@ defmodule FzHttp.Devices do
|
||||
defp field_empty?(_), do: false
|
||||
|
||||
defp ipv4? do
|
||||
Application.fetch_env!(:fz_http, :wireguard_ipv4_enabled)
|
||||
FzHttp.Config.fetch_env!(:fz_http, :wireguard_ipv4_enabled)
|
||||
end
|
||||
|
||||
defp ipv6? do
|
||||
Application.fetch_env!(:fz_http, :wireguard_ipv6_enabled)
|
||||
FzHttp.Config.fetch_env!(:fz_http, :wireguard_ipv6_enabled)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,141 +2,114 @@ defmodule FzHttp.Devices.Device do
|
||||
@moduledoc """
|
||||
Manages Device things
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
use FzHttp, :schema
|
||||
import Ecto.Changeset
|
||||
alias FzHttp.Validators.Common
|
||||
alias FzHttp.Devices
|
||||
require Logger
|
||||
|
||||
import FzHttp.Validators.Common,
|
||||
only: [
|
||||
trim: 2,
|
||||
validate_omitted: 2,
|
||||
validate_no_duplicates: 2,
|
||||
validate_no_mask: 2,
|
||||
validate_list_of_ips_or_cidrs: 2
|
||||
]
|
||||
|
||||
import FzHttp.Queries.INET
|
||||
|
||||
alias FzHttp.{Devices, Users.User}
|
||||
|
||||
@description_max_length 2048
|
||||
|
||||
# Fields for which to trim whitespace after cast, before validation
|
||||
@whitespace_trimmed_fields ~w(
|
||||
allowed_ips
|
||||
dns
|
||||
endpoint
|
||||
name
|
||||
description
|
||||
)a
|
||||
# WireGuard base64-encoded string length
|
||||
@key_length 44
|
||||
|
||||
schema "devices" do
|
||||
field :rx_bytes, :integer
|
||||
field :tx_bytes, :integer
|
||||
field :uuid, Ecto.UUID, autogenerate: true
|
||||
field :name, :string
|
||||
field :description, :string
|
||||
field :public_key, :string
|
||||
field :preshared_key, FzHttp.Encrypted.Binary
|
||||
field :use_site_allowed_ips, :boolean, read_after_writes: true, default: true
|
||||
field :use_site_dns, :boolean, read_after_writes: true, default: true
|
||||
field :use_site_endpoint, :boolean, read_after_writes: true, default: true
|
||||
field :use_site_mtu, :boolean, read_after_writes: true, default: true
|
||||
field :use_site_persistent_keepalive, :boolean, read_after_writes: true, default: true
|
||||
field :use_default_allowed_ips, :boolean, read_after_writes: true, default: true
|
||||
field :use_default_dns, :boolean, read_after_writes: true, default: true
|
||||
field :use_default_endpoint, :boolean, read_after_writes: true, default: true
|
||||
field :use_default_mtu, :boolean, read_after_writes: true, default: true
|
||||
field :use_default_persistent_keepalive, :boolean, read_after_writes: true, default: true
|
||||
field :endpoint, :string
|
||||
field :mtu, :integer
|
||||
field :persistent_keepalive, :integer
|
||||
field :allowed_ips, :string
|
||||
field :dns, :string
|
||||
field :remote_ip, EctoNetwork.INET
|
||||
field :ipv4, EctoNetwork.INET, read_after_writes: true
|
||||
field :ipv6, EctoNetwork.INET, read_after_writes: true
|
||||
field :ipv4, EctoNetwork.INET
|
||||
field :ipv6, EctoNetwork.INET
|
||||
|
||||
field :latest_handshake, :utc_datetime_usec
|
||||
field :key_regenerated_at, :utc_datetime_usec, read_after_writes: true
|
||||
|
||||
belongs_to :user, User
|
||||
belongs_to :user, FzHttp.Users.User
|
||||
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def create_changeset(device \\ %__MODULE__{}, attrs) do
|
||||
device
|
||||
|> shared_cast(attrs)
|
||||
|> put_next_ip(:ipv4)
|
||||
|> put_next_ip(:ipv6)
|
||||
|> shared_changeset()
|
||||
@fields ~w[
|
||||
latest_handshake
|
||||
rx_bytes
|
||||
tx_bytes
|
||||
use_default_allowed_ips
|
||||
use_default_dns
|
||||
use_default_endpoint
|
||||
use_default_mtu
|
||||
use_default_persistent_keepalive
|
||||
allowed_ips
|
||||
dns
|
||||
endpoint
|
||||
mtu
|
||||
persistent_keepalive
|
||||
remote_ip
|
||||
ipv4
|
||||
ipv6
|
||||
user_id
|
||||
name
|
||||
description
|
||||
public_key
|
||||
preshared_key
|
||||
key_regenerated_at
|
||||
]a
|
||||
|
||||
@required_fields ~w[user_id name public_key]a
|
||||
|
||||
def create_changeset(attrs) do
|
||||
%__MODULE__{}
|
||||
|> cast(attrs, @fields)
|
||||
|> Common.put_default_value(:name, &FzHttp.Devices.new_name/0)
|
||||
|> Common.put_default_value(:preshared_key, &FzCommon.FzCrypto.psk/0)
|
||||
|> changeset()
|
||||
|> validate_max_devices()
|
||||
|> validate_required(@required_fields)
|
||||
end
|
||||
|
||||
def update_changeset(device, attrs) do
|
||||
device
|
||||
|> shared_cast(attrs)
|
||||
|> shared_changeset()
|
||||
|> cast(attrs, @fields)
|
||||
|> changeset()
|
||||
|> validate_required(@required_fields)
|
||||
end
|
||||
|
||||
@hash_range 2 ** 16
|
||||
|
||||
def new_name(name \\ FzCommon.NameGenerator.generate()) do
|
||||
hash =
|
||||
name
|
||||
|> :erlang.phash2(@hash_range)
|
||||
|> Integer.to_string(16)
|
||||
|> String.pad_leading(4, "0")
|
||||
|
||||
if String.length(name) > 15 do
|
||||
String.slice(name, 0..10) <> hash
|
||||
else
|
||||
name
|
||||
end
|
||||
end
|
||||
|
||||
defp shared_cast(device, attrs) do
|
||||
device
|
||||
|> cast(attrs, [
|
||||
:latest_handshake,
|
||||
:rx_bytes,
|
||||
:tx_bytes,
|
||||
:use_site_allowed_ips,
|
||||
:use_site_dns,
|
||||
:use_site_endpoint,
|
||||
:use_site_mtu,
|
||||
:use_site_persistent_keepalive,
|
||||
:allowed_ips,
|
||||
:dns,
|
||||
:endpoint,
|
||||
:mtu,
|
||||
:persistent_keepalive,
|
||||
:remote_ip,
|
||||
:ipv4,
|
||||
:ipv6,
|
||||
:user_id,
|
||||
:name,
|
||||
:description,
|
||||
:public_key,
|
||||
:preshared_key,
|
||||
:key_regenerated_at
|
||||
])
|
||||
|> trim(@whitespace_trimmed_fields)
|
||||
end
|
||||
|
||||
defp shared_changeset(changeset) do
|
||||
defp changeset(changeset) do
|
||||
changeset
|
||||
|> validate_required([
|
||||
:user_id,
|
||||
:name,
|
||||
:public_key
|
||||
])
|
||||
|> validate_required_unless_site([:endpoint])
|
||||
|> validate_omitted_if_site([
|
||||
:allowed_ips,
|
||||
:dns,
|
||||
:endpoint,
|
||||
:persistent_keepalive,
|
||||
:mtu
|
||||
])
|
||||
|> validate_list_of_ips_or_cidrs(:allowed_ips)
|
||||
|> validate_no_duplicates(:dns)
|
||||
|> Common.trim_change(:allowed_ips)
|
||||
|> Common.trim_change(:dns)
|
||||
|> Common.trim_change(:endpoint)
|
||||
|> Common.trim_change(:name)
|
||||
|> Common.trim_change(:description)
|
||||
|> Common.validate_base64(:public_key)
|
||||
|> Common.validate_base64(:preshared_key)
|
||||
|> validate_length(:public_key, is: @key_length)
|
||||
|> validate_length(:preshared_key, is: @key_length)
|
||||
|> validate_length(:description, max: @description_max_length)
|
||||
|> validate_length(:name, min: 1)
|
||||
|> assoc_constraint(:user)
|
||||
|> validate_required_unless_default([:endpoint])
|
||||
|> validate_omitted_if_default(~w[
|
||||
allowed_ips
|
||||
dns
|
||||
endpoint
|
||||
persistent_keepalive
|
||||
mtu
|
||||
]a)
|
||||
|> Common.validate_list_of_ips_or_cidrs(:allowed_ips)
|
||||
|> Common.validate_no_duplicates(:dns)
|
||||
|> validate_number(:persistent_keepalive,
|
||||
greater_than_or_equal_to: 0,
|
||||
less_than_or_equal_to: 120
|
||||
@@ -145,25 +118,69 @@ defmodule FzHttp.Devices.Device do
|
||||
greater_than_or_equal_to: 576,
|
||||
less_than_or_equal_to: 1500
|
||||
)
|
||||
|> validate_length(:description, max: @description_max_length)
|
||||
|> validate_ipv4_required()
|
||||
|> validate_ipv6_required()
|
||||
|> prepare_changes(fn changeset ->
|
||||
changeset
|
||||
|> maybe_put_default_ip(:ipv4)
|
||||
|> maybe_put_default_ip(:ipv6)
|
||||
|> validate_exclusion(:ipv4, [ipv4_address()])
|
||||
|> validate_exclusion(:ipv6, [ipv6_address()])
|
||||
end)
|
||||
|> unique_constraint(:ipv4)
|
||||
|> unique_constraint(:ipv6)
|
||||
|> validate_exclusion(:ipv4, [ipv4_address()])
|
||||
|> validate_exclusion(:ipv6, [ipv6_address()])
|
||||
|> validate_in_network(:ipv4)
|
||||
|> validate_in_network(:ipv6)
|
||||
|> validate_no_mask(:ipv4)
|
||||
|> validate_no_mask(:ipv6)
|
||||
|> unique_constraint(:public_key)
|
||||
|> unique_constraint([:user_id, :name])
|
||||
end
|
||||
|
||||
defp maybe_put_default_ip(changeset, field) do
|
||||
if FzHttp.Config.fetch_env!(:fz_http, :"wireguard_#{field}_enabled") == true do
|
||||
case fetch_field(changeset, field) do
|
||||
{:data, nil} -> put_default_ip(changeset, field)
|
||||
:error -> put_default_ip(changeset, field)
|
||||
_ -> changeset
|
||||
end
|
||||
|> validate_required(field)
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
defp put_default_ip(changeset, field) do
|
||||
cidr_string = FzHttp.Config.fetch_env!(:fz_http, :"wireguard_#{field}_network")
|
||||
{:ok, cidr_inet} = EctoNetwork.INET.cast(cidr_string)
|
||||
cidr = CIDR.parse(cidr_string)
|
||||
offset = Enum.random(2..(cidr.hosts - 2))
|
||||
|
||||
{:ok, gateway_address} =
|
||||
FzHttp.Config.fetch_env!(:fz_http, :"wireguard_#{field}_address")
|
||||
|> EctoNetwork.INET.cast()
|
||||
|
||||
Devices.Device.Query.next_available_address(cidr_inet, offset, [gateway_address])
|
||||
|> FzHttp.Repo.one()
|
||||
|> case do
|
||||
nil -> add_error(changeset, :base, "CIDR #{cidr} is exhausted")
|
||||
ip -> put_change(changeset, field, ip)
|
||||
end
|
||||
end
|
||||
|
||||
defp ipv4_address do
|
||||
FzHttp.Config.fetch_env!(:fz_http, :wireguard_ipv4_address)
|
||||
|> EctoNetwork.INET.cast()
|
||||
end
|
||||
|
||||
defp ipv6_address do
|
||||
FzHttp.Config.fetch_env!(:fz_http, :wireguard_ipv6_address)
|
||||
|> EctoNetwork.INET.cast()
|
||||
end
|
||||
|
||||
defp validate_max_devices(changeset) do
|
||||
user_id = changeset.changes.user_id || changeset.data.user_id
|
||||
count = Devices.count(user_id)
|
||||
max_devices = Application.fetch_env!(:fz_http, :max_devices_per_user)
|
||||
# XXX: This suffers from a race condition because the count happens in a separate transaction.
|
||||
# At the moment it's not a big concern. Fixing it would require locking against INSERTs or DELETEs
|
||||
# while counts are happening.
|
||||
count =
|
||||
get_field(changeset, :user_id)
|
||||
|> Devices.count()
|
||||
|
||||
max_devices = FzHttp.Config.fetch_env!(:fz_http, :max_devices_per_user)
|
||||
|
||||
if count >= max_devices do
|
||||
add_error(
|
||||
@@ -176,99 +193,27 @@ defmodule FzHttp.Devices.Device do
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_omitted_if_site(changeset, fields) when is_list(fields) do
|
||||
validate_omitted(changeset, filter_site_fields(changeset, fields, use_site: true))
|
||||
defp validate_omitted_if_default(changeset, fields) when is_list(fields) do
|
||||
Common.validate_omitted(
|
||||
changeset,
|
||||
filter_default_fields(changeset, fields, use_default: true)
|
||||
)
|
||||
end
|
||||
|
||||
defp validate_required_unless_site(changeset, fields) when is_list(fields) do
|
||||
validate_required(changeset, filter_site_fields(changeset, fields, use_site: false))
|
||||
defp validate_required_unless_default(changeset, fields) when is_list(fields) do
|
||||
validate_required(changeset, filter_default_fields(changeset, fields, use_default: false))
|
||||
end
|
||||
|
||||
defp filter_site_fields(changeset, fields, use_site: use_site) when is_boolean(use_site) do
|
||||
defp filter_default_fields(changeset, fields, use_default: use_default)
|
||||
when is_boolean(use_default) do
|
||||
fields
|
||||
|> Enum.map(fn field -> String.to_atom("use_site_#{field}") end)
|
||||
|> Enum.filter(fn site_field -> get_field(changeset, site_field) == use_site end)
|
||||
|> Enum.map(fn field -> String.to_atom("use_default_#{field}") end)
|
||||
|> Enum.filter(fn default_field -> get_field(changeset, default_field) == use_default end)
|
||||
|> Enum.map(fn field ->
|
||||
field
|
||||
|> Atom.to_string()
|
||||
|> String.trim("use_site_")
|
||||
|> String.trim("use_default_")
|
||||
|> String.to_atom()
|
||||
end)
|
||||
end
|
||||
|
||||
defp validate_ipv4_required(changeset) do
|
||||
if Application.fetch_env!(:fz_http, :wireguard_ipv4_enabled) do
|
||||
validate_required(changeset, :ipv4)
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_ipv6_required(changeset) do
|
||||
if Application.fetch_env!(:fz_http, :wireguard_ipv6_enabled) do
|
||||
validate_required(changeset, :ipv6)
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_in_network(%Ecto.Changeset{changes: %{ipv4: ip}} = changeset, :ipv4) do
|
||||
net = Application.fetch_env!(:fz_http, :wireguard_ipv4_network)
|
||||
add_net_error_if_outside_bounds(changeset, net, ip, :ipv4)
|
||||
end
|
||||
|
||||
defp validate_in_network(changeset, :ipv4), do: changeset
|
||||
|
||||
defp validate_in_network(%Ecto.Changeset{changes: %{ipv6: ip}} = changeset, :ipv6) do
|
||||
net = Application.fetch_env!(:fz_http, :wireguard_ipv6_network)
|
||||
add_net_error_if_outside_bounds(changeset, net, ip, :ipv6)
|
||||
end
|
||||
|
||||
defp validate_in_network(changeset, :ipv6), do: changeset
|
||||
|
||||
defp add_net_error_if_outside_bounds(changeset, net, ip, ip_type) do
|
||||
%{address: address} = ip
|
||||
cidr = CIDR.parse(net)
|
||||
|
||||
if CIDR.match!(cidr, address) do
|
||||
changeset
|
||||
else
|
||||
add_error(changeset, ip_type, "IP must be contained within network #{net}")
|
||||
end
|
||||
end
|
||||
|
||||
defp put_next_ip(changeset, ip_type) when ip_type in [:ipv4, :ipv6] do
|
||||
case changeset do
|
||||
# Don't put a new ip if the user is trying to assign one manually
|
||||
%Ecto.Changeset{changes: %{^ip_type => _ip}} ->
|
||||
changeset
|
||||
|
||||
_ ->
|
||||
if ip = next_available(ip_type) do
|
||||
put_change(changeset, ip_type, ip)
|
||||
else
|
||||
add_error(
|
||||
changeset,
|
||||
:base,
|
||||
"#{ip_type} address pool is exhausted. Increase network size or remove some devices."
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp ipv4_address do
|
||||
{:ok, inet} =
|
||||
Application.fetch_env!(:fz_http, :wireguard_ipv4_address)
|
||||
|> EctoNetwork.INET.cast()
|
||||
|
||||
inet
|
||||
end
|
||||
|
||||
defp ipv6_address do
|
||||
{:ok, inet} =
|
||||
Application.fetch_env!(:fz_http, :wireguard_ipv6_address)
|
||||
|> EctoNetwork.INET.cast()
|
||||
|
||||
inet
|
||||
end
|
||||
end
|
||||
|
||||
126
apps/fz_http/lib/fz_http/devices/device/query.ex
Normal file
126
apps/fz_http/lib/fz_http/devices/device/query.ex
Normal file
@@ -0,0 +1,126 @@
|
||||
defmodule FzHttp.Devices.Device.Query do
|
||||
import Ecto.Query
|
||||
|
||||
@doc """
|
||||
Returns IP address at given integer offset relative to start of CIDR range.
|
||||
"""
|
||||
defmacro offset_to_ip(field, cidr) do
|
||||
quote do
|
||||
fragment("host(?)::inet + ?", unquote(cidr), unquote(field))
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns index of last IP address available for allocation in CIDR sequence.
|
||||
|
||||
Notice: the very last address in CIDR is typically a broadcast address that we won't allow to use for devices.
|
||||
"""
|
||||
defmacro cidr_end_offset(cidr) do
|
||||
quote do
|
||||
fragment(
|
||||
"host(broadcast(?))::inet - host(?)::inet - 1",
|
||||
unquote(cidr),
|
||||
unquote(cidr)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Acquires a transactional advisory lock for an IP address using "devices" table oid as namespace.
|
||||
|
||||
To fit bigint offset into int lock identifier we rollover at the integer max value.
|
||||
"""
|
||||
defmacro acquire_advisory_lock(field) do
|
||||
quote do
|
||||
fragment(
|
||||
"pg_try_advisory_xact_lock('devices'::regclass::oid::int, mod(?, 2147483647)::int)",
|
||||
unquote(field)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def all do
|
||||
from(device in FzHttp.Devices.Device, as: :device)
|
||||
end
|
||||
|
||||
@doc """
|
||||
This function returns a query to fetch next available IP address, it works in 2 steps:
|
||||
|
||||
1. It starts by forward-scanning starting for available addresses at `offset` in a given `network_cidr`
|
||||
up to the end of CIDR range;
|
||||
|
||||
2. If forward-scan failed, scan backwards from the offset (exclusive) to start of CIDR range.
|
||||
|
||||
During the search, addresses occupied by other devices or reserved are skipped.
|
||||
|
||||
We also exclude first (X.X.X.0) and last (broadcast) address in a CIDR from a search range,
|
||||
to prevent issues with legacy firewalls that consider them "class C" space network addresses.
|
||||
"""
|
||||
def next_available_address(network_cidr, offset, reserved_address) do
|
||||
forward_search_queryable =
|
||||
series_from_offset_inclusive_to_end_of_cidr(network_cidr, offset)
|
||||
|> select_not_used_ips(network_cidr, reserved_address)
|
||||
|
||||
reverse_search_queryable =
|
||||
series_from_start_of_cidr_to_offset_exclusive(network_cidr, offset)
|
||||
|> select_not_used_ips(network_cidr, reserved_address)
|
||||
|
||||
union_all(forward_search_queryable, ^reverse_search_queryable)
|
||||
|> limit(1)
|
||||
end
|
||||
|
||||
# Although sequences can work with inet types, we iterate over the sequence using an
|
||||
# offset relative to start of the given CIDR range.
|
||||
#
|
||||
# This way is chosen because IPv6 can not be cast to bigint, so by using it directly
|
||||
# we won't be able to increment/decrement it while building a sequence.
|
||||
#
|
||||
# At the same time offset will fit to bigint even for largest CIDR ranges that Firezone supports.
|
||||
#
|
||||
# XXX: We can make this code prettier once https://github.com/elixir-ecto/ecto/commit/8f7bb2665bce30dfab18cfed01585c96495575a6 is released.
|
||||
defp series_from_offset_inclusive_to_end_of_cidr(network_cidr, offset) do
|
||||
from(
|
||||
i in fragment(
|
||||
"SELECT generate_series((?)::bigint, (?)::bigint, ?) AS ip",
|
||||
^offset,
|
||||
cidr_end_offset(^network_cidr),
|
||||
1
|
||||
),
|
||||
as: :q
|
||||
)
|
||||
end
|
||||
|
||||
defp series_from_start_of_cidr_to_offset_exclusive(_network_cidr, offset) do
|
||||
from(
|
||||
i in fragment(
|
||||
"SELECT generate_series((?)::bigint, (?)::bigint, ?) AS ip",
|
||||
^(offset - 1),
|
||||
2,
|
||||
-1
|
||||
),
|
||||
as: :q
|
||||
)
|
||||
end
|
||||
|
||||
defp select_not_used_ips(queryable, network_cidr, reserved_ips) do
|
||||
queryable
|
||||
|> where(
|
||||
[q: q],
|
||||
offset_to_ip(q.ip, ^network_cidr) not in subquery(used_ips_subquery(network_cidr))
|
||||
)
|
||||
|> where([q: q], offset_to_ip(q.ip, ^network_cidr) not in ^reserved_ips)
|
||||
|> where([q: q], acquire_advisory_lock(q.ip) == true)
|
||||
|> select([q: q], offset_to_ip(q.ip, ^network_cidr))
|
||||
end
|
||||
|
||||
defp used_ips_subquery(queryable \\ all(), address)
|
||||
|
||||
defp used_ips_subquery(queryable, %Postgrex.INET{address: address})
|
||||
when tuple_size(address) == 4 do
|
||||
select(queryable, [device: device], device.ipv4)
|
||||
end
|
||||
|
||||
defp used_ips_subquery(queryable, %Postgrex.INET{address: _address}) do
|
||||
select(queryable, [device: device], device.ipv6)
|
||||
end
|
||||
end
|
||||
@@ -1,35 +1,19 @@
|
||||
defmodule FzHttp.Devices.DeviceSetting do
|
||||
@moduledoc """
|
||||
Device setting parsed from either a Device struct or map.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
import FzHttp.Devices, only: [decode: 1]
|
||||
use FzHttp, :schema
|
||||
alias FzHttp.Devices
|
||||
|
||||
@primary_key false
|
||||
embedded_schema do
|
||||
field :ip, :string
|
||||
field :ip6, :string
|
||||
field :user_id, :integer
|
||||
field :user_id, Ecto.UUID
|
||||
end
|
||||
|
||||
def parse(device) when is_struct(device) do
|
||||
def parse(device_or_device_as_map) do
|
||||
%__MODULE__{
|
||||
ip: decode(device.ipv4),
|
||||
ip6: decode(device.ipv6),
|
||||
user_id: device.user_id
|
||||
ip: Devices.decode(device_or_device_as_map.ipv4),
|
||||
ip6: Devices.decode(device_or_device_as_map.ipv6),
|
||||
user_id: device_or_device_as_map.user_id
|
||||
}
|
||||
end
|
||||
|
||||
def parse(device) when is_map(device) do
|
||||
device =
|
||||
device
|
||||
|> Map.put(:ip, Map.get(device, :ipv4))
|
||||
|> Map.put(:ip6, Map.get(device, :ipv6))
|
||||
|
||||
%__MODULE__{}
|
||||
|> cast(device, [:ip, :ip6, :user_id])
|
||||
|> apply_changes()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -33,7 +33,7 @@ defmodule FzHttp.Devices.StatsUpdater do
|
||||
|
||||
# XXX: Come up with a better way to update devices in Sandbox mode
|
||||
defp device_to_update(public_key) do
|
||||
if Application.fetch_env!(:fz_http, :sandbox) do
|
||||
if FzHttp.Config.fetch_env!(:fz_http, :sandbox) do
|
||||
Repo.one(
|
||||
from Device,
|
||||
order_by: fragment("RANDOM()"),
|
||||
|
||||
@@ -9,7 +9,7 @@ defmodule FzHttp.Events do
|
||||
|
||||
# set_config is used because devices need to be re-evaluated in case a
|
||||
# device is added to a User that's not active.
|
||||
def add(subject, device) when subject == "devices" do
|
||||
def add("devices", device) do
|
||||
with :ok <- GenServer.call(wall_pid(), {:add_device, Devices.setting_projection(device)}),
|
||||
:ok <- GenServer.call(vpn_pid(), {:set_config, Devices.to_peer_list()}) do
|
||||
:ok
|
||||
@@ -28,11 +28,11 @@ defmodule FzHttp.Events do
|
||||
end
|
||||
end
|
||||
|
||||
def add(subject, rule) when subject == "rules" do
|
||||
def add("rules", rule) do
|
||||
GenServer.call(wall_pid(), {:add_rule, Rules.setting_projection(rule)})
|
||||
end
|
||||
|
||||
def add(subject, user) when subject == "users" do
|
||||
def add("users", user) do
|
||||
# Security note: It's important to let an exception here crash this service
|
||||
# otherwise, nft could have succeeded in adding the user's set but not the rules
|
||||
# this means that in `update_device` add_device can succeed adding the device to the user's set
|
||||
@@ -40,7 +40,7 @@ defmodule FzHttp.Events do
|
||||
GenServer.call(wall_pid(), {:add_user, Users.setting_projection(user)})
|
||||
end
|
||||
|
||||
def delete(subject, device) when subject == "devices" do
|
||||
def delete("devices", device) do
|
||||
with :ok <- GenServer.call(wall_pid(), {:delete_device, Devices.setting_projection(device)}),
|
||||
:ok <- GenServer.call(vpn_pid(), {:remove_peer, device.public_key}) do
|
||||
:ok
|
||||
@@ -59,11 +59,11 @@ defmodule FzHttp.Events do
|
||||
end
|
||||
end
|
||||
|
||||
def delete(subject, rule) when subject == "rules" do
|
||||
def delete("rules", rule) do
|
||||
GenServer.call(wall_pid(), {:delete_rule, Rules.setting_projection(rule)})
|
||||
end
|
||||
|
||||
def delete(subject, user) when subject == "users" do
|
||||
def delete("users", user) do
|
||||
GenServer.call(wall_pid(), {:delete_user, Users.setting_projection(user)})
|
||||
end
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ defmodule FzHttp.Int4Range do
|
||||
# Note: we represent a port range as a string: lower - upper for ease of use
|
||||
# with Phoenix LiveView and nftables
|
||||
use Ecto.Type
|
||||
@format_error "Range Error: Bad format"
|
||||
@format_error "bad format"
|
||||
@cast_error "lower value cannot be higher than upper value"
|
||||
|
||||
def type, do: :int4range
|
||||
|
||||
@@ -30,7 +31,7 @@ defmodule FzHttp.Int4Range do
|
||||
end
|
||||
|
||||
def cast([lower, upper]) when upper >= lower, do: {:ok, "#{lower} - #{upper}"}
|
||||
def cast([_, _]), do: {:error, message: "Range Error: Lower bound higher than upper bound"}
|
||||
def cast([_, _]), do: {:error, message: @cast_error}
|
||||
|
||||
def load(%Postgrex.Range{
|
||||
lower: lower,
|
||||
|
||||
@@ -18,7 +18,7 @@ defmodule FzHttpWeb.Mailer do
|
||||
|
||||
def default_email do
|
||||
Email.new()
|
||||
|> Email.from(Application.fetch_env!(:fz_http, FzHttpWeb.Mailer)[:from_email])
|
||||
|> Email.from(FzHttp.Config.fetch_env!(:fz_http, FzHttpWeb.Mailer)[:from_email])
|
||||
end
|
||||
|
||||
def configs_for(provider) do
|
||||
|
||||
@@ -2,13 +2,8 @@ defmodule FzHttp.MFA.Method do
|
||||
@moduledoc """
|
||||
Multi Factor Authentication methods
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
use FzHttp, :schema
|
||||
import Ecto.Changeset
|
||||
import FzHttp.Validators.Common, only: [trim: 2]
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@whitespace_trimmed_fields :name
|
||||
|
||||
schema "mfa_methods" do
|
||||
field :name, :string
|
||||
@@ -16,18 +11,19 @@ defmodule FzHttp.MFA.Method do
|
||||
field :credential_id, :string
|
||||
field :last_used_at, :utc_datetime_usec
|
||||
field :payload, FzHttp.Encrypted.Map
|
||||
field :user_id, :id
|
||||
field :secret, :string, virtual: true
|
||||
field :code, :string, virtual: true
|
||||
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
belongs_to :user, FzHttp.Users.User
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(method, attrs) do
|
||||
method
|
||||
|> cast(attrs, [:name, :type, :credential_id, :payload, :last_used_at, :secret, :code])
|
||||
|> trim(@whitespace_trimmed_fields)
|
||||
|> update_change(:name, &String.trim/1)
|
||||
|> cast_payload()
|
||||
|> validate_required([:name, :type, :payload])
|
||||
|> validate_code()
|
||||
|
||||
@@ -2,8 +2,7 @@ defmodule FzHttp.OIDC.Connection do
|
||||
@moduledoc """
|
||||
OIDC connections
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
use FzHttp, :schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "oidc_connections" do
|
||||
@@ -11,9 +10,10 @@ defmodule FzHttp.OIDC.Connection do
|
||||
field :refresh_response, :map
|
||||
field :refresh_token, :string
|
||||
field :refreshed_at, :utc_datetime_usec
|
||||
field :user_id, :id
|
||||
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
belongs_to :user, FzHttp.Users.User
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
|
||||
@@ -6,7 +6,7 @@ defmodule FzHttp.OIDC.Refresher do
|
||||
|
||||
import Ecto.{Changeset, Query}
|
||||
import FzHttpWeb.OIDC.Helpers
|
||||
alias FzHttp.Configurations, as: Conf
|
||||
|
||||
alias FzHttp.{OIDC, OIDC.Connection, Repo, Users}
|
||||
require Logger
|
||||
|
||||
@@ -33,14 +33,12 @@ defmodule FzHttp.OIDC.Refresher do
|
||||
{:stop, :shutdown, user_id}
|
||||
end
|
||||
|
||||
defp do_refresh(user_id, %{provider: provider_key, refresh_token: refresh_token} = conn) do
|
||||
{:ok, provider} = atomize_provider(provider_key)
|
||||
|
||||
Logger.info("Refreshing user\##{user_id} @ #{provider}...")
|
||||
defp do_refresh(user_id, %{provider: provider_id, refresh_token: refresh_token} = conn) do
|
||||
Logger.info("Refreshing user\##{user_id} @ #{provider_id}...")
|
||||
|
||||
result =
|
||||
openid_connect().fetch_tokens(
|
||||
provider,
|
||||
provider_id,
|
||||
%{grant_type: "refresh_token", refresh_token: refresh_token}
|
||||
)
|
||||
|
||||
@@ -79,6 +77,6 @@ defmodule FzHttp.OIDC.Refresher do
|
||||
end
|
||||
|
||||
defp enabled? do
|
||||
Conf.get!(:disable_vpn_on_oidc_error)
|
||||
FzHttp.Configurations.get!(:disable_vpn_on_oidc_error)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
defmodule FzHttp.OIDC.StartProxy do
|
||||
@moduledoc """
|
||||
This proxy simply gets the relevant config at an appropriate timing
|
||||
(after `FzHttp.Configurations.Cache` has started) and pass to `OpenIDConnect.Worker`
|
||||
"""
|
||||
|
||||
alias FzHttp.Configurations, as: Conf
|
||||
|
||||
require Logger
|
||||
|
||||
def child_spec(arg) do
|
||||
@@ -13,43 +10,44 @@ defmodule FzHttp.OIDC.StartProxy do
|
||||
end
|
||||
|
||||
def start_link(:test) do
|
||||
auth_oidc_env = Conf.get!(:openid_connect_providers)
|
||||
Conf.Cache.put!(:parsed_openid_connect_providers, parse(auth_oidc_env))
|
||||
:ignore
|
||||
end
|
||||
|
||||
def start_link(_) do
|
||||
auth_oidc_env = Conf.get!(:openid_connect_providers)
|
||||
FzHttp.Configurations.get!(:openid_connect_providers)
|
||||
|> parse()
|
||||
|> OpenIDConnect.Worker.start_link()
|
||||
end
|
||||
|
||||
if parsed = auth_oidc_env && parse(auth_oidc_env) do
|
||||
Conf.Cache.put!(:parsed_openid_connect_providers, parsed)
|
||||
OpenIDConnect.Worker.start_link(parsed)
|
||||
else
|
||||
:ignore
|
||||
# XXX: Remove when configurations support test fixtures
|
||||
if Mix.env() == :test do
|
||||
def restart, do: :ignore
|
||||
else
|
||||
def restart do
|
||||
:ok = Supervisor.terminate_child(FzHttp.Supervisor, __MODULE__)
|
||||
Supervisor.restart_child(FzHttp.Supervisor, __MODULE__)
|
||||
end
|
||||
end
|
||||
|
||||
defp parse(auth_oidc_env) when is_binary(auth_oidc_env) do
|
||||
auth_oidc_env |> Jason.decode!() |> parse()
|
||||
end
|
||||
# Convert the configuration record to something openid_connect expects,
|
||||
# atom-keyed configs eg. [provider: [client_id: "CLIENT_ID" ...]]
|
||||
defp parse(nil), do: []
|
||||
|
||||
defp parse(auth_oidc_config) when is_map(auth_oidc_config) do
|
||||
external_url = Application.fetch_env!(:fz_http, :external_url)
|
||||
defp parse(auth_oidc_config) when is_list(auth_oidc_config) do
|
||||
external_url = FzHttp.Config.fetch_env!(:fz_http, :external_url)
|
||||
|
||||
# Convert Map to something openid_connect expects, atomic keyed configs
|
||||
# eg. [provider: [client_id: "CLIENT_ID" ...]]
|
||||
Enum.map(auth_oidc_config, fn {provider, settings} ->
|
||||
Enum.map(auth_oidc_config, fn provider ->
|
||||
{
|
||||
String.to_atom(provider),
|
||||
provider.id,
|
||||
[
|
||||
discovery_document_uri: settings["discovery_document_uri"],
|
||||
client_id: settings["client_id"],
|
||||
client_secret: settings["client_secret"],
|
||||
discovery_document_uri: provider.discovery_document_uri,
|
||||
client_id: provider.client_id,
|
||||
client_secret: provider.client_secret,
|
||||
redirect_uri:
|
||||
settings["redirect_uri"] || "#{external_url}/auth/oidc/#{provider}/callback/",
|
||||
response_type: settings["response_type"],
|
||||
scope: settings["scope"],
|
||||
label: settings["label"]
|
||||
provider.redirect_uri || "#{external_url}/auth/oidc/#{provider.id}/callback/",
|
||||
response_type: provider.response_type,
|
||||
scope: provider.scope,
|
||||
label: provider.label
|
||||
]
|
||||
}
|
||||
end)
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
defmodule FzHttp.Queries.INET do
|
||||
@moduledoc """
|
||||
Raw SQL INET queries
|
||||
"""
|
||||
|
||||
# XXX: This needs to be an insert to avoid the deadlocks
|
||||
@next_available_ipv4_query """
|
||||
WITH combined AS (
|
||||
SELECT $2 AS ipv4
|
||||
UNION ALL
|
||||
SELECT devices.ipv4 FROM devices
|
||||
)
|
||||
SELECT combined.ipv4 + 1 AS ipv4
|
||||
FROM combined
|
||||
WHERE combined.ipv4 + 1 < host(broadcast($1))::INET
|
||||
AND combined.ipv4 + 1 != $2
|
||||
AND combined.ipv4 >= host($1)::INET
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 from combined d2 WHERE d2.ipv4 = combined.ipv4 + 1
|
||||
)
|
||||
ORDER BY combined.ipv4
|
||||
LIMIT 1
|
||||
"""
|
||||
|
||||
# XXX: This needs to be an insert to avoid the deadlocks
|
||||
@next_available_ipv6_query """
|
||||
WITH combined AS (
|
||||
SELECT $2 AS ipv6
|
||||
UNION ALL
|
||||
SELECT devices.ipv6 FROM devices
|
||||
)
|
||||
SELECT combined.ipv6 + 1 AS ipv6
|
||||
FROM combined
|
||||
WHERE combined.ipv6 + 1 < host(broadcast($1))::INET
|
||||
AND combined.ipv6 + 1 != $2
|
||||
AND combined.ipv6 >= host($1)::INET
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 from combined d2 WHERE d2.ipv6 = combined.ipv6 + 1
|
||||
)
|
||||
ORDER BY combined.ipv6
|
||||
LIMIT 1
|
||||
"""
|
||||
|
||||
def next_available(type) do
|
||||
network = wireguard_network(type)
|
||||
address = wireguard_address(type)
|
||||
query = next_available_query(type)
|
||||
|
||||
case FzHttp.Repo.query(query, [network, address]) do
|
||||
{:ok, %Postgrex.Result{rows: [[%Postgrex.INET{} = inet]]}} ->
|
||||
inet
|
||||
|
||||
{:ok, %Postgrex.Result{rows: []}} ->
|
||||
nil
|
||||
|
||||
{:error, error} ->
|
||||
raise(error)
|
||||
end
|
||||
end
|
||||
|
||||
defp wireguard_network(type) do
|
||||
network_key = "wireguard_#{type}_network" |> String.to_existing_atom()
|
||||
{:ok, network} = EctoNetwork.INET.cast(Application.fetch_env!(:fz_http, network_key))
|
||||
network
|
||||
end
|
||||
|
||||
defp wireguard_address(type) do
|
||||
address_key = "wireguard_#{type}_address" |> String.to_existing_atom()
|
||||
{:ok, address} = EctoNetwork.INET.cast(Application.fetch_env!(:fz_http, address_key))
|
||||
address
|
||||
end
|
||||
|
||||
defp next_available_query(:ipv4) do
|
||||
@next_available_ipv4_query
|
||||
end
|
||||
|
||||
defp next_available_query(:ipv6) do
|
||||
@next_available_ipv6_query
|
||||
end
|
||||
end
|
||||
@@ -21,16 +21,22 @@ defmodule FzHttp.Release do
|
||||
def create_admin_user do
|
||||
load_app()
|
||||
|
||||
if Repo.exists?(from u in User, where: u.email == ^email()) do
|
||||
change_password(email(), default_password())
|
||||
reset_role(email(), :admin)
|
||||
else
|
||||
Users.create_admin_user(
|
||||
email: email(),
|
||||
password: default_password(),
|
||||
password_confirmation: default_password()
|
||||
)
|
||||
end
|
||||
reply =
|
||||
if Repo.exists?(from u in User, where: u.email == ^email()) do
|
||||
change_password(email(), default_password())
|
||||
reset_role(email(), :admin)
|
||||
else
|
||||
Users.create_admin_user(
|
||||
email: email(),
|
||||
password: default_password(),
|
||||
password_confirmation: default_password()
|
||||
)
|
||||
end
|
||||
|
||||
# Notify the user
|
||||
IO.puts("Password reset! Check $HOME/.firezone/.env for sign in credentials.")
|
||||
|
||||
reply
|
||||
end
|
||||
|
||||
def change_password(email, password) do
|
||||
@@ -50,11 +56,11 @@ defmodule FzHttp.Release do
|
||||
end
|
||||
|
||||
def repos do
|
||||
Application.fetch_env!(@app, :ecto_repos)
|
||||
FzHttp.Config.fetch_env!(@app, :ecto_repos)
|
||||
end
|
||||
|
||||
defp email do
|
||||
Application.fetch_env!(@app, :admin_email)
|
||||
FzHttp.Config.fetch_env!(@app, :admin_email)
|
||||
end
|
||||
|
||||
defp load_app do
|
||||
@@ -66,6 +72,6 @@ defmodule FzHttp.Release do
|
||||
end
|
||||
|
||||
defp default_password do
|
||||
Application.fetch_env!(@app, :default_admin_password)
|
||||
FzHttp.Config.fetch_env!(@app, :default_admin_password)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,7 +7,7 @@ defmodule FzHttp.Rules do
|
||||
import Ecto.Changeset
|
||||
alias FzHttp.{Repo, Rules.Rule, Rules.RuleSetting, Telemetry}
|
||||
|
||||
def port_rules_supported?, do: Application.fetch_env!(:fz_wall, :port_based_rules_supported)
|
||||
def port_rules_supported?, do: FzHttp.Config.fetch_env!(:fz_wall, :port_based_rules_supported)
|
||||
|
||||
defp scope(port_based_rules) when port_based_rules == true do
|
||||
Rule
|
||||
@@ -75,6 +75,12 @@ defmodule FzHttp.Rules do
|
||||
result
|
||||
end
|
||||
|
||||
def update_rule(%Rule{} = rule, attrs \\ %{}) do
|
||||
rule
|
||||
|> Rule.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
def delete_rule(%Rule{} = rule) do
|
||||
Telemetry.delete_rule()
|
||||
Repo.delete(rule)
|
||||
|
||||
@@ -2,23 +2,22 @@ defmodule FzHttp.Rules.Rule do
|
||||
@moduledoc """
|
||||
Not really sure what to write here. I'll update this later.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
use FzHttp, :schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@exclusion_msg "Destination overlaps with an existing rule"
|
||||
@port_range_msg "Port is not within valid range"
|
||||
@port_type_msg "Please specify a port-range for the given port type"
|
||||
@exclusion_msg "destination overlaps with an existing rule"
|
||||
@port_range_msg "port is not within valid range"
|
||||
@port_type_msg "port_type must be specified with port_range"
|
||||
|
||||
schema "rules" do
|
||||
field :uuid, Ecto.UUID, autogenerate: true
|
||||
field :destination, EctoNetwork.INET, read_after_writes: true
|
||||
field :action, Ecto.Enum, values: [:drop, :accept], default: :drop
|
||||
field :port_type, Ecto.Enum, values: [:tcp, :udp], default: nil
|
||||
field :port_range, FzHttp.Int4Range, default: nil
|
||||
|
||||
belongs_to :user, FzHttp.Users.User
|
||||
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def changeset(rule, attrs) do
|
||||
|
||||
@@ -2,8 +2,7 @@ defmodule FzHttp.Rules.RuleSetting do
|
||||
@moduledoc """
|
||||
Rule setting parsed from either a Rule struct or map.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
use FzHttp, :schema
|
||||
import Ecto.Changeset
|
||||
import FzHttp.Devices, only: [decode: 1]
|
||||
|
||||
@@ -11,7 +10,7 @@ defmodule FzHttp.Rules.RuleSetting do
|
||||
embedded_schema do
|
||||
field :action, Ecto.Enum, values: [:drop, :accept]
|
||||
field :destination, :string
|
||||
field :user_id, :integer
|
||||
field :user_id, Ecto.UUID
|
||||
field :port_type, Ecto.Enum, values: [:tcp, :udp], default: nil
|
||||
field :port_range, FzHttp.Int4Range, default: nil
|
||||
end
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
defmodule FzHttp.SAML.StartProxy do
|
||||
@moduledoc """
|
||||
This proxy starts Samly.Provider with proper configs
|
||||
(after `FzHttp.Conf.Cache` has started)
|
||||
"""
|
||||
|
||||
alias FzHttp.Configurations, as: Conf
|
||||
|
||||
def child_spec(arg) do
|
||||
%{id: __MODULE__, start: {__MODULE__, :start_link, [arg]}}
|
||||
end
|
||||
|
||||
def start_link(:test) do
|
||||
start_link(nil)
|
||||
end
|
||||
|
||||
def start_link(_) do
|
||||
providers = FzHttp.Configurations.get!(:saml_identity_providers)
|
||||
samly = Samly.Provider.start_link()
|
||||
|
||||
Application.fetch_env!(:samly, Samly.Provider)
|
||||
FzHttp.Config.fetch_env!(:samly, Samly.Provider)
|
||||
|> set_service_provider()
|
||||
|> set_identity_providers()
|
||||
|> set_identity_providers(providers)
|
||||
|> refresh()
|
||||
|
||||
samly
|
||||
end
|
||||
|
||||
def set_service_provider(samly_configs) do
|
||||
entity_id = Application.fetch_env!(:fz_http, :saml_entity_id)
|
||||
keyfile = Application.fetch_env!(:fz_http, :saml_keyfile_path)
|
||||
certfile = Application.fetch_env!(:fz_http, :saml_certfile_path)
|
||||
entity_id = FzHttp.Config.fetch_env!(:fz_http, :saml_entity_id)
|
||||
keyfile = FzHttp.Config.fetch_env!(:fz_http, :saml_keyfile_path)
|
||||
certfile = FzHttp.Config.fetch_env!(:fz_http, :saml_certfile_path)
|
||||
|
||||
# Only one service provider definition: us.
|
||||
Keyword.put(samly_configs, :service_providers, [
|
||||
@@ -37,21 +39,23 @@ defmodule FzHttp.SAML.StartProxy do
|
||||
])
|
||||
end
|
||||
|
||||
def set_identity_providers(samly_configs, providers \\ Conf.get!(:saml_identity_providers)) do
|
||||
external_url = Application.fetch_env!(:fz_http, :external_url)
|
||||
def set_identity_providers(samly_configs, providers) do
|
||||
external_url = FzHttp.Config.fetch_env!(:fz_http, :external_url)
|
||||
|
||||
identity_providers =
|
||||
providers
|
||||
|> Enum.map(fn {id, setting} ->
|
||||
|> Enum.map(fn provider ->
|
||||
# XXX We should not set default values here, instead they should be part
|
||||
# of the changeset and always valid in database
|
||||
%{
|
||||
id: id,
|
||||
id: provider.id,
|
||||
sp_id: "firezone",
|
||||
metadata: Map.get(setting, "metadata"),
|
||||
base_url: Map.get(setting, "base_url", Path.join(external_url, "/auth/saml")),
|
||||
sign_requests: Map.get(setting, "sign_requests", true),
|
||||
sign_metadata: Map.get(setting, "sign_metadata", true),
|
||||
signed_assertion_in_resp: Map.get(setting, "signed_assertion_in_resp", true),
|
||||
signed_envelopes_in_resp: Map.get(setting, "signed_envelopes_in_resp", true)
|
||||
metadata: provider.metadata,
|
||||
base_url: provider.base_url || Path.join(external_url, "/auth/saml"),
|
||||
sign_requests: provider.sign_requests || true,
|
||||
sign_metadata: provider.sign_metadata || true,
|
||||
signed_assertion_in_resp: provider.signed_assertion_in_resp || true,
|
||||
signed_envelopes_in_resp: provider.signed_envelopes_in_resp || true
|
||||
}
|
||||
end)
|
||||
|
||||
@@ -62,4 +66,17 @@ defmodule FzHttp.SAML.StartProxy do
|
||||
Application.put_env(:samly, Samly.Provider, samly_configs)
|
||||
Samly.Provider.refresh_providers()
|
||||
end
|
||||
|
||||
# XXX: This should be removed when the configurations singleton record is removed.
|
||||
#
|
||||
# Needed to prevent the test suite from recursively restarting this module as
|
||||
# it put!()'s mock data
|
||||
if Mix.env() == :test do
|
||||
def restart, do: :ignore
|
||||
else
|
||||
def restart do
|
||||
:ok = Supervisor.terminate_child(FzHttp.Supervisor, __MODULE__)
|
||||
Supervisor.restart_child(FzHttp.Supervisor, __MODULE__)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
defmodule FzHttp.Sites do
|
||||
@moduledoc """
|
||||
The Sites context.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias FzHttp.{Repo, Sites.Site}
|
||||
|
||||
def new_site(attrs \\ %{}) do
|
||||
Site.changeset(%Site{}, attrs)
|
||||
end
|
||||
|
||||
def get_site! do
|
||||
get_site!(name: "default")
|
||||
end
|
||||
|
||||
def get_site!(name: name) do
|
||||
Repo.one!(
|
||||
from s in Site,
|
||||
where: s.name == ^name
|
||||
)
|
||||
end
|
||||
|
||||
def get_site!(id) do
|
||||
Repo.get!(Site, id)
|
||||
end
|
||||
|
||||
def change_site(%Site{} = site) do
|
||||
Site.changeset(site, %{})
|
||||
end
|
||||
|
||||
def update_site(%Site{} = site, attrs) do
|
||||
site
|
||||
|> Site.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
def vpn_sessions_expire? do
|
||||
freq = vpn_duration()
|
||||
freq > 0 && freq < Site.max_vpn_session_duration()
|
||||
end
|
||||
|
||||
def vpn_duration do
|
||||
get_site!().vpn_session_duration
|
||||
end
|
||||
end
|
||||
@@ -1,74 +0,0 @@
|
||||
defmodule FzHttp.Sites.Site do
|
||||
@moduledoc """
|
||||
Represents a VPN / Firewall site and its config.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
import FzHttp.Validators.Common,
|
||||
only: [
|
||||
trim: 2,
|
||||
validate_list_of_ips_or_cidrs: 2,
|
||||
validate_no_duplicates: 2
|
||||
]
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
||||
# Postgres max int size is 4 bytes
|
||||
@max_pg_integer 2_147_483_647
|
||||
|
||||
@minute 60
|
||||
@hour 60 * @minute
|
||||
@min_mtu 576
|
||||
@max_mtu 1500
|
||||
@min_persistent_keepalive 0
|
||||
@max_persistent_keepalive 1 * @hour
|
||||
@min_vpn_session_duration 0
|
||||
@max_vpn_session_duration @max_pg_integer
|
||||
@whitespace_trimmed_fields ~w(name dns allowed_ips endpoint)a
|
||||
|
||||
schema "sites" do
|
||||
field :name, :string
|
||||
field :dns, :string
|
||||
field :allowed_ips, :string
|
||||
field :endpoint, :string
|
||||
field :persistent_keepalive, :integer
|
||||
field :mtu, :integer
|
||||
field :vpn_session_duration, :integer
|
||||
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
end
|
||||
|
||||
def changeset(site, attrs) do
|
||||
site
|
||||
|> cast(attrs, [
|
||||
:name,
|
||||
:dns,
|
||||
:allowed_ips,
|
||||
:endpoint,
|
||||
:persistent_keepalive,
|
||||
:mtu,
|
||||
:vpn_session_duration
|
||||
])
|
||||
|> trim(@whitespace_trimmed_fields)
|
||||
|> validate_required(:name)
|
||||
|> validate_no_duplicates(:dns)
|
||||
|> validate_list_of_ips_or_cidrs(:allowed_ips)
|
||||
|> validate_no_duplicates(:allowed_ips)
|
||||
|> validate_number(:mtu, greater_than_or_equal_to: @min_mtu, less_than_or_equal_to: @max_mtu)
|
||||
|> validate_number(:persistent_keepalive,
|
||||
greater_than_or_equal_to: @min_persistent_keepalive,
|
||||
less_than_or_equal_to: @max_persistent_keepalive
|
||||
)
|
||||
|> validate_number(:vpn_session_duration,
|
||||
greater_than_or_equal_to: @min_vpn_session_duration,
|
||||
less_than_or_equal_to: @max_vpn_session_duration
|
||||
)
|
||||
end
|
||||
|
||||
def max_vpn_session_duration do
|
||||
@max_vpn_session_duration
|
||||
end
|
||||
end
|
||||
@@ -5,9 +5,25 @@ defmodule FzHttp.Telemetry do
|
||||
|
||||
require Logger
|
||||
|
||||
alias FzHttp.Configurations, as: Conf
|
||||
alias FzHttp.{Devices, MFA, Users}
|
||||
|
||||
def create_api_token do
|
||||
telemetry_module().capture(
|
||||
"add_api_token",
|
||||
common_fields()
|
||||
)
|
||||
end
|
||||
|
||||
def delete_api_token(api_token) do
|
||||
telemetry_module().capture(
|
||||
"delete_api_token",
|
||||
common_fields() ++
|
||||
[
|
||||
api_token_created_at: api_token.inserted_at
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
def add_device do
|
||||
telemetry_module().capture(
|
||||
"add_device",
|
||||
@@ -75,9 +91,6 @@ defmodule FzHttp.Telemetry do
|
||||
telemetry_module().capture("ping", ping_data())
|
||||
end
|
||||
|
||||
defp count(subject) when is_map(subject), do: count(Map.keys(subject))
|
||||
defp count(subject) when is_list(subject), do: length(subject)
|
||||
|
||||
# How far back to count handshakes as an active device
|
||||
@active_device_window 86_400
|
||||
def ping_data do
|
||||
@@ -91,15 +104,18 @@ defmodule FzHttp.Telemetry do
|
||||
max_devices_for_users: Devices.max_count_by_user_id(),
|
||||
users_with_mfa: MFA.count_distinct_by_user_id(),
|
||||
users_with_mfa_totp: MFA.count_distinct_totp_by_user_id(),
|
||||
openid_providers: count(Conf.get!(:parsed_openid_connect_providers)),
|
||||
saml_providers: count(Conf.get!(:saml_identity_providers)),
|
||||
unprivileged_device_management: Conf.get!(:allow_unprivileged_device_management),
|
||||
unprivileged_device_configuration: Conf.get!(:allow_unprivileged_device_configuration),
|
||||
local_authentication: Conf.get!(:local_auth_enabled),
|
||||
disable_vpn_on_oidc_error: Conf.get!(:disable_vpn_on_oidc_error),
|
||||
openid_providers: length(FzHttp.Configurations.get!(:openid_connect_providers)),
|
||||
saml_providers: length(FzHttp.Configurations.get!(:saml_identity_providers)),
|
||||
unprivileged_device_management:
|
||||
FzHttp.Configurations.get!(:allow_unprivileged_device_management),
|
||||
unprivileged_device_configuration:
|
||||
FzHttp.Configurations.get!(:allow_unprivileged_device_configuration),
|
||||
local_authentication: FzHttp.Configurations.get!(:local_auth_enabled),
|
||||
disable_vpn_on_oidc_error: FzHttp.Configurations.get!(:disable_vpn_on_oidc_error),
|
||||
outbound_email: outbound_email?(),
|
||||
external_database: external_database?(Map.new(conf(FzHttp.Repo))),
|
||||
logo_type: Conf.logo_type(Conf.get!(:logo))
|
||||
external_database:
|
||||
external_database?(Map.new(FzHttp.Config.fetch_env!(:fz_http, FzHttp.Repo))),
|
||||
logo_type: FzHttp.Configurations.logo_type(FzHttp.Configurations.get!(:logo))
|
||||
]
|
||||
end
|
||||
|
||||
@@ -109,7 +125,7 @@ defmodule FzHttp.Telemetry do
|
||||
|
||||
defp common_fields do
|
||||
[
|
||||
distinct_id: conf(:telemetry_id),
|
||||
distinct_id: FzHttp.Config.fetch_env!(:fz_http, :telemetry_id),
|
||||
fqdn: fqdn(),
|
||||
version: version(),
|
||||
kernel_version: "#{os_type()} #{os_version()}"
|
||||
@@ -117,12 +133,12 @@ defmodule FzHttp.Telemetry do
|
||||
end
|
||||
|
||||
defp telemetry_module do
|
||||
Application.fetch_env!(:fz_http, :telemetry_module)
|
||||
FzHttp.Config.fetch_env!(:fz_http, :telemetry_module)
|
||||
end
|
||||
|
||||
defp fqdn do
|
||||
:fz_http
|
||||
|> Application.fetch_env!(FzHttpWeb.Endpoint)
|
||||
|> FzHttp.Config.fetch_env!(FzHttpWeb.Endpoint)
|
||||
|> Keyword.get(:url)
|
||||
|> Keyword.get(:host)
|
||||
end
|
||||
@@ -146,7 +162,7 @@ defmodule FzHttp.Telemetry do
|
||||
end
|
||||
|
||||
defp outbound_email? do
|
||||
from_email = conf(FzHttpWeb.Mailer)[:from_email]
|
||||
from_email = FzHttp.Config.fetch_env!(:fz_http, FzHttpWeb.Mailer)[:from_email]
|
||||
|
||||
!is_nil(from_email)
|
||||
end
|
||||
@@ -170,8 +186,4 @@ defmodule FzHttp.Telemetry do
|
||||
"0.0.0"
|
||||
end
|
||||
end
|
||||
|
||||
defp conf(key) do
|
||||
Application.fetch_env!(:fz_http, key)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,7 +8,6 @@ defmodule FzHttp.Users do
|
||||
|
||||
alias FzHttp.Devices.Device
|
||||
alias FzHttp.Repo
|
||||
alias FzHttp.Sites.Site
|
||||
alias FzHttp.Telemetry
|
||||
alias FzHttp.Users.User
|
||||
alias FzHttpWeb.Mailer
|
||||
@@ -57,6 +56,10 @@ defmodule FzHttp.Users do
|
||||
Repo.get_by(User, email: email)
|
||||
end
|
||||
|
||||
def get_by_email!(email) do
|
||||
Repo.get_by!(User, email: email)
|
||||
end
|
||||
|
||||
def create_admin_user(attrs) do
|
||||
create_user_with_role(attrs, :admin)
|
||||
end
|
||||
@@ -215,7 +218,7 @@ defmodule FzHttp.Users do
|
||||
end
|
||||
|
||||
def vpn_session_expired?(user, duration) do
|
||||
max = Site.max_vpn_session_duration()
|
||||
max = FzHttp.Configurations.Configuration.max_vpn_session_duration()
|
||||
|
||||
case duration do
|
||||
0 ->
|
||||
|
||||
@@ -2,22 +2,21 @@ defmodule FzHttp.Users.User do
|
||||
@moduledoc """
|
||||
Represents a User.
|
||||
"""
|
||||
use FzHttp, :schema
|
||||
import Ecto.Changeset
|
||||
import FzHttp.Users.PasswordHelpers
|
||||
|
||||
alias FzHttp.{
|
||||
ApiTokens.ApiToken,
|
||||
Devices.Device,
|
||||
OIDC.Connection,
|
||||
Validators.Common
|
||||
}
|
||||
|
||||
@min_password_length 12
|
||||
@max_password_length 64
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
import FzHttp.Users.PasswordHelpers
|
||||
import FzHttp.Validators.Common, only: [trim: 2]
|
||||
|
||||
alias FzHttp.{Devices.Device, OIDC.Connection}
|
||||
|
||||
# Fields for which to trim whitespace after cast, before validation
|
||||
@whitespace_trimmed_fields :email
|
||||
|
||||
schema "users" do
|
||||
field :uuid, Ecto.UUID, autogenerate: true
|
||||
field :role, Ecto.Enum, values: [:unprivileged, :admin], default: :unprivileged
|
||||
field :email, :string
|
||||
field :last_signed_in_at, :utc_datetime_usec
|
||||
@@ -33,10 +32,11 @@ defmodule FzHttp.Users.User do
|
||||
field :password_confirmation, :string, virtual: true
|
||||
field :current_password, :string, virtual: true
|
||||
|
||||
has_many :devices, Device, on_delete: :delete_all
|
||||
has_many :oidc_connections, Connection, on_delete: :delete_all
|
||||
has_many :devices, Device
|
||||
has_many :oidc_connections, Connection
|
||||
has_many :api_tokens, ApiToken
|
||||
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def create_changeset(user, attrs \\ %{}) do
|
||||
@@ -47,7 +47,7 @@ defmodule FzHttp.Users.User do
|
||||
:password,
|
||||
:password_confirmation
|
||||
])
|
||||
|> trim(@whitespace_trimmed_fields)
|
||||
|> update_change(:email, &String.trim/1)
|
||||
|> validate_required([:email])
|
||||
|> validate_password_equality()
|
||||
|> validate_length(:password, min: @min_password_length, max: @max_password_length)
|
||||
@@ -87,7 +87,7 @@ defmodule FzHttp.Users.User do
|
||||
def update_email(user, attrs) do
|
||||
user
|
||||
|> cast(attrs, [:email])
|
||||
|> trim(:email)
|
||||
|> Common.trim_change(:email)
|
||||
|> validate_required([:email])
|
||||
|> validate_format(:email, ~r/@/)
|
||||
end
|
||||
|
||||
@@ -11,20 +11,6 @@ defmodule FzHttp.Validators.Common do
|
||||
valid_cidr?: 1
|
||||
]
|
||||
|
||||
defp do_trim(nil), do: nil
|
||||
|
||||
defp do_trim(str) when is_binary(str), do: String.trim(str)
|
||||
|
||||
def trim(changeset, field) when is_atom(field) do
|
||||
trim(changeset, [field])
|
||||
end
|
||||
|
||||
def trim(changeset, fields) when is_list(fields) do
|
||||
Enum.reduce(fields, changeset, fn field, cs ->
|
||||
update_change(cs, field, &do_trim/1)
|
||||
end)
|
||||
end
|
||||
|
||||
def validate_uri(changeset, fields) when is_list(fields) do
|
||||
Enum.reduce(fields, changeset, fn field, accumulated_changeset ->
|
||||
validate_uri(accumulated_changeset, field)
|
||||
@@ -80,6 +66,15 @@ defmodule FzHttp.Validators.Common do
|
||||
end)
|
||||
end
|
||||
|
||||
def validate_base64(changeset, field) do
|
||||
validate_change(changeset, field, fn _cur, value ->
|
||||
case Base.decode64(value) do
|
||||
:error -> [{field, "must be a base64-encoded string"}]
|
||||
{:ok, _decoded} -> []
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def validate_omitted(changeset, fields) when is_list(fields) do
|
||||
Enum.reduce(fields, changeset, fn field, accumulated_changeset ->
|
||||
validate_omitted(accumulated_changeset, field)
|
||||
@@ -96,22 +91,6 @@ defmodule FzHttp.Validators.Common do
|
||||
end)
|
||||
end
|
||||
|
||||
def validate_no_mask(%Ecto.Changeset{changes: %{ipv4: %{netmask: nm}}} = changeset, :ipv4)
|
||||
when nm in [nil, 32],
|
||||
do: changeset
|
||||
|
||||
def validate_no_mask(%Ecto.Changeset{changes: %{ipv6: %{netmask: nm}}} = changeset, :ipv6)
|
||||
when nm in [nil, 128],
|
||||
do: changeset
|
||||
|
||||
def validate_no_mask(%Ecto.Changeset{changes: %{ipv4: %{netmask: _}}} = changeset, :ipv4),
|
||||
do: netmask_error(changeset, :ipv4)
|
||||
|
||||
def validate_no_mask(%Ecto.Changeset{changes: %{ipv6: %{netmask: _}}} = changeset, :ipv6),
|
||||
do: netmask_error(changeset, :ipv6)
|
||||
|
||||
def validate_no_mask(changeset, _), do: changeset
|
||||
|
||||
defp split_comma_list(text) do
|
||||
text
|
||||
|> String.split(",")
|
||||
@@ -126,11 +105,25 @@ defmodule FzHttp.Validators.Common do
|
||||
end
|
||||
end
|
||||
|
||||
defp netmask_error(changeset, ip_type) do
|
||||
add_error(
|
||||
changeset,
|
||||
ip_type,
|
||||
"Only IPs without netmask are supported."
|
||||
)
|
||||
@doc """
|
||||
Puts the change if field is not changed or it's value is set to `nil`.
|
||||
"""
|
||||
def put_default_value(changeset, _field, nil) do
|
||||
changeset
|
||||
end
|
||||
|
||||
def put_default_value(changeset, field, value) do
|
||||
case fetch_field(changeset, field) do
|
||||
{:data, nil} -> put_change(changeset, field, maybe_apply(value))
|
||||
:error -> put_change(changeset, field, maybe_apply(value))
|
||||
_ -> changeset
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_apply(fun) when is_function(fun, 0), do: fun.()
|
||||
defp maybe_apply(value), do: value
|
||||
|
||||
def trim_change(changeset, field) do
|
||||
update_change(changeset, field, &if(!is_nil(&1), do: String.trim(&1)))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,7 +25,6 @@ defmodule FzHttpWeb do
|
||||
import FzHttpWeb.Gettext
|
||||
import Phoenix.LiveView.Controller
|
||||
import FzHttpWeb.ControllerHelpers
|
||||
alias FzHttp.Configurations, as: Conf
|
||||
|
||||
unquote(verified_routes())
|
||||
end
|
||||
@@ -52,10 +51,6 @@ defmodule FzHttpWeb do
|
||||
import FzHttpWeb.LiveHelpers
|
||||
|
||||
unquote(verified_routes())
|
||||
|
||||
def render_common(template, assigns \\ []) do
|
||||
render(FzHttpWeb.CommonView, template, assigns)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -63,7 +58,7 @@ defmodule FzHttpWeb do
|
||||
quote do
|
||||
use Phoenix.LiveView, layout: {FzHttpWeb.LayoutView, :live}
|
||||
import FzHttpWeb.LiveHelpers
|
||||
alias FzHttp.Configurations, as: Conf
|
||||
|
||||
alias Phoenix.LiveView.JS
|
||||
|
||||
unquote(view_helpers())
|
||||
@@ -74,7 +69,7 @@ defmodule FzHttpWeb do
|
||||
quote do
|
||||
use Phoenix.LiveView, layout: nil
|
||||
import FzHttpWeb.LiveHelpers
|
||||
alias FzHttp.Configurations, as: Conf
|
||||
|
||||
alias Phoenix.LiveView.JS
|
||||
|
||||
unquote(view_helpers())
|
||||
@@ -87,7 +82,6 @@ defmodule FzHttpWeb do
|
||||
use Phoenix.LiveComponent
|
||||
use Phoenix.Component, global_prefixes: ~w(x-)
|
||||
import FzHttpWeb.LiveHelpers
|
||||
alias FzHttp.Configurations, as: Conf
|
||||
|
||||
unquote(view_helpers())
|
||||
end
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
defmodule FzHttpWeb.Authentication do
|
||||
defmodule FzHttpWeb.Auth.HTML.Authentication do
|
||||
@moduledoc """
|
||||
Authentication helpers.
|
||||
HTML Authentication implementation module for Guardian.
|
||||
"""
|
||||
use Guardian, otp_app: :fz_http
|
||||
use FzHttpWeb, :controller
|
||||
|
||||
alias FzHttp.Configurations, as: Conf
|
||||
alias FzHttp.Telemetry
|
||||
alias FzHttp.Users
|
||||
alias FzHttp.Users.User
|
||||
|
||||
import FzHttpWeb.OIDC.Helpers
|
||||
|
||||
require Logger
|
||||
|
||||
@guardian_token_name "guardian_default_token"
|
||||
|
||||
@impl Guardian
|
||||
def subject_for_token(resource, _claims) do
|
||||
{:ok, to_string(resource.id)}
|
||||
{:ok, resource.id}
|
||||
end
|
||||
|
||||
@impl Guardian
|
||||
def resource_from_claims(%{"sub" => id}) do
|
||||
case Users.get_user(id) do
|
||||
nil -> {:error, :resource_not_found}
|
||||
@@ -56,34 +59,32 @@ defmodule FzHttpWeb.Authentication do
|
||||
def sign_in(conn, user, auth) do
|
||||
Telemetry.login()
|
||||
Users.update_last_signed_in(user, auth)
|
||||
%{provider: provider} = auth
|
||||
%{provider: provider_id} = auth
|
||||
|
||||
conn =
|
||||
with :identity <- provider,
|
||||
with :identity <- provider_id,
|
||||
true <- FzHttp.MFA.exists?(user) do
|
||||
Plug.Conn.put_session(conn, "mfa_required_at", DateTime.utc_now())
|
||||
else
|
||||
_ -> conn
|
||||
end
|
||||
|> Plug.Conn.put_session("login_method", provider)
|
||||
# XXX: OIDC and SAML provider IDs can be strings, so normalize to string here
|
||||
|> Plug.Conn.put_session("login_method", to_string(provider_id))
|
||||
|
||||
__MODULE__.Plug.sign_in(conn, user)
|
||||
end
|
||||
|
||||
def sign_out(conn) do
|
||||
with {:ok, provider_key} <- parse_provider(Plug.Conn.get_session(conn, "login_method")),
|
||||
{:ok, provider} <- atomize_provider(provider_key),
|
||||
{:ok, client_id} <-
|
||||
parse_client_id(Conf.get!(:parsed_openid_connect_providers)[provider]),
|
||||
{:ok, token} <- parse_token(Plug.Conn.get_session(conn, "id_token")),
|
||||
{:ok, end_session_uri} <-
|
||||
parse_end_session_uri(
|
||||
openid_connect().end_session_uri(provider, %{
|
||||
client_id: client_id,
|
||||
id_token_hint: token,
|
||||
post_logout_redirect_uri: url(~p"/")
|
||||
})
|
||||
) do
|
||||
with provider_id when not is_nil(provider_id) <- Plug.Conn.get_session(conn, "login_method"),
|
||||
provider when not is_nil(provider) <-
|
||||
FzHttp.Configurations.get_provider_by_id(:openid_connect_providers, provider_id),
|
||||
token when not is_nil(token) <- Plug.Conn.get_session(conn, "id_token"),
|
||||
end_session_uri when not is_nil(end_session_uri) <-
|
||||
openid_connect().end_session_uri(provider_id, %{
|
||||
client_id: provider.client_id,
|
||||
id_token_hint: token,
|
||||
post_logout_redirect_uri: url(~p"/")
|
||||
}) do
|
||||
conn
|
||||
|> __MODULE__.Plug.sign_out()
|
||||
|> Plug.Conn.configure_session(drop: true)
|
||||
@@ -97,19 +98,6 @@ defmodule FzHttpWeb.Authentication do
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_provider(nil), do: {:error, "provider not present"}
|
||||
defp parse_provider(p) when is_binary(p), do: {:ok, p}
|
||||
defp parse_provider(p) when is_atom(p), do: {:ok, "#{p}"}
|
||||
|
||||
defp parse_client_id(nil), do: {:error, "client_id missing"}
|
||||
defp parse_client_id(c), do: {:ok, c[:client_id]}
|
||||
|
||||
defp parse_token(nil), do: {:error, "token missing"}
|
||||
defp parse_token(t), do: {:ok, t}
|
||||
|
||||
defp parse_end_session_uri(nil), do: {:error, "end_session_uri missing"}
|
||||
defp parse_end_session_uri(e), do: {:ok, e}
|
||||
|
||||
def get_current_user(%Plug.Conn{} = conn) do
|
||||
__MODULE__.Plug.current_resource(conn)
|
||||
end
|
||||
@@ -1,11 +1,11 @@
|
||||
defmodule FzHttpWeb.Authentication.ErrorHandler do
|
||||
defmodule FzHttpWeb.Auth.HTML.ErrorHandler do
|
||||
@moduledoc """
|
||||
Error Handler module implementation for Guardian.
|
||||
HTML Error Handler module implementation for Guardian.
|
||||
"""
|
||||
|
||||
use FzHttpWeb, :controller
|
||||
alias FzHttpWeb.Authentication
|
||||
import FzHttpWeb.ControllerHelpers, only: [root_path_for_role: 1]
|
||||
alias FzHttpWeb.Auth.HTML.Authentication
|
||||
import FzHttpWeb.ControllerHelpers, only: [root_path_for_user: 1]
|
||||
require Logger
|
||||
|
||||
@behaviour Guardian.Plug.ErrorHandler
|
||||
@@ -15,7 +15,7 @@ defmodule FzHttpWeb.Authentication.ErrorHandler do
|
||||
user = Authentication.get_current_user(conn)
|
||||
|
||||
conn
|
||||
|> redirect(to: root_path_for_role(user.role))
|
||||
|> redirect(to: root_path_for_user(user))
|
||||
end
|
||||
|
||||
@impl Guardian.Plug.ErrorHandler
|
||||
@@ -25,13 +25,8 @@ defmodule FzHttpWeb.Authentication.ErrorHandler do
|
||||
end
|
||||
|
||||
@impl Guardian.Plug.ErrorHandler
|
||||
def auth_error(conn, {type, reason}, opts) do
|
||||
Logger.warn("""
|
||||
ErrorHandler.auth_error: Could not validate user.
|
||||
Type: #{type}
|
||||
Reason: #{reason}
|
||||
Opts: #{opts}
|
||||
""")
|
||||
def auth_error(conn, {type, reason}, _opts) do
|
||||
Logger.info("Web auth error. Type: #{type}. Reason: #{reason}.")
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("text/plain")
|
||||
@@ -1,12 +1,12 @@
|
||||
defmodule FzHttpWeb.Authentication.Pipeline do
|
||||
defmodule FzHttpWeb.Auth.HTML.Pipeline do
|
||||
@moduledoc """
|
||||
Plug implementation module for Guardian.
|
||||
HTML Plug implementation module for Guardian.
|
||||
"""
|
||||
|
||||
use Guardian.Plug.Pipeline,
|
||||
otp_app: :fz_http,
|
||||
error_handler: FzHttpWeb.Authentication.ErrorHandler,
|
||||
module: FzHttpWeb.Authentication
|
||||
error_handler: FzHttpWeb.Auth.HTML.ErrorHandler,
|
||||
module: FzHttpWeb.Auth.HTML.Authentication
|
||||
|
||||
@claims %{"typ" => "access"}
|
||||
|
||||
38
apps/fz_http/lib/fz_http_web/auth/json/authentication.ex
Normal file
38
apps/fz_http/lib/fz_http_web/auth/json/authentication.ex
Normal file
@@ -0,0 +1,38 @@
|
||||
defmodule FzHttpWeb.Auth.JSON.Authentication do
|
||||
@moduledoc """
|
||||
API Authentication implementation module for Guardian.
|
||||
"""
|
||||
use Guardian, otp_app: :fz_http
|
||||
|
||||
alias FzHttp.{
|
||||
ApiTokens.ApiToken,
|
||||
ApiTokens,
|
||||
Users.User,
|
||||
Users
|
||||
}
|
||||
|
||||
@impl Guardian
|
||||
def subject_for_token(user, _claims) do
|
||||
{:ok, user.id}
|
||||
end
|
||||
|
||||
@impl Guardian
|
||||
def resource_from_claims(%{"api" => api_token_id}) do
|
||||
with %ApiTokens.ApiToken{} = api_token <- ApiTokens.get_unexpired_api_token(api_token_id),
|
||||
%Users.User{} = user <- Users.get_user(api_token.user_id) do
|
||||
{:ok, user}
|
||||
else
|
||||
_ ->
|
||||
{:error, :resource_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def fz_encode_and_sign(%ApiToken{} = api_token, %User{} = user) do
|
||||
claims = %{
|
||||
"api" => api_token.id,
|
||||
"exp" => DateTime.to_unix(api_token.expires_at)
|
||||
}
|
||||
|
||||
Guardian.encode_and_sign(__MODULE__, user, claims)
|
||||
end
|
||||
end
|
||||
18
apps/fz_http/lib/fz_http_web/auth/json/error_handler.ex
Normal file
18
apps/fz_http/lib/fz_http_web/auth/json/error_handler.ex
Normal file
@@ -0,0 +1,18 @@
|
||||
defmodule FzHttpWeb.Auth.JSON.ErrorHandler do
|
||||
@moduledoc """
|
||||
API Error Handler module implementation for Guardian.
|
||||
"""
|
||||
use FzHttpWeb, :controller
|
||||
require Logger
|
||||
|
||||
@behaviour Guardian.Plug.ErrorHandler
|
||||
|
||||
@impl Guardian.Plug.ErrorHandler
|
||||
def auth_error(conn, {type, reason}, _opts) do
|
||||
Logger.info("API auth error. Type: #{type}. Reason: #{reason}.")
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(401, to_string(type))
|
||||
end
|
||||
end
|
||||
17
apps/fz_http/lib/fz_http_web/auth/json/pipeline.ex
Normal file
17
apps/fz_http/lib/fz_http_web/auth/json/pipeline.ex
Normal file
@@ -0,0 +1,17 @@
|
||||
defmodule FzHttpWeb.Auth.JSON.Pipeline do
|
||||
@moduledoc """
|
||||
API Plug implementation module for Guardian.
|
||||
"""
|
||||
|
||||
use Guardian.Plug.Pipeline,
|
||||
otp_app: :fz_http,
|
||||
error_handler: FzHttpWeb.Auth.JSON.ErrorHandler,
|
||||
module: FzHttpWeb.Auth.JSON.Authentication
|
||||
|
||||
# 90 days
|
||||
@max_age 60 * 60 * 24 * 90
|
||||
|
||||
plug Guardian.Plug.VerifyHeader, max_age: @max_age
|
||||
plug Guardian.Plug.EnsureAuthenticated
|
||||
plug Guardian.Plug.LoadResource
|
||||
end
|
||||
@@ -52,7 +52,7 @@ defmodule FzHttpWeb.NotificationChannel do
|
||||
end
|
||||
|
||||
defp presence_list(socket) do
|
||||
ids_to_show = [Integer.to_string(socket.assigns.current_user.id)]
|
||||
ids_to_show = [socket.assigns.current_user.id]
|
||||
|
||||
Presence.list(socket)
|
||||
|> Map.take(ids_to_show)
|
||||
|
||||
@@ -4,11 +4,13 @@ defmodule FzHttpWeb.ControllerHelpers do
|
||||
"""
|
||||
use FzHttpWeb, :helper
|
||||
|
||||
def root_path_for_role(:admin) do
|
||||
alias FzHttp.Users.User
|
||||
|
||||
def root_path_for_user(%User{role: :admin}) do
|
||||
~p"/users"
|
||||
end
|
||||
|
||||
def root_path_for_role(:unprivileged) do
|
||||
def root_path_for_user(%User{role: :unprivileged}) do
|
||||
~p"/user_devices"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,7 +8,7 @@ defmodule FzHttpWeb.AuthController do
|
||||
@local_auth_providers [:identity, :magic_link]
|
||||
|
||||
alias FzHttp.Users
|
||||
alias FzHttpWeb.Authentication
|
||||
alias FzHttpWeb.Auth.HTML.Authentication
|
||||
alias FzHttpWeb.OAuth.PKCE
|
||||
alias FzHttpWeb.OIDC.State
|
||||
alias FzHttpWeb.UserFromAuth
|
||||
@@ -21,8 +21,6 @@ defmodule FzHttpWeb.AuthController do
|
||||
plug Ueberauth
|
||||
|
||||
def request(conn, _params) do
|
||||
# XXX: Helpers.callback_url/1 generates the wrong URL behind nginx.
|
||||
# This is a bug in Ueberauth. auth_path is used instead.
|
||||
path = ~p"/auth/identity/callback"
|
||||
|
||||
conn
|
||||
@@ -30,9 +28,7 @@ defmodule FzHttpWeb.AuthController do
|
||||
end
|
||||
|
||||
def callback(%{assigns: %{ueberauth_failure: %{errors: errors}}} = conn, _params) do
|
||||
msg =
|
||||
errors
|
||||
|> Enum.map_join(". ", fn error -> error.message end)
|
||||
msg = Enum.map_join(errors, ". ", fn error -> error.message end)
|
||||
|
||||
conn
|
||||
|> put_flash(:error, msg)
|
||||
@@ -61,24 +57,24 @@ defmodule FzHttpWeb.AuthController do
|
||||
end
|
||||
end
|
||||
|
||||
def callback(conn, %{"provider" => provider_key, "state" => state} = params) do
|
||||
def callback(conn, %{"provider" => provider_id, "state" => state} = params)
|
||||
when is_binary(provider_id) do
|
||||
token_params = Map.merge(params, PKCE.token_params(conn))
|
||||
|
||||
with {:ok, provider} <- atomize_provider(provider_key),
|
||||
:ok <- State.verify_state(conn, state),
|
||||
{:ok, tokens} <- openid_connect().fetch_tokens(provider, token_params),
|
||||
{:ok, claims} <- openid_connect().verify(provider, tokens["id_token"]) do
|
||||
case UserFromAuth.find_or_create(provider_key, claims) do
|
||||
with :ok <- State.verify_state(conn, state),
|
||||
{:ok, tokens} <- openid_connect().fetch_tokens(provider_id, token_params),
|
||||
{:ok, claims} <- openid_connect().verify(provider_id, tokens["id_token"]) do
|
||||
case UserFromAuth.find_or_create(provider_id, claims) do
|
||||
{:ok, user} ->
|
||||
# only first-time connect will include refresh token
|
||||
# XXX: Remove this when SCIM 2.0 is implemented
|
||||
with %{"refresh_token" => refresh_token} <- tokens do
|
||||
FzHttp.OIDC.create_connection(user.id, provider_key, refresh_token)
|
||||
FzHttp.OIDC.create_connection(user.id, provider_id, refresh_token)
|
||||
end
|
||||
|
||||
conn
|
||||
|> put_session("id_token", tokens["id_token"])
|
||||
|> maybe_sign_in(user, %{provider: provider})
|
||||
|> maybe_sign_in(user, %{provider: provider_id})
|
||||
|
||||
{:error, reason} ->
|
||||
conn
|
||||
@@ -104,9 +100,16 @@ defmodule FzHttpWeb.AuthController do
|
||||
end
|
||||
end
|
||||
|
||||
def delete(conn, _params) do
|
||||
# This can be called if the user attempts to visit one of the callback redirect URLs
|
||||
# directly.
|
||||
def callback(conn, params) do
|
||||
conn
|
||||
|> Authentication.sign_out()
|
||||
|> put_flash(:error, inspect(params) <> inspect(conn.assigns))
|
||||
|> redirect(to: ~p"/")
|
||||
end
|
||||
|
||||
def delete(conn, _params) do
|
||||
Authentication.sign_out(conn)
|
||||
end
|
||||
|
||||
def reset_password(conn, _params) do
|
||||
@@ -139,7 +142,7 @@ defmodule FzHttpWeb.AuthController do
|
||||
end
|
||||
end
|
||||
|
||||
def redirect_oidc_auth_uri(conn, %{"provider" => provider_key}) do
|
||||
def redirect_oidc_auth_uri(conn, %{"provider" => provider_id}) when is_binary(provider_id) do
|
||||
verifier = PKCE.code_verifier()
|
||||
|
||||
params = %{
|
||||
@@ -149,27 +152,17 @@ defmodule FzHttpWeb.AuthController do
|
||||
code_challenge: PKCE.code_challenge(verifier)
|
||||
}
|
||||
|
||||
with {:ok, provider} <- atomize_provider(provider_key),
|
||||
uri <- openid_connect().authorization_uri(provider, params) do
|
||||
conn
|
||||
|> PKCE.put_cookie(verifier)
|
||||
|> State.put_cookie(params.state)
|
||||
|> redirect(external: uri)
|
||||
else
|
||||
_ ->
|
||||
msg = "OpenIDConnect error: provider #{provider_key} not found in config"
|
||||
Logger.warn(msg)
|
||||
uri = openid_connect().authorization_uri(provider_id, params)
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("text/plain")
|
||||
|> send_resp(400, "OIDC Error. Check logs.")
|
||||
|> halt()
|
||||
end
|
||||
conn
|
||||
|> PKCE.put_cookie(verifier)
|
||||
|> State.put_cookie(params.state)
|
||||
|> redirect(external: uri)
|
||||
end
|
||||
|
||||
defp maybe_sign_in(conn, user, %{provider: provider} = auth)
|
||||
when provider in @local_auth_providers do
|
||||
if Conf.get!(:local_auth_enabled) do
|
||||
defp maybe_sign_in(conn, user, %{provider: provider_key} = auth)
|
||||
when is_atom(provider_key) and provider_key in @local_auth_providers do
|
||||
if FzHttp.Configurations.get!(:local_auth_enabled) do
|
||||
do_sign_in(conn, user, auth)
|
||||
else
|
||||
conn
|
||||
@@ -186,6 +179,6 @@ defmodule FzHttpWeb.AuthController do
|
||||
|> Authentication.sign_in(user, auth)
|
||||
|> configure_session(renew: true)
|
||||
|> put_session(:live_socket_id, "users_socket:#{user.id}")
|
||||
|> redirect(to: root_path_for_role(user.role))
|
||||
|> redirect(to: root_path_for_user(user))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
defmodule FzHttpWeb.JSON.ConfigurationController do
|
||||
use FzHttpWeb, :controller
|
||||
|
||||
alias FzHttp.{Configurations.Configuration, Configurations}
|
||||
|
||||
action_fallback FzHttpWeb.JSON.FallbackController
|
||||
|
||||
def show(conn, _params) do
|
||||
configuration = Configurations.get_configuration!()
|
||||
render(conn, "show.json", configuration: configuration)
|
||||
end
|
||||
|
||||
def update(conn, %{"configuration" => params}) do
|
||||
configuration = Configurations.get_configuration!()
|
||||
|
||||
with {:ok, %Configuration{} = configuration} <-
|
||||
Configurations.update_configuration(configuration, params) do
|
||||
render(conn, "show.json", configuration: configuration)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
defmodule FzHttpWeb.JSON.DeviceController do
|
||||
@moduledoc """
|
||||
REST API Controller for Devices.
|
||||
"""
|
||||
|
||||
use FzHttpWeb, :controller
|
||||
|
||||
action_fallback FzHttpWeb.JSON.FallbackController
|
||||
|
||||
alias FzHttp.Devices
|
||||
|
||||
def index(conn, _params) do
|
||||
devices = Devices.list_devices()
|
||||
render(conn, "index.json", devices: devices)
|
||||
end
|
||||
|
||||
def create(conn, %{"device" => device_params}) do
|
||||
with {:ok, device} <- Devices.create_device(device_params) do
|
||||
conn
|
||||
|> put_status(:created)
|
||||
|> put_resp_header("location", ~p"/v0/devices/#{device}")
|
||||
|> render("show.json", device: device)
|
||||
end
|
||||
end
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
device = Devices.get_device!(id)
|
||||
render(conn, "show.json", device: device)
|
||||
end
|
||||
|
||||
def update(conn, %{"id" => id, "device" => device_params}) do
|
||||
device = Devices.get_device!(id)
|
||||
|
||||
with {:ok, device} <- Devices.update_device(device, device_params) do
|
||||
render(conn, "show.json", device: device)
|
||||
end
|
||||
end
|
||||
|
||||
def delete(conn, %{"id" => id}) do
|
||||
device = Devices.get_device!(id)
|
||||
|
||||
with {:ok, _device} <- Devices.delete_device(device) do
|
||||
send_resp(conn, :no_content, "")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
defmodule FzHttpWeb.JSON.FallbackController do
|
||||
@moduledoc """
|
||||
Translates controller action results into valid `Plug.Conn` responses.
|
||||
|
||||
See `Phoenix.Controller.action_fallback/1` for more details.
|
||||
"""
|
||||
use FzHttpWeb, :controller
|
||||
|
||||
# This clause is an example of how to handle resources that cannot be found.
|
||||
def call(conn, {:error, :not_found}) do
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> put_view(FzHttpWeb.ErrorView)
|
||||
|> render("404.json")
|
||||
end
|
||||
|
||||
def call(conn, {:error, :internal_server_error}) do
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> put_view(FzHttpWeb.ErrorView)
|
||||
|> render("500.json")
|
||||
end
|
||||
|
||||
def call(conn, {:error, %Ecto.Changeset{valid?: false} = changeset}) do
|
||||
conn
|
||||
|> put_status(422)
|
||||
|> put_view(FzHttpWeb.JSON.ChangesetView)
|
||||
|> render("error.json", changeset: changeset)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
defmodule FzHttpWeb.JSON.RuleController do
|
||||
@moduledoc """
|
||||
REST API Controller for Rules.
|
||||
"""
|
||||
|
||||
use FzHttpWeb, :controller
|
||||
|
||||
action_fallback FzHttpWeb.JSON.FallbackController
|
||||
|
||||
alias FzHttp.Rules
|
||||
|
||||
def index(conn, _params) do
|
||||
# XXX: Add user-scoped rules
|
||||
rules = Rules.list_rules()
|
||||
render(conn, "index.json", rules: rules)
|
||||
end
|
||||
|
||||
def create(conn, %{"rule" => rule_params}) do
|
||||
with {:ok, rule} <- Rules.create_rule(rule_params) do
|
||||
conn
|
||||
|> put_status(:created)
|
||||
|> put_resp_header("location", ~p"/v0/rules/#{rule}")
|
||||
|> render("show.json", rule: rule)
|
||||
end
|
||||
end
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
rule = Rules.get_rule!(id)
|
||||
render(conn, "show.json", rule: rule)
|
||||
end
|
||||
|
||||
def update(conn, %{"id" => id, "rule" => rule_params}) do
|
||||
rule = Rules.get_rule!(id)
|
||||
|
||||
with {:ok, rule} <- Rules.update_rule(rule, rule_params) do
|
||||
render(conn, "show.json", rule: rule)
|
||||
end
|
||||
end
|
||||
|
||||
def delete(conn, %{"id" => id}) do
|
||||
rule = Rules.get_rule!(id)
|
||||
|
||||
with {:ok, _rule} <- Rules.delete_rule(rule) do
|
||||
send_resp(conn, :no_content, "")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,51 @@
|
||||
defmodule FzHttpWeb.JSON.UserController do
|
||||
use FzHttpWeb, :controller
|
||||
|
||||
alias FzHttp.Users
|
||||
alias FzHttp.Users.User
|
||||
|
||||
action_fallback FzHttpWeb.JSON.FallbackController
|
||||
|
||||
def index(conn, _params) do
|
||||
users = Users.list_users()
|
||||
render(conn, "index.json", users: users)
|
||||
end
|
||||
|
||||
def create(conn, %{"user" => user_params}) do
|
||||
with {:ok, %User{} = user} <- Users.create_user(user_params) do
|
||||
conn
|
||||
|> put_status(:created)
|
||||
|> put_resp_header("location", ~p"/v0/users/#{user}")
|
||||
|> render("show.json", user: user)
|
||||
end
|
||||
end
|
||||
|
||||
def show(conn, %{"id" => id_or_email}) do
|
||||
user = get_user_by_id_or_email(id_or_email)
|
||||
render(conn, "show.json", user: user)
|
||||
end
|
||||
|
||||
def update(conn, %{"id" => id, "user" => user_params}) do
|
||||
user = Users.get_user!(id)
|
||||
|
||||
with {:ok, %User{} = user} <- Users.admin_update_user(user, user_params) do
|
||||
render(conn, "show.json", user: user)
|
||||
end
|
||||
end
|
||||
|
||||
def delete(conn, %{"id" => id}) do
|
||||
user = Users.get_user!(id)
|
||||
|
||||
with {:ok, %User{}} <- Users.delete_user(user) do
|
||||
send_resp(conn, :no_content, "")
|
||||
end
|
||||
end
|
||||
|
||||
defp get_user_by_id_or_email(id_or_email) do
|
||||
if String.contains?(id_or_email, "@") do
|
||||
Users.get_by_email!(id_or_email)
|
||||
else
|
||||
Users.get_user!(id_or_email)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,15 +4,13 @@ defmodule FzHttpWeb.RootController do
|
||||
"""
|
||||
use FzHttpWeb, :controller
|
||||
|
||||
alias FzHttp.Configurations, as: Conf
|
||||
|
||||
def index(conn, _params) do
|
||||
conn
|
||||
|> render(
|
||||
"auth.html",
|
||||
local_enabled: Conf.get!(:local_auth_enabled),
|
||||
openid_connect_providers: Conf.get!(:parsed_openid_connect_providers),
|
||||
saml_identity_providers: Conf.get!(:saml_identity_providers)
|
||||
local_enabled: FzHttp.Configurations.get!(:local_auth_enabled),
|
||||
openid_connect_providers: FzHttp.Configurations.get!(:openid_connect_providers),
|
||||
saml_identity_providers: FzHttp.Configurations.get!(:saml_identity_providers)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,7 +4,7 @@ defmodule FzHttpWeb.UserController do
|
||||
"""
|
||||
|
||||
alias FzHttp.Users
|
||||
alias FzHttpWeb.Authentication
|
||||
alias FzHttpWeb.Auth.HTML.Authentication
|
||||
use FzHttpWeb, :controller
|
||||
|
||||
require Logger
|
||||
|
||||
@@ -5,9 +5,9 @@ defmodule FzHttpWeb.HeaderHelpers do
|
||||
|
||||
@remote_ip_headers ["x-forwarded-for"]
|
||||
|
||||
def external_trusted_proxies, do: conf(:external_trusted_proxies)
|
||||
def external_trusted_proxies, do: FzHttp.Config.fetch_env!(:fz_http, :external_trusted_proxies)
|
||||
|
||||
def clients, do: conf(:private_clients)
|
||||
def clients, do: FzHttp.Config.fetch_env!(:fz_http, :private_clients)
|
||||
|
||||
def proxied?, do: not (external_trusted_proxies() == false)
|
||||
|
||||
@@ -18,8 +18,4 @@ defmodule FzHttpWeb.HeaderHelpers do
|
||||
clients: clients()
|
||||
]
|
||||
end
|
||||
|
||||
defp conf(key) when is_atom(key) do
|
||||
Application.fetch_env!(:fz_http, key)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -51,7 +51,7 @@ defmodule FzHttpWeb.DeviceLive.Admin.Show do
|
||||
allowed_ips: Devices.allowed_ips(device),
|
||||
dns: Devices.dns(device),
|
||||
endpoint: Devices.endpoint(device),
|
||||
port: Application.fetch_env!(:fz_vpn, :wireguard_port),
|
||||
port: FzHttp.Config.fetch_env!(:fz_vpn, :wireguard_port),
|
||||
mtu: Devices.mtu(device),
|
||||
persistent_keepalive: Devices.persistent_keepalive(device),
|
||||
config: Devices.as_config(device)
|
||||
|
||||
@@ -4,9 +4,7 @@ defmodule FzHttpWeb.DeviceLive.NewFormComponent do
|
||||
"""
|
||||
use FzHttpWeb, :live_component
|
||||
|
||||
alias FzHttp.Configurations, as: Conf
|
||||
alias FzHttp.Devices
|
||||
alias FzHttp.Sites
|
||||
alias FzHttpWeb.ErrorHelpers
|
||||
|
||||
@impl Phoenix.LiveComponent
|
||||
@@ -17,6 +15,13 @@ defmodule FzHttpWeb.DeviceLive.NewFormComponent do
|
||||
|> assign(:config, nil)}
|
||||
end
|
||||
|
||||
@default_fields ~w(
|
||||
default_client_mtu
|
||||
default_client_endpoint
|
||||
default_client_persistent_keepalive
|
||||
default_client_dns
|
||||
default_client_allowed_ips
|
||||
)a
|
||||
@impl Phoenix.LiveComponent
|
||||
def update(assigns, socket) do
|
||||
changeset = new_changeset(socket)
|
||||
@@ -25,9 +30,7 @@ defmodule FzHttpWeb.DeviceLive.NewFormComponent do
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign(:changeset, changeset)
|
||||
|> assign(
|
||||
Map.take(Sites.get_site!(), [:mtu, :endpoint, :persistent_keepalive, :dns, :allowed_ips])
|
||||
)
|
||||
|> assign(Map.take(FzHttp.Configurations.get_configuration!(), @default_fields))
|
||||
|> assign(Devices.defaults(changeset))}
|
||||
end
|
||||
|
||||
@@ -78,8 +81,8 @@ defmodule FzHttpWeb.DeviceLive.NewFormComponent do
|
||||
|
||||
defp authorized_to_create?(socket) do
|
||||
has_role?(socket, :admin) ||
|
||||
(Conf.get!(:allow_unprivileged_device_management) &&
|
||||
to_string(socket.assigns.current_user.id) == to_string(socket.assigns.target_user_id))
|
||||
(FzHttp.Configurations.get!(:allow_unprivileged_device_management) &&
|
||||
socket.assigns.current_user.id == socket.assigns.target_user_id)
|
||||
end
|
||||
|
||||
# update/2 is called twice: on load and then connect.
|
||||
@@ -87,9 +90,10 @@ defmodule FzHttpWeb.DeviceLive.NewFormComponent do
|
||||
# XXX: Clean this up using assign_new/3
|
||||
defp new_changeset(socket) do
|
||||
if connected?(socket) do
|
||||
Devices.new_device()
|
||||
%{name: FzHttp.Devices.new_name()}
|
||||
else
|
||||
Devices.new_device(%{"name" => nil})
|
||||
%{}
|
||||
end
|
||||
|> Devices.new_device()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -95,17 +95,17 @@
|
||||
|
||||
<%= if FzHttpWeb.DeviceView.can_configure_devices?(@current_user) do %>
|
||||
<div class="field">
|
||||
<%= label(f, :use_site_allowed_ips, "Use Default Allowed IPs", class: "label") %>
|
||||
<%= label(f, :use_default_allowed_ips, "Use Default Allowed IPs", class: "label") %>
|
||||
<div class="control">
|
||||
<label class="radio">
|
||||
<%= radio_button(f, :use_site_allowed_ips, true) %> Yes
|
||||
<%= radio_button(f, :use_default_allowed_ips, true) %> Yes
|
||||
</label>
|
||||
<label class="radio">
|
||||
<%= radio_button(f, :use_site_allowed_ips, false) %> No
|
||||
<%= radio_button(f, :use_default_allowed_ips, false) %> No
|
||||
</label>
|
||||
</div>
|
||||
<p class="help">
|
||||
Default: <%= @allowed_ips %>
|
||||
Default: <%= @default_client_allowed_ips %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
<div class="control">
|
||||
<%= textarea(f, :allowed_ips,
|
||||
class: "textarea #{input_error_class(f, :allowed_ips)}",
|
||||
disabled: @use_site_allowed_ips
|
||||
disabled: @use_default_allowed_ips
|
||||
) %>
|
||||
</div>
|
||||
<p class="help is-danger">
|
||||
@@ -123,17 +123,17 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= label(f, :use_site_dns, "Use Default DNS Servers", class: "label") %>
|
||||
<%= label(f, :use_default_dns, "Use Default DNS Servers", class: "label") %>
|
||||
<div class="control">
|
||||
<label class="radio">
|
||||
<%= radio_button(f, :use_site_dns, true) %> Yes
|
||||
<%= radio_button(f, :use_default_dns, true) %> Yes
|
||||
</label>
|
||||
<label class="radio">
|
||||
<%= radio_button(f, :use_site_dns, false) %> No
|
||||
<%= radio_button(f, :use_default_dns, false) %> No
|
||||
</label>
|
||||
</div>
|
||||
<p class="help">
|
||||
Default: <%= @dns %>
|
||||
Default: <%= @default_client_dns %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
<div class="control">
|
||||
<%= text_input(f, :dns,
|
||||
class: "input #{input_error_class(f, :dns)}",
|
||||
disabled: @use_site_dns
|
||||
disabled: @use_default_dns
|
||||
) %>
|
||||
</div>
|
||||
<p class="help is-danger">
|
||||
@@ -151,17 +151,17 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= label(f, :use_site_endpoint, "Use Default Endpoint", class: "label") %>
|
||||
<%= label(f, :use_default_endpoint, "Use Default Endpoint", class: "label") %>
|
||||
<div class="control">
|
||||
<label class="radio">
|
||||
<%= radio_button(f, :use_site_endpoint, true) %> Yes
|
||||
<%= radio_button(f, :use_default_endpoint, true) %> Yes
|
||||
</label>
|
||||
<label class="radio">
|
||||
<%= radio_button(f, :use_site_endpoint, false) %> No
|
||||
<%= radio_button(f, :use_default_endpoint, false) %> No
|
||||
</label>
|
||||
</div>
|
||||
<p class="help">
|
||||
Default: <%= @endpoint %>
|
||||
Default: <%= @default_client_endpoint %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
<div class="control">
|
||||
<%= text_input(f, :endpoint,
|
||||
class: "input #{input_error_class(f, :endpoint)}",
|
||||
disabled: @use_site_endpoint
|
||||
disabled: @use_default_endpoint
|
||||
) %>
|
||||
</div>
|
||||
<p class="help is-danger">
|
||||
@@ -180,17 +180,17 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= label(f, :use_site_mtu, "Use Default MTU", class: "label") %>
|
||||
<%= label(f, :use_default_mtu, "Use Default MTU", class: "label") %>
|
||||
<div class="control">
|
||||
<label class="radio">
|
||||
<%= radio_button(f, :use_site_mtu, true) %> Yes
|
||||
<%= radio_button(f, :use_default_mtu, true) %> Yes
|
||||
</label>
|
||||
<label class="radio">
|
||||
<%= radio_button(f, :use_site_mtu, false) %> No
|
||||
<%= radio_button(f, :use_default_mtu, false) %> No
|
||||
</label>
|
||||
</div>
|
||||
<p class="help">
|
||||
Default: <%= @mtu %>
|
||||
Default: <%= @default_client_mtu %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -200,7 +200,7 @@
|
||||
<div class="control">
|
||||
<%= text_input(f, :mtu,
|
||||
class: "input #{input_error_class(f, :mtu)}",
|
||||
disabled: @use_site_mtu
|
||||
disabled: @use_default_mtu
|
||||
) %>
|
||||
</div>
|
||||
<p class="help is-danger">
|
||||
@@ -209,19 +209,19 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= label(f, :use_site_persistent_keepalive, "Use Default Persistent Keepalive",
|
||||
<%= label(f, :use_default_persistent_keepalive, "Use Default Persistent Keepalive",
|
||||
class: "label"
|
||||
) %>
|
||||
<div class="control">
|
||||
<label class="radio">
|
||||
<%= radio_button(f, :use_site_persistent_keepalive, true) %> Yes
|
||||
<%= radio_button(f, :use_default_persistent_keepalive, true) %> Yes
|
||||
</label>
|
||||
<label class="radio">
|
||||
<%= radio_button(f, :use_site_persistent_keepalive, false) %> No
|
||||
<%= radio_button(f, :use_default_persistent_keepalive, false) %> No
|
||||
</label>
|
||||
</div>
|
||||
<p class="help">
|
||||
Default: <%= @persistent_keepalive %>
|
||||
Default: <%= @default_client_persistent_keepalive %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -235,7 +235,7 @@
|
||||
<div class="control">
|
||||
<%= text_input(f, :persistent_keepalive,
|
||||
class: "input #{input_error_class(f, :persistent_keepalive)}",
|
||||
disabled: @use_site_persistent_keepalive
|
||||
disabled: @use_default_persistent_keepalive
|
||||
) %>
|
||||
</div>
|
||||
<p class="help is-danger">
|
||||
|
||||
@@ -29,12 +29,13 @@
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<%= if length(@devices) > 0 do %>
|
||||
<%= unless Enum.empty?(@devices) do %>
|
||||
<table class="table is-bordered is-hoverable is-striped is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Assigned Device IP</th>
|
||||
<th>Tunnel IPv4</th>
|
||||
<th>Tunnel IPv6</th>
|
||||
<th>Public key</th>
|
||||
<th>Created</th>
|
||||
</tr>
|
||||
@@ -47,10 +48,8 @@
|
||||
<%= device.name %>
|
||||
</.link>
|
||||
</td>
|
||||
<td class="code">
|
||||
<span><%= device.ipv4 %></span>
|
||||
<span><%= device.ipv6 %></span>
|
||||
</td>
|
||||
<td class="code"><%= device.ipv4 %></td>
|
||||
<td class="code"><%= device.ipv6 %></td>
|
||||
<td class="code"><%= device.public_key %></td>
|
||||
<td
|
||||
id={"device-#{device.id}-inserted-at"}
|
||||
|
||||
@@ -3,7 +3,7 @@ defmodule FzHttpWeb.DeviceLive.Unprivileged.Show do
|
||||
Shows a device for an unprivileged user.
|
||||
"""
|
||||
use FzHttpWeb, :live_view
|
||||
alias FzHttp.Configurations, as: Conf
|
||||
|
||||
alias FzHttp.Devices
|
||||
alias FzHttp.Users
|
||||
|
||||
@@ -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) ||
|
||||
Conf.get!(:allow_unprivileged_device_management)) do
|
||||
FzHttp.Configurations.get!(:allow_unprivileged_device_management)) do
|
||||
Devices.delete_device(device)
|
||||
else
|
||||
{:not_authorized}
|
||||
@@ -56,7 +56,7 @@ defmodule FzHttpWeb.DeviceLive.Unprivileged.Show do
|
||||
user: Users.get_user!(device.user_id),
|
||||
page_title: device.name,
|
||||
allowed_ips: Devices.allowed_ips(device),
|
||||
port: Application.fetch_env!(:fz_vpn, :wireguard_port),
|
||||
port: FzHttp.Config.fetch_env!(:fz_vpn, :wireguard_port),
|
||||
dns: Devices.dns(device),
|
||||
endpoint: Devices.endpoint(device),
|
||||
mtu: Devices.mtu(device),
|
||||
|
||||
@@ -4,7 +4,7 @@ defmodule FzHttpWeb.LiveAuth do
|
||||
"""
|
||||
|
||||
use Phoenix.Component
|
||||
alias FzHttpWeb.Authentication
|
||||
alias FzHttpWeb.Auth.HTML.Authentication
|
||||
import FzHttpWeb.AuthorizationHelpers
|
||||
|
||||
require Logger
|
||||
@@ -24,7 +24,7 @@ defmodule FzHttpWeb.LiveAuth do
|
||||
%{role: :unprivileged} = user,
|
||||
%{assigns: %{live_action: :new}, view: FzHttpWeb.DeviceLive.Unprivileged.Index} = socket
|
||||
) do
|
||||
if Application.fetch_env!(:fz_http, :allow_unprivileged_device_management) do
|
||||
if FzHttp.Configurations.get!(:allow_unprivileged_device_management) do
|
||||
{:cont, assign_new(socket, :current_user, fn -> user end)}
|
||||
else
|
||||
{:halt, not_authorized(socket)}
|
||||
|
||||
@@ -117,7 +117,7 @@ defmodule FzHttpWeb.MFALive.Auth do
|
||||
{:ok, _method} ->
|
||||
{:noreply,
|
||||
push_redirect(socket,
|
||||
to: root_path_for_role(socket.assigns.current_user.role)
|
||||
to: root_path_for_user(socket.assigns.current_user)
|
||||
)}
|
||||
|
||||
{:error, changeset} ->
|
||||
|
||||
@@ -32,7 +32,7 @@ defmodule FzHttpWeb.ModalComponent do
|
||||
</div>
|
||||
</section>
|
||||
<footer class="modal-card-foot is-justify-content-flex-end">
|
||||
<%= if !assigns[:hide_footer_content] do %>
|
||||
<%= if !(assigns[:hide_footer_content] || @opts[:hide_footer_content]) do %>
|
||||
<%= Phoenix.View.render(FzHttpWeb.SharedView, "submit_button.html",
|
||||
button_text: @opts[:button_text],
|
||||
form: @opts[:form]
|
||||
@@ -48,14 +48,4 @@ defmodule FzHttpWeb.ModalComponent do
|
||||
def handle_event("close", _, socket) do
|
||||
{:noreply, push_patch(socket, to: socket.assigns.return_to)}
|
||||
end
|
||||
|
||||
@impl Phoenix.LiveComponent
|
||||
@doc """
|
||||
XXX: This is needed due to a bug on pages with dropdowns.
|
||||
Basically this modal receives the phx-click-away event and the
|
||||
server crashes if this is not implemented.
|
||||
"""
|
||||
def handle_event("close_dropdown", _params, socket) do
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,6 +10,30 @@
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= if @live_action == :new_api_token do %>
|
||||
<%= live_modal(
|
||||
FzHttpWeb.SettingLive.NewApiTokenComponent,
|
||||
return_to: ~p"/settings/account",
|
||||
title: "Add API Token",
|
||||
id: "new_api_token",
|
||||
form: "api-token-form",
|
||||
user: @current_user,
|
||||
changeset: FzHttp.ApiTokens.new_api_token()
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= if @live_action == :show_api_token do %>
|
||||
<%= live_modal(
|
||||
FzHttpWeb.SettingLive.ShowApiTokenComponent,
|
||||
return_to: ~p"/settings/account",
|
||||
title: "API Token #{@api_token_id}",
|
||||
id: "show_api_token",
|
||||
hide_footer_content: true,
|
||||
user: @current_user,
|
||||
api_token: @api_token
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= if @live_action == :register_mfa do %>
|
||||
<.live_component
|
||||
module={FzHttpWeb.MFA.RegisterComponent}
|
||||
@@ -96,6 +120,96 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section is-main-section">
|
||||
<h4 class="title is-4">
|
||||
API Tokens
|
||||
</h4>
|
||||
|
||||
<div class="block">
|
||||
<p>
|
||||
Manage API tokens.
|
||||
<a href="https://docs.firezone.dev/reference/rest-api/?utm_source=product">
|
||||
Read more about API tokens ->
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<%= if Enum.any?(@api_tokens) do %>
|
||||
<table class="table is-bordered is-hoverable is-striped is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Created at</th>
|
||||
<th>Identifier</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for api_token <- @api_tokens do %>
|
||||
<tr>
|
||||
<td
|
||||
data-timestamp={api_token.inserted_at}
|
||||
phx-hook="FormatTimestamp"
|
||||
id={"inserted_at-#{api_token.id}"}
|
||||
>
|
||||
…
|
||||
</td>
|
||||
<td>
|
||||
<.link patch={~p"/settings/account/api_token/#{api_token}"}>
|
||||
<%= api_token.id %>
|
||||
</.link>
|
||||
</td>
|
||||
<td>
|
||||
<%= if ApiTokens.api_token_expired?(api_token) do %>
|
||||
<span class="icon has-text-danger is-small">
|
||||
<i class="mdi mdi-close-circle"></i>
|
||||
</span>
|
||||
Expired at
|
||||
<% else %>
|
||||
<span class="icon has-text-success is-small">
|
||||
<i class="mdi mdi-check-circle"></i>
|
||||
</span>
|
||||
Expires at
|
||||
<% end %>
|
||||
<span
|
||||
data-timestamp={api_token.expires_at}
|
||||
phx-hook="FormatTimestamp"
|
||||
id={"expired-at-#{api_token.id}"}
|
||||
>
|
||||
…
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<.link
|
||||
data-confirm="Are you sure?"
|
||||
phx-click="delete_api_token"
|
||||
phx-value-id={api_token.id}
|
||||
>
|
||||
Delete
|
||||
</.link>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p>
|
||||
No API tokens.
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= if length(@api_tokens) < FzHttp.ApiTokens.ApiToken.max_per_user() do %>
|
||||
<.link patch={~p"/settings/account/api_token"} class="button">
|
||||
<span class="icon is-small">
|
||||
<i class="mdi mdi-plus"></i>
|
||||
</span>
|
||||
<span>Add API Token</span>
|
||||
</.link>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<section class="section is-main-section">
|
||||
<h4 class="title is-4">
|
||||
Multi Factor Authentication
|
||||
|
||||
@@ -4,20 +4,31 @@ defmodule FzHttpWeb.SettingLive.Account do
|
||||
"""
|
||||
use FzHttpWeb, :live_view
|
||||
|
||||
alias FzHttp.{MFA, Users}
|
||||
alias FzHttpWeb.{Endpoint, Presence}
|
||||
alias FzHttp.{
|
||||
ApiTokens,
|
||||
MFA,
|
||||
Users
|
||||
}
|
||||
|
||||
alias FzHttpWeb.{
|
||||
Endpoint,
|
||||
Presence
|
||||
}
|
||||
|
||||
@live_sessions_topic "notification:session"
|
||||
@page_title "Account Settings"
|
||||
@page_subtitle "Configure settings related to your Firezone web portal account."
|
||||
|
||||
@impl Phoenix.LiveView
|
||||
def mount(_params, _session, socket) do
|
||||
def mount(params, _session, socket) do
|
||||
Endpoint.subscribe(@live_sessions_topic)
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:api_token_id, params["api_token_id"])
|
||||
|> assign(:subscribe_link, subscribe_link())
|
||||
|> assign(:allow_delete, length(Users.list_admins()) > 1)
|
||||
|> assign(:api_tokens, ApiTokens.list_api_tokens(socket.assigns.current_user.id))
|
||||
|> assign(:changeset, Users.change_user(socket.assigns.current_user))
|
||||
|> assign(:methods, MFA.list_methods(socket.assigns.current_user))
|
||||
|> assign(:page_title, @page_title)
|
||||
@@ -29,15 +40,42 @@ defmodule FzHttpWeb.SettingLive.Account do
|
||||
)}
|
||||
end
|
||||
|
||||
@impl Phoenix.LiveView
|
||||
def handle_params(%{"api_token_id" => api_token_id}, _url, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:api_token, ApiTokens.get_api_token!(api_token_id))}
|
||||
end
|
||||
|
||||
@impl Phoenix.LiveView
|
||||
def handle_params(_params, _url, socket) do
|
||||
admins = Users.list_admins()
|
||||
{:noreply, assign(socket, :allow_delete, length(admins) > 1)}
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:allow_delete, length(Users.list_admins()) > 1)
|
||||
|> assign(:api_tokens, ApiTokens.list_api_tokens(socket.assigns.current_user.id))}
|
||||
end
|
||||
|
||||
@impl Phoenix.LiveView
|
||||
def handle_event("delete_api_token", %{"id" => id}, socket) do
|
||||
api_token = ApiTokens.get_api_token!(id)
|
||||
|
||||
if api_token.user_id == socket.assigns.current_user.id do
|
||||
{:ok, _deleted} = ApiTokens.delete_api_token(api_token)
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:api_tokens, ApiTokens.list_api_tokens(socket.assigns.current_user.id))}
|
||||
end
|
||||
|
||||
@impl Phoenix.LiveView
|
||||
def handle_event("delete_authenticator", %{"id" => id}, socket) do
|
||||
{:ok, _deleted} = id |> MFA.get_method!() |> MFA.delete_method()
|
||||
method = MFA.get_method!(id)
|
||||
|
||||
# A user can only delete his/her own MFA method!
|
||||
if method.user_id == socket.assigns.current_user.id do
|
||||
{:ok, _deleted} = MFA.delete_method(method)
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
@@ -58,7 +96,7 @@ defmodule FzHttpWeb.SettingLive.Account do
|
||||
end
|
||||
|
||||
defp get_metas(presences, user_id) do
|
||||
get_in(presences, [to_string(user_id), :metas]) || []
|
||||
get_in(presences, [user_id, :metas]) || []
|
||||
end
|
||||
|
||||
defp subscribe_link do
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
<section class="section is-main-section">
|
||||
<%= render(FzHttpWeb.SharedView, "flash.html", assigns) %>
|
||||
|
||||
<h4 class="title is-4">Site Defaults</h4>
|
||||
<h4 class="title is-4">Client Defaults</h4>
|
||||
|
||||
<div class="block">
|
||||
<%= live_component(
|
||||
FzHttpWeb.SettingLive.SiteFormComponent,
|
||||
FzHttpWeb.SettingLive.ClientDefaultsFormComponent,
|
||||
changeset: @changeset,
|
||||
id: :site_form_component
|
||||
id: :client_defaults_form_component
|
||||
) %>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,32 @@
|
||||
defmodule FzHttpWeb.SettingLive.ClientDefaultsFormComponent do
|
||||
@moduledoc """
|
||||
Handles updating client defaults form.
|
||||
"""
|
||||
use FzHttpWeb, :live_component
|
||||
|
||||
alias FzHttp.Configurations
|
||||
|
||||
@impl Phoenix.LiveComponent
|
||||
def update(assigns, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(assigns)}
|
||||
end
|
||||
|
||||
@impl Phoenix.LiveComponent
|
||||
def handle_event("save", %{"configuration" => configuration_params}, socket) do
|
||||
configuration = Configurations.get_configuration!()
|
||||
|
||||
case Configurations.update_configuration(configuration, configuration_params) do
|
||||
{:ok, configuration} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:changeset, Configurations.change_configuration(configuration))}
|
||||
|
||||
{:error, changeset} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:changeset, changeset)}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,19 +1,19 @@
|
||||
<div class="block">
|
||||
<.form :let={f} for={@changeset} id={@id} phx-target={@myself} phx-submit="save">
|
||||
<div class="field">
|
||||
<%= label(f, :allowed_ips, "Allowed IPs", class: "label") %>
|
||||
<%= label(f, :default_client_allowed_ips, "Allowed IPs", class: "label") %>
|
||||
|
||||
<div class="control">
|
||||
<%= textarea(
|
||||
f,
|
||||
:allowed_ips,
|
||||
:default_client_allowed_ips,
|
||||
placeholder: "0.0.0.0/0, ::/0",
|
||||
class: "textarea #{input_error_class(f, :allowed_ips)}"
|
||||
class: "textarea #{input_error_class(f, :default_client_allowed_ips)}"
|
||||
) %>
|
||||
</div>
|
||||
|
||||
<p class="help is-danger">
|
||||
<%= error_tag(f, :allowed_ips) %>
|
||||
<%= error_tag(f, :default_client_allowed_ips) %>
|
||||
</p>
|
||||
<p class="help">
|
||||
Configures the default AllowedIPs setting for devices.
|
||||
@@ -25,40 +25,40 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= label(f, :dns, "DNS Servers", class: "label") %>
|
||||
<%= label(f, :default_client_dns, "DNS Servers", class: "label") %>
|
||||
|
||||
<div class="control">
|
||||
<%= text_input(
|
||||
f,
|
||||
:dns,
|
||||
:default_client_dns,
|
||||
placeholder: "1.1.1.1, 1.0.0.1",
|
||||
class: "input #{input_error_class(f, :dns)}"
|
||||
class: "input #{input_error_class(f, :default_client_dns)}"
|
||||
) %>
|
||||
</div>
|
||||
|
||||
<p class="help is-danger">
|
||||
<%= error_tag(f, :dns) %>
|
||||
<%= error_tag(f, :default_client_dns) %>
|
||||
</p>
|
||||
<p class="help">
|
||||
Comma-separated list of DNS servers to use for devices.
|
||||
Leaving this blank will omit the <code>DNS</code> section in
|
||||
generated device configs.
|
||||
Leave this blank to omit the <code>DNS</code> section from
|
||||
generated configs.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= label(f, :endpoint, "Endpoint", class: "label") %>
|
||||
<%= label(f, :default_client_endpoint, "Endpoint", class: "label") %>
|
||||
|
||||
<div class="control">
|
||||
<%= text_input(
|
||||
f,
|
||||
:endpoint,
|
||||
:default_client_endpoint,
|
||||
placeholder: "firezone.example.com",
|
||||
class: "input #{input_error_class(f, :endpoint)}"
|
||||
class: "input #{input_error_class(f, :default_client_endpoint)}"
|
||||
) %>
|
||||
</div>
|
||||
<p class="help is-danger">
|
||||
<%= error_tag(f, :endpoint) %>
|
||||
<%= error_tag(f, :default_client_endpoint) %>
|
||||
</p>
|
||||
<p class="help">
|
||||
IPv4, IPv6 address, or FQDN that devices will be configured to connect
|
||||
@@ -67,41 +67,41 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= label(f, :persistent_keepalive, "Persistent Keepalive", class: "label") %>
|
||||
<%= label(f, :default_client_persistent_keepalive, "Persistent Keepalive", class: "label") %>
|
||||
|
||||
<div class="control">
|
||||
<%= text_input(
|
||||
f,
|
||||
:persistent_keepalive,
|
||||
:default_client_persistent_keepalive,
|
||||
placeholder: "25",
|
||||
class: "input #{input_error_class(f, :persistent_keepalive)}"
|
||||
class: "input #{input_error_class(f, :default_client_persistent_keepalive)}"
|
||||
) %>
|
||||
<p class="help is-danger">
|
||||
<%= error_tag(f, :persistent_keepalive) %>
|
||||
<%= error_tag(f, :default_client_persistent_keepalive) %>
|
||||
</p>
|
||||
<p class="help">
|
||||
Interval in seconds to send persistent keepalive packets from clients. Most users won't need to change
|
||||
this. Leave this blank to omit this field from generated configs.
|
||||
Interval in seconds to send persistent keepalive packets from devices. Most users won't
|
||||
need to change this. Leave this blank to omit this field from generated configs.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= label(f, :mtu, "MTU", class: "label") %>
|
||||
<%= label(f, :default_client_mtu, "MTU", class: "label") %>
|
||||
|
||||
<div class="control">
|
||||
<%= text_input(
|
||||
f,
|
||||
:mtu,
|
||||
:default_client_mtu,
|
||||
placeholder: "1280",
|
||||
class: "input #{input_error_class(f, :mtu)}"
|
||||
class: "input #{input_error_class(f, :default_client_mtu)}"
|
||||
) %>
|
||||
</div>
|
||||
<p class="help is-danger">
|
||||
<%= error_tag(f, :mtu) %>
|
||||
<%= error_tag(f, :default_client_mtu) %>
|
||||
</p>
|
||||
<p class="help">
|
||||
WireGuard interface MTU for client configs. 1280 is a safe bet for most networks.
|
||||
WireGuard interface MTU for devices. 1280 is a safe bet for most networks.
|
||||
Leave this blank to omit this field from generated configs.
|
||||
</p>
|
||||
</div>
|
||||
@@ -1,13 +1,13 @@
|
||||
defmodule FzHttpWeb.SettingLive.Site do
|
||||
defmodule FzHttpWeb.SettingLive.ClientDefaults do
|
||||
@moduledoc """
|
||||
Manages the defaults view.
|
||||
"""
|
||||
use FzHttpWeb, :live_view
|
||||
|
||||
alias FzHttp.Sites
|
||||
alias FzHttp.Configurations
|
||||
|
||||
@page_title "Site Settings"
|
||||
@page_subtitle "Configure default WireGuard settings for this site."
|
||||
@page_title "Client Defaults"
|
||||
@page_subtitle "Configure default values for generating WireGuard client configurations."
|
||||
|
||||
@impl Phoenix.LiveView
|
||||
def mount(_params, _session, socket) do
|
||||
@@ -19,6 +19,6 @@ defmodule FzHttpWeb.SettingLive.Site do
|
||||
end
|
||||
|
||||
defp changeset do
|
||||
Sites.get_site!() |> Sites.change_site()
|
||||
Configurations.get_configuration!() |> Configurations.change_configuration()
|
||||
end
|
||||
end
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="block">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<%= for type <- Conf.logo_types do %>
|
||||
<%= for type <- FzHttp.Configurations.logo_types do %>
|
||||
<label class="radio">
|
||||
<input
|
||||
type="radio"
|
||||
|
||||
@@ -4,16 +4,14 @@ defmodule FzHttpWeb.SettingLive.Customization do
|
||||
"""
|
||||
use FzHttpWeb, :live_view
|
||||
|
||||
alias FzHttp.Configurations, as: Conf
|
||||
|
||||
@max_logo_size 1024 ** 2
|
||||
@page_title "Customization"
|
||||
@page_subtitle "Customize the look and feel of your Firezone web portal."
|
||||
|
||||
@impl Phoenix.LiveView
|
||||
def mount(_params, _session, socket) do
|
||||
logo = Conf.get!(:logo)
|
||||
logo_type = Conf.logo_type(logo)
|
||||
logo = FzHttp.Configurations.get!(:logo)
|
||||
logo_type = FzHttp.Configurations.logo_type(logo)
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
@@ -39,14 +37,14 @@ defmodule FzHttpWeb.SettingLive.Customization do
|
||||
|
||||
@impl Phoenix.LiveView
|
||||
def handle_event("save", %{"default" => "true"}, socket) do
|
||||
{:ok, config} = Conf.update_configuration(%{logo: nil})
|
||||
{:ok, config} = FzHttp.Configurations.update_configuration(%{logo: nil})
|
||||
|
||||
{:noreply, assign(socket, :logo, config.logo)}
|
||||
end
|
||||
|
||||
@impl Phoenix.LiveView
|
||||
def handle_event("save", %{"url" => url}, socket) do
|
||||
{:ok, config} = Conf.update_configuration(%{logo: %{"url" => url}})
|
||||
{:ok, config} = FzHttp.Configurations.update_configuration(%{logo: %{"url" => url}})
|
||||
|
||||
{:noreply, assign(socket, :logo, config.logo)}
|
||||
end
|
||||
@@ -61,7 +59,9 @@ defmodule FzHttpWeb.SettingLive.Customization do
|
||||
|
||||
# enforce OK, error from update_configuration instead of consume_uploaded_entry
|
||||
{:ok, config} =
|
||||
Conf.update_configuration(%{logo: %{"data" => data, "type" => entry.client_type}})
|
||||
FzHttp.Configurations.update_configuration(%{
|
||||
logo: %{"data" => data, "type" => entry.client_type}
|
||||
})
|
||||
|
||||
{:ok, config}
|
||||
end)
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
defmodule FzHttpWeb.SettingLive.NewApiTokenComponent do
|
||||
@moduledoc """
|
||||
Live component to manage creating API Tokens
|
||||
"""
|
||||
use FzHttpWeb, :live_component
|
||||
|
||||
alias FzHttp.ApiTokens
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<.form
|
||||
:let={f}
|
||||
for={@changeset}
|
||||
autocomplete="off"
|
||||
id="api-token-form"
|
||||
phx-target={@myself}
|
||||
phx-submit="save"
|
||||
>
|
||||
<%= if @changeset.action do %>
|
||||
<div class="notification is-danger">
|
||||
<div class="flash-error">
|
||||
<%= error_tag(f, :base) %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<%= label(f, :expires_in, class: "label") %>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field is-expanded">
|
||||
<div class="field has-addons">
|
||||
<p class="control is-expanded">
|
||||
<%= text_input(f, :expires_in, class: "input #{input_error_class(f, :expires_in)}") %>
|
||||
</p>
|
||||
<p class="control">
|
||||
<a class="button is-static">
|
||||
days
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="help is-danger">
|
||||
<%= error_tag(f, :expires_in) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</.form>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def handle_event("save", %{"api_token" => api_token_params}, socket) do
|
||||
user = socket.assigns.user
|
||||
|
||||
case ApiTokens.create_user_api_token(user, api_token_params) do
|
||||
{:ok, api_token} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> push_patch(to: ~p"/settings/account/api_token/#{api_token}")}
|
||||
|
||||
{:error, changeset} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:changeset, changeset)}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,8 +4,6 @@ defmodule FzHttpWeb.SettingLive.OIDCFormComponent do
|
||||
"""
|
||||
use FzHttpWeb, :live_component
|
||||
|
||||
alias FzHttp.Configurations, as: Conf
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
@@ -173,40 +171,42 @@ defmodule FzHttpWeb.SettingLive.OIDCFormComponent do
|
||||
changeset =
|
||||
assigns.providers
|
||||
|> Map.get(assigns.provider_id, %{})
|
||||
|> Map.put("id", assigns.provider_id)
|
||||
|> FzHttp.Conf.OIDCConfig.changeset()
|
||||
|> Map.put(:id, assigns.provider_id)
|
||||
|> FzHttp.Configurations.Configuration.OpenIDConnectProvider.changeset()
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign(:external_url, Application.fetch_env!(:fz_http, :external_url))
|
||||
|> assign(:external_url, FzHttp.Config.fetch_env!(:fz_http, :external_url))
|
||||
|> assign(:changeset, changeset)}
|
||||
end
|
||||
|
||||
def handle_event("save", %{"oidc_config" => params}, socket) do
|
||||
def handle_event("save", %{"open_id_connect_provider" => params}, socket) do
|
||||
changeset =
|
||||
params
|
||||
|> FzHttp.Conf.OIDCConfig.changeset()
|
||||
|> FzHttp.Configurations.Configuration.OpenIDConnectProvider.changeset()
|
||||
|> Map.put(:action, :validate)
|
||||
|
||||
update =
|
||||
case changeset do
|
||||
%{valid?: true} ->
|
||||
changeset
|
||||
|> Ecto.Changeset.apply_changes()
|
||||
|> Map.from_struct()
|
||||
|> Map.new(fn {k, v} -> {to_string(k), v} end)
|
||||
|> then(fn data ->
|
||||
{id, data} = Map.pop(data, "id")
|
||||
{:ok, _} =
|
||||
changeset
|
||||
|> Ecto.Changeset.apply_changes()
|
||||
|> Map.from_struct()
|
||||
|> Map.new(fn {k, v} -> {to_string(k), v} end)
|
||||
|> then(fn data ->
|
||||
id = Map.get(data, "id")
|
||||
|
||||
%{
|
||||
openid_connect_providers:
|
||||
socket.assigns.providers
|
||||
|> Map.delete(socket.assigns.provider_id)
|
||||
|> Map.put(id, data)
|
||||
}
|
||||
end)
|
||||
|> Conf.update_configuration()
|
||||
%{
|
||||
openid_connect_providers:
|
||||
socket.assigns.providers
|
||||
|> Map.delete(socket.assigns.provider_id)
|
||||
|> Map.put(id, data)
|
||||
|> Map.values()
|
||||
}
|
||||
end)
|
||||
|> FzHttp.Configurations.update_configuration()
|
||||
|
||||
_ ->
|
||||
{:error, changeset}
|
||||
@@ -214,9 +214,6 @@ defmodule FzHttpWeb.SettingLive.OIDCFormComponent do
|
||||
|
||||
case update do
|
||||
{:ok, _config} ->
|
||||
:ok = Supervisor.terminate_child(FzHttp.Supervisor, FzHttp.OIDC.StartProxy)
|
||||
{:ok, _pid} = Supervisor.restart_child(FzHttp.Supervisor, FzHttp.OIDC.StartProxy)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Updated successfully.")
|
||||
|
||||
@@ -4,8 +4,6 @@ defmodule FzHttpWeb.SettingLive.SAMLFormComponent do
|
||||
"""
|
||||
use FzHttpWeb, :live_component
|
||||
|
||||
alias FzHttp.Configurations, as: Conf
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
@@ -192,16 +190,17 @@ defmodule FzHttpWeb.SettingLive.SAMLFormComponent do
|
||||
end
|
||||
|
||||
def update(assigns, socket) do
|
||||
external_url = Application.fetch_env!(:fz_http, :external_url)
|
||||
external_url = FzHttp.Config.fetch_env!(:fz_http, :external_url)
|
||||
|
||||
changeset =
|
||||
assigns.providers
|
||||
|> Map.get(assigns.provider_id, %{})
|
||||
|> Map.merge(%{
|
||||
"id" => assigns.provider_id,
|
||||
"base_url" => Path.join(external_url, "/auth/saml")
|
||||
id: assigns.provider_id,
|
||||
# XXX this should be part of changeset itself
|
||||
base_url: Path.join(external_url, "/auth/saml")
|
||||
})
|
||||
|> FzHttp.Conf.SAMLConfig.changeset()
|
||||
|> FzHttp.Configurations.Configuration.SAMLIdentityProvider.changeset()
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
@@ -209,41 +208,39 @@ defmodule FzHttpWeb.SettingLive.SAMLFormComponent do
|
||||
|> assign(:changeset, changeset)}
|
||||
end
|
||||
|
||||
def handle_event("save", %{"saml_config" => params}, socket) do
|
||||
def handle_event("save", %{"saml_identity_provider" => params}, socket) do
|
||||
changeset =
|
||||
params
|
||||
|> FzHttp.Conf.SAMLConfig.changeset()
|
||||
|> FzHttp.Configurations.Configuration.SAMLIdentityProvider.changeset()
|
||||
|> Map.put(:action, :validate)
|
||||
|
||||
update =
|
||||
case changeset do
|
||||
%{valid?: true} ->
|
||||
changeset
|
||||
|> Ecto.Changeset.apply_changes()
|
||||
|> Map.from_struct()
|
||||
|> Map.new(fn {k, v} -> {to_string(k), v} end)
|
||||
|> then(fn data ->
|
||||
{id, data} = Map.pop(data, "id")
|
||||
{:ok, _} =
|
||||
changeset
|
||||
|> Ecto.Changeset.apply_changes()
|
||||
|> Map.from_struct()
|
||||
|> Map.new(fn {k, v} -> {to_string(k), v} end)
|
||||
|> then(fn data ->
|
||||
id = Map.get(data, "id")
|
||||
|
||||
%{
|
||||
saml_identity_providers:
|
||||
socket.assigns.providers
|
||||
|> Map.delete(socket.assigns.provider_id)
|
||||
|> Map.put(id, data)
|
||||
}
|
||||
end)
|
||||
|> Conf.update_configuration()
|
||||
%{
|
||||
saml_identity_providers:
|
||||
socket.assigns.providers
|
||||
|> Map.delete(socket.assigns.provider_id)
|
||||
|> Map.put(id, data)
|
||||
|> Map.values()
|
||||
}
|
||||
end)
|
||||
|> FzHttp.Configurations.update_configuration()
|
||||
|
||||
_ ->
|
||||
{:error, changeset}
|
||||
end
|
||||
|
||||
case update do
|
||||
{:ok, config} ->
|
||||
Application.fetch_env!(:samly, Samly.Provider)
|
||||
|> FzHttp.SAML.StartProxy.set_identity_providers(config.saml_identity_providers)
|
||||
|> FzHttp.SAML.StartProxy.refresh()
|
||||
|
||||
{:ok, _config} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Updated successfully.")
|
||||
|
||||
@@ -33,7 +33,12 @@
|
||||
<h4 class="title is-4">Authentication</h4>
|
||||
|
||||
<div class="block">
|
||||
<.form :let={f} for={@site_changeset} phx-change="change" phx-submit="save_site">
|
||||
<.form
|
||||
:let={f}
|
||||
for={@configuration_changeset}
|
||||
phx-change="change"
|
||||
phx-submit="save_configuration"
|
||||
>
|
||||
<div class="field">
|
||||
<%= label(f, :vpn_session_duration, "Require Authentication For VPN Sessions",
|
||||
class: "label"
|
||||
@@ -73,8 +78,8 @@
|
||||
type="checkbox"
|
||||
phx-click="toggle"
|
||||
phx-value-config="local_auth_enabled"
|
||||
checked={Conf.get!(:local_auth_enabled)}
|
||||
value={if(!Conf.get!(:local_auth_enabled), do: "on")}
|
||||
checked={FzHttp.Configurations.get!(:local_auth_enabled)}
|
||||
value={if(!FzHttp.Configurations.get!(:local_auth_enabled), do: "on")}
|
||||
/>
|
||||
<span class="check"></span>
|
||||
</label>
|
||||
@@ -95,8 +100,10 @@
|
||||
type="checkbox"
|
||||
phx-click="toggle"
|
||||
phx-value-config="allow_unprivileged_device_management"
|
||||
checked={Conf.get!(:allow_unprivileged_device_management)}
|
||||
value={if(!Conf.get!(:allow_unprivileged_device_management), do: "on")}
|
||||
checked={FzHttp.Configurations.get!(:allow_unprivileged_device_management)}
|
||||
value={
|
||||
if(!FzHttp.Configurations.get!(:allow_unprivileged_device_management), do: "on")
|
||||
}
|
||||
/>
|
||||
<span class="check"></span>
|
||||
</label>
|
||||
@@ -119,8 +126,10 @@
|
||||
type="checkbox"
|
||||
phx-click="toggle"
|
||||
phx-value-config="allow_unprivileged_device_configuration"
|
||||
checked={Conf.get!(:allow_unprivileged_device_configuration)}
|
||||
value={if(!Conf.get!(:allow_unprivileged_device_configuration), do: "on")}
|
||||
checked={FzHttp.Configurations.get!(:allow_unprivileged_device_configuration)}
|
||||
value={
|
||||
if(!FzHttp.Configurations.get!(:allow_unprivileged_device_configuration), do: "on")
|
||||
}
|
||||
/>
|
||||
<span class="check"></span>
|
||||
</label>
|
||||
@@ -132,8 +141,7 @@
|
||||
|
||||
<div class="block">
|
||||
<p>
|
||||
Single Sign-On can be configured in the main Firezone configuration
|
||||
file. Refer to the
|
||||
Configure OIDC or SAML Single Sign-On below. See the
|
||||
<a href="https://docs.firezone.dev/authenticate/">Firezone documentation</a>
|
||||
for more details.
|
||||
</p>
|
||||
@@ -152,8 +160,8 @@
|
||||
type="checkbox"
|
||||
phx-click="toggle"
|
||||
phx-value-config="disable_vpn_on_oidc_error"
|
||||
checked={Conf.get!(:disable_vpn_on_oidc_error)}
|
||||
value={if(!Conf.get!(:disable_vpn_on_oidc_error), do: "on")}
|
||||
checked={FzHttp.Configurations.get!(:disable_vpn_on_oidc_error)}
|
||||
value={if(!FzHttp.Configurations.get!(:disable_vpn_on_oidc_error), do: "on")}
|
||||
/>
|
||||
<span class="check"></span>
|
||||
</label>
|
||||
@@ -178,10 +186,10 @@
|
||||
<%= for {k, v} <- @oidc_configs do %>
|
||||
<tr>
|
||||
<td><%= k %></td>
|
||||
<td><%= v["label"] %></td>
|
||||
<td><%= v["client_id"] %></td>
|
||||
<td><%= v["discovery_document_uri"] %></td>
|
||||
<td><%= v["scope"] %></td>
|
||||
<td><%= v.label %></td>
|
||||
<td><%= v.client_id %></td>
|
||||
<td><%= v.discovery_document_uri %></td>
|
||||
<td><%= v.scope %></td>
|
||||
<td>
|
||||
<%= live_patch(to: ~p"/settings/security/oidc/#{k}/edit",
|
||||
class: "button") do %>
|
||||
@@ -223,9 +231,9 @@
|
||||
<%= for {k, v} <- @saml_configs do %>
|
||||
<tr>
|
||||
<td><%= k %></td>
|
||||
<td><%= v["label"] %></td>
|
||||
<td><%= v.label %></td>
|
||||
<td>
|
||||
<div class="line-clamp"><%= v["metadata"] %></div>
|
||||
<div class="line-clamp"><%= v.metadata %></div>
|
||||
</td>
|
||||
<td>
|
||||
<%= live_patch(to: ~p"/settings/security/saml/#{k}/edit",
|
||||
|
||||
@@ -7,29 +7,36 @@ defmodule FzHttpWeb.SettingLive.Security do
|
||||
import Ecto.Changeset
|
||||
import FzCommon.FzCrypto, only: [rand_string: 1]
|
||||
|
||||
alias FzHttp.Configurations, as: Conf
|
||||
alias FzHttp.{Sites, Sites.Site}
|
||||
alias FzHttp.Configurations
|
||||
|
||||
@page_title "Security Settings"
|
||||
@page_subtitle "Configure security-related settings."
|
||||
|
||||
@impl Phoenix.LiveView
|
||||
def mount(_params, _session, socket) do
|
||||
config_changeset = Conf.change_configuration()
|
||||
config_changeset = Configurations.change_configuration()
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:form_changed, false)
|
||||
|> assign(:session_duration_options, session_duration_options())
|
||||
|> assign(:site_changeset, site_changeset())
|
||||
|> assign(:configuration_changeset, configuration_changeset())
|
||||
|> assign(:config_changeset, config_changeset)
|
||||
|> assign(:oidc_configs, config_changeset.data.openid_connect_providers || %{})
|
||||
|> assign(:saml_configs, config_changeset.data.saml_identity_providers || %{})
|
||||
|> assign(:oidc_configs, map_providers(config_changeset.data.openid_connect_providers))
|
||||
|> assign(:saml_configs, map_providers(config_changeset.data.saml_identity_providers))
|
||||
|> assign(:field_titles, field_titles(config_changeset))
|
||||
|> assign(:page_subtitle, @page_subtitle)
|
||||
|> assign(:page_title, @page_title)}
|
||||
end
|
||||
|
||||
defp map_providers(nil), do: %{}
|
||||
|
||||
defp map_providers(providers) when is_list(providers) do
|
||||
for provider <- providers,
|
||||
into: %{},
|
||||
do: {provider.id, Map.from_struct(provider)}
|
||||
end
|
||||
|
||||
@impl Phoenix.LiveView
|
||||
def handle_params(params, _uri, socket) do
|
||||
{:noreply, assign(socket, :id, params["id"])}
|
||||
@@ -44,30 +51,32 @@ defmodule FzHttpWeb.SettingLive.Security do
|
||||
|
||||
@impl Phoenix.LiveView
|
||||
def handle_event(
|
||||
"save_site",
|
||||
%{"site" => %{"vpn_session_duration" => vpn_session_duration}},
|
||||
"save_configuration",
|
||||
%{"configuration" => %{"vpn_session_duration" => vpn_session_duration}},
|
||||
socket
|
||||
) do
|
||||
site = Sites.get_site!()
|
||||
configuration = Configurations.get_configuration!()
|
||||
|
||||
case Sites.update_site(site, %{vpn_session_duration: vpn_session_duration}) do
|
||||
{:ok, site} ->
|
||||
case Configurations.update_configuration(configuration, %{
|
||||
vpn_session_duration: vpn_session_duration
|
||||
}) do
|
||||
{:ok, configuration} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:form_changed, false)
|
||||
|> assign(:site_changeset, Sites.change_site(site))}
|
||||
|> assign(:configuration_changeset, Configurations.change_configuration(configuration))}
|
||||
|
||||
{:error, site_changeset} ->
|
||||
{:error, configuration_changeset} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:site_changeset, site_changeset)}
|
||||
|> assign(:configuration_changeset, configuration_changeset)}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Phoenix.LiveView
|
||||
def handle_event("toggle", %{"config" => config} = params, socket) do
|
||||
toggle_value = !!params["value"]
|
||||
{:ok, _conf} = Conf.update_configuration(%{config => toggle_value})
|
||||
{:ok, _conf} = Configurations.update_configuration(%{config => toggle_value})
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@@ -78,9 +87,11 @@ defmodule FzHttpWeb.SettingLive.Security do
|
||||
field_key = Map.fetch!(@types, type)
|
||||
|
||||
providers =
|
||||
get_in(socket.assigns.config_changeset, [Access.key!(:data), Access.key!(field_key)])
|
||||
socket.assigns.config_changeset
|
||||
|> get_in([Access.key!(:data), Access.key!(field_key)])
|
||||
|> Enum.reject(&(&1.id == key))
|
||||
|
||||
{:ok, conf} = Conf.update_configuration(%{field_key => Map.delete(providers, key)})
|
||||
{:ok, conf} = Configurations.update_configuration(%{field_key => providers})
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
@@ -94,7 +105,7 @@ defmodule FzHttpWeb.SettingLive.Security do
|
||||
def session_duration_options do
|
||||
[
|
||||
Never: 0,
|
||||
Once: Site.max_vpn_session_duration(),
|
||||
Once: FzHttp.Configurations.Configuration.max_vpn_session_duration(),
|
||||
"Every Hour": @hour,
|
||||
"Every Day": @day,
|
||||
"Every Week": 7 * @day,
|
||||
@@ -103,9 +114,9 @@ defmodule FzHttpWeb.SettingLive.Security do
|
||||
]
|
||||
end
|
||||
|
||||
defp site_changeset do
|
||||
Sites.get_site!()
|
||||
|> Sites.change_site()
|
||||
defp configuration_changeset do
|
||||
Configurations.get_configuration!()
|
||||
|> Configurations.change_configuration()
|
||||
end
|
||||
|
||||
@fields ~w(
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
defmodule FzHttpWeb.SettingLive.ShowApiTokenComponent do
|
||||
use FzHttpWeb, :live_component
|
||||
|
||||
alias Phoenix.LiveView.JS
|
||||
alias FzHttpWeb.Auth.JSON.Authentication
|
||||
|
||||
def update(assigns, socket) do
|
||||
if connected?(socket) do
|
||||
{:ok, secret, _claims} = Authentication.fz_encode_and_sign(assigns.api_token, assigns.user)
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:secret, secret)}
|
||||
else
|
||||
{:ok, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<%= if assigns[:secret] do %>
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<h6 class="title is-6">
|
||||
API token secret:
|
||||
</h6>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<button
|
||||
class="button copy-button"
|
||||
phx-click={JS.dispatch("firezone:clipcopy", to: "#api-token-secret")}
|
||||
>
|
||||
<span class="icon" title="Click to copy API token">
|
||||
<i class="mdi mdi-content-copy"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block">
|
||||
<pre class="multiline"><code id="api-token-secret"><%= @secret %></code></pre>
|
||||
</div>
|
||||
<div class="block">
|
||||
<p><strong>Warning!</strong> This token is sensitive data. Store it somewhere safe.</p>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="block">
|
||||
<h6 class="title is-6">cURL example:</h6>
|
||||
<pre><code><i># List all users</i>
|
||||
curl -H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer <%= @secret %>' \
|
||||
<%= Application.fetch_env!(:fz_http, :external_url) %>/v0/users</code></pre>
|
||||
</div>
|
||||
<div class="block has-text-right">
|
||||
<a href="https://docs.firezone.dev/reference/rest-api?utm_source=product">
|
||||
Explore the REST API docs ->
|
||||
</a>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
@@ -1,32 +0,0 @@
|
||||
defmodule FzHttpWeb.SettingLive.SiteFormComponent do
|
||||
@moduledoc """
|
||||
Handles updating site values.
|
||||
"""
|
||||
use FzHttpWeb, :live_component
|
||||
|
||||
alias FzHttp.Sites
|
||||
|
||||
@impl Phoenix.LiveComponent
|
||||
def update(assigns, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(assigns)}
|
||||
end
|
||||
|
||||
@impl Phoenix.LiveComponent
|
||||
def handle_event("save", %{"site" => site_params}, socket) do
|
||||
site = Sites.get_site!()
|
||||
|
||||
case Sites.update_site(site, site_params) do
|
||||
{:ok, site} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:changeset, Sites.change_site(site))}
|
||||
|
||||
{:error, changeset} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:changeset, changeset)}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -8,7 +8,6 @@ defmodule FzHttpWeb.SettingLive.Unprivileged.Account do
|
||||
"""
|
||||
use FzHttpWeb, :live_view
|
||||
|
||||
alias FzHttp.Configurations, as: Conf
|
||||
alias FzHttp.{MFA, Users}
|
||||
alias FzHttpWeb.{Endpoint, Presence}
|
||||
|
||||
@@ -22,7 +21,7 @@ defmodule FzHttpWeb.SettingLive.Unprivileged.Account do
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:local_auth_enabled, Conf.get!(:local_auth_enabled))
|
||||
|> assign(:local_auth_enabled, FzHttp.Configurations.get!(:local_auth_enabled))
|
||||
|> assign(:changeset, Users.change_user(socket.assigns.current_user))
|
||||
|> assign(:methods, MFA.list_methods(socket.assigns.current_user))
|
||||
|> assign(:page_title, @page_title)
|
||||
@@ -61,6 +60,6 @@ defmodule FzHttpWeb.SettingLive.Unprivileged.Account do
|
||||
end
|
||||
|
||||
defp get_metas(presences, user_id) do
|
||||
get_in(presences, [to_string(user_id), :metas]) || []
|
||||
get_in(presences, [user_id, :metas]) || []
|
||||
end
|
||||
end
|
||||
|
||||
@@ -37,7 +37,7 @@ defmodule FzHttpWeb.SidebarComponent do
|
||||
<p class="menu-label">Settings</p>
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
<%= live_redirect(to: ~p"/settings/site", class: nav_class(@path, "/settings/site")) do %>
|
||||
<%= live_redirect(to: ~p"/settings/client_defaults", class: nav_class(@path, "/settings/client_defaults")) do %>
|
||||
<span class="icon"><i class="mdi mdi-cog"></i></span>
|
||||
<span class="menu-item-label">Defaults</span>
|
||||
<% end %>
|
||||
|
||||
@@ -5,7 +5,7 @@ defmodule FzHttpWeb.LiveHelpers do
|
||||
https://bernheisel.com/blog/phoenix-liveview-and-views
|
||||
"""
|
||||
use Phoenix.Component
|
||||
alias FzHttp.{Sites, Users}
|
||||
alias FzHttp.{Configurations, Users}
|
||||
|
||||
def live_modal(component, opts) do
|
||||
path = Keyword.fetch!(opts, :return_to)
|
||||
@@ -36,19 +36,19 @@ defmodule FzHttpWeb.LiveHelpers do
|
||||
end
|
||||
|
||||
def admin_email do
|
||||
Application.fetch_env!(:fz_http, :admin_email)
|
||||
FzHttp.Config.fetch_env!(:fz_http, :admin_email)
|
||||
end
|
||||
|
||||
def vpn_sessions_expire? do
|
||||
Sites.vpn_sessions_expire?()
|
||||
Configurations.vpn_sessions_expire?()
|
||||
end
|
||||
|
||||
def vpn_expires_at(user) do
|
||||
Users.vpn_session_expires_at(user, Sites.vpn_duration())
|
||||
Users.vpn_session_expires_at(user, Configurations.vpn_duration())
|
||||
end
|
||||
|
||||
def vpn_expired?(user) do
|
||||
Users.vpn_session_expired?(user, Sites.vpn_duration())
|
||||
Users.vpn_session_expired?(user, Configurations.vpn_duration())
|
||||
end
|
||||
|
||||
defp status_digit(response_code) when is_integer(response_code) do
|
||||
|
||||
@@ -43,7 +43,7 @@ defmodule FzHttpWeb.OAuth.PKCE do
|
||||
max_age: @pkce_valid_duration,
|
||||
sign: true,
|
||||
same_site: "Lax",
|
||||
secure: Application.fetch_env!(:fz_http, :cookie_secure)
|
||||
secure: FzHttp.Config.fetch_env!(:fz_http, :cookie_secure)
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,14 +3,7 @@ defmodule FzHttpWeb.OIDC.Helpers do
|
||||
Just some, ya know, helpers for OIDC flows.
|
||||
"""
|
||||
|
||||
# openid_connect expects providers as keys...
|
||||
def atomize_provider(key) do
|
||||
{:ok, String.to_existing_atom(key)}
|
||||
rescue
|
||||
ArgumentError -> {:error, "OIDC Provider not found"}
|
||||
end
|
||||
|
||||
def openid_connect do
|
||||
Application.fetch_env!(:fz_http, :openid_connect)
|
||||
FzHttp.Config.fetch_env!(:fz_http, :openid_connect)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -33,7 +33,7 @@ defmodule FzHttpWeb.OIDC.State do
|
||||
max_age: @oidc_state_valid_duration,
|
||||
sign: true,
|
||||
same_site: "Lax",
|
||||
secure: Application.fetch_env!(:fz_http, :cookie_secure)
|
||||
secure: FzHttp.Config.fetch_env!(:fz_http, :cookie_secure)
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,8 +7,8 @@ defmodule FzHttpWeb.Plug.Authorization do
|
||||
|
||||
use FzHttpWeb, :controller
|
||||
|
||||
import FzHttpWeb.ControllerHelpers, only: [root_path_for_role: 1]
|
||||
alias FzHttpWeb.Authentication
|
||||
alias FzHttp.Users.User
|
||||
alias FzHttpWeb.Auth.HTML.Authentication
|
||||
|
||||
@not_authorized "Not authorized."
|
||||
|
||||
@@ -21,15 +21,16 @@ defmodule FzHttpWeb.Plug.Authorization do
|
||||
def call(conn, role), do: require_user_with_role(conn, role)
|
||||
|
||||
def require_user_with_role(conn, role) do
|
||||
user = Authentication.get_current_user(conn)
|
||||
|
||||
if user.role == role do
|
||||
with %User{} = user <- Authentication.get_current_user(conn),
|
||||
^role <- user.role do
|
||||
conn
|
||||
else
|
||||
conn
|
||||
|> put_flash(:error, @not_authorized)
|
||||
|> redirect(to: root_path_for_role(user.role))
|
||||
|> halt()
|
||||
_ ->
|
||||
conn
|
||||
|> Authentication.sign_out()
|
||||
|> put_flash(:error, @not_authorized)
|
||||
|> redirect(to: ~p"/")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,6 +16,7 @@ defmodule FzHttpWeb.Router do
|
||||
|
||||
pipeline :api do
|
||||
plug :accepts, ["json"]
|
||||
plug FzHttpWeb.Auth.JSON.Pipeline
|
||||
end
|
||||
|
||||
pipeline :require_admin_user do
|
||||
@@ -35,8 +36,8 @@ defmodule FzHttpWeb.Router do
|
||||
plug Guardian.Plug.EnsureNotAuthenticated
|
||||
end
|
||||
|
||||
pipeline :guardian do
|
||||
plug FzHttpWeb.Authentication.Pipeline
|
||||
pipeline :html_auth do
|
||||
plug FzHttpWeb.Auth.HTML.Pipeline
|
||||
end
|
||||
|
||||
pipeline :samly do
|
||||
@@ -48,7 +49,7 @@ defmodule FzHttpWeb.Router do
|
||||
scope "/auth", FzHttpWeb do
|
||||
pipe_through [
|
||||
:browser,
|
||||
:guardian,
|
||||
:html_auth,
|
||||
:require_unauthenticated
|
||||
]
|
||||
|
||||
@@ -73,7 +74,7 @@ defmodule FzHttpWeb.Router do
|
||||
scope "/", FzHttpWeb do
|
||||
pipe_through [
|
||||
:browser,
|
||||
:guardian,
|
||||
:html_auth,
|
||||
:require_unauthenticated
|
||||
]
|
||||
|
||||
@@ -83,7 +84,7 @@ defmodule FzHttpWeb.Router do
|
||||
scope "/mfa", FzHttpWeb do
|
||||
pipe_through([
|
||||
:browser,
|
||||
:guardian
|
||||
:html_auth
|
||||
])
|
||||
|
||||
live_session(
|
||||
@@ -101,7 +102,7 @@ defmodule FzHttpWeb.Router do
|
||||
scope "/", FzHttpWeb do
|
||||
pipe_through [
|
||||
:browser,
|
||||
:guardian,
|
||||
:html_auth,
|
||||
:require_authenticated
|
||||
]
|
||||
|
||||
@@ -112,7 +113,7 @@ defmodule FzHttpWeb.Router do
|
||||
scope "/", FzHttpWeb do
|
||||
pipe_through [
|
||||
:browser,
|
||||
:guardian,
|
||||
:html_auth,
|
||||
:require_authenticated,
|
||||
:require_unprivileged_user
|
||||
]
|
||||
@@ -137,7 +138,7 @@ defmodule FzHttpWeb.Router do
|
||||
scope "/", FzHttpWeb do
|
||||
pipe_through [
|
||||
:browser,
|
||||
:guardian,
|
||||
:html_auth,
|
||||
:require_authenticated,
|
||||
:require_admin_user
|
||||
]
|
||||
@@ -159,7 +160,7 @@ defmodule FzHttpWeb.Router do
|
||||
live "/rules", RuleLive.Index, :index
|
||||
live "/devices", DeviceLive.Admin.Index, :index
|
||||
live "/devices/:id", DeviceLive.Admin.Show, :show
|
||||
live "/settings/site", SettingLive.Site, :show
|
||||
live "/settings/client_defaults", SettingLive.ClientDefaults, :show
|
||||
|
||||
live "/settings/security", SettingLive.Security, :show
|
||||
live "/settings/security/oidc/:id/edit", SettingLive.Security, :edit_oidc
|
||||
@@ -168,12 +169,23 @@ defmodule FzHttpWeb.Router do
|
||||
live "/settings/account", SettingLive.Account, :show
|
||||
live "/settings/account/edit", SettingLive.Account, :edit
|
||||
live "/settings/account/register_mfa", SettingLive.Account, :register_mfa
|
||||
live "/settings/account/api_token", SettingLive.Account, :new_api_token
|
||||
live "/settings/account/api_token/:api_token_id", SettingLive.Account, :show_api_token
|
||||
live "/settings/customization", SettingLive.Customization, :show
|
||||
live "/diagnostics/connectivity_checks", ConnectivityCheckLive.Index, :index
|
||||
live "/notifications", NotificationsLive.Index, :index
|
||||
end
|
||||
end
|
||||
|
||||
scope "/v0", FzHttpWeb.JSON do
|
||||
pipe_through :api
|
||||
|
||||
resources "/configuration", ConfigurationController, singleton: true, only: [:show, :update]
|
||||
resources "/users", UserController, except: [:new, :edit]
|
||||
resources "/devices", DeviceController, except: [:new, :edit]
|
||||
resources "/rules", RuleController, except: [:new, :edit]
|
||||
end
|
||||
|
||||
if Mix.env() == :dev do
|
||||
import Phoenix.LiveDashboard.Router
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user