mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
Authentication for the live app (#1674)
Co-authored-by: Jamil <jamilbk@users.noreply.github.com>
This commit is contained in:
34
.github/workflows/elixir.yml
vendored
34
.github/workflows/elixir.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
- uses: erlef/setup-beam@v1
|
||||
with:
|
||||
otp-version: "25"
|
||||
elixir-version: "1.14"
|
||||
elixir-version: "1.15"
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/cache@v3
|
||||
name: Elixir Deps Cache
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
cache-name: cache-elixir-deps-${{ env.MIX_ENV }}
|
||||
with:
|
||||
path: elixir/deps
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/elixir/mix.lock') }}
|
||||
key: ${{ runner.os }}-${{ steps.setup-beam.outputs.elixir-version }}-${{ steps.setup-beam.outputs.otp-version }}-${{ env.cache-name }}-${{ hashFiles('**/elixir/mix.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ env.cache-name }}-
|
||||
- uses: actions/cache@v3
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
cache-name: cache-elixir-build-${{ env.MIX_ENV }}
|
||||
with:
|
||||
path: elixir/_build
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/elixir/mix.lock') }}
|
||||
key: ${{ runner.os }}-${{ steps.setup-beam.outputs.elixir-version }}-${{ steps.setup-beam.outputs.otp-version }}-${{ env.cache-name }}-${{ hashFiles('**/elixir/mix.lock') }}
|
||||
- name: Install Dependencies
|
||||
run: mix deps.get --only $MIX_ENV
|
||||
- name: Compile Dependencies
|
||||
@@ -66,11 +66,10 @@ jobs:
|
||||
run: |
|
||||
mix ecto.create
|
||||
mix ecto.migrate
|
||||
- name: Run Tests and Upload Coverage Report
|
||||
- name: Run Tests
|
||||
env:
|
||||
E2E_MAX_WAIT_SECONDS: 20
|
||||
run: |
|
||||
# XXX: This can fail when coveralls is down
|
||||
mix test --warnings-as-errors
|
||||
- name: Test Report
|
||||
uses: dorny/test-reporter@v1
|
||||
@@ -92,7 +91,7 @@ jobs:
|
||||
id: setup-beam
|
||||
with:
|
||||
otp-version: "25"
|
||||
elixir-version: "1.14"
|
||||
elixir-version: "1.15"
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/cache@v3
|
||||
name: Elixir Deps Cache
|
||||
@@ -100,7 +99,7 @@ jobs:
|
||||
cache-name: cache-elixir-deps-${{ env.MIX_ENV }}
|
||||
with:
|
||||
path: elixir/deps
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/elixir/mix.lock') }}
|
||||
key: ${{ runner.os }}-${{ steps.setup-beam.outputs.elixir-version }}-${{ steps.setup-beam.outputs.otp-version }}-${{ env.cache-name }}-${{ hashFiles('**/elixir/mix.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ env.cache-name }}-
|
||||
- uses: actions/cache@v3
|
||||
@@ -109,7 +108,7 @@ jobs:
|
||||
cache-name: cache-elixir-build-${{ env.MIX_ENV }}
|
||||
with:
|
||||
path: elixir/_build
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/elixir/mix.lock') }}
|
||||
key: ${{ runner.os }}-${{ steps.setup-beam.outputs.elixir-version }}-${{ steps.setup-beam.outputs.otp-version }}-${{ env.cache-name }}-${{ hashFiles('**/elixir/mix.lock') }}
|
||||
- name: Install Dependencies
|
||||
run: mix deps.get --only $MIX_ENV
|
||||
- name: Compile Dependencies
|
||||
@@ -145,7 +144,7 @@ jobs:
|
||||
- uses: erlef/setup-beam@v1
|
||||
with:
|
||||
otp-version: "25"
|
||||
elixir-version: "1.14"
|
||||
elixir-version: "1.15"
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/cache@v3
|
||||
name: Elixir Deps Cache
|
||||
@@ -153,7 +152,7 @@ jobs:
|
||||
cache-name: cache-elixir-deps-${{ env.MIX_ENV }}
|
||||
with:
|
||||
path: elixir/deps
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/elixir/mix.lock') }}
|
||||
key: ${{ runner.os }}-${{ steps.setup-beam.outputs.elixir-version }}-${{ steps.setup-beam.outputs.otp-version }}-${{ env.cache-name }}-${{ hashFiles('**/elixir/mix.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ env.cache-name }}-
|
||||
- uses: actions/cache@v3
|
||||
@@ -162,7 +161,7 @@ jobs:
|
||||
cache-name: cache-elixir-build-${{ env.MIX_ENV }}
|
||||
with:
|
||||
path: elixir/_build
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/elixir/mix.lock') }}
|
||||
key: ${{ runner.os }}-${{ steps.setup-beam.outputs.elixir-version }}-${{ steps.setup-beam.outputs.otp-version }}-${{ env.cache-name }}-${{ hashFiles('**/elixir/mix.lock') }}
|
||||
- name: Install Dependencies
|
||||
run: mix deps.get --only $MIX_ENV
|
||||
- name: Compile Dependencies
|
||||
@@ -214,14 +213,14 @@ jobs:
|
||||
- uses: erlef/setup-beam@v1
|
||||
with:
|
||||
otp-version: "25"
|
||||
elixir-version: "1.14"
|
||||
elixir-version: "1.15"
|
||||
- uses: actions/cache@v3
|
||||
name: Elixir Deps Cache
|
||||
env:
|
||||
cache-name: cache-elixir-deps-${{ env.MIX_ENV }}-${{ env.MIX_ENV }}
|
||||
with:
|
||||
path: elixir/deps
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/elixir/mix.lock') }}
|
||||
key: ${{ runner.os }}-${{ steps.setup-beam.outputs.elixir-version }}-${{ steps.setup-beam.outputs.otp-version }}-${{ env.cache-name }}-${{ hashFiles('**/elixir/mix.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ env.cache-name }}-
|
||||
- uses: actions/cache@v3
|
||||
@@ -230,7 +229,7 @@ jobs:
|
||||
cache-name: cache-elixir-build-${{ env.MIX_ENV }}
|
||||
with:
|
||||
path: elixir/_build
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/elixir/mix.lock') }}
|
||||
key: ${{ runner.os }}-${{ steps.setup-beam.outputs.elixir-version }}-${{ steps.setup-beam.outputs.otp-version }}-${{ env.cache-name }}-${{ hashFiles('**/elixir/mix.lock') }}
|
||||
- name: Install Dependencies
|
||||
run: mix deps.get --only $MIX_ENV
|
||||
- name: Compile
|
||||
@@ -316,7 +315,7 @@ jobs:
|
||||
- uses: erlef/setup-beam@v1
|
||||
with:
|
||||
otp-version: "25"
|
||||
elixir-version: "1.14"
|
||||
elixir-version: "1.15"
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
@@ -330,7 +329,7 @@ jobs:
|
||||
cache-name: cache-elixir-deps-${{ env.MIX_ENV }}-${{ env.MIX_ENV }}
|
||||
with:
|
||||
path: elixir/deps
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/elixir/mix.lock') }}
|
||||
key: ${{ runner.os }}-${{ steps.setup-beam.outputs.elixir-version }}-${{ steps.setup-beam.outputs.otp-version }}-${{ env.cache-name }}-${{ hashFiles('**/elixir/mix.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ env.cache-name }}-
|
||||
- uses: actions/cache@v3
|
||||
@@ -339,7 +338,7 @@ jobs:
|
||||
cache-name: cache-elixir-build-${{ env.MIX_ENV }}
|
||||
with:
|
||||
path: elixir/_build
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/elixir/mix.lock') }}
|
||||
key: ${{ runner.os }}-${{ steps.setup-beam.outputs.elixir-version }}-${{ steps.setup-beam.outputs.otp-version }}-${{ env.cache-name }}-${{ hashFiles('**/elixir/mix.lock') }}
|
||||
- uses: actions/cache@v3
|
||||
name: pnpm Deps Cache
|
||||
env:
|
||||
@@ -356,6 +355,7 @@ jobs:
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
- run: |
|
||||
export DISPLAY=:99
|
||||
chromedriver --url-base=/wd/hub &
|
||||
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
|
||||
- name: Install Dependencies
|
||||
run: mix deps.get --only $MIX_ENV
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# These are used for the dev environment.
|
||||
# This should match the versions used in the built product.
|
||||
nodejs 18.16.0
|
||||
elixir 1.14.4-otp-25
|
||||
erlang 25.3.2
|
||||
elixir 1.15.0-otp-25
|
||||
erlang 25.3.2.2
|
||||
terraform 1.5.0
|
||||
|
||||
# Used for static analysis
|
||||
|
||||
@@ -26,16 +26,31 @@ services:
|
||||
- app
|
||||
|
||||
vault:
|
||||
image: vault
|
||||
image: vault:1.13.3
|
||||
environment:
|
||||
VAULT_ADDR: 'http://127.0.0.1:8200'
|
||||
VAULT_DEV_ROOT_TOKEN_ID: 'firezone'
|
||||
VAULT_LOG_LEVEL: 'debug'
|
||||
ports:
|
||||
- 8200:8200/tcp
|
||||
cap_add:
|
||||
- IPC_LOCK
|
||||
networks:
|
||||
- app
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"wget",
|
||||
"--spider",
|
||||
"--proxy",
|
||||
"off",
|
||||
"http://localhost:8200/v1/sys/health?standbyok=true"
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 5s
|
||||
|
||||
# Firezone Components
|
||||
web:
|
||||
@@ -93,6 +108,8 @@ services:
|
||||
# Client info
|
||||
USER_AGENT: "iOS/12.5 (iPhone) connlib/0.7.412"
|
||||
depends_on:
|
||||
vault:
|
||||
condition: 'service_healthy'
|
||||
postgres:
|
||||
condition: 'service_healthy'
|
||||
networks:
|
||||
@@ -218,6 +235,8 @@ services:
|
||||
# Seeds
|
||||
STATIC_SEEDS: "true"
|
||||
depends_on:
|
||||
vault:
|
||||
condition: 'service_healthy'
|
||||
postgres:
|
||||
condition: 'service_healthy'
|
||||
networks:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
ARG ELIXIR_VERSION=1.14.3
|
||||
ARG OTP_VERSION=25.2.1
|
||||
ARG ALPINE_VERSION=3.16.3
|
||||
ARG ALPINE_VERSION=3.18.2
|
||||
ARG OTP_VERSION=26.0.1
|
||||
ARG ELIXIR_VERSION=1.15.0
|
||||
|
||||
ARG BUILDER_IMAGE="firezone/elixir:${ELIXIR_VERSION}-otp-${OTP_VERSION}"
|
||||
ARG RUNNER_IMAGE="alpine:${ALPINE_VERSION}"
|
||||
|
||||
@@ -56,9 +56,14 @@ defmodule API.MixProject do
|
||||
|
||||
# Other deps
|
||||
{:jason, "~> 1.2"},
|
||||
{:remote_ip, "~> 1.1"}
|
||||
{:remote_ip, "~> 1.1"},
|
||||
|
||||
# Test deps
|
||||
{:credo, "~> 1.5", only: [:dev, :test], runtime: false},
|
||||
{:dialyxir, "~> 1.1", only: [:dev], runtime: false},
|
||||
{:junit_formatter, "~> 3.3", only: [:test]},
|
||||
{:mix_audit, "~> 2.1", only: [:dev, :test]},
|
||||
{:sobelow, "~> 0.12", only: [:dev, :test]}
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
@@ -12,12 +12,12 @@ defmodule API.Device.SocketTest do
|
||||
|
||||
describe "connect/3" do
|
||||
test "returns error when token is missing" do
|
||||
assert connect(Socket, %{}, @connect_info) == {:error, :missing_token}
|
||||
assert connect(Socket, %{}, connect_info: @connect_info) == {:error, :missing_token}
|
||||
end
|
||||
|
||||
test "returns error when token is invalid" do
|
||||
attrs = connect_attrs(token: "foo")
|
||||
assert connect(Socket, attrs, @connect_info) == {:error, :invalid_token}
|
||||
assert connect(Socket, attrs, connect_info: @connect_info) == {:error, :invalid_token}
|
||||
end
|
||||
|
||||
test "creates a new device" do
|
||||
@@ -26,7 +26,7 @@ defmodule API.Device.SocketTest do
|
||||
|
||||
attrs = connect_attrs(token: token)
|
||||
|
||||
assert {:ok, socket} = connect(Socket, attrs, connect_info(subject))
|
||||
assert {:ok, socket} = connect(Socket, attrs, connect_info: connect_info(subject))
|
||||
assert device = Map.fetch!(socket.assigns, :device)
|
||||
|
||||
assert device.external_id == attrs["external_id"]
|
||||
@@ -43,7 +43,7 @@ defmodule API.Device.SocketTest do
|
||||
|
||||
attrs = connect_attrs(token: token, external_id: existing_device.external_id)
|
||||
|
||||
assert {:ok, socket} = connect(Socket, attrs, connect_info(subject))
|
||||
assert {:ok, socket} = connect(Socket, attrs, connect_info: connect_info(subject))
|
||||
assert device = Repo.one(Domain.Devices.Device)
|
||||
assert device.id == socket.assigns.device.id
|
||||
end
|
||||
|
||||
@@ -14,7 +14,7 @@ defmodule API.Gateway.SocketTest do
|
||||
|
||||
describe "connect/3" do
|
||||
test "returns error when token is missing" do
|
||||
assert connect(Socket, %{}, @connect_info) == {:error, :missing_token}
|
||||
assert connect(Socket, %{}, connect_info: @connect_info) == {:error, :missing_token}
|
||||
end
|
||||
|
||||
test "creates a new gateway" do
|
||||
@@ -23,7 +23,7 @@ defmodule API.Gateway.SocketTest do
|
||||
|
||||
attrs = connect_attrs(token: encrypted_secret)
|
||||
|
||||
assert {:ok, socket} = connect(Socket, attrs, @connect_info)
|
||||
assert {:ok, socket} = connect(Socket, attrs, connect_info: @connect_info)
|
||||
assert gateway = Map.fetch!(socket.assigns, :gateway)
|
||||
|
||||
assert gateway.external_id == attrs["external_id"]
|
||||
@@ -40,14 +40,14 @@ defmodule API.Gateway.SocketTest do
|
||||
|
||||
attrs = connect_attrs(token: encrypted_secret, external_id: existing_gateway.external_id)
|
||||
|
||||
assert {:ok, socket} = connect(Socket, attrs, @connect_info)
|
||||
assert {:ok, socket} = connect(Socket, attrs, connect_info: @connect_info)
|
||||
assert gateway = Repo.one(Domain.Gateways.Gateway)
|
||||
assert gateway.id == socket.assigns.gateway.id
|
||||
end
|
||||
|
||||
test "returns error when token is invalid" do
|
||||
attrs = connect_attrs(token: "foo")
|
||||
assert connect(Socket, attrs, @connect_info) == {:error, :invalid_token}
|
||||
assert connect(Socket, attrs, connect_info: @connect_info) == {:error, :invalid_token}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ defmodule API.Relay.SocketTest do
|
||||
|
||||
describe "connect/3" do
|
||||
test "returns error when token is missing" do
|
||||
assert connect(Socket, %{}, @connect_info) == {:error, :missing_token}
|
||||
assert connect(Socket, %{}, connect_info: @connect_info) == {:error, :missing_token}
|
||||
end
|
||||
|
||||
test "creates a new relay" do
|
||||
@@ -23,7 +23,7 @@ defmodule API.Relay.SocketTest do
|
||||
|
||||
attrs = connect_attrs(token: encrypted_secret)
|
||||
|
||||
assert {:ok, socket} = connect(Socket, attrs, @connect_info)
|
||||
assert {:ok, socket} = connect(Socket, attrs, connect_info: @connect_info)
|
||||
assert relay = Map.fetch!(socket.assigns, :relay)
|
||||
|
||||
assert relay.ipv4.address == attrs["ipv4"]
|
||||
@@ -40,14 +40,14 @@ defmodule API.Relay.SocketTest do
|
||||
|
||||
attrs = connect_attrs(token: encrypted_secret, ipv4: existing_relay.ipv4)
|
||||
|
||||
assert {:ok, socket} = connect(Socket, attrs, @connect_info)
|
||||
assert {:ok, socket} = connect(Socket, attrs, connect_info: @connect_info)
|
||||
assert relay = Repo.one(Domain.Relays.Relay)
|
||||
assert relay.id == socket.assigns.relay.id
|
||||
end
|
||||
|
||||
test "returns error when token is invalid" do
|
||||
attrs = connect_attrs(token: "foo")
|
||||
assert connect(Socket, attrs, @connect_info) == {:error, :invalid_token}
|
||||
assert connect(Socket, attrs, connect_info: @connect_info) == {:error, :invalid_token}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
defmodule Domain.Accounts do
|
||||
alias Domain.Repo
|
||||
alias Domain.{Repo, Validator}
|
||||
alias Domain.Auth
|
||||
alias Domain.Accounts.Account
|
||||
|
||||
def fetch_account_by_id(id) do
|
||||
if Validator.valid_uuid?(id) do
|
||||
Account.Query.by_id(id)
|
||||
|> Repo.fetch()
|
||||
else
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def create_account(attrs) do
|
||||
Account.Changeset.create_changeset(attrs)
|
||||
|> Repo.insert()
|
||||
|
||||
12
elixir/apps/domain/lib/domain/accounts/account/query.ex
Normal file
12
elixir/apps/domain/lib/domain/accounts/account/query.ex
Normal file
@@ -0,0 +1,12 @@
|
||||
defmodule Domain.Accounts.Account.Query do
|
||||
use Domain, :query
|
||||
|
||||
def all do
|
||||
from(account in Domain.Accounts.Account, as: :account)
|
||||
# |> where([account: account], is_nil(account.deleted_at))
|
||||
end
|
||||
|
||||
def by_id(queryable \\ all(), id) do
|
||||
where(queryable, [account: account], account.id == ^id)
|
||||
end
|
||||
end
|
||||
@@ -4,9 +4,7 @@ defmodule Domain.Actors.Actor do
|
||||
schema "actors" do
|
||||
field :type, Ecto.Enum, values: [:account_user, :account_admin_user, :service_account]
|
||||
|
||||
# TODO:
|
||||
# field :first_name, :string
|
||||
# field :last_name, :string
|
||||
field :name, :string
|
||||
|
||||
has_many :identities, Domain.Auth.Identity
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ defmodule Domain.Actors.Actor.Changeset do
|
||||
|
||||
def create_changeset(%Auth.Provider{} = provider, attrs) do
|
||||
%Actors.Actor{}
|
||||
|> cast(attrs, ~w[type]a)
|
||||
|> validate_required(~w[type]a)
|
||||
|> cast(attrs, ~w[type name]a)
|
||||
|> validate_required(~w[type name]a)
|
||||
|> put_change(:account_id, provider.account_id)
|
||||
end
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
defmodule Domain.Auth do
|
||||
use Supervisor
|
||||
alias Domain.Repo
|
||||
alias Domain.Config
|
||||
alias Domain.{Repo, Config, Validator}
|
||||
alias Domain.{Accounts, Actors}
|
||||
alias Domain.Auth.{Authorizer, Subject, Context, Permission, Roles, Role, Identity}
|
||||
alias Domain.Auth.{Adapters, Provider}
|
||||
@@ -25,9 +24,20 @@ defmodule Domain.Auth do
|
||||
|
||||
# Providers
|
||||
|
||||
def fetch_provider_by_id(id) do
|
||||
Provider.Query.by_id(id)
|
||||
|> Repo.fetch()
|
||||
def fetch_active_provider_by_id(id) do
|
||||
if Validator.valid_uuid?(id) do
|
||||
Provider.Query.by_id(id)
|
||||
|> Provider.Query.not_disabled()
|
||||
|> Repo.fetch()
|
||||
else
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def list_active_providers_for_account(%Accounts.Account{} = account) do
|
||||
Provider.Query.by_account_id(account.id)
|
||||
|> Provider.Query.not_disabled()
|
||||
|> Repo.list()
|
||||
end
|
||||
|
||||
def create_provider(%Accounts.Account{} = account, attrs, %Subject{} = subject) do
|
||||
@@ -173,11 +183,13 @@ defmodule Domain.Auth do
|
||||
|
||||
# Sign Up / In / Off
|
||||
|
||||
def sign_in(%Provider{} = provider, provider_identifier, secret, user_agent, remote_ip) do
|
||||
with {:ok, identity} <-
|
||||
fetch_identity_by_provider_and_identifier(provider, provider_identifier),
|
||||
{:ok, identity, expires_at} <-
|
||||
Adapters.verify_secret(provider, identity, secret) do
|
||||
def sign_in(%Provider{} = provider, id_or_provider_identifier, secret, user_agent, remote_ip) do
|
||||
identity_queryable =
|
||||
Identity.Query.by_provider_id(provider.id)
|
||||
|> Identity.Query.by_id_or_provider_identifier(id_or_provider_identifier)
|
||||
|
||||
with {:ok, identity} <- Repo.fetch(identity_queryable),
|
||||
{:ok, identity, expires_at} <- Adapters.verify_secret(provider, identity, secret) do
|
||||
{:ok, build_subject(identity, expires_at, user_agent, remote_ip)}
|
||||
else
|
||||
{:error, :not_found} -> {:error, :unauthorized}
|
||||
@@ -186,7 +198,18 @@ defmodule Domain.Auth do
|
||||
end
|
||||
end
|
||||
|
||||
def sign_in(session_token, user_agent, remote_ip) do
|
||||
def sign_in(%Provider{} = provider, payload, user_agent, remote_ip) do
|
||||
with {:ok, identity, expires_at} <-
|
||||
Adapters.verify_identity(provider, payload) do
|
||||
{:ok, build_subject(identity, expires_at, user_agent, remote_ip)}
|
||||
else
|
||||
{:error, :not_found} -> {:error, :unauthorized}
|
||||
{:error, :invalid} -> {:error, :unauthorized}
|
||||
{:error, :expired} -> {:error, :unauthorized}
|
||||
end
|
||||
end
|
||||
|
||||
def sign_in(session_token, user_agent, remote_ip) when is_binary(session_token) do
|
||||
with {:ok, identity, expires_at} <-
|
||||
verify_session_token(session_token, user_agent, remote_ip) do
|
||||
{:ok, build_subject(identity, expires_at, user_agent, remote_ip)}
|
||||
@@ -198,7 +221,7 @@ defmodule Domain.Auth do
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_identity_by_provider_and_identifier(%Provider{} = provider, provider_identifier) do
|
||||
def fetch_identity_by_provider_and_identifier(%Provider{} = provider, provider_identifier) do
|
||||
Identity.Query.by_provider_id(provider.id)
|
||||
|> Identity.Query.by_provider_identifier(provider_identifier)
|
||||
|> Repo.fetch()
|
||||
@@ -298,7 +321,7 @@ defmodule Domain.Auth do
|
||||
_remote_ip
|
||||
) do
|
||||
with {:ok, identity} <- fetch_identity_by_id(identity_id),
|
||||
{:ok, provider} <- fetch_provider_by_id(identity.provider_id),
|
||||
{:ok, provider} <- fetch_active_provider_by_id(identity.provider_id),
|
||||
{:ok, identity, expires_at} <-
|
||||
Adapters.verify_secret(provider, identity, secret) do
|
||||
{:ok, identity, expires_at}
|
||||
|
||||
@@ -25,13 +25,28 @@ defmodule Domain.Auth.Adapter do
|
||||
@callback ensure_deprovisioned(%Ecto.Changeset{data: %Provider{}}) ::
|
||||
%Ecto.Changeset{data: %Provider{}}
|
||||
|
||||
@doc """
|
||||
A callback invoked during sign-in, should verify the secret and return the identity
|
||||
if it's valid, or an error otherwise.
|
||||
"""
|
||||
@callback verify_secret(%Identity{}, secret :: term()) ::
|
||||
{:ok, %Identity{}, expires_at :: %DateTime{} | nil}
|
||||
| {:error, :invalid_secret}
|
||||
| {:error, :expired_secret}
|
||||
| {:error, :internal_error}
|
||||
defmodule Local do
|
||||
@doc """
|
||||
A callback invoked during sign-in, should verify the secret and return the identity
|
||||
if it's valid, or an error otherwise.
|
||||
|
||||
Used by secret-based providers, eg.: UserPass, Email.
|
||||
"""
|
||||
@callback verify_secret(%Identity{}, secret :: term()) ::
|
||||
{:ok, %Identity{}, expires_at :: %DateTime{} | nil}
|
||||
| {:error, :invalid_secret}
|
||||
| {:error, :expired_secret}
|
||||
| {:error, :internal_error}
|
||||
end
|
||||
|
||||
defmodule IdP do
|
||||
@doc """
|
||||
Used for adapters that are not secret-based, eg. OpenID Connect.
|
||||
"""
|
||||
@callback verify_identity(%Provider{}, payload :: term()) ::
|
||||
{:ok, %Identity{}, expires_at :: %DateTime{} | nil}
|
||||
| {:error, :invalid_secret}
|
||||
| {:error, :expired_secret}
|
||||
| {:error, :internal_error}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -52,6 +52,18 @@ defmodule Domain.Auth.Adapters do
|
||||
end
|
||||
end
|
||||
|
||||
def verify_identity(%Provider{} = provider, payload) do
|
||||
adapter = fetch_adapter!(provider)
|
||||
|
||||
case adapter.verify_identity(provider, payload) do
|
||||
{:ok, %Identity{} = identity, expires_at} -> {:ok, identity, expires_at}
|
||||
{:error, :not_found} -> {:error, :not_found}
|
||||
{:error, :invalid} -> {:error, :invalid}
|
||||
{:error, :expired} -> {:error, :expired}
|
||||
{:error, :internal_error} -> {:error, :internal_error}
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_adapter!(provider) do
|
||||
Map.fetch!(@adapters, provider.adapter)
|
||||
end
|
||||
|
||||
@@ -4,6 +4,7 @@ defmodule Domain.Auth.Adapters.Email do
|
||||
alias Domain.Auth.{Identity, Provider, Adapter}
|
||||
|
||||
@behaviour Adapter
|
||||
@behaviour Adapter.Local
|
||||
|
||||
@sign_in_token_expiration_seconds 15 * 60
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do
|
||||
require Logger
|
||||
|
||||
@behaviour Adapter
|
||||
@behaviour Adapter.IdP
|
||||
|
||||
def start_link(_init_arg) do
|
||||
Supervisor.start_link(__MODULE__, nil, name: __MODULE__)
|
||||
@@ -69,8 +70,8 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def verify_secret(%Identity{} = identity, {redirect_uri, code_verifier, code}) do
|
||||
sync_identity(identity, %{
|
||||
def verify_identity(%Provider{} = provider, {redirect_uri, code_verifier, code}) do
|
||||
sync_identity(provider, %{
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: redirect_uri,
|
||||
code: code,
|
||||
@@ -78,25 +79,28 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do
|
||||
})
|
||||
|> case do
|
||||
{:ok, identity, expires_at} -> {:ok, identity, expires_at}
|
||||
{:error, :expired_token} -> {:error, :expired_secret}
|
||||
{:error, :invalid_token} -> {:error, :invalid_secret}
|
||||
{:error, :not_found} -> {:error, :not_found}
|
||||
{:error, :expired_token} -> {:error, :expired}
|
||||
{:error, :invalid_token} -> {:error, :invalid}
|
||||
{:error, :internal_error} -> {:error, :internal_error}
|
||||
end
|
||||
end
|
||||
|
||||
def refresh_token(%Identity{} = identity) do
|
||||
sync_identity(identity, %{
|
||||
identity = Repo.preload(identity, :provider)
|
||||
|
||||
sync_identity(identity.provider, %{
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: identity.provider_state["refresh_token"]
|
||||
})
|
||||
end
|
||||
|
||||
defp sync_identity(%Identity{} = identity, token_params) do
|
||||
{config, identity} = config_for_identity(identity)
|
||||
defp sync_identity(%Provider{} = provider, token_params) do
|
||||
config = config_for_provider(provider)
|
||||
|
||||
with {:ok, tokens} <- OpenIDConnect.fetch_tokens(config, token_params),
|
||||
{:ok, claims} <- OpenIDConnect.verify(config, tokens["id_token"]),
|
||||
{:ok, userinfo} <- OpenIDConnect.fetch_userinfo(config, tokens["id_token"]) do
|
||||
{:ok, userinfo} <- OpenIDConnect.fetch_userinfo(config, tokens["access_token"]) do
|
||||
# TODO: sync groups
|
||||
expires_at =
|
||||
cond do
|
||||
@@ -110,17 +114,22 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do
|
||||
nil
|
||||
end
|
||||
|
||||
Identity.Query.by_id(identity.id)
|
||||
provider_identifier = claims["sub"]
|
||||
|
||||
Identity.Query.by_provider_id(provider.id)
|
||||
|> Identity.Query.by_provider_identifier(provider_identifier)
|
||||
|> Repo.fetch_and_update(
|
||||
with: fn identity ->
|
||||
Identity.Changeset.update_provider_state(identity, %{
|
||||
id_token: tokens["id_token"],
|
||||
access_token: tokens["access_token"],
|
||||
refresh_token: tokens["refresh_token"],
|
||||
expires_at: expires_at,
|
||||
userinfo: userinfo,
|
||||
claims: claims
|
||||
})
|
||||
Identity.Changeset.update_provider_state(
|
||||
identity,
|
||||
%{
|
||||
access_token: tokens["access_token"],
|
||||
refresh_token: tokens["refresh_token"],
|
||||
expires_at: expires_at,
|
||||
userinfo: userinfo,
|
||||
claims: claims
|
||||
}
|
||||
)
|
||||
end
|
||||
)
|
||||
|> case do
|
||||
@@ -136,8 +145,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do
|
||||
|
||||
{:error, other} ->
|
||||
Logger.error("Failed to connect OpenID Connect provider",
|
||||
provider_id: identity.provider_id,
|
||||
identity_id: identity.id,
|
||||
provider_id: provider.id,
|
||||
reason: inspect(other)
|
||||
)
|
||||
|
||||
@@ -145,11 +153,6 @@ defmodule Domain.Auth.Adapters.OpenIDConnect do
|
||||
end
|
||||
end
|
||||
|
||||
defp config_for_identity(%Identity{} = identity) do
|
||||
identity = Repo.preload(identity, :provider)
|
||||
{config_for_provider(identity.provider), identity}
|
||||
end
|
||||
|
||||
defp config_for_provider(%Provider{} = provider) do
|
||||
Ecto.embedded_load(Settings, provider.adapter_config, :json)
|
||||
|> Map.from_struct()
|
||||
|
||||
@@ -9,6 +9,7 @@ defmodule Domain.Auth.Adapters.Token do
|
||||
alias Domain.Auth.Adapters.Token.State
|
||||
|
||||
@behaviour Adapter
|
||||
@behaviour Adapter.Local
|
||||
|
||||
def start_link(_init_arg) do
|
||||
Supervisor.start_link(__MODULE__, nil, name: __MODULE__)
|
||||
|
||||
@@ -9,6 +9,7 @@ defmodule Domain.Auth.Adapters.UserPass do
|
||||
alias Domain.Auth.Adapters.UserPass.Password
|
||||
|
||||
@behaviour Adapter
|
||||
@behaviour Adapter.Local
|
||||
|
||||
def start_link(_init_arg) do
|
||||
Supervisor.start_link(__MODULE__, nil, name: __MODULE__)
|
||||
|
||||
@@ -4,7 +4,7 @@ defmodule Domain.Auth.Adapters.UserPass.Password.Changeset do
|
||||
|
||||
@fields ~w[password]a
|
||||
@min_password_length 12
|
||||
@max_password_length 64
|
||||
@max_password_length 72
|
||||
|
||||
def create_changeset(attrs) do
|
||||
changeset(%Password{}, attrs)
|
||||
@@ -15,7 +15,18 @@ defmodule Domain.Auth.Adapters.UserPass.Password.Changeset do
|
||||
|> cast(attrs, @fields)
|
||||
|> validate_required(@fields)
|
||||
|> validate_confirmation(:password, required: true)
|
||||
|> validate_length(:password, min: @min_password_length, max: @max_password_length)
|
||||
|> validate_length(:password,
|
||||
min: @min_password_length,
|
||||
max: @max_password_length,
|
||||
count: :bytes
|
||||
)
|
||||
# We can improve password strength checks later if we decide to run this provider in production.
|
||||
# |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
|
||||
# |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")
|
||||
# |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
|
||||
# |> validate_no_repetitive_characters(:password)
|
||||
# |> validate_no_sequential_characters(:password)
|
||||
# |> validate_no_public_context(:password)
|
||||
|> put_hash(:password, to: :password_hash)
|
||||
|> redact_field(:password)
|
||||
|> redact_field(:password_confirmation)
|
||||
|
||||
@@ -4,6 +4,9 @@ defmodule Domain.Auth.Identity.Query do
|
||||
def all do
|
||||
from(identities in Domain.Auth.Identity, as: :identities)
|
||||
|> where([identities: identities], is_nil(identities.deleted_at))
|
||||
|> join(:inner, [identities: identities], actors in assoc(identities, :actor), as: :actors)
|
||||
|> where([actors: actors], is_nil(actors.deleted_at))
|
||||
|> where([actors: actors], is_nil(actors.disabled_at))
|
||||
end
|
||||
|
||||
def by_id(queryable \\ all(), id)
|
||||
@@ -43,6 +46,19 @@ defmodule Domain.Auth.Identity.Query do
|
||||
)
|
||||
end
|
||||
|
||||
def by_id_or_provider_identifier(queryable \\ all(), id_or_provider_identifier) do
|
||||
if Domain.Validator.valid_uuid?(id_or_provider_identifier) do
|
||||
where(
|
||||
queryable,
|
||||
[identities: identities],
|
||||
identities.provider_identifier == ^id_or_provider_identifier or
|
||||
identities.id == ^id_or_provider_identifier
|
||||
)
|
||||
else
|
||||
by_provider_identifier(queryable, id_or_provider_identifier)
|
||||
end
|
||||
end
|
||||
|
||||
def lock(queryable \\ all()) do
|
||||
lock(queryable, "FOR UPDATE")
|
||||
end
|
||||
|
||||
@@ -12,6 +12,14 @@ defmodule Domain.Auth.Provider.Changeset do
|
||||
|> put_change(:account_id, account.id)
|
||||
|> validate_length(:name, min: 1, max: 255)
|
||||
|> validate_required(@required_fields)
|
||||
|> unique_constraint(:adapter,
|
||||
name: :auth_providers_account_id_adapter_index,
|
||||
message: "this provider is already enabled"
|
||||
)
|
||||
|> unique_constraint(:adapter,
|
||||
name: :auth_providers_account_id_oidc_adapter_index,
|
||||
message: "this provider is already connected"
|
||||
)
|
||||
end
|
||||
|
||||
def disable_provider(%Provider{} = provider) do
|
||||
|
||||
@@ -91,7 +91,7 @@ defmodule Domain.Config.Errors do
|
||||
end
|
||||
|
||||
def legacy_key_used(key, legacy_key, removed_at) do
|
||||
Logger.warn(
|
||||
Logger.warning(
|
||||
"A legacy configuration option '#{legacy_key}' is used and it will be removed in v#{removed_at}. " <>
|
||||
"Please use '#{Domain.Config.Resolver.env_key(key)}' configuration option instead."
|
||||
)
|
||||
|
||||
@@ -21,7 +21,9 @@ defmodule Domain.Validator do
|
||||
end
|
||||
|
||||
def validate_email(changeset, field) do
|
||||
validate_format(changeset, field, ~r/@/, message: "is invalid email address")
|
||||
changeset
|
||||
|> validate_format(field, ~r/^[^\s]+@[^\s]+$/, message: "is an invalid email address")
|
||||
|> validate_length(field, max: 160)
|
||||
end
|
||||
|
||||
def validate_uri(changeset, field, opts \\ []) when is_atom(field) do
|
||||
|
||||
@@ -35,7 +35,8 @@ defmodule Domain.MixProject do
|
||||
extra_applications: [
|
||||
:logger,
|
||||
:runtime_tools,
|
||||
:crypto
|
||||
:crypto,
|
||||
:dialyzer
|
||||
]
|
||||
]
|
||||
end
|
||||
@@ -77,7 +78,12 @@ defmodule Domain.MixProject do
|
||||
{:opentelemetry_finch, "~> 0.2.0"},
|
||||
|
||||
# Test and dev deps
|
||||
{:bypass, "~> 2.1", only: :test}
|
||||
{:bypass, "~> 2.1", only: :test},
|
||||
{:credo, "~> 1.5", only: [:dev, :test], runtime: false},
|
||||
{:dialyxir, "~> 1.1", only: [:dev], runtime: false},
|
||||
{:junit_formatter, "~> 3.3", only: [:test]},
|
||||
{:mix_audit, "~> 2.1", only: [:dev, :test]},
|
||||
{:sobelow, "~> 0.12", only: [:dev, :test]}
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
defmodule Domain.Repo.Migrations.MakeAuthProvidersUnique do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create(
|
||||
index(:auth_providers, [:account_id, :adapter],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL AND adapter in ('email', 'userpass', 'token')"
|
||||
)
|
||||
)
|
||||
|
||||
execute("""
|
||||
CREATE UNIQUE INDEX auth_providers_account_id_oidc_adapter_index ON auth_providers (account_id, adapter, (adapter_config->>'client_id'))
|
||||
WHERE deleted_at IS NULL AND adapter = 'openid_connect';
|
||||
""")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
defmodule Domain.Repo.Migrations.AddActorName do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:actors) do
|
||||
add(:name, :string, null: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
36
elixir/apps/domain/test/domain/accounts_test.exs
Normal file
36
elixir/apps/domain/test/domain/accounts_test.exs
Normal file
@@ -0,0 +1,36 @@
|
||||
defmodule Domain.AccountsTest do
|
||||
use Domain.DataCase, async: true
|
||||
import Domain.Accounts
|
||||
alias Domain.{AccountsFixtures, AuthFixtures}
|
||||
|
||||
describe "fetch_account_by_id/1" do
|
||||
test "returns error when account is not found" do
|
||||
assert fetch_account_by_id(Ecto.UUID.generate()) == {:error, :not_found}
|
||||
end
|
||||
|
||||
test "returns error when id is not a valid UUIDv4" do
|
||||
assert fetch_account_by_id("foo") == {:error, :not_found}
|
||||
end
|
||||
|
||||
test "returns account" do
|
||||
account = AccountsFixtures.create_account()
|
||||
assert {:ok, returned_account} = fetch_account_by_id(account.id)
|
||||
assert returned_account.id == account.id
|
||||
end
|
||||
end
|
||||
|
||||
describe "ensure_has_access_to/2" do
|
||||
test "returns :ok if subject has access to the account" do
|
||||
subject = AuthFixtures.create_subject()
|
||||
|
||||
assert ensure_has_access_to(subject, subject.account) == :ok
|
||||
end
|
||||
|
||||
test "returns :error if subject has no access to the account" do
|
||||
account = AccountsFixtures.create_account()
|
||||
subject = AuthFixtures.create_subject()
|
||||
|
||||
assert ensure_has_access_to(subject, account) == {:error, :unauthorized}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -212,7 +212,8 @@ defmodule Domain.ActorsTest do
|
||||
refute changeset.valid?
|
||||
|
||||
assert errors_on(changeset) == %{
|
||||
type: ["can't be blank"]
|
||||
type: ["can't be blank"],
|
||||
name: ["can't be blank"]
|
||||
}
|
||||
end
|
||||
|
||||
@@ -341,12 +342,12 @@ defmodule Domain.ActorsTest do
|
||||
MapSet.difference(admin_permissions, MapSet.new(required_permissions))
|
||||
|> MapSet.to_list()
|
||||
|
||||
attrs = %{type: :account_admin_user}
|
||||
attrs = %{type: :account_admin_user, name: "John Smith"}
|
||||
|
||||
assert create_actor(provider, provider_identifier, attrs, subject) ==
|
||||
{:error, {:unauthorized, privilege_escalation: missing_permissions}}
|
||||
|
||||
attrs = %{"type" => "account_admin_user"}
|
||||
attrs = %{"type" => "account_admin_user", "name" => "John Smith"}
|
||||
|
||||
assert create_actor(provider, provider_identifier, attrs, subject) ==
|
||||
{:error, {:unauthorized, privilege_escalation: missing_permissions}}
|
||||
@@ -452,12 +453,35 @@ defmodule Domain.ActorsTest do
|
||||
allow_child_sandbox_access(test_pid)
|
||||
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_email_provider(account: account)
|
||||
|
||||
actor_one = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
actor_two = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
actor_one =
|
||||
ActorsFixtures.create_actor(
|
||||
type: :account_admin_user,
|
||||
account: account,
|
||||
provider: provider
|
||||
)
|
||||
|
||||
identity_one = AuthFixtures.create_identity(account: account, actor: actor_one)
|
||||
identity_two = AuthFixtures.create_identity(account: account, actor: actor_two)
|
||||
actor_two =
|
||||
ActorsFixtures.create_actor(
|
||||
type: :account_admin_user,
|
||||
account: account,
|
||||
provider: provider
|
||||
)
|
||||
|
||||
identity_one =
|
||||
AuthFixtures.create_identity(
|
||||
account: account,
|
||||
actor: actor_one,
|
||||
provider: provider
|
||||
)
|
||||
|
||||
identity_two =
|
||||
AuthFixtures.create_identity(
|
||||
account: account,
|
||||
actor: actor_two,
|
||||
provider: provider
|
||||
)
|
||||
|
||||
subject_one = AuthFixtures.create_subject(identity_one)
|
||||
subject_two = AuthFixtures.create_subject(identity_two)
|
||||
@@ -634,12 +658,35 @@ defmodule Domain.ActorsTest do
|
||||
allow_child_sandbox_access(test_pid)
|
||||
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_email_provider(account: account)
|
||||
|
||||
actor_one = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
actor_two = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
actor_one =
|
||||
ActorsFixtures.create_actor(
|
||||
type: :account_admin_user,
|
||||
account: account,
|
||||
provider: provider
|
||||
)
|
||||
|
||||
identity_one = AuthFixtures.create_identity(account: account, actor: actor_one)
|
||||
identity_two = AuthFixtures.create_identity(account: account, actor: actor_two)
|
||||
actor_two =
|
||||
ActorsFixtures.create_actor(
|
||||
type: :account_admin_user,
|
||||
account: account,
|
||||
provider: provider
|
||||
)
|
||||
|
||||
identity_one =
|
||||
AuthFixtures.create_identity(
|
||||
account: account,
|
||||
actor: actor_one,
|
||||
provider: provider
|
||||
)
|
||||
|
||||
identity_two =
|
||||
AuthFixtures.create_identity(
|
||||
account: account,
|
||||
actor: actor_two,
|
||||
provider: provider
|
||||
)
|
||||
|
||||
subject_one = AuthFixtures.create_subject(identity_one)
|
||||
subject_two = AuthFixtures.create_subject(identity_two)
|
||||
|
||||
@@ -107,7 +107,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
assert authorization_uri ==
|
||||
"http://localhost:#{bypass.port}/authorize" <>
|
||||
"?access_type=offline" <>
|
||||
"&client_id=google-client-id" <>
|
||||
"&client_id=#{provider.adapter_config["client_id"]}" <>
|
||||
"&code_challenge=#{Domain.Auth.Adapters.OpenIDConnect.PKCE.code_challenge(verifier)}" <>
|
||||
"&code_challenge_method=S256" <>
|
||||
"&redirect_uri=https%3A%2F%2Fexample.com%2F" <>
|
||||
@@ -133,7 +133,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "verify_secret/2" do
|
||||
describe "verify_identity/2" do
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
|
||||
@@ -151,7 +151,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
identity: identity,
|
||||
bypass: bypass
|
||||
} do
|
||||
{token, claims} = generate_token(provider, identity)
|
||||
{token, claims} = AuthFixtures.generate_openid_connect_token(provider, identity)
|
||||
|
||||
AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token})
|
||||
AuthFixtures.expect_userinfo(bypass)
|
||||
@@ -160,13 +160,12 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
redirect_uri = "https://example.com/"
|
||||
payload = {redirect_uri, code_verifier, "MyFakeCode"}
|
||||
|
||||
assert {:ok, identity, expires_at} = verify_secret(identity, payload)
|
||||
assert {:ok, identity, expires_at} = verify_identity(provider, payload)
|
||||
|
||||
assert identity.provider_state == %{
|
||||
access_token: nil,
|
||||
claims: claims,
|
||||
expires_at: expires_at,
|
||||
id_token: token,
|
||||
refresh_token: nil,
|
||||
userinfo: %{
|
||||
"email" => "ada@example.com",
|
||||
@@ -187,7 +186,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
identity: identity,
|
||||
bypass: bypass
|
||||
} do
|
||||
{token, _claims} = generate_token(provider, identity)
|
||||
{token, _claims} = AuthFixtures.generate_openid_connect_token(provider, identity)
|
||||
|
||||
AuthFixtures.expect_refresh_token(bypass, %{
|
||||
"token_type" => "Bearer",
|
||||
@@ -203,9 +202,8 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
redirect_uri = "https://example.com/"
|
||||
payload = {redirect_uri, code_verifier, "MyFakeCode"}
|
||||
|
||||
assert {:ok, identity, _expires_at} = verify_secret(identity, payload)
|
||||
assert {:ok, identity, _expires_at} = verify_identity(provider, payload)
|
||||
|
||||
assert identity.provider_state.id_token == token
|
||||
assert identity.provider_state.access_token == "MY_ACCESS_TOKEN"
|
||||
assert identity.provider_state.refresh_token == "MY_REFRESH_TOKEN"
|
||||
assert DateTime.diff(identity.provider_state.expires_at, DateTime.utc_now()) in 3595..3605
|
||||
@@ -218,7 +216,10 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
} do
|
||||
forty_seconds_ago = DateTime.utc_now() |> DateTime.add(-40, :second) |> DateTime.to_unix()
|
||||
|
||||
{token, _claims} = generate_token(provider, identity, %{"exp" => forty_seconds_ago})
|
||||
{token, _claims} =
|
||||
AuthFixtures.generate_openid_connect_token(provider, identity, %{
|
||||
"exp" => forty_seconds_ago
|
||||
})
|
||||
|
||||
AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token})
|
||||
|
||||
@@ -226,11 +227,11 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
redirect_uri = "https://example.com/"
|
||||
payload = {redirect_uri, code_verifier, "MyFakeCode"}
|
||||
|
||||
assert verify_secret(identity, payload) == {:error, :expired_secret}
|
||||
assert verify_identity(provider, payload) == {:error, :expired}
|
||||
end
|
||||
|
||||
test "returns error when token is invalid", %{
|
||||
identity: identity,
|
||||
provider: provider,
|
||||
bypass: bypass
|
||||
} do
|
||||
token = "foo"
|
||||
@@ -241,11 +242,61 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
redirect_uri = "https://example.com/"
|
||||
payload = {redirect_uri, code_verifier, "MyFakeCode"}
|
||||
|
||||
assert verify_secret(identity, payload) == {:error, :invalid_secret}
|
||||
assert verify_identity(provider, payload) == {:error, :invalid}
|
||||
end
|
||||
|
||||
test "returns error when identity does not exist", %{
|
||||
identity: identity,
|
||||
provider: provider,
|
||||
bypass: bypass
|
||||
} do
|
||||
{token, _claims} =
|
||||
AuthFixtures.generate_openid_connect_token(provider, identity, %{"sub" => "foo@bar.com"})
|
||||
|
||||
AuthFixtures.expect_refresh_token(bypass, %{
|
||||
"token_type" => "Bearer",
|
||||
"id_token" => token,
|
||||
"access_token" => "MY_ACCESS_TOKEN",
|
||||
"refresh_token" => "MY_REFRESH_TOKEN",
|
||||
"expires_in" => 3600
|
||||
})
|
||||
|
||||
AuthFixtures.expect_userinfo(bypass)
|
||||
|
||||
code_verifier = PKCE.code_verifier()
|
||||
redirect_uri = "https://example.com/"
|
||||
payload = {redirect_uri, code_verifier, "MyFakeCode"}
|
||||
|
||||
assert verify_identity(provider, payload) == {:error, :not_found}
|
||||
end
|
||||
|
||||
test "returns error when identity does not belong to provider", %{
|
||||
account: account,
|
||||
provider: provider,
|
||||
bypass: bypass
|
||||
} do
|
||||
identity = AuthFixtures.create_identity(account: account)
|
||||
{token, _claims} = AuthFixtures.generate_openid_connect_token(provider, identity)
|
||||
|
||||
AuthFixtures.expect_refresh_token(bypass, %{
|
||||
"token_type" => "Bearer",
|
||||
"id_token" => token,
|
||||
"access_token" => "MY_ACCESS_TOKEN",
|
||||
"refresh_token" => "MY_REFRESH_TOKEN",
|
||||
"expires_in" => 3600
|
||||
})
|
||||
|
||||
AuthFixtures.expect_userinfo(bypass)
|
||||
|
||||
code_verifier = PKCE.code_verifier()
|
||||
redirect_uri = "https://example.com/"
|
||||
payload = {redirect_uri, code_verifier, "MyFakeCode"}
|
||||
|
||||
assert verify_identity(provider, payload) == {:error, :not_found}
|
||||
end
|
||||
|
||||
test "returns error when provider is down", %{
|
||||
identity: identity,
|
||||
provider: provider,
|
||||
bypass: bypass
|
||||
} do
|
||||
Bypass.down(bypass)
|
||||
@@ -254,7 +305,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
redirect_uri = "https://example.com/"
|
||||
payload = {redirect_uri, code_verifier, "MyFakeCode"}
|
||||
|
||||
assert verify_secret(identity, payload) == {:error, :internal_error}
|
||||
assert verify_identity(provider, payload) == {:error, :internal_error}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -276,7 +327,7 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
identity: identity,
|
||||
bypass: bypass
|
||||
} do
|
||||
{token, claims} = generate_token(provider, identity)
|
||||
{token, claims} = AuthFixtures.generate_openid_connect_token(provider, identity)
|
||||
|
||||
AuthFixtures.expect_refresh_token(bypass, %{
|
||||
"token_type" => "Bearer",
|
||||
@@ -294,7 +345,6 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
access_token: "MY_ACCESS_TOKEN",
|
||||
claims: claims,
|
||||
expires_at: expires_at,
|
||||
id_token: token,
|
||||
refresh_token: "MY_REFRESH_TOKEN",
|
||||
userinfo: %{
|
||||
"email" => "ada@example.com",
|
||||
@@ -312,27 +362,4 @@ defmodule Domain.Auth.Adapters.OpenIDConnectTest do
|
||||
assert DateTime.diff(expires_at, DateTime.utc_now()) in 5..15
|
||||
end
|
||||
end
|
||||
|
||||
defp generate_token(provider, identity, claims \\ %{}) do
|
||||
jwk = AuthFixtures.jwks_attrs()
|
||||
|
||||
claims =
|
||||
Map.merge(
|
||||
%{
|
||||
"email" => "foo@example.com",
|
||||
"sub" => identity.provider_identifier,
|
||||
"aud" => provider.adapter_config["client_id"],
|
||||
"exp" => DateTime.utc_now() |> DateTime.add(10, :second) |> DateTime.to_unix()
|
||||
},
|
||||
claims
|
||||
)
|
||||
|
||||
{_alg, token} =
|
||||
jwk
|
||||
|> JOSE.JWK.from()
|
||||
|> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"})
|
||||
|> JOSE.JWS.compact()
|
||||
|
||||
{token, claims}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -50,7 +50,7 @@ defmodule Domain.Auth.Adapters.UserPassTest do
|
||||
|
||||
assert errors_on(changeset) == %{
|
||||
provider_virtual_state: %{
|
||||
password: ["should be at least 12 character(s)"],
|
||||
password: ["should be at least 12 byte(s)"],
|
||||
password_confirmation: ["does not match confirmation"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
# defmodule Domain.Auth.OIDC.RefresherTest do
|
||||
# use Domain.DataCase, async: true
|
||||
# alias Domain.Auth.OIDC.Refresher
|
||||
# alias Domain.UsersFixtures
|
||||
|
||||
# setup do
|
||||
# user = UsersFixtures.create_user_with_role(:account_admin_user)
|
||||
# {bypass, [provider_attrs]} = Domain.AuthFixtures.start_openid_providers(["google"])
|
||||
|
||||
# conn =
|
||||
# Repo.insert!(%Domain.Auth.OIDC.Connection{
|
||||
# user_id: user.id,
|
||||
# provider: "google",
|
||||
# refresh_token: "REFRESH_TOKEN"
|
||||
# })
|
||||
|
||||
# %{user: user, conn: conn, bypass: bypass, provider_attrs: provider_attrs}
|
||||
# end
|
||||
|
||||
# describe "refresh failed" do
|
||||
# test "disable user", %{user: user, conn: conn, bypass: bypass} do
|
||||
# Domain.AuthFixtures.expect_refresh_token_failure(bypass)
|
||||
|
||||
# assert Refresher.refresh(user.id) == {:stop, :shutdown, user.id}
|
||||
# user = Repo.reload(user)
|
||||
# assert user.disabled_at
|
||||
|
||||
# conn = Repo.reload(conn)
|
||||
# assert %{"error" => _} = conn.refresh_response
|
||||
# end
|
||||
# end
|
||||
|
||||
# describe "refresh succeeded" do
|
||||
# test "does not change user", %{user: user, conn: conn, bypass: bypass} do
|
||||
# Domain.AuthFixtures.expect_refresh_token(bypass)
|
||||
|
||||
# assert Refresher.refresh(user.id) == {:stop, :shutdown, user.id}
|
||||
# user = Repo.reload(user)
|
||||
# refute user.disabled_at
|
||||
|
||||
# conn = Repo.reload(conn)
|
||||
# refute match?(%{"error" => _}, conn.refresh_response)
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
@@ -6,6 +6,79 @@ defmodule Domain.AuthTest do
|
||||
alias Domain.Auth.Authorizer
|
||||
alias Domain.{AccountsFixtures, AuthFixtures}
|
||||
|
||||
describe "fetch_active_provider_by_id/1" do
|
||||
test "returns error when provider does not exist" do
|
||||
assert fetch_active_provider_by_id(Ecto.UUID.generate()) == {:error, :not_found}
|
||||
end
|
||||
|
||||
test "returns error when provider is disabled" do
|
||||
account = AccountsFixtures.create_account()
|
||||
AuthFixtures.create_userpass_provider(account: account)
|
||||
provider = AuthFixtures.create_email_provider(account: account)
|
||||
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
actor_default_type: :account_admin_user,
|
||||
account: account,
|
||||
provider: provider
|
||||
)
|
||||
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
{:ok, _provider} = disable_provider(provider, subject)
|
||||
|
||||
assert fetch_active_provider_by_id(provider.id) == {:error, :not_found}
|
||||
end
|
||||
|
||||
test "returns error when provider is deleted" do
|
||||
account = AccountsFixtures.create_account()
|
||||
AuthFixtures.create_userpass_provider(account: account)
|
||||
provider = AuthFixtures.create_email_provider(account: account)
|
||||
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
actor_default_type: :account_admin_user,
|
||||
account: account,
|
||||
provider: provider
|
||||
)
|
||||
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
{:ok, _provider} = delete_provider(provider, subject)
|
||||
|
||||
assert fetch_active_provider_by_id(provider.id) == {:error, :not_found}
|
||||
end
|
||||
|
||||
test "returns provider" do
|
||||
provider = AuthFixtures.create_email_provider()
|
||||
assert {:ok, fetched_provider} = fetch_active_provider_by_id(provider.id)
|
||||
assert fetched_provider.id == provider.id
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_active_providers_for_account/1" do
|
||||
test "returns active providers for a given account" do
|
||||
account = AccountsFixtures.create_account()
|
||||
|
||||
userpass_provider = AuthFixtures.create_userpass_provider(account: account)
|
||||
email_provider = AuthFixtures.create_email_provider(account: account)
|
||||
token_provider = AuthFixtures.create_token_provider(account: account)
|
||||
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
actor_default_type: :account_admin_user,
|
||||
account: account,
|
||||
provider: email_provider
|
||||
)
|
||||
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
{:ok, _provider} = disable_provider(token_provider, subject)
|
||||
{:ok, _provider} = delete_provider(email_provider, subject)
|
||||
|
||||
assert {:ok, [provider]} = list_active_providers_for_account(account)
|
||||
assert provider.id == userpass_provider.id
|
||||
end
|
||||
end
|
||||
|
||||
describe "create_provider/2" do
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
@@ -48,6 +121,56 @@ defmodule Domain.AuthTest do
|
||||
}
|
||||
end
|
||||
|
||||
test "returns error if email provider is already enabled", %{
|
||||
account: account
|
||||
} do
|
||||
# email, userpass, token
|
||||
AuthFixtures.create_email_provider(account: account)
|
||||
attrs = AuthFixtures.provider_attrs(adapter: :email)
|
||||
assert {:error, changeset} = create_provider(account, attrs)
|
||||
refute changeset.valid?
|
||||
assert errors_on(changeset) == %{adapter: ["this provider is already enabled"]}
|
||||
end
|
||||
|
||||
test "returns error if userpass provider is already enabled", %{
|
||||
account: account
|
||||
} do
|
||||
# userpass, userpass, token
|
||||
AuthFixtures.create_userpass_provider(account: account)
|
||||
attrs = AuthFixtures.provider_attrs(adapter: :userpass)
|
||||
assert {:error, changeset} = create_provider(account, attrs)
|
||||
refute changeset.valid?
|
||||
assert errors_on(changeset) == %{adapter: ["this provider is already enabled"]}
|
||||
end
|
||||
|
||||
test "returns error if token provider is already enabled", %{
|
||||
account: account
|
||||
} do
|
||||
AuthFixtures.create_token_provider(account: account)
|
||||
attrs = AuthFixtures.provider_attrs(adapter: :token)
|
||||
assert {:error, changeset} = create_provider(account, attrs)
|
||||
refute changeset.valid?
|
||||
assert errors_on(changeset) == %{adapter: ["this provider is already enabled"]}
|
||||
end
|
||||
|
||||
test "returns error if openid connect provider is already enabled", %{
|
||||
account: account
|
||||
} do
|
||||
{provider, _bypass} =
|
||||
AuthFixtures.start_openid_providers(["google"])
|
||||
|> AuthFixtures.create_openid_connect_provider(account: account)
|
||||
|
||||
attrs =
|
||||
AuthFixtures.provider_attrs(
|
||||
adapter: :openid_connect,
|
||||
adapter_config: provider.adapter_config
|
||||
)
|
||||
|
||||
assert {:error, changeset} = create_provider(account, attrs)
|
||||
refute changeset.valid?
|
||||
assert errors_on(changeset) == %{adapter: ["this provider is already connected"]}
|
||||
end
|
||||
|
||||
test "creates a provider", %{
|
||||
account: account
|
||||
} do
|
||||
@@ -135,7 +258,7 @@ defmodule Domain.AuthTest do
|
||||
subject: subject,
|
||||
provider: provider
|
||||
} do
|
||||
other_provider = AuthFixtures.create_email_provider(account: account)
|
||||
other_provider = AuthFixtures.create_userpass_provider(account: account)
|
||||
|
||||
assert {:ok, provider} = disable_provider(provider, subject)
|
||||
assert provider.disabled_at
|
||||
@@ -168,7 +291,7 @@ defmodule Domain.AuthTest do
|
||||
subject: subject,
|
||||
provider: provider
|
||||
} do
|
||||
other_provider = AuthFixtures.create_email_provider(account: account)
|
||||
other_provider = AuthFixtures.create_userpass_provider(account: account)
|
||||
{:ok, _other_provider} = disable_provider(other_provider, subject)
|
||||
|
||||
assert disable_provider(provider, subject) == {:error, :cant_disable_the_last_provider}
|
||||
@@ -184,7 +307,7 @@ defmodule Domain.AuthTest do
|
||||
account = AccountsFixtures.create_account()
|
||||
|
||||
provider_one = AuthFixtures.create_email_provider(account: account)
|
||||
provider_two = AuthFixtures.create_email_provider(account: account)
|
||||
provider_two = AuthFixtures.create_userpass_provider(account: account)
|
||||
|
||||
actor =
|
||||
ActorsFixtures.create_actor(
|
||||
@@ -224,7 +347,7 @@ defmodule Domain.AuthTest do
|
||||
subject: subject,
|
||||
account: account
|
||||
} do
|
||||
provider = AuthFixtures.create_email_provider(account: account)
|
||||
provider = AuthFixtures.create_userpass_provider(account: account)
|
||||
assert {:ok, _provider} = disable_provider(provider, subject)
|
||||
assert {:ok, provider} = disable_provider(provider, subject)
|
||||
assert {:ok, _provider} = disable_provider(provider, subject)
|
||||
@@ -233,7 +356,7 @@ defmodule Domain.AuthTest do
|
||||
test "does not allow to disable providers in other accounts", %{
|
||||
subject: subject
|
||||
} do
|
||||
provider = AuthFixtures.create_email_provider()
|
||||
provider = AuthFixtures.create_userpass_provider()
|
||||
assert disable_provider(provider, subject) == {:error, :not_found}
|
||||
end
|
||||
|
||||
@@ -334,7 +457,7 @@ defmodule Domain.AuthTest do
|
||||
subject: subject,
|
||||
provider: provider
|
||||
} do
|
||||
other_provider = AuthFixtures.create_email_provider(account: account)
|
||||
other_provider = AuthFixtures.create_userpass_provider(account: account)
|
||||
|
||||
assert {:ok, provider} = delete_provider(provider, subject)
|
||||
assert provider.deleted_at
|
||||
@@ -367,7 +490,7 @@ defmodule Domain.AuthTest do
|
||||
subject: subject,
|
||||
provider: provider
|
||||
} do
|
||||
other_provider = AuthFixtures.create_email_provider(account: account)
|
||||
other_provider = AuthFixtures.create_userpass_provider(account: account)
|
||||
{:ok, _other_provider} = delete_provider(other_provider, subject)
|
||||
|
||||
assert delete_provider(provider, subject) == {:error, :cant_delete_the_last_provider}
|
||||
@@ -383,7 +506,7 @@ defmodule Domain.AuthTest do
|
||||
account = AccountsFixtures.create_account()
|
||||
|
||||
provider_one = AuthFixtures.create_email_provider(account: account)
|
||||
provider_two = AuthFixtures.create_email_provider(account: account)
|
||||
provider_two = AuthFixtures.create_userpass_provider(account: account)
|
||||
|
||||
actor =
|
||||
ActorsFixtures.create_actor(
|
||||
@@ -419,7 +542,7 @@ defmodule Domain.AuthTest do
|
||||
subject: subject,
|
||||
account: account
|
||||
} do
|
||||
provider = AuthFixtures.create_email_provider(account: account)
|
||||
provider = AuthFixtures.create_userpass_provider(account: account)
|
||||
assert {:ok, deleted_provider} = delete_provider(provider, subject)
|
||||
assert delete_provider(provider, subject) == {:error, :not_found}
|
||||
assert delete_provider(deleted_provider, subject) == {:error, :not_found}
|
||||
@@ -428,7 +551,7 @@ defmodule Domain.AuthTest do
|
||||
test "does not allow to delete providers in other accounts", %{
|
||||
subject: subject
|
||||
} do
|
||||
provider = AuthFixtures.create_email_provider()
|
||||
provider = AuthFixtures.create_userpass_provider()
|
||||
assert delete_provider(provider, subject) == {:error, :not_found}
|
||||
end
|
||||
|
||||
@@ -496,7 +619,7 @@ defmodule Domain.AuthTest do
|
||||
|
||||
provider_identifier = Ecto.UUID.generate()
|
||||
assert {:error, changeset} = create_identity(actor, provider, provider_identifier)
|
||||
assert errors_on(changeset) == %{provider_identifier: ["is invalid email address"]}
|
||||
assert errors_on(changeset) == %{provider_identifier: ["is an invalid email address"]}
|
||||
|
||||
provider_identifier = nil
|
||||
assert {:error, changeset} = create_identity(actor, provider, provider_identifier)
|
||||
@@ -540,7 +663,7 @@ defmodule Domain.AuthTest do
|
||||
} do
|
||||
provider_identifier = Ecto.UUID.generate()
|
||||
assert {:error, changeset} = replace_identity(identity, provider_identifier, subject)
|
||||
assert errors_on(changeset) == %{provider_identifier: ["is invalid email address"]}
|
||||
assert errors_on(changeset) == %{provider_identifier: ["is an invalid email address"]}
|
||||
|
||||
provider_identifier = nil
|
||||
assert {:error, changeset} = replace_identity(identity, provider_identifier, subject)
|
||||
@@ -750,7 +873,7 @@ defmodule Domain.AuthTest do
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
|
||||
test "returns subject on success", %{
|
||||
test "returns subject on success using provider identifier", %{
|
||||
account: account,
|
||||
provider: provider,
|
||||
user_agent: user_agent,
|
||||
@@ -769,6 +892,25 @@ defmodule Domain.AuthTest do
|
||||
assert subject.context.user_agent == user_agent
|
||||
end
|
||||
|
||||
test "returns subject on success using identity id", %{
|
||||
account: account,
|
||||
provider: provider,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip
|
||||
} do
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider)
|
||||
secret = identity.provider_virtual_state.sign_in_token
|
||||
|
||||
assert {:ok, %Auth.Subject{} = subject} =
|
||||
sign_in(provider, identity.id, secret, user_agent, remote_ip)
|
||||
|
||||
assert subject.account.id == account.id
|
||||
assert subject.actor.id == identity.actor_id
|
||||
assert subject.identity.id == identity.id
|
||||
assert subject.context.remote_ip == remote_ip
|
||||
assert subject.context.user_agent == user_agent
|
||||
end
|
||||
|
||||
test "returned subject expiration depends on user type", %{
|
||||
account: account,
|
||||
provider: provider,
|
||||
@@ -814,6 +956,56 @@ defmodule Domain.AuthTest do
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
|
||||
test "returns error when identity is disabled", %{
|
||||
account: account,
|
||||
provider: provider,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip
|
||||
} do
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor)
|
||||
secret = identity.provider_virtual_state.sign_in_token
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
{:ok, identity} = delete_identity(identity, subject)
|
||||
|
||||
assert sign_in(provider, identity.provider_identifier, secret, user_agent, remote_ip) ==
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
|
||||
test "returns error when actor is disabled", %{
|
||||
account: account,
|
||||
provider: provider,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip
|
||||
} do
|
||||
actor =
|
||||
ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
|> ActorsFixtures.disable()
|
||||
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor)
|
||||
secret = identity.provider_virtual_state.sign_in_token
|
||||
|
||||
assert sign_in(provider, identity.provider_identifier, secret, user_agent, remote_ip) ==
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
|
||||
test "returns error when actor is deleted", %{
|
||||
account: account,
|
||||
provider: provider,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip
|
||||
} do
|
||||
actor =
|
||||
ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
|> ActorsFixtures.delete()
|
||||
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor)
|
||||
secret = identity.provider_virtual_state.sign_in_token
|
||||
|
||||
assert sign_in(provider, identity.provider_identifier, secret, user_agent, remote_ip) ==
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
|
||||
test "returns error when provider is deleted", %{
|
||||
account: account,
|
||||
provider: provider,
|
||||
@@ -851,6 +1043,278 @@ defmodule Domain.AuthTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "sign_in/4" do
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
|
||||
{provider, bypass} =
|
||||
AuthFixtures.start_openid_providers(["google"])
|
||||
|> AuthFixtures.create_openid_connect_provider(account: account)
|
||||
|
||||
user_agent = AuthFixtures.user_agent()
|
||||
remote_ip = AuthFixtures.remote_ip()
|
||||
|
||||
%{
|
||||
bypass: bypass,
|
||||
account: account,
|
||||
provider: provider,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip
|
||||
}
|
||||
end
|
||||
|
||||
test "returns error when provider_identifier does not exist", %{
|
||||
bypass: bypass,
|
||||
account: account,
|
||||
provider: provider,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip
|
||||
} do
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider)
|
||||
|
||||
{token, _claims} =
|
||||
AuthFixtures.generate_openid_connect_token(provider, identity, %{
|
||||
"sub" => "foo@bar.com"
|
||||
})
|
||||
|
||||
AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token})
|
||||
AuthFixtures.expect_userinfo(bypass)
|
||||
|
||||
code_verifier = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_verifier()
|
||||
redirect_uri = "https://example.com/"
|
||||
payload = {redirect_uri, code_verifier, "MyFakeCode"}
|
||||
|
||||
assert sign_in(provider, payload, user_agent, remote_ip) ==
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
|
||||
test "returns error when token is invalid", %{
|
||||
bypass: bypass,
|
||||
provider: provider,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip
|
||||
} do
|
||||
AuthFixtures.expect_refresh_token(bypass, %{"id_token" => "foo"})
|
||||
|
||||
code_verifier = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_verifier()
|
||||
redirect_uri = "https://example.com/"
|
||||
payload = {redirect_uri, code_verifier, "MyFakeCode"}
|
||||
|
||||
assert sign_in(provider, payload, user_agent, remote_ip) ==
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
|
||||
test "returns subject on success using sub claim", %{
|
||||
bypass: bypass,
|
||||
account: account,
|
||||
provider: provider,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip
|
||||
} do
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider)
|
||||
|
||||
token =
|
||||
AuthFixtures.sign_openid_connect_token(%{
|
||||
"sub" => identity.provider_identifier,
|
||||
"aud" => provider.adapter_config["client_id"],
|
||||
"exp" => DateTime.utc_now() |> DateTime.add(10, :second) |> DateTime.to_unix()
|
||||
})
|
||||
|
||||
AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token})
|
||||
AuthFixtures.expect_userinfo(bypass)
|
||||
|
||||
code_verifier = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_verifier()
|
||||
redirect_uri = "https://example.com/"
|
||||
payload = {redirect_uri, code_verifier, "MyFakeCode"}
|
||||
|
||||
assert {:ok, %Auth.Subject{} = subject} =
|
||||
sign_in(provider, payload, user_agent, remote_ip)
|
||||
|
||||
assert subject.account.id == account.id
|
||||
assert subject.actor.id == identity.actor_id
|
||||
assert subject.identity.id == identity.id
|
||||
assert subject.context.remote_ip == remote_ip
|
||||
assert subject.context.user_agent == user_agent
|
||||
end
|
||||
|
||||
# test "returned subject expiration depends on user type", %{
|
||||
# account: account,
|
||||
# provider: provider,
|
||||
# user_agent: user_agent,
|
||||
# remote_ip: remote_ip
|
||||
# } do
|
||||
# actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
# identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor)
|
||||
|
||||
# code_verifier = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_verifier()
|
||||
# redirect_uri = "https://example.com/"
|
||||
# payload = {redirect_uri, code_verifier, "MyFakeCode"}
|
||||
|
||||
# assert {:ok, %Auth.Subject{} = subject} =
|
||||
# sign_in(provider, payload, user_agent, remote_ip)
|
||||
|
||||
# three_hours = 3 * 60 * 60
|
||||
# assert_datetime_diff(subject.expires_at, DateTime.utc_now(), three_hours)
|
||||
|
||||
# actor = ActorsFixtures.create_actor(type: :account_user, account: account)
|
||||
# identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor)
|
||||
|
||||
# assert {:ok, %Auth.Subject{} = subject} =
|
||||
# sign_in(provider, payload, user_agent, remote_ip)
|
||||
|
||||
# one_week = 7 * 24 * 60 * 60
|
||||
# assert_datetime_diff(subject.expires_at, DateTime.utc_now(), one_week)
|
||||
# end
|
||||
|
||||
test "returns error when provider is disabled", %{
|
||||
bypass: bypass,
|
||||
account: account,
|
||||
provider: provider,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip
|
||||
} do
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
{:ok, _provider} = disable_provider(provider, subject)
|
||||
|
||||
{token, _claims} = AuthFixtures.generate_openid_connect_token(provider, identity)
|
||||
AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token})
|
||||
AuthFixtures.expect_userinfo(bypass)
|
||||
|
||||
code_verifier = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_verifier()
|
||||
redirect_uri = "https://example.com/"
|
||||
payload = {redirect_uri, code_verifier, "MyFakeCode"}
|
||||
|
||||
assert sign_in(provider, payload, user_agent, remote_ip) ==
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
|
||||
test "returns error when identity is disabled", %{
|
||||
bypass: bypass,
|
||||
account: account,
|
||||
provider: provider,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip
|
||||
} do
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
{:ok, identity} = delete_identity(identity, subject)
|
||||
|
||||
{token, _claims} = AuthFixtures.generate_openid_connect_token(provider, identity)
|
||||
AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token})
|
||||
AuthFixtures.expect_userinfo(bypass)
|
||||
|
||||
code_verifier = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_verifier()
|
||||
redirect_uri = "https://example.com/"
|
||||
payload = {redirect_uri, code_verifier, "MyFakeCode"}
|
||||
|
||||
assert sign_in(provider, payload, user_agent, remote_ip) ==
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
|
||||
test "returns error when actor is disabled", %{
|
||||
bypass: bypass,
|
||||
account: account,
|
||||
provider: provider,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip
|
||||
} do
|
||||
actor =
|
||||
ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
|> ActorsFixtures.disable()
|
||||
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor)
|
||||
|
||||
{token, _claims} = AuthFixtures.generate_openid_connect_token(provider, identity)
|
||||
AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token})
|
||||
AuthFixtures.expect_userinfo(bypass)
|
||||
|
||||
code_verifier = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_verifier()
|
||||
redirect_uri = "https://example.com/"
|
||||
payload = {redirect_uri, code_verifier, "MyFakeCode"}
|
||||
|
||||
assert sign_in(provider, payload, user_agent, remote_ip) ==
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
|
||||
test "returns error when actor is deleted", %{
|
||||
bypass: bypass,
|
||||
account: account,
|
||||
provider: provider,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip
|
||||
} do
|
||||
actor =
|
||||
ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
|> ActorsFixtures.delete()
|
||||
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor)
|
||||
|
||||
{token, _claims} = AuthFixtures.generate_openid_connect_token(provider, identity)
|
||||
AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token})
|
||||
AuthFixtures.expect_userinfo(bypass)
|
||||
|
||||
code_verifier = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_verifier()
|
||||
redirect_uri = "https://example.com/"
|
||||
payload = {redirect_uri, code_verifier, "MyFakeCode"}
|
||||
|
||||
assert sign_in(provider, payload, user_agent, remote_ip) ==
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
|
||||
test "returns error when provider is deleted", %{
|
||||
bypass: bypass,
|
||||
account: account,
|
||||
provider: provider,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip
|
||||
} do
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider, actor: actor)
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
{:ok, _provider} = delete_provider(provider, subject)
|
||||
|
||||
{token, _claims} = AuthFixtures.generate_openid_connect_token(provider, identity)
|
||||
AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token})
|
||||
AuthFixtures.expect_userinfo(bypass)
|
||||
|
||||
code_verifier = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_verifier()
|
||||
redirect_uri = "https://example.com/"
|
||||
payload = {redirect_uri, code_verifier, "MyFakeCode"}
|
||||
|
||||
assert sign_in(provider, payload, user_agent, remote_ip) ==
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
|
||||
test "updates last signed in fields for identity on success", %{
|
||||
bypass: bypass,
|
||||
account: account,
|
||||
provider: provider,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip
|
||||
} do
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider)
|
||||
|
||||
{token, _claims} = AuthFixtures.generate_openid_connect_token(provider, identity)
|
||||
AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token})
|
||||
AuthFixtures.expect_userinfo(bypass)
|
||||
|
||||
code_verifier = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_verifier()
|
||||
redirect_uri = "https://example.com/"
|
||||
payload = {redirect_uri, code_verifier, "MyFakeCode"}
|
||||
|
||||
assert {:ok, _subject} =
|
||||
sign_in(provider, payload, user_agent, remote_ip)
|
||||
|
||||
assert updated_identity = Repo.one(Auth.Identity)
|
||||
assert updated_identity.last_seen_at != identity.last_seen_at
|
||||
assert updated_identity.last_seen_remote_ip != identity.last_seen_remote_ip
|
||||
assert updated_identity.last_seen_user_agent != identity.last_seen_user_agent
|
||||
end
|
||||
end
|
||||
|
||||
describe "sign_in/3" do
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
|
||||
@@ -4,7 +4,11 @@ defmodule Domain.ActorsFixtures do
|
||||
alias Domain.{AccountsFixtures, AuthFixtures}
|
||||
|
||||
def actor_attrs(attrs \\ %{}) do
|
||||
first_name = Enum.random(~w[Wade Dave Seth Riley Gilbert Jorge Dan Brian Roberto Ramon])
|
||||
last_name = Enum.random(~w[Robyn Traci Desiree Jon Bob Karl Joe Alberta Lynda Cara Brandi])
|
||||
|
||||
Enum.into(attrs, %{
|
||||
name: "#{first_name} #{last_name}",
|
||||
type: :account_user
|
||||
})
|
||||
end
|
||||
@@ -19,7 +23,11 @@ defmodule Domain.ActorsFixtures do
|
||||
|
||||
{provider, attrs} =
|
||||
Map.pop_lazy(attrs, :provider, fn ->
|
||||
AuthFixtures.create_email_provider(account: account)
|
||||
{provider, _bypass} =
|
||||
AuthFixtures.start_openid_providers(["google"])
|
||||
|> AuthFixtures.create_openid_connect_provider(account: account)
|
||||
|
||||
provider
|
||||
end)
|
||||
|
||||
attrs = actor_attrs(attrs)
|
||||
@@ -37,4 +45,8 @@ defmodule Domain.ActorsFixtures do
|
||||
def disable(actor) do
|
||||
update(actor, %{disabled_at: DateTime.utc_now()})
|
||||
end
|
||||
|
||||
def delete(actor) do
|
||||
update(actor, %{deleted_at: DateTime.utc_now()})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,8 +4,10 @@ defmodule Domain.AuthFixtures do
|
||||
alias Domain.AccountsFixtures
|
||||
alias Domain.ActorsFixtures
|
||||
|
||||
def user_password, do: "Hello w0rld!"
|
||||
def remote_ip, do: {100, 64, 100, 58}
|
||||
def user_agent, do: "iOS/12.5 (iPhone) connlib/0.7.412"
|
||||
def email, do: "user-#{counter()}@example.com"
|
||||
|
||||
def random_provider_identifier(%Domain.Auth.Provider{adapter: :email, name: name}) do
|
||||
"user-#{counter()}@#{String.downcase(name)}.com"
|
||||
@@ -54,7 +56,7 @@ defmodule Domain.AuthFixtures do
|
||||
end)
|
||||
|
||||
attrs =
|
||||
%{adapter_config: provider_attrs}
|
||||
%{adapter: :openid_connect, adapter_config: provider_attrs}
|
||||
|> Map.merge(attrs)
|
||||
|> provider_attrs()
|
||||
|
||||
@@ -100,7 +102,11 @@ defmodule Domain.AuthFixtures do
|
||||
|
||||
{provider, attrs} =
|
||||
Map.pop_lazy(attrs, :provider, fn ->
|
||||
create_email_provider(account: account)
|
||||
{provider, _bypass} =
|
||||
start_openid_providers(["google"])
|
||||
|> create_openid_connect_provider(account: account)
|
||||
|
||||
provider
|
||||
end)
|
||||
|
||||
{provider_identifier, attrs} =
|
||||
@@ -108,9 +114,13 @@ defmodule Domain.AuthFixtures do
|
||||
random_provider_identifier(provider)
|
||||
end)
|
||||
|
||||
{actor_default_type, attrs} =
|
||||
Map.pop(attrs, :actor_default_type, :account_user)
|
||||
|
||||
{actor, _attrs} =
|
||||
Map.pop_lazy(attrs, :actor, fn ->
|
||||
ActorsFixtures.create_actor(
|
||||
type: actor_default_type,
|
||||
account: account,
|
||||
provider: provider,
|
||||
provider_identifier: provider_identifier
|
||||
@@ -134,10 +144,27 @@ defmodule Domain.AuthFixtures do
|
||||
end
|
||||
end
|
||||
|
||||
def delete_identity(identity) do
|
||||
identity
|
||||
|> Ecto.Changeset.change(deleted_at: DateTime.utc_now())
|
||||
|> Repo.update!()
|
||||
end
|
||||
|
||||
def create_subject do
|
||||
account = AccountsFixtures.create_account()
|
||||
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
identity = create_identity(actor: actor, account: account)
|
||||
|
||||
{provider, _bypass} =
|
||||
start_openid_providers(["google"])
|
||||
|> create_openid_connect_provider(account: account)
|
||||
|
||||
actor =
|
||||
ActorsFixtures.create_actor(
|
||||
type: :account_admin_user,
|
||||
account: account,
|
||||
provider: provider
|
||||
)
|
||||
|
||||
identity = create_identity(actor: actor, account: account, provider: provider)
|
||||
create_subject(identity)
|
||||
end
|
||||
|
||||
@@ -186,13 +213,12 @@ defmodule Domain.AuthFixtures do
|
||||
Enum.into(overrides, %{
|
||||
"id" => "google",
|
||||
"discovery_document_uri" => "https://firezone.example.com/.well-known/openid-configuration",
|
||||
"client_id" => "google-client-id",
|
||||
"client_id" => "google-client-id-#{counter()}",
|
||||
"client_secret" => "google-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/google/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile",
|
||||
"label" => "OIDC Google",
|
||||
"auto_create_users" => false
|
||||
"label" => "OIDC Google"
|
||||
})
|
||||
end
|
||||
|
||||
@@ -201,79 +227,72 @@ defmodule Domain.AuthFixtures do
|
||||
%{
|
||||
"id" => "google",
|
||||
"discovery_document_uri" => discovery_document_url,
|
||||
"client_id" => "google-client-id",
|
||||
"client_id" => "google-client-id-#{counter()}",
|
||||
"client_secret" => "google-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/google/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile",
|
||||
"label" => "OIDC Google",
|
||||
"auto_create_users" => false
|
||||
"label" => "OIDC Google"
|
||||
},
|
||||
%{
|
||||
"id" => "okta",
|
||||
"discovery_document_uri" => discovery_document_url,
|
||||
"client_id" => "okta-client-id",
|
||||
"client_id" => "okta-client-id-#{counter()}",
|
||||
"client_secret" => "okta-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/okta/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile offline_access",
|
||||
"label" => "OIDC Okta",
|
||||
"auto_create_users" => false
|
||||
"label" => "OIDC Okta"
|
||||
},
|
||||
%{
|
||||
"id" => "auth0",
|
||||
"discovery_document_uri" => discovery_document_url,
|
||||
"client_id" => "auth0-client-id",
|
||||
"client_id" => "auth0-client-id-#{counter()}",
|
||||
"client_secret" => "auth0-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/auth0/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile",
|
||||
"label" => "OIDC Auth0",
|
||||
"auto_create_users" => false
|
||||
"label" => "OIDC Auth0"
|
||||
},
|
||||
%{
|
||||
"id" => "azure",
|
||||
"discovery_document_uri" => discovery_document_url,
|
||||
"client_id" => "azure-client-id",
|
||||
"client_id" => "azure-client-id-#{counter()}",
|
||||
"client_secret" => "azure-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/azure/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile offline_access",
|
||||
"label" => "OIDC Azure",
|
||||
"auto_create_users" => false
|
||||
"label" => "OIDC Azure"
|
||||
},
|
||||
%{
|
||||
"id" => "onelogin",
|
||||
"discovery_document_uri" => discovery_document_url,
|
||||
"client_id" => "onelogin-client-id",
|
||||
"client_id" => "onelogin-client-id-#{counter()}",
|
||||
"client_secret" => "onelogin-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/onelogin/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile offline_access",
|
||||
"label" => "OIDC Onelogin",
|
||||
"auto_create_users" => false
|
||||
"label" => "OIDC Onelogin"
|
||||
},
|
||||
%{
|
||||
"id" => "keycloak",
|
||||
"discovery_document_uri" => discovery_document_url,
|
||||
"client_id" => "keycloak-client-id",
|
||||
"client_id" => "keycloak-client-id-#{counter()}",
|
||||
"client_secret" => "keycloak-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/keycloak/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile offline_access",
|
||||
"label" => "OIDC Keycloak",
|
||||
"auto_create_users" => false
|
||||
"label" => "OIDC Keycloak"
|
||||
},
|
||||
%{
|
||||
"id" => "vault",
|
||||
"discovery_document_uri" => discovery_document_url,
|
||||
"client_id" => "vault-client-id",
|
||||
"client_id" => "vault-client-id-#{counter()}",
|
||||
"client_secret" => "vault-client-secret",
|
||||
"redirect_uri" => "https://firezone.example.com/auth/oidc/vault/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile offline_access",
|
||||
"label" => "OIDC Vault",
|
||||
"auto_create_users" => false
|
||||
"label" => "OIDC Vault"
|
||||
}
|
||||
]
|
||||
end
|
||||
@@ -444,7 +463,34 @@ defmodule Domain.AuthFixtures do
|
||||
{bypass, "#{endpoint}/.well-known/openid-configuration"}
|
||||
end
|
||||
|
||||
def fetch_conn_params(conn) do
|
||||
def generate_openid_connect_token(provider, identity, claims \\ %{}) do
|
||||
claims =
|
||||
Map.merge(
|
||||
%{
|
||||
"email" => identity.provider_identifier,
|
||||
"sub" => identity.provider_identifier,
|
||||
"aud" => provider.adapter_config["client_id"],
|
||||
"exp" => DateTime.utc_now() |> DateTime.add(10, :second) |> DateTime.to_unix()
|
||||
},
|
||||
claims
|
||||
)
|
||||
|
||||
{sign_openid_connect_token(claims), claims}
|
||||
end
|
||||
|
||||
def sign_openid_connect_token(claims) do
|
||||
jwk = jwks_attrs()
|
||||
|
||||
{_alg, token} =
|
||||
jwk
|
||||
|> JOSE.JWK.from()
|
||||
|> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"})
|
||||
|> JOSE.JWS.compact()
|
||||
|
||||
token
|
||||
end
|
||||
|
||||
defp fetch_conn_params(conn) do
|
||||
opts = Plug.Parsers.init(parsers: [:urlencoded, :json], pass: ["*/*"], json_decoder: Jason)
|
||||
|
||||
conn
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[
|
||||
import_deps: [:phoenix, :phoenix_live_view],
|
||||
plugins: [Phoenix.LiveView.HTMLFormatter],
|
||||
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"],
|
||||
inputs: ["*.{xml.heex,html.heex,ex,exs}", "{config,lib,test}/**/*.{xml.heex,html.heex,ex,exs}"],
|
||||
locals_without_parens: [
|
||||
assert_authenticated: 2,
|
||||
assert_unauthenticated: 1
|
||||
|
||||
2
elixir/apps/web/.gitignore
vendored
2
elixir/apps/web/.gitignore
vendored
@@ -17,7 +17,7 @@ web-*.tar
|
||||
/priv/static/assets/
|
||||
|
||||
# Ignore Tailwind temporary files
|
||||
assets/tmp
|
||||
/assets/tmp
|
||||
|
||||
# Ignore digested assets cache.
|
||||
/priv/static/cache_manifest.json
|
||||
|
||||
@@ -7,7 +7,8 @@ const path = require("path")
|
||||
const defaultTheme = require("tailwindcss/defaultTheme")
|
||||
|
||||
module.exports = {
|
||||
darkMode: "media",
|
||||
// Use "media" to synchronize dark mode with the OS, "class" to require manual toggle
|
||||
darkMode: "class",
|
||||
content: [
|
||||
"./node_modules/flowbite/**/*.js",
|
||||
"./js/**/*.js",
|
||||
|
||||
@@ -50,10 +50,10 @@ defmodule Web do
|
||||
end
|
||||
end
|
||||
|
||||
def live_view do
|
||||
def live_view(opts \\ []) do
|
||||
quote do
|
||||
use Phoenix.LiveView,
|
||||
layout: {Web.Layouts, :app}
|
||||
layout: Keyword.get(unquote(opts), :layout, {Web.Layouts, :app})
|
||||
|
||||
unquote(html_helpers())
|
||||
end
|
||||
@@ -124,4 +124,8 @@ defmodule Web do
|
||||
defmacro __using__(which) when is_atom(which) do
|
||||
apply(__MODULE__, which, [])
|
||||
end
|
||||
|
||||
defmacro __using__({which, opts}) when is_atom(which) do
|
||||
apply(__MODULE__, which, [opts])
|
||||
end
|
||||
end
|
||||
|
||||
221
elixir/apps/web/lib/web/auth.ex
Normal file
221
elixir/apps/web/lib/web/auth.ex
Normal file
@@ -0,0 +1,221 @@
|
||||
defmodule Web.Auth do
|
||||
use Web, :verified_routes
|
||||
alias Domain.Auth
|
||||
|
||||
def signed_in_path(%Auth.Subject{actor: %{type: :account_admin_user}} = subject),
|
||||
do: ~p"/#{subject.account}/dashboard"
|
||||
|
||||
def signed_in_path(%Auth.Subject{actor: %{type: :account_user}} = subject),
|
||||
do: ~p"/#{subject.account}"
|
||||
|
||||
def put_subject_in_session(conn, %Auth.Subject{} = subject) do
|
||||
{:ok, session_token} = Domain.Auth.create_session_token_from_subject(subject)
|
||||
|
||||
conn
|
||||
|> Plug.Conn.put_session(:signed_in_at, DateTime.utc_now())
|
||||
|> Plug.Conn.put_session(:session_token, session_token)
|
||||
|> Plug.Conn.put_session(:live_socket_id, "actors_sessions:#{subject.actor.id}")
|
||||
end
|
||||
|
||||
@doc """
|
||||
Logs the user out.
|
||||
|
||||
It clears all session data for safety. See `renew_session/1`.
|
||||
"""
|
||||
def sign_out(%Plug.Conn{} = conn) do
|
||||
# token = Plug.Conn.get_session(conn, :session_token)
|
||||
# subject && Accounts.delete_user_session_token(subject)
|
||||
|
||||
if live_socket_id = Plug.Conn.get_session(conn, :live_socket_id) do
|
||||
conn.private.phoenix_endpoint.broadcast(live_socket_id, "disconnect", %{})
|
||||
end
|
||||
|
||||
conn
|
||||
|> renew_session()
|
||||
end
|
||||
|
||||
@doc """
|
||||
This function renews the session ID and erases the whole
|
||||
session to avoid fixation attacks.
|
||||
"""
|
||||
def renew_session(%Plug.Conn{} = conn) do
|
||||
preferred_locale = Plug.Conn.get_session(conn, :preferred_locale)
|
||||
|
||||
conn
|
||||
|> Plug.Conn.configure_session(renew: true)
|
||||
|> Plug.Conn.clear_session()
|
||||
|> Plug.Conn.put_session(:preferred_locale, preferred_locale)
|
||||
end
|
||||
|
||||
###########################
|
||||
## Plugs
|
||||
###########################
|
||||
|
||||
@doc """
|
||||
Fetches the user agent value from headers and assigns it the connection.
|
||||
"""
|
||||
def fetch_user_agent(%Plug.Conn{} = conn, _opts) do
|
||||
case Plug.Conn.get_req_header(conn, "user-agent") do
|
||||
[user_agent | _] -> Plug.Conn.assign(conn, :user_agent, user_agent)
|
||||
_ -> conn
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fetches the session token from the session and assigns the subject to the connection.
|
||||
"""
|
||||
def fetch_subject(%Plug.Conn{} = conn, _opts) do
|
||||
with token when not is_nil(token) <- Plug.Conn.get_session(conn, :session_token),
|
||||
{:ok, subject} <-
|
||||
Domain.Auth.sign_in(token, conn.assigns.user_agent, conn.remote_ip),
|
||||
true <- conn.path_params["account_id"] == subject.account.id do
|
||||
Plug.Conn.assign(conn, :subject, subject)
|
||||
else
|
||||
_ -> conn
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Used for routes that require the user to not be authenticated.
|
||||
"""
|
||||
def redirect_if_user_is_authenticated(%Plug.Conn{} = conn, _opts) do
|
||||
if conn.assigns[:subject] do
|
||||
conn
|
||||
|> Phoenix.Controller.redirect(to: signed_in_path(conn.assigns.subject))
|
||||
|> Plug.Conn.halt()
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Used for routes that require the user to be authenticated.
|
||||
|
||||
This plug will only work if there is an `account_id` in the path params.
|
||||
"""
|
||||
def ensure_authenticated(%Plug.Conn{} = conn, _opts) do
|
||||
if conn.assigns[:subject] do
|
||||
conn
|
||||
else
|
||||
conn
|
||||
|> Phoenix.Controller.put_flash(:error, "You must log in to access this page.")
|
||||
|> maybe_store_return_to()
|
||||
|> Phoenix.Controller.redirect(to: ~p"/#{conn.path_params["account_id"]}/sign_in")
|
||||
|> Plug.Conn.halt()
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Used for routes that require the user to be authenticated as a specific kind of actor.
|
||||
|
||||
This plug will only work if there is an `account_id` in the path params.
|
||||
"""
|
||||
def ensure_authenticated_actor_type(%Plug.Conn{} = conn, type) do
|
||||
if not is_nil(conn.assigns[:subject]) and conn.assigns[:subject].actor.type == type do
|
||||
conn
|
||||
else
|
||||
conn
|
||||
|> Web.FallbackController.call({:error, :not_found})
|
||||
|> Plug.Conn.halt()
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_store_return_to(%{method: "GET"} = conn) do
|
||||
Plug.Conn.put_session(conn, :user_return_to, Phoenix.Controller.current_path(conn))
|
||||
end
|
||||
|
||||
defp maybe_store_return_to(conn), do: conn
|
||||
|
||||
###########################
|
||||
## LiveView
|
||||
###########################
|
||||
|
||||
@doc """
|
||||
Handles mounting and authenticating the actor in LiveViews.
|
||||
|
||||
Notice: every protected route should have `account_id` in the path params.
|
||||
|
||||
## `on_mount` arguments
|
||||
|
||||
* `:mount_subject` - assigns user_agent and subject to the socket assigns based on
|
||||
session_token, or nil if there's no session_token or no matching user.
|
||||
|
||||
* `:ensure_authenticated` - authenticates the user from the session,
|
||||
and assigns the subject to socket assigns based on session_token.
|
||||
Redirects to login page if there's no logged user.
|
||||
|
||||
* `:redirect_if_user_is_authenticated` - authenticates the user from the session.
|
||||
Redirects to signed_in_path if there's a logged user.
|
||||
|
||||
## Examples
|
||||
|
||||
Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate
|
||||
the subject:
|
||||
|
||||
defmodule Web.PageLive do
|
||||
use Web, :live_view
|
||||
|
||||
on_mount {Web.UserAuth, :mount_subject}
|
||||
...
|
||||
end
|
||||
|
||||
Or use the `live_session` of your router to invoke the on_mount callback:
|
||||
|
||||
live_session :authenticated, on_mount: [{Web.UserAuth, :ensure_authenticated}] do
|
||||
live "/:account_id/profile", ProfileLive, :index
|
||||
end
|
||||
"""
|
||||
def on_mount(:mount_subject, params, session, socket) do
|
||||
{:cont, mount_subject(socket, params, session)}
|
||||
end
|
||||
|
||||
def on_mount(:ensure_authenticated, params, session, socket) do
|
||||
socket = mount_subject(socket, params, session)
|
||||
|
||||
if socket.assigns.subject do
|
||||
{:cont, socket}
|
||||
else
|
||||
socket =
|
||||
socket
|
||||
|> Phoenix.LiveView.put_flash(:error, "You must log in to access this page.")
|
||||
|> Phoenix.LiveView.redirect(to: ~p"/#{socket.assigns.account}/sign_in")
|
||||
|
||||
{:halt, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def on_mount(:ensure_account_admin_user_actor, params, session, socket) do
|
||||
socket = mount_subject(socket, params, session)
|
||||
|
||||
if socket.assigns.subject.actor.type == :account_admin_user do
|
||||
{:cont, socket}
|
||||
else
|
||||
raise Ecto.NoResultsError
|
||||
end
|
||||
end
|
||||
|
||||
def on_mount(:redirect_if_user_is_authenticated, params, session, socket) do
|
||||
socket = mount_subject(socket, params, session)
|
||||
|
||||
if socket.assigns.subject do
|
||||
{:halt, Phoenix.LiveView.redirect(socket, to: signed_in_path(socket.assigns.subject))}
|
||||
else
|
||||
{:cont, socket}
|
||||
end
|
||||
end
|
||||
|
||||
defp mount_subject(socket, params, session) do
|
||||
Phoenix.Component.assign_new(socket, :subject, fn ->
|
||||
user_agent = Phoenix.LiveView.get_connect_info(socket, :user_agent)
|
||||
remote_ip = Phoenix.LiveView.get_connect_info(socket, :peer_data).address
|
||||
|
||||
with token when not is_nil(token) <- session["session_token"],
|
||||
{:ok, subject} <- Domain.Auth.sign_in(token, user_agent, remote_ip),
|
||||
true <- params["account_id"] == subject.account.id do
|
||||
subject
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -11,9 +11,22 @@ defmodule Web.CoreComponents do
|
||||
"""
|
||||
use Phoenix.Component
|
||||
use Web, :verified_routes
|
||||
|
||||
alias Phoenix.LiveView.JS
|
||||
import Web.Gettext
|
||||
alias Phoenix.LiveView.JS
|
||||
|
||||
def logo(assigns) do
|
||||
~H"""
|
||||
<a
|
||||
href="https://www.firezone.dev/?utm_source=product"
|
||||
class="flex items-center mb-6 text-2xl font-semibold"
|
||||
>
|
||||
<img src={~p"/images/logo.svg"} class="mr-3 h-8" alt="Firezone Logo" />
|
||||
<span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">
|
||||
firezone
|
||||
</span>
|
||||
</a>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a generic <p> tag using our color scheme.
|
||||
@@ -507,13 +520,12 @@ defmodule Web.CoreComponents do
|
||||
<div
|
||||
:if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
|
||||
id={@id}
|
||||
phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
|
||||
role="alert"
|
||||
class={[
|
||||
"fixed top-2 right-2 w-80 sm:w-96 z-50 rounded-lg p-3 ring-1",
|
||||
@kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
|
||||
@kind == :error && "bg-rose-50 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
|
||||
"p-4 mb-4 text-sm rounded-lg flash-#{@kind}",
|
||||
@kind == :info && "text-yellow-800 bg-yellow-50 dark:bg-gray-800 dark:text-yellow-300",
|
||||
@kind == :error && "text-red-800 bg-red-50 dark:bg-gray-800 dark:text-red-400"
|
||||
]}
|
||||
role="alert"
|
||||
{@rest}
|
||||
>
|
||||
<p :if={@title} class="flex items-center gap-1.5 text-sm font-semibold leading-6">
|
||||
@@ -521,10 +533,7 @@ defmodule Web.CoreComponents do
|
||||
<.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="h-4 w-4" />
|
||||
<%= @title %>
|
||||
</p>
|
||||
<p class="mt-2 text-sm leading-5"><%= msg %></p>
|
||||
<button type="button" class="group absolute top-1 right-1 p-2" aria-label={gettext("close")}>
|
||||
<.icon name="hero-x-mark-solid" class="h-5 w-5 opacity-40 group-hover:opacity-70" />
|
||||
</button>
|
||||
<%= msg %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
@@ -581,7 +590,7 @@ defmodule Web.CoreComponents do
|
||||
def simple_form(assigns) do
|
||||
~H"""
|
||||
<.form :let={f} for={@for} as={@as} {@rest}>
|
||||
<div class="mt-10 space-y-8 bg-white">
|
||||
<div class="space-y-8 bg-white">
|
||||
<%= render_slot(@inner_block, f) %>
|
||||
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
|
||||
<%= render_slot(action, f) %>
|
||||
@@ -610,8 +619,12 @@ defmodule Web.CoreComponents do
|
||||
<button
|
||||
type={@type}
|
||||
class={[
|
||||
"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
|
||||
"text-sm font-semibold leading-6 text-white active:text-white/80",
|
||||
"phx-submit-loading:opacity-75",
|
||||
"text-white bg-primary-600 font-medium rounded-lg text-sm px-5 py-2.5 text-center",
|
||||
"hover:bg-primary-700",
|
||||
"focus:ring-4 focus:outline-none focus:ring-primary-300",
|
||||
"dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800",
|
||||
"active:text-white/80",
|
||||
@class
|
||||
]}
|
||||
{@rest}
|
||||
@@ -960,6 +973,70 @@ defmodule Web.CoreComponents do
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders Gravatar img tag.
|
||||
"""
|
||||
attr :email, :string, required: true
|
||||
attr :size, :integer, default: 40
|
||||
attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
|
||||
|
||||
def gravatar(assigns) do
|
||||
~H"""
|
||||
<img
|
||||
src={"https://www.gravatar.com/avatar/#{Base.encode16(:crypto.hash(:md5, @email), case: :lower)}?s=#{@size}&d=retro"}
|
||||
{@rest}
|
||||
/>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Intersperses separator slot between a list of items.
|
||||
|
||||
Useful when you need to add a separator between items such as when
|
||||
rendering breadcrumbs for navigation. Provides each item to the
|
||||
inner block.
|
||||
|
||||
## Examples
|
||||
|
||||
```heex
|
||||
<.intersperse :let={item}>
|
||||
<:separator>
|
||||
<span class="sep">|</span>
|
||||
</:separator>
|
||||
|
||||
<:item>
|
||||
home
|
||||
</:item>
|
||||
|
||||
<:item>
|
||||
profile
|
||||
</:item>
|
||||
|
||||
<:item>
|
||||
settings
|
||||
</:item>
|
||||
</.intersperse>
|
||||
```
|
||||
|
||||
Renders the following markup:
|
||||
|
||||
home <span class="sep">|</span> profile <span class="sep">|</span> settings
|
||||
"""
|
||||
slot :separator, required: true, doc: "the slot for the separator"
|
||||
slot :item, required: true, doc: "the slots to intersperse with separators"
|
||||
|
||||
def intersperse_blocks(assigns) do
|
||||
~H"""
|
||||
<%= for item <- Enum.intersperse(@item, :separator) do %>
|
||||
<%= if item == :separator do %>
|
||||
<%= render_slot(@separator) %>
|
||||
<% else %>
|
||||
<%= render_slot(item) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
"""
|
||||
end
|
||||
|
||||
## JS Commands
|
||||
|
||||
def show(js \\ %JS{}, selector) do
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
defmodule Web.Layouts do
|
||||
use Web, :html
|
||||
import Web.Endpoint, only: [static_path: 1]
|
||||
|
||||
embed_templates "layouts/*"
|
||||
end
|
||||
|
||||
@@ -56,11 +56,7 @@
|
||||
data-dropdown-toggle="dropdown"
|
||||
>
|
||||
<span class="sr-only">Open user menu</span>
|
||||
<img
|
||||
class="w-8 h-8 rounded-full"
|
||||
src="https://flowbite.com/docs/images/people/profile-picture-5.jpg"
|
||||
alt="user photo"
|
||||
/>
|
||||
<.gravatar size={25} email={@subject.identity.provider_identifier} class="rounded-full" />
|
||||
</button>
|
||||
<!-- Dropdown menu -->
|
||||
<div
|
||||
@@ -69,10 +65,10 @@
|
||||
>
|
||||
<div class="py-3 px-4">
|
||||
<span class="block text-sm font-semibold text-gray-900 dark:text-white">
|
||||
Steve Johnson
|
||||
<%= @subject.actor.name %>
|
||||
</span>
|
||||
<span class="block text-sm text-gray-900 truncate dark:text-white">
|
||||
steve@tesla.com
|
||||
<%= @subject.identity.provider_identifier %>
|
||||
</span>
|
||||
</div>
|
||||
<ul class="py-1 text-gray-700 dark:text-gray-300" aria-labelledby="dropdown">
|
||||
@@ -88,7 +84,7 @@
|
||||
<ul class="py-1 text-gray-700 dark:text-gray-300" aria-labelledby="dropdown">
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
href={~p"/#{@subject.account.id}/sign_out"}
|
||||
class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
>
|
||||
Sign out
|
||||
@@ -101,7 +97,7 @@
|
||||
</nav>
|
||||
<!-- Sidebar -->
|
||||
<aside
|
||||
class="fixed top-0 left-0 z-40 w-64 h-screen pt-14 transition-transform -translate-x-full bg-white border-r border-gray-200 md:translate-x-0 dark:bg-gray-800 dark:border-gray-700"
|
||||
class="fixed top-0 left-0 z-40 w-64 h-screen pt-14 pb-8 transition-transform -translate-x-full bg-white border-r border-gray-200 md:translate-x-0 dark:bg-gray-800 dark:border-gray-700"
|
||||
aria-label="Sidenav"
|
||||
id="drawer-navigation"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<main class="h-auto pt-16">
|
||||
<%= @inner_content %>
|
||||
</main>
|
||||
@@ -5,23 +5,9 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<!-- iOS App Favicon -->
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href={static_path("/images/apple-touch-icon.png")}
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href={static_path("/images/favicon-32x32.png")}
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href={static_path("/images/favicon-16x16.png")}
|
||||
/>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href={~p"/images/apple-touch-icon.png"} />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href={~p"/images/favicon-32x32.png"} />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href={~p"/images/favicon-16x16.png"} />
|
||||
<!-- Windows App Favicon -->
|
||||
<meta name="msapplication-config" content={~p"/browser/config.xml"} />
|
||||
<meta name="msapplication-TileColor" content="331700" />
|
||||
@@ -31,8 +17,19 @@
|
||||
<.live_title suffix=" · Firezone">
|
||||
<%= assigns[:page_title] || "Firezone" %>
|
||||
</.live_title>
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
|
||||
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
||||
<link
|
||||
phx-track-static
|
||||
rel="stylesheet"
|
||||
nonce={@conn.private.csp_nonce}
|
||||
href={~p"/assets/app.css"}
|
||||
/>
|
||||
<script
|
||||
defer
|
||||
phx-track-static
|
||||
type="text/javascript"
|
||||
nonce={@conn.private.csp_nonce}
|
||||
src={~p"/assets/app.js"}
|
||||
>
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-gray-50 dark:bg-gray-900">
|
||||
|
||||
217
elixir/apps/web/lib/web/controllers/auth_controller.ex
Normal file
217
elixir/apps/web/lib/web/controllers/auth_controller.ex
Normal file
@@ -0,0 +1,217 @@
|
||||
defmodule Web.AuthController do
|
||||
use Web, :controller
|
||||
alias Web.Auth
|
||||
alias Domain.Auth.Adapters.OpenIDConnect
|
||||
|
||||
# This is the cookie which will be used to store the
|
||||
# state and code verifier for OpenID Connect IdP's
|
||||
@state_cookie_key_prefix "fz_auth_state_"
|
||||
@state_cookie_options [
|
||||
sign: true,
|
||||
max_age: 300,
|
||||
same_site: "Lax",
|
||||
secure: true,
|
||||
http_only: true
|
||||
]
|
||||
|
||||
action_fallback Web.FallbackController
|
||||
|
||||
@doc """
|
||||
This is a callback for the UserPass provider which checks login and password to authenticate the user.
|
||||
"""
|
||||
def verify_credentials(conn, %{
|
||||
"account_id" => account_id,
|
||||
"provider_id" => provider_id,
|
||||
"userpass" => %{
|
||||
"provider_identifier" => provider_identifier,
|
||||
"secret" => secret
|
||||
}
|
||||
}) do
|
||||
with {:ok, provider} <- Domain.Auth.fetch_active_provider_by_id(provider_id),
|
||||
{:ok, subject} <-
|
||||
Domain.Auth.sign_in(
|
||||
provider,
|
||||
provider_identifier,
|
||||
secret,
|
||||
conn.assigns.user_agent,
|
||||
conn.remote_ip
|
||||
) do
|
||||
redirect_to = get_session(conn, :user_return_to) || Auth.signed_in_path(subject)
|
||||
|
||||
conn
|
||||
|> Web.Auth.renew_session()
|
||||
|> Web.Auth.put_subject_in_session(subject)
|
||||
|> redirect(to: redirect_to)
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
conn
|
||||
|> put_flash(:userpass_provider_identifier, String.slice(provider_identifier, 0, 160))
|
||||
|> put_flash(:error, "You can not use this method to sign in.")
|
||||
|> redirect(to: "/#{account_id}/sign_in")
|
||||
|
||||
{:error, _reason} ->
|
||||
conn
|
||||
|> put_flash(:userpass_provider_identifier, String.slice(provider_identifier, 0, 160))
|
||||
|> put_flash(:error, "Invalid username or password.")
|
||||
|> redirect(to: "/#{account_id}/sign_in")
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
This is a callback for the Email provider which sends login link.
|
||||
"""
|
||||
def request_magic_link(conn, %{
|
||||
"account_id" => account_id,
|
||||
"provider_id" => provider_id,
|
||||
"email" => %{
|
||||
"provider_identifier" => provider_identifier
|
||||
}
|
||||
}) do
|
||||
_ =
|
||||
with {:ok, provider} <- Domain.Auth.fetch_active_provider_by_id(provider_id),
|
||||
{:ok, identity} <-
|
||||
Domain.Auth.fetch_identity_by_provider_and_identifier(provider, provider_identifier),
|
||||
{:ok, identity} <- Domain.Auth.Adapters.Email.request_sign_in_token(identity) do
|
||||
Web.Mailer.AuthEmail.sign_in_link_email(identity)
|
||||
|> Web.Mailer.deliver()
|
||||
end
|
||||
|
||||
redirect(conn, to: "/#{account_id}/sign_in/providers/email/#{provider_id}")
|
||||
end
|
||||
|
||||
@doc """
|
||||
This is a callback for the Email provider which handles both form submission and redirect login link
|
||||
to authenticate a user.
|
||||
"""
|
||||
def verify_sign_in_token(conn, %{
|
||||
"account_id" => account_id,
|
||||
"provider_id" => provider_id,
|
||||
"identity_id" => identity_id,
|
||||
"secret" => secret
|
||||
}) do
|
||||
with {:ok, provider} <- Domain.Auth.fetch_active_provider_by_id(provider_id),
|
||||
{:ok, subject} <-
|
||||
Domain.Auth.sign_in(
|
||||
provider,
|
||||
identity_id,
|
||||
secret,
|
||||
conn.assigns.user_agent,
|
||||
conn.remote_ip
|
||||
) do
|
||||
redirect_to = get_session(conn, :user_return_to) || Auth.signed_in_path(subject)
|
||||
|
||||
conn
|
||||
|> Web.Auth.renew_session()
|
||||
|> Web.Auth.put_subject_in_session(subject)
|
||||
|> redirect(to: redirect_to)
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
conn
|
||||
|> put_flash(:error, "You can not use this method to sign in.")
|
||||
|> redirect(to: "/#{account_id}/sign_in")
|
||||
|
||||
{:error, _reason} ->
|
||||
conn
|
||||
|> put_flash(:error, "The sign in link is invalid or expired.")
|
||||
|> redirect(to: "/#{account_id}/sign_in")
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
This controller redirects user to IdP for authentication while persisting
|
||||
verification state to prevent various attacks on OpenID Connect.
|
||||
"""
|
||||
def redirect_to_idp(conn, %{"account_id" => account_id, "provider_id" => provider_id}) do
|
||||
with {:ok, provider} <- Domain.Auth.fetch_active_provider_by_id(provider_id) do
|
||||
redirect_url =
|
||||
url(~p"/#{provider.account_id}/sign_in/providers/#{provider.id}/handle_callback")
|
||||
|
||||
{:ok, authorization_url, {state, code_verifier}} =
|
||||
OpenIDConnect.authorization_uri(provider, redirect_url)
|
||||
|
||||
key = state_cookie_key(provider.id)
|
||||
value = :erlang.term_to_binary({state, code_verifier})
|
||||
|
||||
conn
|
||||
|> put_resp_cookie(key, value, @state_cookie_options)
|
||||
|> redirect(external: authorization_url)
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
conn
|
||||
|> put_flash(:error, "You can not use this method to sign in.")
|
||||
|> redirect(to: "/#{account_id}/sign_in")
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
This controller handles IdP redirect back to the Firezone.
|
||||
"""
|
||||
def handle_idp_callback(conn, %{
|
||||
"account_id" => account_id,
|
||||
"provider_id" => provider_id,
|
||||
"state" => state,
|
||||
"code" => code
|
||||
}) do
|
||||
key = state_cookie_key(provider_id)
|
||||
|
||||
with {:ok, code_verifier} <- fetch_verified_state(conn, key, state),
|
||||
{:ok, provider} <- Domain.Auth.fetch_active_provider_by_id(provider_id),
|
||||
payload =
|
||||
{
|
||||
url(~p"/#{account_id}/sign_in/providers/#{provider_id}/handle_callback"),
|
||||
code_verifier,
|
||||
code
|
||||
},
|
||||
{:ok, subject} <-
|
||||
Domain.Auth.sign_in(
|
||||
provider,
|
||||
payload,
|
||||
conn.assigns.user_agent,
|
||||
conn.remote_ip
|
||||
) do
|
||||
redirect_to = get_session(conn, :user_return_to) || Auth.signed_in_path(subject)
|
||||
|
||||
conn
|
||||
|> delete_resp_cookie(key, @state_cookie_options)
|
||||
|> Web.Auth.renew_session()
|
||||
|> Web.Auth.put_subject_in_session(subject)
|
||||
|> redirect(to: redirect_to)
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
conn
|
||||
|> put_flash(:error, "You can not use this method to sign in.")
|
||||
|> redirect(to: "/#{account_id}/sign_in")
|
||||
|
||||
{:error, :invalid_state} ->
|
||||
conn
|
||||
|> put_flash(:error, "Your session has expired, please try again.")
|
||||
|> redirect(to: "/#{account_id}/sign_in")
|
||||
|
||||
{:error, _reason} ->
|
||||
conn
|
||||
|> put_flash(:error, "You can not authenticate to this account.")
|
||||
|> redirect(to: "/#{account_id}/sign_in")
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_verified_state(conn, key, state) do
|
||||
conn = fetch_cookies(conn, signed: [key])
|
||||
|
||||
with {:ok, encoded_state} <- Map.fetch(conn.cookies, key),
|
||||
{^state, verifier} <- :erlang.binary_to_term(encoded_state, [:safe]) do
|
||||
{:ok, verifier}
|
||||
else
|
||||
_ -> {:error, :invalid_state}
|
||||
end
|
||||
end
|
||||
|
||||
defp state_cookie_key(provider_id) do
|
||||
@state_cookie_key_prefix <> provider_id
|
||||
end
|
||||
|
||||
def sign_out(conn, %{"account_id" => account_id}) do
|
||||
conn
|
||||
|> Auth.sign_out()
|
||||
|> redirect(to: ~p"/#{account_id}/sign_in")
|
||||
end
|
||||
end
|
||||
11
elixir/apps/web/lib/web/controllers/fallback_controller.ex
Normal file
11
elixir/apps/web/lib/web/controllers/fallback_controller.ex
Normal file
@@ -0,0 +1,11 @@
|
||||
defmodule Web.FallbackController do
|
||||
use Web, :controller
|
||||
|
||||
def call(conn, {:error, :not_found}) do
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> put_layout(html: {Web.Layouts, :root})
|
||||
|> put_view(Web.ErrorHTML)
|
||||
|> render("404.html")
|
||||
end
|
||||
end
|
||||
@@ -1,22 +1,21 @@
|
||||
defmodule Web.Endpoint do
|
||||
use Phoenix.Endpoint, otp_app: :web
|
||||
|
||||
if Application.compile_env(:domain, :sql_sandbox) do
|
||||
plug Phoenix.Ecto.SQL.Sandbox
|
||||
end
|
||||
|
||||
plug Plug.RewriteOn, [:x_forwarded_host, :x_forwarded_port, :x_forwarded_proto]
|
||||
plug Plug.MethodOverride
|
||||
plug :put_hsts_header
|
||||
plug Plug.Head
|
||||
plug Web.Plugs.SecureHeaders
|
||||
|
||||
socket "/live", Phoenix.LiveView.Socket,
|
||||
websocket: [
|
||||
connect_info: [
|
||||
:user_agent,
|
||||
:peer_data,
|
||||
:x_headers,
|
||||
:uri,
|
||||
session: {Web.Session, :options, []}
|
||||
]
|
||||
],
|
||||
longpoll: false
|
||||
plug RemoteIp,
|
||||
headers: ["x-forwarded-for"],
|
||||
proxies: {__MODULE__, :external_trusted_proxies, []},
|
||||
clients: {__MODULE__, :clients, []}
|
||||
|
||||
plug Plug.RequestId
|
||||
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
|
||||
|
||||
# Serve at "/" the static files from "priv/static" directory.
|
||||
#
|
||||
@@ -34,50 +33,31 @@ defmodule Web.Endpoint do
|
||||
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
|
||||
|
||||
plug Phoenix.LiveReloader
|
||||
plug Phoenix.CodeReloader
|
||||
plug Phoenix.CodeReloader, reloadable_apps: [:domain, :web]
|
||||
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :domain
|
||||
end
|
||||
|
||||
plug RemoteIp,
|
||||
headers: ["x-forwarded-for"],
|
||||
proxies: {__MODULE__, :external_trusted_proxies, []},
|
||||
clients: {__MODULE__, :clients, []}
|
||||
|
||||
plug Plug.RequestId
|
||||
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
|
||||
socket "/live", Phoenix.LiveView.Socket,
|
||||
websocket: [
|
||||
connect_info: [
|
||||
:user_agent,
|
||||
:peer_data,
|
||||
:x_headers,
|
||||
:uri,
|
||||
session: {Web.Session, :options, []}
|
||||
]
|
||||
],
|
||||
longpoll: false
|
||||
|
||||
plug Plug.Parsers,
|
||||
parsers: [:urlencoded, :multipart, :json],
|
||||
pass: ["*/*"],
|
||||
json_decoder: Phoenix.json_library()
|
||||
|
||||
# We wrap Plug.Session because it's options are resolved at compile-time,
|
||||
# which doesn't work with Elixir releases and runtime configuration
|
||||
plug :session
|
||||
plug Web.Session
|
||||
|
||||
plug Web.Router
|
||||
|
||||
def put_hsts_header(conn, _opts) do
|
||||
scheme =
|
||||
config(:url, [])
|
||||
|> Keyword.get(:scheme)
|
||||
|
||||
if scheme == "https" do
|
||||
put_resp_header(
|
||||
conn,
|
||||
"strict-transport-security",
|
||||
"max-age=63072000; includeSubDomains; preload"
|
||||
)
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
|
||||
def session(conn, _opts) do
|
||||
opts = Web.Session.options()
|
||||
Plug.Session.call(conn, Plug.Session.init(opts))
|
||||
end
|
||||
|
||||
def external_trusted_proxies do
|
||||
Domain.Config.fetch_env!(:web, :external_trusted_proxies)
|
||||
|> Enum.map(&to_string/1)
|
||||
|
||||
58
elixir/apps/web/lib/web/live/auth_live/email_live.ex
Normal file
58
elixir/apps/web/lib/web/live/auth_live/email_live.ex
Normal file
@@ -0,0 +1,58 @@
|
||||
defmodule Web.Auth.EmailLive do
|
||||
use Web, {:live_view, layout: {Web.Layouts, :public}}
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<section class="bg-gray-50 dark:bg-gray-900">
|
||||
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||
<.logo />
|
||||
|
||||
<div class="w-full col-span-6 mx-auto bg-white rounded-lg shadow dark:bg-gray-800 md:mt-0 sm:max-w-lg xl:p-0">
|
||||
<div class="p-6 space-y-4 lg:space-y-6 sm:p-8">
|
||||
<h1 class="text-xl font-bold leading-tight tracking-tight text-gray-900 sm:text-2xl dark:text-white">
|
||||
Please check your email
|
||||
</h1>
|
||||
<p>
|
||||
Should the provided email be registered, a sign-in link will be dispatched to your email account.
|
||||
Please click this link to proceed with your login.
|
||||
</p>
|
||||
<p>
|
||||
Did not receive it? <a href="?reset">Resend email</a>.
|
||||
</p>
|
||||
<div class="flex">
|
||||
<.dev_email_provider_link url="https://mail.google.com/mail/" name="Gmail" />
|
||||
<.email_provider_link url="https://mail.google.com/mail/" name="Gmail" />
|
||||
<.email_provider_link url="https://outlook.live.com/mail/" name="Outlook" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
"""
|
||||
end
|
||||
|
||||
if Mix.env() in [:dev, :test] do
|
||||
def dev_email_provider_link(assigns) do
|
||||
~H"""
|
||||
<.email_provider_link url={~p"/dev/mailbox"} name="Local" />
|
||||
"""
|
||||
end
|
||||
else
|
||||
def dev_email_provider_link(assigns), do: ~H""
|
||||
end
|
||||
|
||||
def email_provider_link(assigns) do
|
||||
~H"""
|
||||
<a
|
||||
href={@url}
|
||||
class="w-1/2 m-2 inline-flex items-center justify-center py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-gray-900 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
||||
>
|
||||
Open <%= @name %>
|
||||
</a>
|
||||
"""
|
||||
end
|
||||
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, socket}
|
||||
end
|
||||
end
|
||||
191
elixir/apps/web/lib/web/live/auth_live/providers_live.ex
Normal file
191
elixir/apps/web/lib/web/live/auth_live/providers_live.ex
Normal file
@@ -0,0 +1,191 @@
|
||||
defmodule Web.Auth.ProvidersLive do
|
||||
use Web, {:live_view, layout: {Web.Layouts, :public}}
|
||||
alias Domain.{Auth, Accounts}
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<section class="bg-gray-50 dark:bg-gray-900">
|
||||
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto lg:py-0">
|
||||
<.logo />
|
||||
|
||||
<div class="w-full col-span-6 mx-auto bg-white rounded-lg shadow dark:bg-gray-800 md:mt-0 sm:max-w-lg xl:p-0">
|
||||
<div class="p-6 space-y-4 lg:space-y-6 sm:p-8">
|
||||
<h1 class="text-xl font-bold leading-tight tracking-tight text-gray-900 sm:text-2xl dark:text-white">
|
||||
Welcome back
|
||||
</h1>
|
||||
|
||||
<.flash flash={@flash} kind={:error} />
|
||||
<.flash flash={@flash} kind={:info} />
|
||||
|
||||
<.intersperse_blocks>
|
||||
<:separator>
|
||||
<.separator />
|
||||
</:separator>
|
||||
|
||||
<:item :if={adapter_enabled?(@providers_by_adapter, :openid_connect)}>
|
||||
<.providers_group_form
|
||||
adapter="openid_connect"
|
||||
providers={@providers_by_adapter[:openid_connect]}
|
||||
/>
|
||||
</:item>
|
||||
|
||||
<:item :if={adapter_enabled?(@providers_by_adapter, :userpass)}>
|
||||
<h3 class="text-m font-bold leading-tight tracking-tight text-gray-900 sm:text-xl dark:text-white">
|
||||
Sign in with username and password
|
||||
</h3>
|
||||
|
||||
<.providers_group_form
|
||||
adapter="userpass"
|
||||
provider={List.first(@providers_by_adapter[:userpass])}
|
||||
flash={@flash}
|
||||
/>
|
||||
</:item>
|
||||
|
||||
<:item :if={adapter_enabled?(@providers_by_adapter, :email)}>
|
||||
<h3 class="text-m font-bold leading-tight tracking-tight text-gray-900 sm:text-xl dark:text-white">
|
||||
Sign in with a magic link
|
||||
</h3>
|
||||
|
||||
<.providers_group_form
|
||||
adapter="email"
|
||||
provider={List.first(@providers_by_adapter[:email])}
|
||||
flash={@flash}
|
||||
/>
|
||||
</:item>
|
||||
</.intersperse_blocks>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
"""
|
||||
end
|
||||
|
||||
def separator(assigns) do
|
||||
~H"""
|
||||
<div class="flex items-center">
|
||||
<div class="w-full h-0.5 bg-gray-200 dark:bg-gray-700"></div>
|
||||
<div class="px-5 text-center text-gray-500 dark:text-gray-400">or</div>
|
||||
<div class="w-full h-0.5 bg-gray-200 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def providers_group_form(%{adapter: "openid_connect"} = assigns) do
|
||||
~H"""
|
||||
<div class="space-y-3 items-center">
|
||||
<.openid_connect_button :for={provider <- @providers} provider={provider} />
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def providers_group_form(%{adapter: "userpass"} = assigns) do
|
||||
provider_identifier = live_flash(assigns.flash, :userpass_provider_identifier)
|
||||
form = to_form(%{"provider_identifier" => provider_identifier}, as: "userpass")
|
||||
assigns = Map.put(assigns, :userpass_form, form)
|
||||
|
||||
~H"""
|
||||
<.simple_form
|
||||
for={@userpass_form}
|
||||
action={~p"/#{@provider.account_id}/sign_in/providers/#{@provider.id}/verify_credentials"}
|
||||
class="space-y-4 lg:space-y-6"
|
||||
id="userpass_form"
|
||||
phx-update="ignore"
|
||||
>
|
||||
<.input
|
||||
field={@userpass_form[:provider_identifier]}
|
||||
type="text"
|
||||
label="Username"
|
||||
placeholder="Enter your username"
|
||||
required
|
||||
/>
|
||||
|
||||
<.input
|
||||
field={@userpass_form[:secret]}
|
||||
type="password"
|
||||
label="Password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
|
||||
<:actions>
|
||||
<.button phx-disable-with="Signing in..." class="w-full">
|
||||
Sign in
|
||||
</.button>
|
||||
</:actions>
|
||||
</.simple_form>
|
||||
"""
|
||||
end
|
||||
|
||||
def providers_group_form(%{adapter: "email"} = assigns) do
|
||||
provider_identifier = live_flash(assigns.flash, :email_provider_identifier)
|
||||
form = to_form(%{"provider_identifier" => provider_identifier}, as: "email")
|
||||
assigns = Map.put(assigns, :email_form, form)
|
||||
|
||||
~H"""
|
||||
<.simple_form
|
||||
for={@email_form}
|
||||
action={~p"/#{@provider.account_id}/sign_in/providers/#{@provider.id}/request_magic_link"}
|
||||
class="space-y-4 lg:space-y-6"
|
||||
id="email_form"
|
||||
phx-update="ignore"
|
||||
>
|
||||
<.input
|
||||
field={@email_form[:provider_identifier]}
|
||||
type="email"
|
||||
label="Email"
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
/>
|
||||
|
||||
<:actions>
|
||||
<.button phx-disable-with="Sending..." class="w-full">
|
||||
Request sign in link
|
||||
</.button>
|
||||
</:actions>
|
||||
</.simple_form>
|
||||
"""
|
||||
end
|
||||
|
||||
def openid_connect_button(assigns) do
|
||||
~H"""
|
||||
<a
|
||||
href={~p"/#{@provider.account_id}/sign_in/providers/#{@provider.id}/redirect"}
|
||||
class="w-full inline-flex items-center justify-center py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-gray-900 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
||||
>
|
||||
Log in with <%= @provider.name %>
|
||||
</a>
|
||||
"""
|
||||
end
|
||||
|
||||
def adapter_enabled?(providers_by_adapter, adapter) do
|
||||
Map.get(providers_by_adapter, adapter, []) != []
|
||||
end
|
||||
|
||||
def mount(%{"account_id" => account_id}, _session, socket) do
|
||||
with {:ok, account} <- Accounts.fetch_account_by_id(account_id),
|
||||
{:ok, [_ | _] = providers} <- Auth.list_active_providers_for_account(account) do
|
||||
{:ok, socket,
|
||||
temporary_assigns: [
|
||||
account: account,
|
||||
providers_by_adapter: Enum.group_by(providers, & &1.adapter),
|
||||
page_title: "Sign in"
|
||||
]}
|
||||
else
|
||||
{:ok, []} ->
|
||||
socket =
|
||||
socket
|
||||
|> put_flash(:error, "This account is disabled.")
|
||||
|> redirect(to: ~p"/")
|
||||
|
||||
{:ok, socket}
|
||||
|
||||
{:error, :not_found} ->
|
||||
socket =
|
||||
socket
|
||||
|> put_flash(:error, "Account not found.")
|
||||
|> redirect(to: ~p"/")
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -161,7 +161,7 @@ defmodule Web.GatewaysLive.Show do
|
||||
<thead class="text-xs text-gray-900 uppercase dark:text-gray-400">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Label
|
||||
Name
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Address
|
||||
@@ -211,7 +211,7 @@ defmodule Web.GatewaysLive.Show do
|
||||
</:title>
|
||||
<:actions>
|
||||
<.delete_button>
|
||||
Delete gateway
|
||||
Delete Gateway
|
||||
</.delete_button>
|
||||
</:actions>
|
||||
</.section_header>
|
||||
|
||||
9
elixir/apps/web/lib/web/live/landing_live.ex
Normal file
9
elixir/apps/web/lib/web/live/landing_live.ex
Normal file
@@ -0,0 +1,9 @@
|
||||
defmodule Web.LandingLive do
|
||||
use Web, {:live_view, layout: {Web.Layouts, :public}}
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
Home page for unauthenticated users and regular account users with app download links.
|
||||
"""
|
||||
end
|
||||
end
|
||||
268
elixir/apps/web/lib/web/live/resources_live/edit.ex
Normal file
268
elixir/apps/web/lib/web/live/resources_live/edit.ex
Normal file
@@ -0,0 +1,268 @@
|
||||
defmodule Web.ResourcesLive.Edit do
|
||||
use Web, :live_view
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.section_header>
|
||||
<:breadcrumbs>
|
||||
<.breadcrumbs entries={[
|
||||
%{label: "Home", path: ~p"/"},
|
||||
%{label: "Resources", path: ~p"/resources"},
|
||||
%{label: "GitLab", path: ~p"/resources/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"},
|
||||
%{label: "Edit", path: ~p"/resources/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89/edit"}
|
||||
]} />
|
||||
</:breadcrumbs>
|
||||
<:title>
|
||||
Edit Resource
|
||||
</:title>
|
||||
</.section_header>
|
||||
<!-- Update Resource -->
|
||||
<section class="bg-white dark:bg-gray-900">
|
||||
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
|
||||
<h2 class="mb-4 text-xl font-bold text-gray-900 dark:text-white">Edit Resource details</h2>
|
||||
<form action="#">
|
||||
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
|
||||
<div>
|
||||
<.label for="address">
|
||||
Address
|
||||
</.label>
|
||||
<input
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
name="address"
|
||||
id="resource-address"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
|
||||
value="www.gitlab.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<.label for="name">
|
||||
Name
|
||||
</.label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="resource-name"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
|
||||
value="GitLab"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<.label for="traffic-filter">
|
||||
Traffic restriction
|
||||
</.label>
|
||||
<div class="h-12 flex items-center my-4">
|
||||
<input
|
||||
id="traffic-filter-option-1"
|
||||
type="radio"
|
||||
name="traffic-filter"
|
||||
value="none"
|
||||
class="w-4 h-4 border-gray-300 focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-600 dark:focus:bg-blue-600 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
<label
|
||||
for="traffic-filter-option-1"
|
||||
class="block ml-4 text-sm font-medium text-gray-900 dark:text-gray-300"
|
||||
>
|
||||
Permit all
|
||||
</label>
|
||||
</div>
|
||||
<div class="h-12 flex items-center mb-4">
|
||||
<input
|
||||
id="traffic-filter-option-2"
|
||||
type="radio"
|
||||
name="traffic-filter"
|
||||
value="icmp"
|
||||
class="w-4 h-4 border-gray-300 focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-600 dark:focus:bg-blue-600 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
<label
|
||||
for="traffic-filter-option-2"
|
||||
class="block ml-4 text-sm font-medium text-gray-900 dark:text-gray-300"
|
||||
>
|
||||
ICMP
|
||||
</label>
|
||||
</div>
|
||||
<div class="h-12 flex items-center mb-4">
|
||||
<input
|
||||
id="traffic-filter-option-3"
|
||||
type="radio"
|
||||
name="traffic-filter"
|
||||
value="tcp"
|
||||
class="w-4 h-4 border-gray-300 focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-600 dark:focus:bg-blue-600 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
<label
|
||||
for="traffic-filter-option-3"
|
||||
class="block ml-4 text-sm font-medium text-gray-900 dark:text-gray-300"
|
||||
>
|
||||
TCP
|
||||
</label>
|
||||
<input
|
||||
disabled
|
||||
placeholder="Enter port range(s)"
|
||||
id="tcp-port"
|
||||
name="tcp-port"
|
||||
class="ml-8 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-48 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
|
||||
/>
|
||||
</div>
|
||||
<div class="h-12 flex items-center">
|
||||
<input
|
||||
id="traffic-filter-option-4"
|
||||
type="radio"
|
||||
name="traffic-filter"
|
||||
value="udp"
|
||||
class="w-4 h-4 border-gray-300 focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-600 dark:focus:bg-blue-600 dark:bg-gray-700 dark:border-gray-600"
|
||||
checked
|
||||
/>
|
||||
<label
|
||||
for="traffic-filter-option-4"
|
||||
class="block ml-4 text-sm font-medium text-gray-900 dark:text-gray-300"
|
||||
>
|
||||
UDP
|
||||
</label>
|
||||
<input
|
||||
value="53"
|
||||
placeholder="Enter port range(s)"
|
||||
id="udp-port"
|
||||
name="udp-port"
|
||||
class="ml-8 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-48 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<.label for="gateways">
|
||||
Gateway(s)
|
||||
</.label>
|
||||
|
||||
<div class="rounded-lg relative overflow-x-auto">
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Name
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
IP
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Status
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
|
||||
<th
|
||||
scope="row"
|
||||
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
aws-primary
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
<code class="block text-xs">201.45.66.101</code>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-green-900 dark:text-green-300">
|
||||
Online
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<a
|
||||
href="#"
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
Link
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
|
||||
<th
|
||||
scope="row"
|
||||
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
aws-secondary
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
<code class="block text-xs">11.34.176.175</code>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-green-900 dark:text-green-300">
|
||||
Online
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<a
|
||||
href="#"
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
Link
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
|
||||
<th
|
||||
scope="row"
|
||||
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
gcp-primary
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
<code class="block text-xs">45.11.23.17</code>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-green-900 dark:text-green-300">
|
||||
Online
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<a
|
||||
href="#"
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
Link
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
|
||||
<th
|
||||
scope="row"
|
||||
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
gcp-secondary
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
<code class="block text-xs">80.113.105.104</code>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-green-900 dark:text-green-300">
|
||||
Online
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<a
|
||||
href="#"
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
Link
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button
|
||||
type="submit"
|
||||
class="text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
"""
|
||||
end
|
||||
end
|
||||
@@ -19,6 +19,387 @@ defmodule Web.ResourcesLive.Index do
|
||||
</.add_button>
|
||||
</:actions>
|
||||
</.section_header>
|
||||
<!-- Resources Table -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden">
|
||||
<div class="flex flex-col md:flex-row items-center justify-between space-y-3 md:space-y-0 md:space-x-4 p-4">
|
||||
<div class="w-full md:w-1/2">
|
||||
<form class="flex items-center">
|
||||
<label for="simple-search" class="sr-only">Search</label>
|
||||
<div class="relative w-full">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<.icon name="hero-magnifying-glass" class="w-5 h-5 text-gray-500 dark:text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="simple-search"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full pl-10 p-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
|
||||
placeholder="Search"
|
||||
required=""
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<.button_group>
|
||||
<:first>
|
||||
All
|
||||
</:first>
|
||||
<:middle>
|
||||
Online
|
||||
</:middle>
|
||||
<:last>
|
||||
Deleted
|
||||
</:last>
|
||||
</.button_group>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
||||
<tr>
|
||||
<th scope="col" class="px-4 py-3">
|
||||
<div class="flex items-center">
|
||||
Name
|
||||
<a href="#">
|
||||
<.icon name="hero-chevron-up-down-solid" class="w-4 h-4 ml-1" />
|
||||
</a>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="px-4 py-3">
|
||||
<div class="flex items-center">
|
||||
Address
|
||||
<a href="#">
|
||||
<.icon name="hero-chevron-up-down-solid" class="w-4 h-4 ml-1" />
|
||||
</a>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="px-4 py-3">
|
||||
<div class="flex items-center">
|
||||
Linked Gateways
|
||||
<a href="#">
|
||||
<.icon name="hero-chevron-up-down-solid" class="w-4 h-4 ml-1" />
|
||||
</a>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="px-4 py-3">
|
||||
<div class="flex items-center">
|
||||
Groups
|
||||
<a href="#">
|
||||
<.icon name="hero-chevron-up-down-solid" class="w-4 h-4 ml-1" />
|
||||
</a>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="px-4 py-3">
|
||||
<span class="sr-only">Actions</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th
|
||||
scope="row"
|
||||
class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
<.link
|
||||
navigate={~p"/resources/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
GitLab
|
||||
</.link>
|
||||
</th>
|
||||
<td class="px-4 py-3">
|
||||
<code class="block text-xs">www.gitlab.com:443</code>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<.link
|
||||
navigate={~p"/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
sjc-egress-1</.link>, <.link
|
||||
navigate={~p"/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
sjc-egress-2</.link>,
|
||||
<.link
|
||||
navigate={~p"/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
sjc-egress-3
|
||||
</.link>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<.link navigate={~p"/groups/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}>
|
||||
<span class="bg-gray-100 text-gray-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-gray-900 dark:text-gray-300">
|
||||
Engineering
|
||||
</span>
|
||||
</.link>
|
||||
<.link navigate={~p"/groups/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}>
|
||||
<span class="bg-gray-100 text-gray-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-gray-900 dark:text-gray-300">
|
||||
DevOps
|
||||
</span>
|
||||
</.link>
|
||||
</td>
|
||||
<td class="px-4 py-3 flex items-center justify-end">
|
||||
<button
|
||||
id="resource-1-dropdown-button"
|
||||
data-dropdown-toggle="resource-1-dropdown"
|
||||
class="inline-flex items-center p-0.5 text-sm font-medium text-center text-gray-500 hover:text-gray-800 rounded-lg focus:outline-none dark:text-gray-400 dark:hover:text-gray-100"
|
||||
type="button"
|
||||
>
|
||||
<.icon name="hero-ellipsis-horizontal" class="w-5 h-5" />
|
||||
</button>
|
||||
<div
|
||||
id="resource-1-dropdown"
|
||||
class="hidden z-10 w-44 bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600"
|
||||
>
|
||||
<ul
|
||||
class="py-1 text-sm text-gray-700 dark:text-gray-200"
|
||||
aria-labelledby="resource-1-dropdown-button"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
>
|
||||
Show
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
>
|
||||
Delete
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th
|
||||
scope="row"
|
||||
class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
<.link
|
||||
navigate={~p"/resources/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
Staging
|
||||
</.link>
|
||||
</th>
|
||||
<td class="px-4 py-3">
|
||||
<code class="block text-xs">10.0.0.0/24</code>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<.link
|
||||
navigate={~p"/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
private-vpc
|
||||
</.link>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<.link navigate={~p"/groups/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}>
|
||||
<span class="bg-gray-100 text-gray-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-gray-900 dark:text-gray-300">
|
||||
Engineering
|
||||
</span>
|
||||
</.link>
|
||||
<.link navigate={~p"/groups/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}>
|
||||
<span class="bg-gray-100 text-gray-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-gray-900 dark:text-gray-300">
|
||||
IT
|
||||
</span>
|
||||
</.link>
|
||||
</td>
|
||||
<td class="px-4 py-3 flex items-center justify-end">
|
||||
<button
|
||||
id="resource-2-dropdown-button"
|
||||
data-dropdown-toggle="resource-2-dropdown"
|
||||
class="inline-flex items-center p-0.5 text-sm font-medium text-center text-gray-500 hover:text-gray-800 rounded-lg focus:outline-none dark:text-gray-400 dark:hover:text-gray-100"
|
||||
type="button"
|
||||
>
|
||||
<.icon name="hero-ellipsis-horizontal" class="w-5 h-5" />
|
||||
</button>
|
||||
<div
|
||||
id="resource-2-dropdown"
|
||||
class="hidden z-10 w-44 bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600"
|
||||
>
|
||||
<ul
|
||||
class="py-1 text-sm text-gray-700 dark:text-gray-200"
|
||||
aria-labelledby="resource-2-dropdown-button"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
>
|
||||
Show
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
>
|
||||
Delete
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th
|
||||
scope="row"
|
||||
class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
<.link
|
||||
navigate={~p"/resources/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
Production
|
||||
</.link>
|
||||
</th>
|
||||
<td class="px-4 py-3">
|
||||
<code class="block text-xs">64.55.78.0/24</code>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<.link
|
||||
navigate={~p"/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
prod-gw-1</.link>,
|
||||
<.link
|
||||
navigate={~p"/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
prod-gw-2
|
||||
</.link>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<.link navigate={~p"/groups/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}>
|
||||
<span class="bg-gray-100 text-gray-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-gray-900 dark:text-gray-300">
|
||||
eng-prod
|
||||
</span>
|
||||
</.link>
|
||||
<.link navigate={~p"/groups/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}>
|
||||
<span class="bg-gray-100 text-gray-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-gray-900 dark:text-gray-300">
|
||||
sec-prod
|
||||
</span>
|
||||
</.link>
|
||||
</td>
|
||||
<td class="px-4 py-3 flex items-center justify-end">
|
||||
<button
|
||||
id="resource-3-dropdown-button"
|
||||
data-dropdown-toggle="resource-3-dropdown"
|
||||
class="inline-flex items-center p-0.5 text-sm font-medium text-center text-gray-500 hover:text-gray-800 rounded-lg focus:outline-none dark:text-gray-400 dark:hover:text-gray-100"
|
||||
type="button"
|
||||
>
|
||||
<.icon name="hero-ellipsis-horizontal" class="w-5 h-5" />
|
||||
</button>
|
||||
<div
|
||||
id="resource-3-dropdown"
|
||||
class="hidden z-10 w-44 bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600"
|
||||
>
|
||||
<ul
|
||||
class="py-1 text-sm text-gray-700 dark:text-gray-200"
|
||||
aria-labelledby="resource-3-dropdown-button"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
>
|
||||
Show
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
>
|
||||
Delete
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th
|
||||
scope="row"
|
||||
class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
<.link
|
||||
navigate={~p"/resources/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
Jira
|
||||
</.link>
|
||||
</th>
|
||||
<td class="px-4 py-3">
|
||||
<code class="block text-xs">jira.company.com:443</code>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<.link
|
||||
navigate={~p"/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
prod-gw-1</.link>,
|
||||
<.link
|
||||
navigate={~p"/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
prod-gw-2
|
||||
</.link>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<.link navigate={~p"/groups/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}>
|
||||
<span class="bg-gray-100 text-gray-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-gray-900 dark:text-gray-300">
|
||||
Engineering
|
||||
</span>
|
||||
</.link>
|
||||
</td>
|
||||
<td class="px-4 py-3 flex items-center justify-end">
|
||||
<button
|
||||
id="resource-3-dropdown-button"
|
||||
data-dropdown-toggle="resource-3-dropdown"
|
||||
class="inline-flex items-center p-0.5 text-sm font-medium text-center text-gray-500 hover:text-gray-800 rounded-lg focus:outline-none dark:text-gray-400 dark:hover:text-gray-100"
|
||||
type="button"
|
||||
>
|
||||
<.icon name="hero-ellipsis-horizontal" class="w-5 h-5" />
|
||||
</button>
|
||||
<div
|
||||
id="resource-3-dropdown"
|
||||
class="hidden z-10 w-44 bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600"
|
||||
>
|
||||
<ul
|
||||
class="py-1 text-sm text-gray-700 dark:text-gray-200"
|
||||
aria-labelledby="resource-3-dropdown-button"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
>
|
||||
Show
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
>
|
||||
Delete
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<.paginator page={3} total_pages={100} collection_base_path={~p"/gateways"} />
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,6 +15,251 @@ defmodule Web.ResourcesLive.New do
|
||||
Add a new Resource
|
||||
</:title>
|
||||
</.section_header>
|
||||
<!-- Add Resource -->
|
||||
<section class="bg-white dark:bg-gray-900">
|
||||
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
|
||||
<h2 class="mb-4 text-xl font-bold text-gray-900 dark:text-white">Resource details</h2>
|
||||
<form action="#">
|
||||
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
|
||||
<div>
|
||||
<.label for="address">
|
||||
Address
|
||||
</.label>
|
||||
<input
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
name="address"
|
||||
id="resource-address"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
|
||||
placeholder="Enter IP address, CIDR, or DNS name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<.label for="name">
|
||||
Name
|
||||
</.label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="resource-name"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
|
||||
placeholder="Name this Resource"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<.label for="traffic-filter">
|
||||
Traffic restriction
|
||||
</.label>
|
||||
<div class="h-12 flex items-center my-4">
|
||||
<input
|
||||
id="traffic-filter-option-1"
|
||||
type="radio"
|
||||
name="traffic-filter"
|
||||
value="none"
|
||||
checked
|
||||
class="w-4 h-4 border-gray-300 focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-600 dark:focus:bg-blue-600 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
<label
|
||||
for="traffic-filter-option-1"
|
||||
class="block ml-4 text-sm font-medium text-gray-900 dark:text-gray-300"
|
||||
>
|
||||
Permit all
|
||||
</label>
|
||||
</div>
|
||||
<div class="h-12 flex items-center mb-4">
|
||||
<input
|
||||
id="traffic-filter-option-2"
|
||||
type="radio"
|
||||
name="traffic-filter"
|
||||
value="icmp"
|
||||
class="w-4 h-4 border-gray-300 focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-600 dark:focus:bg-blue-600 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
<label
|
||||
for="traffic-filter-option-2"
|
||||
class="block ml-4 text-sm font-medium text-gray-900 dark:text-gray-300"
|
||||
>
|
||||
ICMP
|
||||
</label>
|
||||
</div>
|
||||
<div class="h-12 flex items-center mb-4">
|
||||
<input
|
||||
id="traffic-filter-option-3"
|
||||
type="radio"
|
||||
name="traffic-filter"
|
||||
value="tcp"
|
||||
class="w-4 h-4 border-gray-300 focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-600 dark:focus:bg-blue-600 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
<label
|
||||
for="traffic-filter-option-3"
|
||||
class="block ml-4 text-sm font-medium text-gray-900 dark:text-gray-300"
|
||||
>
|
||||
TCP
|
||||
</label>
|
||||
<input
|
||||
placeholder="Enter port range(s)"
|
||||
id="tcp-port"
|
||||
name="tcp-port"
|
||||
class="ml-8 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-auto p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
|
||||
/>
|
||||
</div>
|
||||
<div class="h-12 flex items-center">
|
||||
<input
|
||||
id="traffic-filter-option-4"
|
||||
type="radio"
|
||||
name="traffic-filter"
|
||||
value="udp"
|
||||
class="w-4 h-4 border-gray-300 focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-600 dark:focus:bg-blue-600 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
<label
|
||||
for="traffic-filter-option-4"
|
||||
class="block ml-4 text-sm font-medium text-gray-900 dark:text-gray-300"
|
||||
>
|
||||
UDP
|
||||
</label>
|
||||
<input
|
||||
placeholder="Enter port range(s)"
|
||||
id="udp-port"
|
||||
name="udp-port"
|
||||
class="ml-8 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-auto p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<.label for="gateways">
|
||||
Gateway(s)
|
||||
</.label>
|
||||
|
||||
<div class="rounded-lg relative overflow-x-auto">
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Name
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
IP
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Status
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
|
||||
<th
|
||||
scope="row"
|
||||
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
aws-primary
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
<code class="block text-xs">201.45.66.101</code>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-green-900 dark:text-green-300">
|
||||
Online
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<a
|
||||
href="#"
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
Link
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
|
||||
<th
|
||||
scope="row"
|
||||
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
aws-secondary
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
<code class="block text-xs">11.34.176.175</code>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-green-900 dark:text-green-300">
|
||||
Online
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<a
|
||||
href="#"
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
Link
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
|
||||
<th
|
||||
scope="row"
|
||||
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
gcp-primary
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
<code class="block text-xs">45.11.23.17</code>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-green-900 dark:text-green-300">
|
||||
Online
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<a
|
||||
href="#"
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
Link
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
|
||||
<th
|
||||
scope="row"
|
||||
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
gcp-secondary
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
<code class="block text-xs">80.113.105.104</code>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-green-900 dark:text-green-300">
|
||||
Online
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<a
|
||||
href="#"
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
Link
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button
|
||||
type="submit"
|
||||
class="text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,7 +3,202 @@ defmodule Web.ResourcesLive.Show do
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
Show Resource
|
||||
<.section_header>
|
||||
<:breadcrumbs>
|
||||
<.breadcrumbs entries={[
|
||||
%{label: "Home", path: ~p"/"},
|
||||
%{label: "Resources", path: ~p"/resources"},
|
||||
%{label: "Engineering Jira", path: ~p"/resources/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
]} />
|
||||
</:breadcrumbs>
|
||||
<:title>
|
||||
Viewing Resource <code>Engineering Jira</code>
|
||||
</:title>
|
||||
<:actions>
|
||||
<.edit_button navigate={~p"/resources/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89/edit"}>
|
||||
Edit Resource
|
||||
</.edit_button>
|
||||
</:actions>
|
||||
</.section_header>
|
||||
<!-- Gateway details -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden">
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||
<tbody>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-700">
|
||||
<th
|
||||
scope="row"
|
||||
class="text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
Engineering Jira
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-700">
|
||||
<th
|
||||
scope="row"
|
||||
class="text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
|
||||
>
|
||||
Address
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
jira.company.com
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-700">
|
||||
<th
|
||||
scope="row"
|
||||
class="text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
|
||||
>
|
||||
Traffic restriction
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
Permit all
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-700">
|
||||
<th
|
||||
scope="row"
|
||||
class="text-right px-6 py-4 font-medium text-gray-900 whitespace-nowrap bg-gray-50 dark:text-white dark:bg-gray-800"
|
||||
>
|
||||
Created
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
4/15/22 12:32 PM by
|
||||
<.link
|
||||
class="text-blue-600 hover:underline"
|
||||
navigate={~p"/users/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
>
|
||||
Andrew Dryga
|
||||
</.link>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Linked Resources table -->
|
||||
<div class="grid grid-cols-1 p-4 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
|
||||
<div class="col-span-full mb-4 xl:mb-2">
|
||||
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">
|
||||
Linked Gateways
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative overflow-x-auto">
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||
<thead class="text-xs text-gray-900 uppercase dark:text-gray-400">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Name
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
IP
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Status
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="bg-white dark:bg-gray-800">
|
||||
<th
|
||||
scope="row"
|
||||
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
<.link
|
||||
navigate={~p"/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
aws-primary
|
||||
</.link>
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
<code class="block text-xs">201.45.66.101</code>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-green-900 dark:text-green-300">
|
||||
Online
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th
|
||||
scope="row"
|
||||
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
<.link
|
||||
navigate={~p"/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
aws-secondary
|
||||
</.link>
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
<code class="block text-xs">11.34.176.175</code>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-green-900 dark:text-green-300">
|
||||
Online
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th
|
||||
scope="row"
|
||||
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
<.link
|
||||
navigate={~p"/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
gcp-primary
|
||||
</.link>
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
<code class="block text-xs">45.11.23.17</code>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-green-900 dark:text-green-300">
|
||||
Online
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th
|
||||
scope="row"
|
||||
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
<.link
|
||||
navigate={~p"/gateways/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
|
||||
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
gcp-secondary
|
||||
</.link>
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
<code class="block text-xs">80.113.105.104</code>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-green-900 dark:text-green-300">
|
||||
Online
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<.section_header>
|
||||
<:title>
|
||||
Danger zone
|
||||
</:title>
|
||||
<:actions>
|
||||
<.delete_button>
|
||||
Delete Resource
|
||||
</.delete_button>
|
||||
</:actions>
|
||||
</.section_header>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,6 +2,20 @@ defmodule Web.Mailer do
|
||||
use Swoosh.Mailer, otp_app: :web
|
||||
alias Swoosh.Email
|
||||
|
||||
defp render_template(view, template, format, assigns) do
|
||||
heex = apply(view, String.to_atom("#{template}_#{format}"), [assigns])
|
||||
assigns = Keyword.merge(assigns, inner_content: heex)
|
||||
Phoenix.Template.render_to_string(view, "#{template}_#{format}", "html", assigns)
|
||||
end
|
||||
|
||||
def render_body(%Swoosh.Email{} = email, view, template, assigns) do
|
||||
assigns = assigns ++ [email: email]
|
||||
|
||||
email
|
||||
|> Email.html_body(render_template(view, template, "html", assigns))
|
||||
|> Email.text_body(render_template(view, template, "text", assigns))
|
||||
end
|
||||
|
||||
def active? do
|
||||
mailer_config = Domain.Config.fetch_env!(:web, Web.Mailer)
|
||||
mailer_config[:from_email] && mailer_config[:adapter]
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
defmodule Web.Mailer.AuthEmail do
|
||||
use Web, :verified_routes
|
||||
use Web, :html
|
||||
import Swoosh.Email
|
||||
import Web.Mailer
|
||||
|
||||
use Phoenix.Swoosh,
|
||||
template_root: Path.join(__DIR__, "templates"),
|
||||
template_path: "auth_email"
|
||||
embed_templates "auth_email/*.html", suffix: "_html"
|
||||
embed_templates "auth_email/*.text", suffix: "_text"
|
||||
|
||||
def sign_in_link_email(%Domain.Auth.Identity{} = identity) do
|
||||
params = %{
|
||||
identity_id: identity.id,
|
||||
secret: identity.provider_virtual_state.sign_in_token
|
||||
}
|
||||
|
||||
sign_in_link =
|
||||
url(
|
||||
~p"/#{identity.account_id}/sign_in/providers/#{identity.provider_id}/verify_sign_in_token?#{params}"
|
||||
)
|
||||
|
||||
default_email()
|
||||
|> subject("Firezone Sign In Link")
|
||||
|> to(identity.provider_identifier)
|
||||
|> render_body(__MODULE__, :sign_in_link, link: sign_in_link)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<h3>Magic sign-in link</h3>
|
||||
|
||||
<p>
|
||||
Dear Firezone user,
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Here is the <a href={@link} target="_blank">magic sign-in link</a>
|
||||
you requested. It is valid for 1 hour.
|
||||
If you didn't request this, you can safely discard this email.
|
||||
</p>
|
||||
|
||||
<small>
|
||||
If the link didn't work, please copy this link and open it in your browser. <%= @link %>
|
||||
</small>
|
||||
@@ -0,0 +1,11 @@
|
||||
Magic sign-in link
|
||||
|
||||
Dear Firezone user,
|
||||
|
||||
Here is the magic sign-in link you requested:
|
||||
|
||||
<%= @link %>
|
||||
|
||||
Please copy this link and open it in your browser. It is valid for 1 hour.
|
||||
|
||||
If you didn't request this, you can safely discard this email.
|
||||
44
elixir/apps/web/lib/web/plugs/secure_headers.ex
Normal file
44
elixir/apps/web/lib/web/plugs/secure_headers.ex
Normal file
@@ -0,0 +1,44 @@
|
||||
defmodule Web.Plugs.SecureHeaders do
|
||||
@behaviour Plug
|
||||
|
||||
def init(opts), do: opts
|
||||
|
||||
def call(%Plug.Conn{} = conn, _opts) do
|
||||
conn
|
||||
|> put_csp_nonce_and_header()
|
||||
|> maybe_put_sts_header()
|
||||
end
|
||||
|
||||
defp put_csp_nonce_and_header(conn) do
|
||||
csp_nonce = Domain.Crypto.rand_string(8)
|
||||
|
||||
policy =
|
||||
Application.fetch_env!(:web, __MODULE__)
|
||||
|> Keyword.fetch!(:csp_policy)
|
||||
|> Enum.map(fn line ->
|
||||
String.replace(line, "${nonce}", csp_nonce)
|
||||
end)
|
||||
|
||||
conn
|
||||
|> Plug.Conn.put_private(:csp_nonce, csp_nonce)
|
||||
|> Phoenix.Controller.put_secure_browser_headers(%{
|
||||
"content-security-policy" => Enum.join(policy, "; ")
|
||||
})
|
||||
end
|
||||
|
||||
defp maybe_put_sts_header(conn) do
|
||||
scheme =
|
||||
conn.private.phoenix_endpoint.config(:url, [])
|
||||
|> Keyword.get(:scheme)
|
||||
|
||||
if scheme == "https" do
|
||||
Plug.Conn.put_resp_header(
|
||||
conn,
|
||||
"strict-transport-security",
|
||||
"max-age=63072000; includeSubDomains; preload"
|
||||
)
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,30 +1,110 @@
|
||||
defmodule Web.Router do
|
||||
use Web, :router
|
||||
import Web.Auth
|
||||
|
||||
pipeline :browser do
|
||||
plug :accepts, ["html"]
|
||||
plug :fetch_session
|
||||
plug :protect_from_forgery
|
||||
plug :fetch_live_flash
|
||||
plug :put_root_layout, {Web.Layouts, :root}
|
||||
plug :protect_from_forgery
|
||||
plug :put_secure_browser_headers
|
||||
plug :fetch_user_agent
|
||||
plug :fetch_subject
|
||||
end
|
||||
|
||||
pipeline :api do
|
||||
plug :accepts, ["json"]
|
||||
# TODO: auth
|
||||
plug :ensure_authenticated
|
||||
plug :ensure_authenticated_actor_type, :service_account
|
||||
end
|
||||
|
||||
pipeline :public do
|
||||
plug :accepts, ["html", "xml"]
|
||||
end
|
||||
|
||||
live_session :admin do
|
||||
scope "/", Web do
|
||||
pipe_through :browser
|
||||
pipeline :ensure_authenticated_admin do
|
||||
plug :ensure_authenticated
|
||||
plug :ensure_authenticated_actor_type, :account_admin_user
|
||||
end
|
||||
|
||||
live "/", DashboardLive
|
||||
scope "/browser", Web do
|
||||
pipe_through :public
|
||||
|
||||
get "/config.xml", BrowserController, :config
|
||||
end
|
||||
|
||||
scope "/", Web do
|
||||
pipe_through :public
|
||||
|
||||
get "/healthz", HealthController, :healthz
|
||||
end
|
||||
|
||||
if Mix.env() in [:dev, :test] do
|
||||
scope "/dev" do
|
||||
pipe_through [:public]
|
||||
forward "/mailbox", Plug.Swoosh.MailboxPreview
|
||||
end
|
||||
end
|
||||
|
||||
scope "/:account_id/sign_in", Web do
|
||||
pipe_through [:browser, :redirect_if_user_is_authenticated]
|
||||
|
||||
live_session :redirect_if_user_is_authenticated,
|
||||
on_mount: [
|
||||
Web.Sandbox,
|
||||
{Web.Auth, :redirect_if_user_is_authenticated}
|
||||
] do
|
||||
live "/", Auth.ProvidersLive, :new
|
||||
|
||||
# Adapter-specific routes
|
||||
## Email
|
||||
live "/providers/email/:provider_id", Auth.EmailLive, :confirm
|
||||
end
|
||||
|
||||
scope "/providers/:provider_id" do
|
||||
# UserPass
|
||||
post "/verify_credentials", AuthController, :verify_credentials
|
||||
|
||||
# Email
|
||||
post "/request_magic_link", AuthController, :request_magic_link
|
||||
get "/verify_sign_in_token", AuthController, :verify_sign_in_token
|
||||
|
||||
# IdP
|
||||
get "/redirect", AuthController, :redirect_to_idp
|
||||
get "/handle_callback", AuthController, :handle_idp_callback
|
||||
end
|
||||
end
|
||||
|
||||
scope "/:account_id", Web do
|
||||
pipe_through [:browser]
|
||||
|
||||
get "/sign_out", AuthController, :sign_out
|
||||
|
||||
live_session :landing,
|
||||
on_mount: [Web.Sandbox] do
|
||||
live "/", LandingLive
|
||||
end
|
||||
end
|
||||
|
||||
scope "/:account_id", Web do
|
||||
pipe_through [:browser, :ensure_authenticated_admin]
|
||||
|
||||
live_session :ensure_authenticated,
|
||||
on_mount: [
|
||||
Web.Sandbox,
|
||||
{Web.Auth, :ensure_authenticated},
|
||||
{Web.Auth, :ensure_account_admin_user_actor}
|
||||
] do
|
||||
live "/dashboard", DashboardLive
|
||||
end
|
||||
end
|
||||
|
||||
scope "/", Web do
|
||||
pipe_through [:browser, :ensure_authenticated]
|
||||
|
||||
get "/", AuthController, :sign_out
|
||||
|
||||
live_session :ensure_authenticated2 do
|
||||
# Users
|
||||
live "/users", UsersLive.Index
|
||||
live "/users/new", UsersLive.New
|
||||
@@ -70,12 +150,4 @@ defmodule Web.Router do
|
||||
live "/settings/api_tokens/new", SettingsLive.ApiTokens.New
|
||||
end
|
||||
end
|
||||
|
||||
scope "/browser", Web do
|
||||
pipe_through :public
|
||||
|
||||
get "/config.xml", BrowserController, :config
|
||||
end
|
||||
|
||||
get "/healthz", Web.HealthController, :healthz
|
||||
end
|
||||
|
||||
@@ -4,6 +4,11 @@ defmodule Web.Sandbox do
|
||||
"""
|
||||
alias Domain.Sandbox
|
||||
|
||||
def on_mount(:default, _params, _session, socket) do
|
||||
socket = allow_live_ecto_sandbox(socket)
|
||||
{:cont, socket}
|
||||
end
|
||||
|
||||
def allow_channel_sql_sandbox(socket) do
|
||||
if Map.has_key?(socket.assigns, :user_agent) do
|
||||
Sandbox.allow(Phoenix.Ecto.SQL.Sandbox, socket.assigns.user_agent)
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
defmodule Web.Session do
|
||||
@moduledoc """
|
||||
We wrap Plug.Session because it's options are resolved at compile-time,
|
||||
which doesn't work with Elixir releases and runtime configuration.
|
||||
"""
|
||||
@behaviour Plug
|
||||
|
||||
# 4 hours
|
||||
@max_cookie_age 14_400
|
||||
|
||||
@@ -6,13 +12,24 @@ defmodule Web.Session do
|
||||
@session_options [
|
||||
store: :cookie,
|
||||
key: "_firezone_key",
|
||||
# XXX: Strict doesn't work for SSO auth
|
||||
# same_site: "Strict",
|
||||
# If `same_site` is set to `Strict` then the cookie will not be sent on
|
||||
# IdP callback redirects, which will break the auth flow.
|
||||
same_site: "Lax",
|
||||
max_age: @max_cookie_age,
|
||||
sign: true,
|
||||
encrypt: true
|
||||
]
|
||||
|
||||
@impl true
|
||||
def init(opts), do: opts
|
||||
|
||||
@impl true
|
||||
def call(conn, _opts) do
|
||||
opts = options() |> Plug.Session.init()
|
||||
Plug.Session.call(conn, opts)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def options do
|
||||
@session_options ++
|
||||
[
|
||||
|
||||
@@ -24,7 +24,11 @@ defmodule Web.MixProject do
|
||||
def application do
|
||||
[
|
||||
mod: {Web.Application, []},
|
||||
extra_applications: [:logger, :runtime_tools]
|
||||
extra_applications: [
|
||||
:logger,
|
||||
:runtime_tools,
|
||||
:dialyzer
|
||||
]
|
||||
]
|
||||
end
|
||||
|
||||
@@ -41,7 +45,7 @@ defmodule Web.MixProject do
|
||||
{:phoenix_html, "~> 3.3"},
|
||||
{:phoenix_ecto, "~> 4.4"},
|
||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||
{:phoenix_live_view, "~> 0.18.16"},
|
||||
{:phoenix_live_view, "~> 0.19.3"},
|
||||
{:plug_cowboy, "~> 2.5"},
|
||||
{:gettext, "~> 0.20"},
|
||||
{:remote_ip, "~> 1.0"},
|
||||
@@ -74,7 +78,12 @@ defmodule Web.MixProject do
|
||||
{:floki, ">= 0.30.0", only: :test},
|
||||
{:bypass, "~> 2.1", only: :test},
|
||||
{:bureaucrat, "~> 0.2.9", only: :test},
|
||||
{:wallaby, "~> 0.30.0", only: :test}
|
||||
{:wallaby, "~> 0.30.0", only: :test},
|
||||
{:credo, "~> 1.5", only: [:dev, :test], runtime: false},
|
||||
{:dialyxir, "~> 1.1", only: [:dev], runtime: false},
|
||||
{:junit_formatter, "~> 3.3", only: [:test]},
|
||||
{:mix_audit, "~> 2.1", only: [:dev, :test]},
|
||||
{:sobelow, "~> 0.12", only: [:dev, :test]}
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
@@ -19,19 +19,13 @@ defmodule Web.AcceptanceCase do
|
||||
@moduletag timeout: 120_000
|
||||
|
||||
setup tags do
|
||||
Application.put_env(:wallaby, :base_url, @endpoint.url)
|
||||
Application.put_env(:wallaby, :base_url, @endpoint.url())
|
||||
tags
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
setup tags do
|
||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Domain.Repo)
|
||||
|
||||
unless tags[:async] do
|
||||
Ecto.Adapters.SQL.Sandbox.mode(Domain.Repo, {:shared, self()})
|
||||
end
|
||||
|
||||
headless? =
|
||||
if tags[:debug] do
|
||||
false
|
||||
@@ -176,7 +170,7 @@ defmodule Web.AcceptanceCase do
|
||||
# Make sure test covers all form fields
|
||||
element_names =
|
||||
session
|
||||
|> find(Query.css(".input,.textarea", visible: true, count: :any))
|
||||
|> find(Query.css("input,textarea", visible: true, count: :any))
|
||||
|> Enum.map(&Wallaby.Element.attr(&1, "name"))
|
||||
|
||||
unless Enum.count(fields) == length(element_names) do
|
||||
@@ -242,13 +236,24 @@ defmodule Web.AcceptanceCase do
|
||||
# end
|
||||
# end
|
||||
|
||||
def wait_until_window_closed(max_seconds) do
|
||||
for _ <- 1..(max_seconds * 2) do
|
||||
with [session | _] <- Wallaby.SessionStore.list_sessions_for(owner_pid: self()),
|
||||
url when is_binary(url) <- current_url(session) do
|
||||
Process.sleep(500)
|
||||
else
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
This is an extension of ExUnit's `test` macro but:
|
||||
|
||||
- it rescues the exceptions from Wallaby and prints them while sleeping the process
|
||||
\- it rescues the exceptions from Wallaby and prints them while sleeping the process
|
||||
(to allow you interacting with the browser) if test has `debug: true` tag;
|
||||
|
||||
- it takes a screenshot on failure if `debug` tag is not set to `true` or unset.
|
||||
\- it takes a screenshot on failure if `debug` tag is not set to `true` or unset.
|
||||
|
||||
Additionally, it will try to await for all the sandboxed processes to finish their work
|
||||
after the test has passed to prevent spamming logs with a lot of crash reports.
|
||||
@@ -260,7 +265,18 @@ defmodule Web.AcceptanceCase do
|
||||
quote do
|
||||
try do
|
||||
unquote(block)
|
||||
if var!(debug?) == true, do: Process.sleep(360_000)
|
||||
|
||||
if var!(debug?) == true do
|
||||
IO.puts(
|
||||
IO.ANSI.red() <>
|
||||
"Warning! This test runs in browser-debug mode, " <>
|
||||
"it will sleep the test process for 60 seconds " <>
|
||||
"or until browser window is closed." <> IO.ANSI.reset()
|
||||
)
|
||||
|
||||
wait_until_window_closed(60)
|
||||
end
|
||||
|
||||
shutdown_live_socket(var!(session))
|
||||
:ok
|
||||
rescue
|
||||
@@ -270,7 +286,8 @@ defmodule Web.AcceptanceCase do
|
||||
IO.puts(
|
||||
IO.ANSI.red() <>
|
||||
"Warning! This test runs in browser-debug mode, " <>
|
||||
"it will sleep the test process for infinity." <> IO.ANSI.reset()
|
||||
"it will sleep the test process for 60 seconds " <>
|
||||
"or until browser window is closed." <> IO.ANSI.reset()
|
||||
)
|
||||
|
||||
IO.puts("")
|
||||
@@ -278,7 +295,9 @@ defmodule Web.AcceptanceCase do
|
||||
IO.puts("Exception was rescued:")
|
||||
IO.puts(Exception.format(:error, e, __STACKTRACE__))
|
||||
IO.puts(IO.ANSI.reset())
|
||||
Process.sleep(:infinity)
|
||||
|
||||
wait_until_window_closed(60)
|
||||
:ok
|
||||
|
||||
Wallaby.screenshot_on_failure?() ->
|
||||
unquote(__MODULE__).take_screenshot(unquote(message))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
defmodule Web.AcceptanceCase.Auth do
|
||||
# import ExUnit.Assertions
|
||||
import ExUnit.Assertions
|
||||
|
||||
def fetch_session_cookie(session) do
|
||||
options = Web.Session.options()
|
||||
@@ -32,70 +32,82 @@ defmodule Web.AcceptanceCase.Auth do
|
||||
end
|
||||
end
|
||||
|
||||
# def authenticate(session, %Domain.Users.User{} = user) do
|
||||
# subject = Domain.Auth.fetch_subject!(user, "127.0.0.1", "AcceptanceCase")
|
||||
# authenticate(session, subject)
|
||||
# end
|
||||
def authenticate(session, %Domain.Auth.Identity{} = identity) do
|
||||
user_agent = fetch_session_user_agent!(session)
|
||||
remote_ip = {127, 0, 0, 1}
|
||||
subject = Domain.Auth.build_subject(identity, nil, user_agent, remote_ip)
|
||||
authenticate(session, subject)
|
||||
end
|
||||
|
||||
# def authenticate(session, %Domain.Auth.Subject{} = subject) do
|
||||
# options = Web.Session.options()
|
||||
def authenticate(session, %Domain.Auth.Subject{} = subject) do
|
||||
options = Web.Session.options()
|
||||
|
||||
# key = Keyword.fetch!(options, :key)
|
||||
# encryption_salt = Keyword.fetch!(options, :encryption_salt)
|
||||
# signing_salt = Keyword.fetch!(options, :signing_salt)
|
||||
# secret_key_base = Web.Endpoint.config(:secret_key_base)
|
||||
key = Keyword.fetch!(options, :key)
|
||||
encryption_salt = Keyword.fetch!(options, :encryption_salt)
|
||||
signing_salt = Keyword.fetch!(options, :signing_salt)
|
||||
secret_key_base = Web.Endpoint.config(:secret_key_base)
|
||||
|
||||
# with {:ok, token, _claims} <- Web.Auth.HTML.Authentication.encode_and_sign(subject) do
|
||||
# encryption_key = Plug.Crypto.KeyGenerator.generate(secret_key_base, encryption_salt, [])
|
||||
# signing_key = Plug.Crypto.KeyGenerator.generate(secret_key_base, signing_salt, [])
|
||||
with {:ok, token} <- Domain.Auth.create_session_token_from_subject(subject) do
|
||||
encryption_key = Plug.Crypto.KeyGenerator.generate(secret_key_base, encryption_salt, [])
|
||||
signing_key = Plug.Crypto.KeyGenerator.generate(secret_key_base, signing_salt, [])
|
||||
|
||||
# cookie =
|
||||
# %{
|
||||
# "guardian_default_token" => token,
|
||||
# "login_method" => "identity",
|
||||
# "logged_in_at" => DateTime.utc_now()
|
||||
# }
|
||||
# |> :erlang.term_to_binary()
|
||||
cookie =
|
||||
%{
|
||||
"session_token" => token,
|
||||
"signed_in_at" => DateTime.utc_now(),
|
||||
"live_socket_id" => "actors_sessions:#{subject.actor.id}"
|
||||
}
|
||||
|> :erlang.term_to_binary()
|
||||
|
||||
# encrypted =
|
||||
# Plug.Crypto.MessageEncryptor.encrypt(
|
||||
# cookie,
|
||||
# encryption_key,
|
||||
# signing_key
|
||||
# )
|
||||
encrypted =
|
||||
Plug.Crypto.MessageEncryptor.encrypt(
|
||||
cookie,
|
||||
encryption_key,
|
||||
signing_key
|
||||
)
|
||||
|
||||
# Wallaby.Browser.set_cookie(session, key, encrypted)
|
||||
# end
|
||||
# end
|
||||
Wallaby.Browser.set_cookie(session, key, encrypted)
|
||||
end
|
||||
end
|
||||
|
||||
# TODO
|
||||
# def assert_unauthenticated(session) do
|
||||
# with {:ok, cookie} <- fetch_session_cookie(session) do
|
||||
# if token = cookie["guardian_default_token"] do
|
||||
# # TODO
|
||||
# # {:ok, claims} = Web.Auth.HTML.Authentication.decode_and_verify(token)
|
||||
# # flunk("User is authenticated, claims: #{inspect(claims)}")
|
||||
# :ok
|
||||
# else
|
||||
# session
|
||||
# end
|
||||
# else
|
||||
# :error -> session
|
||||
# end
|
||||
# end
|
||||
TODO
|
||||
|
||||
# def assert_authenticated(session, user) do
|
||||
# with {:ok, cookie} <- fetch_session_cookie(session) do
|
||||
# # TODO
|
||||
# # {:ok, claims} <-
|
||||
# # Web.Auth.HTML.Authentication.decode_and_verify(cookie["guardian_default_token"]),
|
||||
# # {:ok, subject} <-
|
||||
# # Web.Auth.HTML.Authentication.resource_from_claims(claims) do
|
||||
# # assert elem(subject.actor, 1).id == user.id
|
||||
# session
|
||||
# else
|
||||
# :error -> flunk("No session cookie found")
|
||||
# other -> flunk("User is not authenticated: #{inspect(other)}")
|
||||
# end
|
||||
# end
|
||||
def assert_unauthenticated(session) do
|
||||
with {:ok, cookie} <- fetch_session_cookie(session) do
|
||||
if token = cookie["session_token"] do
|
||||
user_agent = fetch_session_user_agent!(session)
|
||||
remote_ip = {127, 0, 0, 1}
|
||||
|
||||
assert {:ok, subject} = Domain.Auth.sign_in(token, user_agent, remote_ip)
|
||||
flunk("User is authenticated, identity: #{inspect(subject.identity)}")
|
||||
:ok
|
||||
else
|
||||
session
|
||||
end
|
||||
else
|
||||
:error -> session
|
||||
end
|
||||
end
|
||||
|
||||
def assert_authenticated(session, identity) do
|
||||
with {:ok, cookie} <- fetch_session_cookie(session),
|
||||
user_agent = fetch_session_user_agent!(session),
|
||||
remote_ip = {127, 0, 0, 1},
|
||||
{:ok, subject} <- Domain.Auth.sign_in(cookie["session_token"], user_agent, remote_ip) do
|
||||
assert subject.identity.id == identity.id,
|
||||
"Expected #{inspect(identity)}, got #{inspect(subject.identity)}"
|
||||
|
||||
session
|
||||
else
|
||||
:error -> flunk("No session cookie found")
|
||||
other -> flunk("User is not authenticated: #{inspect(other)}")
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_session_user_agent!(session) do
|
||||
Enum.find_value(session.capabilities.chromeOptions.args, fn
|
||||
"--user-agent=" <> user_agent -> user_agent
|
||||
_ -> nil
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
defmodule Web.AcceptanceCase.Vault do
|
||||
use Wallaby.DSL
|
||||
alias Domain.AuthFixtures
|
||||
|
||||
@vault_root_token "firezone"
|
||||
@vault_endpoint "http://127.0.0.1:8200"
|
||||
@@ -12,7 +13,12 @@ defmodule Web.AcceptanceCase.Vault do
|
||||
def upsert_user(username, email, password) do
|
||||
:ok = ensure_userpass_auth_enabled()
|
||||
|
||||
:ok = request(:put, "auth/userpass/users/#{username}", %{password: password})
|
||||
:ok =
|
||||
request(:put, "auth/userpass/users/#{username}", %{
|
||||
password: password,
|
||||
token_policies: "oidc-auth",
|
||||
token_ttl: "1h"
|
||||
})
|
||||
|
||||
# User Entity and Entity Alias are created automatically when user logs-in for
|
||||
# the first time
|
||||
@@ -21,21 +27,20 @@ defmodule Web.AcceptanceCase.Vault do
|
||||
|
||||
entity_id = params["auth"]["entity_id"]
|
||||
|
||||
:ok = request(:put, "identity/entity/id/#{entity_id}", %{metadata: %{email: email}})
|
||||
:ok =
|
||||
request(:put, "identity/entity/id/#{entity_id}", %{
|
||||
metadata: %{email: email, name: username}
|
||||
})
|
||||
|
||||
:ok
|
||||
{:ok, entity_id}
|
||||
end
|
||||
|
||||
def setup_oidc_provider(
|
||||
account,
|
||||
endpoint_url,
|
||||
attrs_overrides \\ %{"auto_create_users" => true}
|
||||
) do
|
||||
# Note: this code is not safe from race conditions because provider name is not unique per test case
|
||||
def setup_oidc_provider(account, endpoint_url) do
|
||||
:ok =
|
||||
request(:put, "identity/oidc/client/firezone", %{
|
||||
assignments: "allow_all",
|
||||
redirect_uris: "#{endpoint_url}/auth/oidc/vault/callback/",
|
||||
scopes_supported: "openid,email"
|
||||
scopes_supported: "openid,email,groups,name"
|
||||
})
|
||||
|
||||
:ok =
|
||||
@@ -45,50 +50,69 @@ defmodule Web.AcceptanceCase.Vault do
|
||||
%{template: Base.encode64("{\"email\": {{identity.entity.metadata.email}}}")}
|
||||
)
|
||||
|
||||
:ok =
|
||||
request(
|
||||
:put,
|
||||
"identity/oidc/scope/name",
|
||||
%{template: Base.encode64("{\"name\": {{identity.entity.metadata.name}}}")}
|
||||
)
|
||||
|
||||
:ok =
|
||||
request(
|
||||
:put,
|
||||
"identity/oidc/scope/groups",
|
||||
%{template: Base.encode64("{\"groups\": {{identity.entity.groups.names}}}")}
|
||||
)
|
||||
|
||||
:ok =
|
||||
request(
|
||||
:put,
|
||||
"identity/oidc/provider/default",
|
||||
%{scopes_supported: "email"}
|
||||
%{scopes_supported: "email,name,groups"}
|
||||
)
|
||||
|
||||
{:ok, {200, params}} = request(:get, "identity/oidc/client/firezone")
|
||||
|
||||
Domain.ConfigFixtures.set_config(
|
||||
account,
|
||||
:openid_connect_providers,
|
||||
[
|
||||
%{
|
||||
"id" => "vault",
|
||||
"discovery_document_uri" =>
|
||||
"http://127.0.0.1:8200/v1/identity/oidc/provider/default/.well-known/openid-configuration",
|
||||
"client_id" => params["data"]["client_id"],
|
||||
"client_secret" => params["data"]["client_secret"],
|
||||
"redirect_uri" => "#{endpoint_url}/auth/oidc/vault/callback/",
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email offline_access",
|
||||
"label" => "OIDC Vault"
|
||||
}
|
||||
|> Map.merge(attrs_overrides)
|
||||
]
|
||||
)
|
||||
attrs = %{
|
||||
"discovery_document_uri" =>
|
||||
"#{@vault_endpoint}/v1/identity/oidc/provider/default/.well-known/openid-configuration",
|
||||
"client_id" => params["data"]["client_id"],
|
||||
"client_secret" => params["data"]["client_secret"],
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email name offline_access"
|
||||
}
|
||||
|
||||
:ok
|
||||
{provider, nil} =
|
||||
AuthFixtures.create_openid_connect_provider({nil, [attrs]}, name: "Vault", account: account)
|
||||
|
||||
:ok =
|
||||
request(:put, "identity/oidc/client/firezone", %{
|
||||
redirect_uris:
|
||||
"#{endpoint_url}/#{account.id}/sign_in/providers/#{provider.id}/handle_callback"
|
||||
})
|
||||
|
||||
provider
|
||||
end
|
||||
|
||||
def userpass_flow(session, oidc_login, oidc_password) do
|
||||
session
|
||||
|> assert_text("Method")
|
||||
|> fill_in(Query.css("#select-ember40"), with: "userpass")
|
||||
|> fill_in(Query.css("[data-test-select=\"auth-method\"]"), with: "userpass")
|
||||
|> fill_in(Query.fillable_field("username"), with: oidc_login)
|
||||
|> fill_in(Query.fillable_field("password"), with: oidc_password)
|
||||
|> click(Query.button("Sign In"))
|
||||
end
|
||||
|
||||
defp request(method, path, params_or_body \\ nil) do
|
||||
content_type =
|
||||
if method == :patch,
|
||||
do: "application/merge-patch+json",
|
||||
else: "application/octet-stream"
|
||||
|
||||
headers = [
|
||||
{"X-Vault-Request", "true"},
|
||||
{"X-Vault-Token", @vault_root_token}
|
||||
{"X-Vault-Token", @vault_root_token},
|
||||
{"Content-Type", content_type}
|
||||
]
|
||||
|
||||
body =
|
||||
|
||||
@@ -15,11 +15,47 @@ defmodule Web.ConnCase do
|
||||
import Phoenix.LiveViewTest
|
||||
import Web.ConnCase
|
||||
|
||||
import Swoosh.TestAssertions
|
||||
|
||||
alias Domain.Repo
|
||||
end
|
||||
end
|
||||
|
||||
setup _tags do
|
||||
{:ok, conn: Phoenix.ConnTest.build_conn()}
|
||||
user_agent = "testing"
|
||||
|
||||
conn =
|
||||
Phoenix.ConnTest.build_conn()
|
||||
|> Plug.Conn.put_req_header("user-agent", user_agent)
|
||||
|> Plug.Test.init_test_session(%{})
|
||||
|
||||
{:ok, conn: conn, user_agent: user_agent}
|
||||
end
|
||||
|
||||
def flash(conn, key) do
|
||||
Phoenix.Flash.get(conn.assigns.flash, key)
|
||||
end
|
||||
|
||||
def authorize_conn(conn, identity) do
|
||||
expires_in = DateTime.utc_now() |> DateTime.add(300, :second)
|
||||
{"user-agent", user_agent} = List.keyfind(conn.req_headers, "user-agent", 0, "FooBar 1.1")
|
||||
subject = Domain.Auth.build_subject(identity, expires_in, user_agent, conn.remote_ip)
|
||||
|
||||
conn
|
||||
|> Web.Auth.renew_session()
|
||||
|> Web.Auth.put_subject_in_session(subject)
|
||||
end
|
||||
|
||||
# @doc """
|
||||
# Logs the given `user` into the `conn`.
|
||||
|
||||
# It returns an updated `conn`.
|
||||
# """
|
||||
# def log_in_user(conn, user) do
|
||||
# token = Domain.Accounts.generate_user_session_token(user)
|
||||
|
||||
# conn
|
||||
# |> Phoenix.ConnTest.init_test_session(%{})
|
||||
# |> Plug.Conn.put_session(:user_token, token)
|
||||
# end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
defmodule Web.MailerTestAdapter do
|
||||
defmodule Web.Mailer.TestAdapter do
|
||||
use Swoosh.Adapter
|
||||
|
||||
@impl true
|
||||
56
elixir/apps/web/test/web/acceptance/auth/email_test.exs
Normal file
56
elixir/apps/web/test/web/acceptance/auth/email_test.exs
Normal file
@@ -0,0 +1,56 @@
|
||||
defmodule Web.Acceptance.Auth.EmailTest do
|
||||
use Web.AcceptanceCase, async: true
|
||||
alias Domain.{AccountsFixtures, AuthFixtures}
|
||||
|
||||
feature "renders success on invalid email to prevent enumeration attacks", %{session: session} do
|
||||
account = AccountsFixtures.create_account()
|
||||
AuthFixtures.create_email_provider(account: account)
|
||||
|
||||
session
|
||||
|> visit(~p"/#{account}/sign_in")
|
||||
|> assert_el(Query.text("Sign in with a magic link"))
|
||||
|> fill_form(%{
|
||||
"email[provider_identifier]" => "foo@bar.com"
|
||||
})
|
||||
|> click(Query.button("Request sign in link"))
|
||||
|> assert_el(Query.text("Please check your email"))
|
||||
end
|
||||
|
||||
feature "allows to log in using email link", %{session: session} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_email_provider(account: account)
|
||||
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
account: account,
|
||||
provider: provider,
|
||||
actor_default_type: :account_admin_user
|
||||
)
|
||||
|
||||
session
|
||||
|> email_login_flow(account, identity.provider_identifier)
|
||||
|> assert_el(Query.css("#user-menu-button"))
|
||||
|> assert_path(~p"/#{account}/dashboard")
|
||||
|> Auth.assert_authenticated(identity)
|
||||
end
|
||||
|
||||
defp email_login_flow(session, account, email) do
|
||||
session
|
||||
|> visit(~p"/#{account}/sign_in")
|
||||
|> assert_el(Query.text("Sign in with a magic link"))
|
||||
|> fill_form(%{
|
||||
"email[provider_identifier]" => email
|
||||
})
|
||||
|> click(Query.button("Request sign in link"))
|
||||
|> assert_el(Query.text("Please check your email"))
|
||||
|> click(Query.link("Open Local"))
|
||||
|> click(Query.link("Firezone Sign In Link"))
|
||||
|> assert_el(Query.text("HTML body preview:"))
|
||||
|
||||
email_text = text(session, Query.css(".body-text"))
|
||||
[link] = Regex.run(~r|http://localhost[^ \n\s]*|, email_text)
|
||||
|
||||
session
|
||||
|> visit(link)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,52 @@
|
||||
defmodule Web.Acceptance.Auth.OpenIDConnectTest do
|
||||
use Web.AcceptanceCase, async: true
|
||||
alias Domain.{AccountsFixtures, AuthFixtures}
|
||||
|
||||
feature "returns error when identity did not exist", %{session: session} do
|
||||
account = AccountsFixtures.create_account()
|
||||
Vault.setup_oidc_provider(account, @endpoint.url)
|
||||
|
||||
oidc_login = "firezone-1"
|
||||
oidc_password = "firezone1234_oidc"
|
||||
email = AuthFixtures.email()
|
||||
|
||||
{:ok, _entity_id} = Vault.upsert_user(oidc_login, email, oidc_password)
|
||||
|
||||
session
|
||||
|> visit(~p"/#{account}/sign_in")
|
||||
|> assert_el(Query.text("Welcome back"))
|
||||
|> click(Query.link("Log in with Vault"))
|
||||
|> Vault.userpass_flow(oidc_login, oidc_password)
|
||||
|> assert_el(Query.text("Welcome back"))
|
||||
|> assert_path(~p"/#{account}/sign_in")
|
||||
|> assert_el(Query.text("You can not authenticate to this account."))
|
||||
end
|
||||
|
||||
feature "authenticates existing user", %{session: session} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = Vault.setup_oidc_provider(account, @endpoint.url)
|
||||
|
||||
oidc_login = "firezone-1"
|
||||
oidc_password = "firezone1234_oidc"
|
||||
email = AuthFixtures.email()
|
||||
|
||||
{:ok, entity_id} = Vault.upsert_user(oidc_login, email, oidc_password)
|
||||
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
account: account,
|
||||
provider: provider,
|
||||
actor_default_type: :account_admin_user,
|
||||
provider_identifier: entity_id
|
||||
)
|
||||
|
||||
session
|
||||
|> visit(~p"/#{account}/sign_in")
|
||||
|> assert_el(Query.text("Welcome back"))
|
||||
|> click(Query.link("Log in with Vault"))
|
||||
|> Vault.userpass_flow(oidc_login, oidc_password)
|
||||
|> assert_el(Query.css("#user-menu-button"))
|
||||
|> Auth.assert_authenticated(identity)
|
||||
|> assert_path(~p"/#{account}/dashboard")
|
||||
end
|
||||
end
|
||||
156
elixir/apps/web/test/web/acceptance/auth/userpass_test.exs
Normal file
156
elixir/apps/web/test/web/acceptance/auth/userpass_test.exs
Normal file
@@ -0,0 +1,156 @@
|
||||
defmodule Web.Acceptance.Auth.UserPassTest do
|
||||
use Web.AcceptanceCase, async: true
|
||||
alias Domain.{AccountsFixtures, AuthFixtures, ActorsFixtures}
|
||||
|
||||
feature "renders error on invalid login or password", %{session: session} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_userpass_provider(account: account)
|
||||
password = "Firezone1234"
|
||||
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
account: account,
|
||||
provider: provider,
|
||||
provider_virtual_state: %{"password" => password, "password_confirmation" => password}
|
||||
)
|
||||
|
||||
session
|
||||
|> password_login_flow(account, identity.provider_identifier, "invalid")
|
||||
|> assert_error_flash("Invalid username or password.")
|
||||
|> password_login_flow(account, "invalid@example.com", password)
|
||||
|> assert_error_flash("Invalid username or password.")
|
||||
end
|
||||
|
||||
feature "renders error on if identity is disabled", %{session: session} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_userpass_provider(account: account)
|
||||
password = "Firezone1234"
|
||||
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
account: account,
|
||||
provider: provider,
|
||||
provider_virtual_state: %{"password" => password, "password_confirmation" => password}
|
||||
)
|
||||
|> AuthFixtures.delete_identity()
|
||||
|
||||
session
|
||||
|> password_login_flow(account, identity.provider_identifier, password)
|
||||
|> assert_error_flash("Invalid username or password.")
|
||||
end
|
||||
|
||||
feature "renders error on if actor is disabled", %{session: session} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_userpass_provider(account: account)
|
||||
provider_identifier = AuthFixtures.random_provider_identifier(provider)
|
||||
|
||||
actor =
|
||||
ActorsFixtures.create_actor(
|
||||
account: account,
|
||||
provider: provider,
|
||||
provider_identifier: provider_identifier
|
||||
)
|
||||
|> ActorsFixtures.disable()
|
||||
|
||||
password = "Firezone1234"
|
||||
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
account: account,
|
||||
provider: provider,
|
||||
actor: actor,
|
||||
provider_virtual_state: %{"password" => password, "password_confirmation" => password}
|
||||
)
|
||||
|
||||
session
|
||||
|> password_login_flow(account, identity.provider_identifier, password)
|
||||
|> assert_error_flash("Invalid username or password.")
|
||||
end
|
||||
|
||||
feature "renders error on if actor is deleted", %{session: session} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_userpass_provider(account: account)
|
||||
provider_identifier = AuthFixtures.random_provider_identifier(provider)
|
||||
|
||||
actor =
|
||||
ActorsFixtures.create_actor(
|
||||
account: account,
|
||||
provider: provider,
|
||||
provider_identifier: provider_identifier
|
||||
)
|
||||
|> ActorsFixtures.delete()
|
||||
|
||||
password = "Firezone1234"
|
||||
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
account: account,
|
||||
provider: provider,
|
||||
actor: actor,
|
||||
provider_virtual_state: %{"password" => password, "password_confirmation" => password}
|
||||
)
|
||||
|
||||
session
|
||||
|> password_login_flow(account, identity.provider_identifier, password)
|
||||
|> assert_error_flash("Invalid username or password.")
|
||||
end
|
||||
|
||||
feature "redirects to dashboard after successful log in as account_admin_user", %{
|
||||
session: session
|
||||
} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_userpass_provider(account: account)
|
||||
password = "Firezone1234"
|
||||
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
account: account,
|
||||
provider: provider,
|
||||
actor_default_type: :account_admin_user,
|
||||
provider_virtual_state: %{"password" => password, "password_confirmation" => password}
|
||||
)
|
||||
|
||||
session
|
||||
|> password_login_flow(account, identity.provider_identifier, password)
|
||||
|> assert_el(Query.css("#user-menu-button"))
|
||||
|> assert_path(~p"/#{account}/dashboard")
|
||||
|> Auth.assert_authenticated(identity)
|
||||
end
|
||||
|
||||
feature "redirects to landing page after successful log in as account_user", %{
|
||||
session: session
|
||||
} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_userpass_provider(account: account)
|
||||
password = "Firezone1234"
|
||||
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
account: account,
|
||||
provider: provider,
|
||||
actor_default_type: :account_user,
|
||||
provider_virtual_state: %{"password" => password, "password_confirmation" => password}
|
||||
)
|
||||
|
||||
session
|
||||
|> password_login_flow(account, identity.provider_identifier, password)
|
||||
|> assert_path(~p"/#{account}")
|
||||
end
|
||||
|
||||
defp password_login_flow(session, account, username, password) do
|
||||
session
|
||||
|> visit(~p"/#{account}/sign_in")
|
||||
|> assert_el(Query.text("Welcome back"))
|
||||
|> assert_el(Query.text("Sign in with username and password"))
|
||||
|> fill_form(%{
|
||||
"userpass[provider_identifier]" => username,
|
||||
"userpass[secret]" => password
|
||||
})
|
||||
|> click(Query.button("Sign in"))
|
||||
end
|
||||
|
||||
defp assert_error_flash(session, text) do
|
||||
assert_text(session, Query.css(".flash-error"), text)
|
||||
session
|
||||
end
|
||||
end
|
||||
97
elixir/apps/web/test/web/acceptance/auth_test.exs
Normal file
97
elixir/apps/web/test/web/acceptance/auth_test.exs
Normal file
@@ -0,0 +1,97 @@
|
||||
defmodule Web.Acceptance.AuthTest do
|
||||
use Web.AcceptanceCase, async: true
|
||||
alias Domain.{AccountsFixtures, AuthFixtures}
|
||||
|
||||
feature "renders all sign in options", %{session: session} do
|
||||
account = AccountsFixtures.create_account()
|
||||
AuthFixtures.create_userpass_provider(account: account)
|
||||
|
||||
AuthFixtures.create_email_provider(account: account)
|
||||
|
||||
AuthFixtures.create_token_provider(account: account)
|
||||
|
||||
{openid_connect_provider, _bypass} =
|
||||
AuthFixtures.start_openid_providers(["google"])
|
||||
|> AuthFixtures.create_openid_connect_provider(account: account)
|
||||
|
||||
session
|
||||
|> visit(~p"/#{account}/sign_in")
|
||||
|> assert_el(Query.text("Welcome back"))
|
||||
|> assert_el(Query.link("Log in with #{openid_connect_provider.name}"))
|
||||
|> assert_el(Query.text("Sign in with username and password"))
|
||||
|> assert_el(Query.text("Sign in with a magic link"))
|
||||
end
|
||||
|
||||
describe "sign out" do
|
||||
feature "signs out admin user", %{session: session} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_userpass_provider(account: account)
|
||||
password = "Firezone1234"
|
||||
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
account: account,
|
||||
provider: provider,
|
||||
actor_default_type: :account_admin_user,
|
||||
provider_virtual_state: %{"password" => password, "password_confirmation" => password}
|
||||
)
|
||||
|
||||
session
|
||||
|> visit(~p"/#{account}")
|
||||
|> Auth.authenticate(identity)
|
||||
|> visit(~p"/#{account}/dashboard")
|
||||
|> assert_el(Query.css("#user-menu-button"))
|
||||
|> click(Query.css("#user-menu-button"))
|
||||
|> click(Query.link("Sign out"))
|
||||
|> assert_el(Query.text("Sign in with username and password"))
|
||||
|> Auth.assert_unauthenticated()
|
||||
|> assert_path(~p"/#{account}/sign_in")
|
||||
end
|
||||
|
||||
feature "signs out unprivileged user", %{session: session} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_userpass_provider(account: account)
|
||||
password = "Firezone1234"
|
||||
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
account: account,
|
||||
provider: provider,
|
||||
actor_default_type: :account_user,
|
||||
provider_virtual_state: %{"password" => password, "password_confirmation" => password}
|
||||
)
|
||||
|
||||
session
|
||||
|> visit(~p"/#{account}")
|
||||
|> Auth.authenticate(identity)
|
||||
|> visit(~p"/#{account}/sign_out")
|
||||
|> assert_el(Query.text("Sign in with username and password"))
|
||||
|> Auth.assert_unauthenticated()
|
||||
|> assert_path(~p"/#{account}/sign_in")
|
||||
end
|
||||
end
|
||||
|
||||
feature "does not allow regular user to navigate to admin routes", %{session: session} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_userpass_provider(account: account)
|
||||
password = "Firezone1234"
|
||||
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
account: account,
|
||||
provider: provider,
|
||||
actor_default_type: :account_user,
|
||||
provider_virtual_state: %{"password" => password, "password_confirmation" => password}
|
||||
)
|
||||
|
||||
session =
|
||||
session
|
||||
|> visit(~p"/#{account}")
|
||||
|> Auth.authenticate(identity)
|
||||
|> visit(~p"/#{account}/dashboard")
|
||||
|
||||
assert text(session) == "Not Found"
|
||||
|
||||
assert_path(session, ~p"/#{account}/dashboard")
|
||||
end
|
||||
end
|
||||
413
elixir/apps/web/test/web/auth_test.exs
Normal file
413
elixir/apps/web/test/web/auth_test.exs
Normal file
@@ -0,0 +1,413 @@
|
||||
defmodule Web.AuthTest do
|
||||
use Web.ConnCase, async: true
|
||||
import Web.Auth
|
||||
alias Phoenix.LiveView
|
||||
alias Domain.{AccountsFixtures, ActorsFixtures, AuthFixtures}
|
||||
|
||||
setup do
|
||||
account = AccountsFixtures.create_account()
|
||||
|
||||
admin_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
|
||||
admin_identity = AuthFixtures.create_identity(account: account, actor: admin_actor)
|
||||
admin_subject = AuthFixtures.create_subject(admin_identity)
|
||||
|
||||
user_actor = ActorsFixtures.create_actor(type: :account_user, account: account)
|
||||
user_identity = AuthFixtures.create_identity(account: account, actor: user_actor)
|
||||
user_subject = AuthFixtures.create_subject(user_identity)
|
||||
|
||||
%{
|
||||
account: account,
|
||||
admin_actor: admin_actor,
|
||||
admin_identity: admin_identity,
|
||||
admin_subject: admin_subject,
|
||||
user_actor: user_actor,
|
||||
user_identity: user_identity,
|
||||
user_subject: user_subject
|
||||
}
|
||||
end
|
||||
|
||||
describe "signed_in_path/1" do
|
||||
test "redirects to dashboard after sign in as account admin", %{admin_subject: subject} do
|
||||
assert signed_in_path(subject) == ~p"/#{subject.account}/dashboard"
|
||||
end
|
||||
|
||||
test "redirects to account landing after sign in as account user", %{user_subject: subject} do
|
||||
assert signed_in_path(subject) == ~p"/#{subject.account}"
|
||||
end
|
||||
end
|
||||
|
||||
describe "put_subject_in_session/2" do
|
||||
test "persists token session", %{conn: conn, admin_subject: subject} do
|
||||
conn = put_subject_in_session(conn, subject)
|
||||
assert token = get_session(conn, "session_token")
|
||||
|
||||
assert {:ok, _subject} =
|
||||
Domain.Auth.sign_in(token, subject.context.user_agent, subject.context.remote_ip)
|
||||
end
|
||||
|
||||
test "persists sign in time in session", %{conn: conn, admin_subject: subject} do
|
||||
conn = put_subject_in_session(conn, subject)
|
||||
assert %DateTime{} = get_session(conn, "signed_in_at")
|
||||
end
|
||||
|
||||
test "persists live socket id in session", %{conn: conn, admin_subject: subject} do
|
||||
conn = put_subject_in_session(conn, subject)
|
||||
assert get_session(conn, "live_socket_id") == "actors_sessions:#{subject.actor.id}"
|
||||
end
|
||||
end
|
||||
|
||||
describe "sign_out/1" do
|
||||
test "erases session and cookies", %{conn: conn, admin_subject: subject} do
|
||||
{:ok, session_token} = Domain.Auth.create_session_token_from_subject(subject)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_session(:session_token, session_token)
|
||||
|> fetch_cookies()
|
||||
|> sign_out()
|
||||
|
||||
refute get_session(conn, :session_token)
|
||||
refute get_session(conn, :live_socket_id)
|
||||
end
|
||||
|
||||
test "keeps preferred_locale session value", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> put_session(:preferred_locale, "uk_UA")
|
||||
|> fetch_cookies()
|
||||
|> sign_out()
|
||||
|
||||
assert get_session(conn, :preferred_locale) == "uk_UA"
|
||||
end
|
||||
|
||||
test "broadcasts to the given live_socket_id", %{admin_subject: subject, conn: conn} do
|
||||
live_socket_id = "actors_sessions:#{subject.actor.id}"
|
||||
Web.Endpoint.subscribe(live_socket_id)
|
||||
|
||||
conn
|
||||
|> put_private(:phoenix_endpoint, @endpoint)
|
||||
|> put_session(:live_socket_id, live_socket_id)
|
||||
|> sign_out()
|
||||
|
||||
assert_receive %Phoenix.Socket.Broadcast{event: "disconnect", topic: ^live_socket_id}
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_user_agent/2" do
|
||||
test "assigns user agent value to connection assigns", %{conn: conn, user_agent: user_agent} do
|
||||
conn = fetch_user_agent(conn, [])
|
||||
assert conn.assigns.user_agent == user_agent
|
||||
end
|
||||
|
||||
test "does nothing when user agent header is not set" do
|
||||
conn =
|
||||
Phoenix.ConnTest.build_conn()
|
||||
|> Plug.Test.init_test_session(%{})
|
||||
|
||||
conn = fetch_user_agent(conn, [])
|
||||
refute Map.has_key?(conn.assigns, :user_agent)
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_subject/2" do
|
||||
setup context do
|
||||
%{conn: assign(context.conn, :user_agent, context.admin_subject.context.user_agent)}
|
||||
end
|
||||
|
||||
test "authenticates user from session", %{conn: conn, admin_subject: subject} do
|
||||
{:ok, session_token} = Domain.Auth.create_session_token_from_subject(subject)
|
||||
|
||||
conn =
|
||||
%{
|
||||
conn
|
||||
| path_params: %{"account_id" => subject.account.id},
|
||||
remote_ip: {100, 64, 100, 58}
|
||||
}
|
||||
|> put_session(:session_token, session_token)
|
||||
|> fetch_subject([])
|
||||
|
||||
assert conn.assigns.subject.identity.id == subject.identity.id
|
||||
assert conn.assigns.subject.actor.id == subject.actor.id
|
||||
end
|
||||
|
||||
test "does not authenticate to an incorrect account", %{conn: conn, admin_subject: subject} do
|
||||
{:ok, session_token} = Domain.Auth.create_session_token_from_subject(subject)
|
||||
|
||||
conn =
|
||||
%{
|
||||
conn
|
||||
| path_params: %{"account_id" => Ecto.UUID.generate()},
|
||||
remote_ip: {100, 64, 100, 58}
|
||||
}
|
||||
|> put_session(:session_token, session_token)
|
||||
|> fetch_subject([])
|
||||
|
||||
refute Map.has_key?(conn.assigns, :subject)
|
||||
end
|
||||
|
||||
test "does not authenticate if data is missing", %{conn: conn} do
|
||||
conn = fetch_subject(conn, [])
|
||||
refute get_session(conn, :session_token)
|
||||
refute Map.has_key?(conn.assigns, :subject)
|
||||
end
|
||||
end
|
||||
|
||||
describe "redirect_if_user_is_authenticated/2" do
|
||||
test "redirects if user is authenticated", %{conn: conn, user_subject: subject} do
|
||||
conn =
|
||||
conn
|
||||
|> assign(:subject, subject)
|
||||
|> redirect_if_user_is_authenticated([])
|
||||
|
||||
assert conn.halted
|
||||
assert redirected_to(conn) == signed_in_path(subject)
|
||||
end
|
||||
|
||||
test "does not redirect if user is not authenticated", %{conn: conn} do
|
||||
conn = redirect_if_user_is_authenticated(conn, [])
|
||||
refute conn.halted
|
||||
refute conn.status
|
||||
end
|
||||
end
|
||||
|
||||
describe "ensure_authenticated/2" do
|
||||
setup context do
|
||||
%{conn: %{context.conn | path_params: %{"account_id" => context.account.id}}}
|
||||
end
|
||||
|
||||
test "redirects if user is not authenticated", %{account: account, conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> fetch_flash()
|
||||
|> ensure_authenticated([])
|
||||
|
||||
assert conn.halted
|
||||
assert redirected_to(conn) == ~p"/#{account}/sign_in"
|
||||
|
||||
assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
|
||||
"You must log in to access this page."
|
||||
end
|
||||
|
||||
test "stores the path to redirect to on GET", %{conn: conn} do
|
||||
halted_conn =
|
||||
%{conn | path_info: ["foo"], query_string: ""}
|
||||
|> fetch_flash()
|
||||
|> ensure_authenticated([])
|
||||
|
||||
assert halted_conn.halted
|
||||
assert get_session(halted_conn, :user_return_to) == "/foo"
|
||||
|
||||
halted_conn =
|
||||
%{conn | path_info: ["foo"], query_string: "bar=baz"}
|
||||
|> fetch_flash()
|
||||
|> ensure_authenticated([])
|
||||
|
||||
assert halted_conn.halted
|
||||
assert get_session(halted_conn, :user_return_to) == "/foo?bar=baz"
|
||||
|
||||
halted_conn =
|
||||
%{conn | path_info: ["foo"], query_string: "bar", method: "POST"}
|
||||
|> fetch_flash()
|
||||
|> ensure_authenticated([])
|
||||
|
||||
assert halted_conn.halted
|
||||
refute get_session(halted_conn, :user_return_to)
|
||||
end
|
||||
|
||||
test "does not redirect if user is authenticated", %{conn: conn, admin_subject: subject} do
|
||||
conn =
|
||||
conn
|
||||
|> assign(:subject, subject)
|
||||
|> ensure_authenticated([])
|
||||
|
||||
refute conn.halted
|
||||
refute conn.status
|
||||
end
|
||||
end
|
||||
|
||||
describe "on_mount: mount_subject" do
|
||||
setup context do
|
||||
socket = %LiveView.Socket{
|
||||
private: %{
|
||||
connect_info: %{
|
||||
user_agent: context.admin_subject.context.user_agent,
|
||||
peer_data: %{address: {100, 64, 100, 58}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
%{socket: socket}
|
||||
end
|
||||
|
||||
test "assigns subject based on a valid session_token", %{
|
||||
conn: conn,
|
||||
socket: socket,
|
||||
admin_subject: subject
|
||||
} do
|
||||
{:ok, session_token} = Domain.Auth.create_session_token_from_subject(subject)
|
||||
session = conn |> put_session(:session_token, session_token) |> get_session()
|
||||
params = %{"account_id" => subject.account.id}
|
||||
|
||||
assert {:cont, updated_socket} = on_mount(:mount_subject, params, session, socket)
|
||||
assert updated_socket.assigns.subject.identity.id == subject.identity.id
|
||||
end
|
||||
|
||||
test "assigns nil to subject assign if there isn't a valid session_token", %{
|
||||
conn: conn,
|
||||
socket: socket,
|
||||
admin_subject: subject
|
||||
} do
|
||||
session_token = "invalid_token"
|
||||
session = conn |> put_session(:session_token, session_token) |> get_session()
|
||||
params = %{"account_id" => subject.account.id}
|
||||
|
||||
assert {:cont, updated_socket} = on_mount(:mount_subject, params, session, socket)
|
||||
assert is_nil(updated_socket.assigns.subject)
|
||||
end
|
||||
|
||||
test "assigns nil to subject assign if there isn't a session_token", %{
|
||||
conn: conn,
|
||||
socket: socket,
|
||||
admin_subject: subject
|
||||
} do
|
||||
session = conn |> get_session()
|
||||
params = %{"account_id" => subject.account.id}
|
||||
|
||||
assert {:cont, updated_socket} = on_mount(:mount_subject, params, session, socket)
|
||||
assert is_nil(updated_socket.assigns.subject)
|
||||
end
|
||||
|
||||
test "assigns nil to subject assign if account_id doesn't match token", %{
|
||||
conn: conn,
|
||||
socket: socket,
|
||||
admin_subject: subject
|
||||
} do
|
||||
{:ok, session_token} = Domain.Auth.create_session_token_from_subject(subject)
|
||||
session = conn |> put_session(:session_token, session_token) |> get_session()
|
||||
params = %{"account_id" => Ecto.UUID.generate()}
|
||||
|
||||
assert {:cont, updated_socket} = on_mount(:mount_subject, params, session, socket)
|
||||
assert is_nil(updated_socket.assigns.subject)
|
||||
end
|
||||
end
|
||||
|
||||
describe "on_mount: ensure_authenticated" do
|
||||
setup context do
|
||||
socket = %LiveView.Socket{
|
||||
endpoint: Web.Endpoint,
|
||||
assigns: %{__changed__: %{}, flash: %{}, account: context.account},
|
||||
private: %{
|
||||
__temp__: %{},
|
||||
connect_info: %{
|
||||
user_agent: context.admin_subject.context.user_agent,
|
||||
peer_data: %{address: {100, 64, 100, 58}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
%{socket: socket}
|
||||
end
|
||||
|
||||
test "authenticates subject based on a valid session_token", %{
|
||||
conn: conn,
|
||||
socket: socket,
|
||||
admin_subject: subject
|
||||
} do
|
||||
{:ok, session_token} = Domain.Auth.create_session_token_from_subject(subject)
|
||||
session = conn |> put_session(:session_token, session_token) |> get_session()
|
||||
params = %{"account_id" => subject.account.id}
|
||||
|
||||
assert {:cont, updated_socket} =
|
||||
on_mount(:ensure_authenticated, params, session, socket)
|
||||
|
||||
assert updated_socket.assigns.subject.identity.id == subject.identity.id
|
||||
assert is_nil(updated_socket.redirected)
|
||||
end
|
||||
|
||||
test "redirects to login page if there isn't a valid session_token", %{
|
||||
conn: conn,
|
||||
socket: socket,
|
||||
admin_subject: subject
|
||||
} do
|
||||
session_token = "invalid_token"
|
||||
session = conn |> put_session(:session_token, session_token) |> get_session()
|
||||
params = %{}
|
||||
|
||||
assert {:halt, updated_socket} =
|
||||
on_mount(:ensure_authenticated, params, session, socket)
|
||||
|
||||
assert is_nil(updated_socket.assigns.subject)
|
||||
|
||||
assert updated_socket.redirected == {:redirect, %{to: ~p"/#{subject.account}/sign_in"}}
|
||||
end
|
||||
|
||||
test "redirects to login page if there isn't a session_token", %{
|
||||
conn: conn,
|
||||
socket: socket,
|
||||
admin_subject: subject
|
||||
} do
|
||||
session = conn |> get_session()
|
||||
params = %{}
|
||||
|
||||
assert {:halt, updated_socket} =
|
||||
on_mount(:ensure_authenticated, params, session, socket)
|
||||
|
||||
assert is_nil(updated_socket.assigns.subject)
|
||||
|
||||
assert updated_socket.redirected == {:redirect, %{to: ~p"/#{subject.account}/sign_in"}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "on_mount: :redirect_if_user_is_authenticated" do
|
||||
setup context do
|
||||
socket = %LiveView.Socket{
|
||||
endpoint: Web.Endpoint,
|
||||
assigns: %{__changed__: %{}, flash: %{}},
|
||||
private: %{
|
||||
__temp__: %{},
|
||||
connect_info: %{
|
||||
user_agent: context.admin_subject.context.user_agent,
|
||||
peer_data: %{address: {100, 64, 100, 58}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
%{socket: socket}
|
||||
end
|
||||
|
||||
test "redirects if there is an authenticated user ", %{
|
||||
conn: conn,
|
||||
socket: socket,
|
||||
admin_subject: subject
|
||||
} do
|
||||
{:ok, session_token} = Domain.Auth.create_session_token_from_subject(subject)
|
||||
|
||||
session =
|
||||
conn
|
||||
|> put_session(:session_token, session_token)
|
||||
|> get_session()
|
||||
|
||||
params = %{"account_id" => subject.account.id}
|
||||
|
||||
assert {:halt, updated_socket} =
|
||||
on_mount(:redirect_if_user_is_authenticated, params, session, socket)
|
||||
|
||||
assert updated_socket.redirected == {:redirect, %{to: ~p"/#{subject.account}/dashboard"}}
|
||||
end
|
||||
|
||||
test "doesn't redirect if there is no authenticated user", %{
|
||||
conn: conn,
|
||||
socket: socket,
|
||||
admin_subject: subject
|
||||
} do
|
||||
session = get_session(conn)
|
||||
|
||||
params = %{"account_id" => subject.account.id}
|
||||
|
||||
assert {:cont, updated_socket} =
|
||||
on_mount(:redirect_if_user_is_authenticated, params, session, socket)
|
||||
|
||||
assert is_nil(updated_socket.redirected)
|
||||
end
|
||||
end
|
||||
end
|
||||
604
elixir/apps/web/test/web/controllers/auth_controller_test.exs
Normal file
604
elixir/apps/web/test/web/controllers/auth_controller_test.exs
Normal file
@@ -0,0 +1,604 @@
|
||||
defmodule Web.AuthControllerTest do
|
||||
use Web.ConnCase, async: true
|
||||
alias Domain.{AccountsFixtures, AuthFixtures}
|
||||
|
||||
describe "verify_credentials/2" do
|
||||
test "redirects with an error when provider does not exist", %{conn: conn} do
|
||||
account_id = Ecto.UUID.generate()
|
||||
provider_id = Ecto.UUID.generate()
|
||||
|
||||
conn =
|
||||
post(conn, ~p"/#{account_id}/sign_in/providers/#{provider_id}/verify_credentials", %{
|
||||
"userpass" => %{
|
||||
"provider_identifier" => "foo",
|
||||
"secret" => "bar"
|
||||
}
|
||||
})
|
||||
|
||||
assert redirected_to(conn) == "/#{account_id}/sign_in"
|
||||
assert flash(conn, :error) == "You can not use this method to sign in."
|
||||
end
|
||||
|
||||
test "redirects back to the form when identity does not exist", %{conn: conn} do
|
||||
provider = AuthFixtures.create_userpass_provider()
|
||||
|
||||
conn =
|
||||
post(
|
||||
conn,
|
||||
~p"/#{provider.account_id}/sign_in/providers/#{provider.id}/verify_credentials",
|
||||
%{
|
||||
"userpass" => %{
|
||||
"provider_identifier" => "foo",
|
||||
"secret" => "bar"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
assert redirected_to(conn) == "/#{provider.account_id}/sign_in"
|
||||
assert flash(conn, :error) == "Invalid username or password."
|
||||
assert flash(conn, :userpass_provider_identifier) == "foo"
|
||||
end
|
||||
|
||||
test "redirects back to the form when credentials are invalid", %{conn: conn} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_userpass_provider(account: account)
|
||||
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
account: account,
|
||||
provider: provider,
|
||||
provider_virtual_state: %{
|
||||
"password" => "Firezone1234",
|
||||
"password_confirmation" => "Firezone1234"
|
||||
}
|
||||
)
|
||||
|
||||
conn =
|
||||
post(
|
||||
conn,
|
||||
~p"/#{provider.account_id}/sign_in/providers/#{provider.id}/verify_credentials",
|
||||
%{
|
||||
"userpass" => %{
|
||||
"provider_identifier" => identity.provider_identifier,
|
||||
"secret" => "bar"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
assert redirected_to(conn) == "/#{account.id}/sign_in"
|
||||
assert flash(conn, :error) == "Invalid username or password."
|
||||
assert flash(conn, :userpass_provider_identifier) == identity.provider_identifier
|
||||
end
|
||||
|
||||
test "trims the provider identifier to 160 characters on error redirect", %{conn: conn} do
|
||||
provider = AuthFixtures.create_userpass_provider()
|
||||
provider_identifier = String.duplicate("a", 161)
|
||||
|
||||
conn =
|
||||
post(
|
||||
conn,
|
||||
~p"/#{provider.account_id}/sign_in/providers/#{provider.id}/verify_credentials",
|
||||
%{
|
||||
"userpass" => %{
|
||||
"provider_identifier" => provider_identifier,
|
||||
"secret" => "bar"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
assert redirected_to(conn) == "/#{provider.account_id}/sign_in"
|
||||
assert flash(conn, :error) == "Invalid username or password."
|
||||
|
||||
assert flash(conn, :userpass_provider_identifier) ==
|
||||
String.slice(provider_identifier, 0, 160)
|
||||
end
|
||||
|
||||
test "redirects to the return to path when credentials are valid", %{conn: conn} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_userpass_provider(account: account)
|
||||
password = "Firezone1234"
|
||||
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
account: account,
|
||||
provider: provider,
|
||||
provider_virtual_state: %{"password" => password, "password_confirmation" => password}
|
||||
)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_session(:user_return_to, "/foo/bar")
|
||||
|> post(
|
||||
~p"/#{provider.account_id}/sign_in/providers/#{provider.id}/verify_credentials",
|
||||
%{
|
||||
"userpass" => %{
|
||||
"provider_identifier" => identity.provider_identifier,
|
||||
"secret" => password
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
assert redirected_to(conn) == "/foo/bar"
|
||||
end
|
||||
|
||||
test "redirects to the dashboard when credentials are valid and return path is empty", %{
|
||||
conn: conn
|
||||
} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_userpass_provider(account: account)
|
||||
password = "Firezone1234"
|
||||
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
actor_default_type: :account_admin_user,
|
||||
account: account,
|
||||
provider: provider,
|
||||
provider_virtual_state: %{"password" => password, "password_confirmation" => password}
|
||||
)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> post(
|
||||
~p"/#{provider.account_id}/sign_in/providers/#{provider.id}/verify_credentials",
|
||||
%{
|
||||
"userpass" => %{
|
||||
"provider_identifier" => identity.provider_identifier,
|
||||
"secret" => password
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
assert redirected_to(conn) == "/#{account.id}/dashboard"
|
||||
end
|
||||
|
||||
test "renews the session when credentials are valid", %{conn: conn} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_userpass_provider(account: account)
|
||||
password = "Firezone1234"
|
||||
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
account: account,
|
||||
provider: provider,
|
||||
provider_virtual_state: %{"password" => password, "password_confirmation" => password}
|
||||
)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_session(:foo, "bar")
|
||||
|> put_session(:session_token, "foo")
|
||||
|> put_session(:preferred_locale, "en_US")
|
||||
|> post(
|
||||
~p"/#{provider.account_id}/sign_in/providers/#{provider.id}/verify_credentials",
|
||||
%{
|
||||
"userpass" => %{
|
||||
"provider_identifier" => identity.provider_identifier,
|
||||
"secret" => password
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
assert %{
|
||||
"live_socket_id" => "actors_sessions:" <> socket_id,
|
||||
"preferred_locale" => "en_US",
|
||||
"session_token" => session_token
|
||||
} = conn.private.plug_session
|
||||
|
||||
assert socket_id == identity.actor_id
|
||||
assert {:ok, subject} = Domain.Auth.sign_in(session_token, "testing", conn.remote_ip)
|
||||
assert subject.identity.id == identity.id
|
||||
assert subject.identity.last_seen_user_agent == "testing"
|
||||
assert subject.identity.last_seen_remote_ip.address == {127, 0, 0, 1}
|
||||
assert subject.identity.last_seen_at
|
||||
end
|
||||
end
|
||||
|
||||
describe "request_magic_link/2" do
|
||||
test "sends a login link to the user email", %{conn: conn} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_email_provider(account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider)
|
||||
|
||||
conn =
|
||||
post(
|
||||
conn,
|
||||
~p"/#{provider.account_id}/sign_in/providers/#{provider.id}/request_magic_link",
|
||||
%{
|
||||
"email" => %{
|
||||
"provider_identifier" => identity.provider_identifier
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
assert_email_sent(fn email ->
|
||||
assert email.subject == "Firezone Sign In Link"
|
||||
|
||||
verify_sign_in_token_path =
|
||||
"/#{account.id}/sign_in/providers/#{provider.id}/verify_sign_in_token"
|
||||
|
||||
assert email.text_body =~
|
||||
"#{verify_sign_in_token_path}?identity_id=#{identity.id}&secret="
|
||||
end)
|
||||
|
||||
assert redirected_to(conn) == "/#{account.id}/sign_in/providers/email/#{provider.id}"
|
||||
end
|
||||
|
||||
test "does not return error if provider is not found", %{conn: conn} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider_id = Ecto.UUID.generate()
|
||||
|
||||
conn =
|
||||
post(
|
||||
conn,
|
||||
~p"/#{account.id}/sign_in/providers/#{provider_id}/request_magic_link",
|
||||
%{"email" => %{"provider_identifier" => "foo"}}
|
||||
)
|
||||
|
||||
assert redirected_to(conn) == "/#{account.id}/sign_in/providers/email/#{provider_id}"
|
||||
end
|
||||
|
||||
test "does not return error if identity is not found", %{conn: conn} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_email_provider(account: account)
|
||||
|
||||
conn =
|
||||
post(
|
||||
conn,
|
||||
~p"/#{account.id}/sign_in/providers/#{provider.id}/request_magic_link",
|
||||
%{"email" => %{"provider_identifier" => "foo"}}
|
||||
)
|
||||
|
||||
assert redirected_to(conn) == "/#{account.id}/sign_in/providers/email/#{provider.id}"
|
||||
end
|
||||
end
|
||||
|
||||
describe "verify_sign_in_token/2" do
|
||||
test "redirects with an error when provider does not exist", %{conn: conn} do
|
||||
account_id = Ecto.UUID.generate()
|
||||
provider_id = Ecto.UUID.generate()
|
||||
|
||||
conn =
|
||||
get(conn, ~p"/#{account_id}/sign_in/providers/#{provider_id}/verify_sign_in_token", %{
|
||||
"identity_id" => Ecto.UUID.generate(),
|
||||
"secret" => "foo"
|
||||
})
|
||||
|
||||
assert redirected_to(conn) == "/#{account_id}/sign_in"
|
||||
assert flash(conn, :error) == "You can not use this method to sign in."
|
||||
end
|
||||
|
||||
test "redirects back to the form when identity does not exist", %{conn: conn} do
|
||||
provider = AuthFixtures.create_email_provider()
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
~p"/#{provider.account_id}/sign_in/providers/#{provider}/verify_sign_in_token",
|
||||
%{
|
||||
"identity_id" => Ecto.UUID.generate(),
|
||||
"secret" => "foo"
|
||||
}
|
||||
)
|
||||
|
||||
assert redirected_to(conn) == "/#{provider.account_id}/sign_in"
|
||||
assert flash(conn, :error) == "The sign in link is invalid or expired."
|
||||
end
|
||||
|
||||
test "redirects back to the form when credentials are invalid", %{conn: conn} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_email_provider(account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider)
|
||||
|
||||
conn =
|
||||
get(conn, ~p"/#{account}/sign_in/providers/#{provider}/verify_sign_in_token", %{
|
||||
"identity_id" => identity.id,
|
||||
"secret" => "bar"
|
||||
})
|
||||
|
||||
assert redirected_to(conn) == "/#{account.id}/sign_in"
|
||||
assert flash(conn, :error) == "The sign in link is invalid or expired."
|
||||
end
|
||||
|
||||
test "redirects to the return to path when credentials are valid", %{conn: conn} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_email_provider(account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_session(:user_return_to, "/foo/bar")
|
||||
|> get(
|
||||
~p"/#{account}/sign_in/providers/#{provider}/verify_sign_in_token",
|
||||
%{
|
||||
"identity_id" => identity.id,
|
||||
"secret" => identity.provider_virtual_state.sign_in_token
|
||||
}
|
||||
)
|
||||
|
||||
assert redirected_to(conn) == "/foo/bar"
|
||||
end
|
||||
|
||||
test "redirects to the dashboard when credentials are valid and return path is empty", %{
|
||||
conn: conn
|
||||
} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_email_provider(account: account)
|
||||
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
actor_default_type: :account_admin_user,
|
||||
account: account,
|
||||
provider: provider
|
||||
)
|
||||
|
||||
conn =
|
||||
get(conn, ~p"/#{account}/sign_in/providers/#{provider}/verify_sign_in_token", %{
|
||||
"identity_id" => identity.id,
|
||||
"secret" => identity.provider_virtual_state.sign_in_token
|
||||
})
|
||||
|
||||
assert redirected_to(conn) == "/#{account.id}/dashboard"
|
||||
end
|
||||
|
||||
test "renews the session when credentials are valid", %{conn: conn} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_email_provider(account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_session(:foo, "bar")
|
||||
|> put_session(:session_token, "foo")
|
||||
|> put_session(:preferred_locale, "en_US")
|
||||
|> get(
|
||||
~p"/#{account}/sign_in/providers/#{provider}/verify_sign_in_token",
|
||||
%{
|
||||
"identity_id" => identity.id,
|
||||
"secret" => identity.provider_virtual_state.sign_in_token
|
||||
}
|
||||
)
|
||||
|
||||
assert %{
|
||||
"live_socket_id" => "actors_sessions:" <> socket_id,
|
||||
"preferred_locale" => "en_US",
|
||||
"session_token" => session_token
|
||||
} = conn.private.plug_session
|
||||
|
||||
assert socket_id == identity.actor_id
|
||||
assert {:ok, subject} = Domain.Auth.sign_in(session_token, "testing", conn.remote_ip)
|
||||
assert subject.identity.id == identity.id
|
||||
assert subject.identity.last_seen_user_agent == "testing"
|
||||
assert subject.identity.last_seen_remote_ip.address == {127, 0, 0, 1}
|
||||
assert subject.identity.last_seen_at
|
||||
end
|
||||
end
|
||||
|
||||
describe "redirect_to_idp/2" do
|
||||
test "redirects with an error when provider does not exist", %{conn: conn} do
|
||||
account_id = Ecto.UUID.generate()
|
||||
provider_id = Ecto.UUID.generate()
|
||||
|
||||
conn = get(conn, ~p"/#{account_id}/sign_in/providers/#{provider_id}/redirect")
|
||||
|
||||
assert redirected_to(conn) == "/#{account_id}/sign_in"
|
||||
assert flash(conn, :error) == "You can not use this method to sign in."
|
||||
end
|
||||
|
||||
test "redirects to IdP when provider exists", %{conn: conn} do
|
||||
account = AccountsFixtures.create_account()
|
||||
|
||||
{provider, _bypass} =
|
||||
AuthFixtures.start_openid_providers(["google"])
|
||||
|> AuthFixtures.create_openid_connect_provider(account: account)
|
||||
|
||||
conn = get(conn, ~p"/#{account.id}/sign_in/providers/#{provider.id}/redirect", %{})
|
||||
|
||||
assert to = redirected_to(conn)
|
||||
uri = URI.parse(to)
|
||||
assert uri.host == "localhost"
|
||||
assert uri.path == "/authorize"
|
||||
|
||||
callback_url = url(~p"/#{account.id}/sign_in/providers/#{provider.id}/handle_callback")
|
||||
{state, verifier} = conn.cookies["fz_auth_state_#{provider.id}"] |> :erlang.binary_to_term()
|
||||
code_challenge = Domain.Auth.Adapters.OpenIDConnect.PKCE.code_challenge(verifier)
|
||||
|
||||
assert URI.decode_query(uri.query) == %{
|
||||
"access_type" => "offline",
|
||||
"client_id" => provider.adapter_config["client_id"],
|
||||
"code_challenge" => code_challenge,
|
||||
"code_challenge_method" => "S256",
|
||||
"redirect_uri" => callback_url,
|
||||
"response_type" => "code",
|
||||
"scope" => "openid email profile",
|
||||
"state" => state
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
describe "handle_idp_callback/2" do
|
||||
setup context do
|
||||
account = AccountsFixtures.create_account()
|
||||
|
||||
{provider, bypass} =
|
||||
AuthFixtures.start_openid_providers(["google"])
|
||||
|> AuthFixtures.create_openid_connect_provider(account: account)
|
||||
|
||||
conn = get(context.conn, ~p"/#{account.id}/sign_in/providers/#{provider.id}/redirect", %{})
|
||||
|
||||
%{
|
||||
account: account,
|
||||
provider: provider,
|
||||
bypass: bypass,
|
||||
redirected_conn: conn
|
||||
}
|
||||
end
|
||||
|
||||
test "redirects with an error when state is invalid", %{conn: conn} do
|
||||
account_id = Ecto.UUID.generate()
|
||||
provider_id = Ecto.UUID.generate()
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> get(~p"/#{account_id}/sign_in/providers/#{provider_id}/handle_callback", %{
|
||||
"state" => "foo",
|
||||
"code" => "bar"
|
||||
})
|
||||
|
||||
assert redirected_to(conn) == "/#{account_id}/sign_in"
|
||||
assert flash(conn, :error) == "Your session has expired, please try again."
|
||||
end
|
||||
|
||||
test "redirects with an error when state cookie does not exist", %{
|
||||
account: account,
|
||||
provider: provider,
|
||||
redirected_conn: redirected_conn,
|
||||
conn: conn
|
||||
} do
|
||||
cookie_key = "fz_auth_state_#{provider.id}"
|
||||
redirected_conn = fetch_cookies(redirected_conn)
|
||||
{state, _verifier} = redirected_conn.cookies[cookie_key] |> :erlang.binary_to_term([:safe])
|
||||
%{value: signed_state} = redirected_conn.resp_cookies[cookie_key]
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_cookie(cookie_key, signed_state)
|
||||
|> get(~p"/#{account}/sign_in/providers/#{Ecto.UUID.generate()}/handle_callback", %{
|
||||
"state" => state,
|
||||
"code" => "bar"
|
||||
})
|
||||
|
||||
assert redirected_to(conn) == ~p"/#{account}/sign_in"
|
||||
assert flash(conn, :error) == "Your session has expired, please try again."
|
||||
end
|
||||
|
||||
test "redirects with an error when provider io disabled", %{
|
||||
account: account,
|
||||
provider: provider,
|
||||
redirected_conn: redirected_conn,
|
||||
conn: conn
|
||||
} do
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
actor_default_type: :account_admin_user,
|
||||
account: account,
|
||||
provider: provider
|
||||
)
|
||||
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
AuthFixtures.create_userpass_provider(account: account)
|
||||
{:ok, _provider} = Domain.Auth.disable_provider(provider, subject)
|
||||
|
||||
cookie_key = "fz_auth_state_#{provider.id}"
|
||||
redirected_conn = fetch_cookies(redirected_conn)
|
||||
{state, _verifier} = redirected_conn.cookies[cookie_key] |> :erlang.binary_to_term([:safe])
|
||||
%{value: signed_state} = redirected_conn.resp_cookies[cookie_key]
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_cookie(cookie_key, signed_state)
|
||||
|> get(~p"/#{account}/sign_in/providers/#{provider}/handle_callback", %{
|
||||
"state" => state,
|
||||
"code" => "bar"
|
||||
})
|
||||
|
||||
assert redirected_to(conn) == ~p"/#{account}/sign_in"
|
||||
assert flash(conn, :error) == "You can not use this method to sign in."
|
||||
end
|
||||
|
||||
test "redirects to the dashboard when credentials are valid and return path is empty", %{
|
||||
account: account,
|
||||
provider: provider,
|
||||
bypass: bypass,
|
||||
conn: conn,
|
||||
redirected_conn: redirected_conn
|
||||
} do
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
actor_default_type: :account_admin_user,
|
||||
account: account,
|
||||
provider: provider
|
||||
)
|
||||
|
||||
{token, _claims} = AuthFixtures.generate_openid_connect_token(provider, identity)
|
||||
AuthFixtures.expect_refresh_token(bypass, %{"id_token" => token})
|
||||
AuthFixtures.expect_userinfo(bypass)
|
||||
|
||||
cookie_key = "fz_auth_state_#{provider.id}"
|
||||
redirected_conn = fetch_cookies(redirected_conn)
|
||||
{state, _verifier} = redirected_conn.cookies[cookie_key] |> :erlang.binary_to_term([:safe])
|
||||
%{value: signed_state} = redirected_conn.resp_cookies[cookie_key]
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_cookie(cookie_key, signed_state)
|
||||
|> put_session(:foo, "bar")
|
||||
|> put_session(:preferred_locale, "en_US")
|
||||
|> get(~p"/#{account.id}/sign_in/providers/#{provider.id}/handle_callback", %{
|
||||
"state" => state,
|
||||
"code" => "MyFakeCode"
|
||||
})
|
||||
|
||||
assert redirected_to(conn) == "/#{account.id}/dashboard"
|
||||
|
||||
assert %{
|
||||
"live_socket_id" => "actors_sessions:" <> socket_id,
|
||||
"preferred_locale" => "en_US",
|
||||
"session_token" => session_token
|
||||
} = conn.private.plug_session
|
||||
|
||||
assert socket_id == identity.actor_id
|
||||
assert {:ok, subject} = Domain.Auth.sign_in(session_token, "testing", conn.remote_ip)
|
||||
assert subject.identity.id == identity.id
|
||||
assert subject.identity.last_seen_user_agent == "testing"
|
||||
assert subject.identity.last_seen_remote_ip.address == {127, 0, 0, 1}
|
||||
assert subject.identity.last_seen_at
|
||||
end
|
||||
end
|
||||
|
||||
describe "sign_out/2" do
|
||||
test "redirects to the sign in page and renews the session", %{conn: conn} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_email_provider(account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> put_session(:preferred_locale, "en_US")
|
||||
|> get(~p"/#{account}/sign_out")
|
||||
|
||||
assert redirected_to(conn) == "/#{account.id}/sign_in"
|
||||
assert conn.private.plug_session == %{"preferred_locale" => "en_US"}
|
||||
end
|
||||
|
||||
test "broadcasts to the given live_socket_id", %{conn: conn} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_email_provider(account: account)
|
||||
identity = AuthFixtures.create_identity(account: account, provider: provider)
|
||||
|
||||
live_socket_id = "actors_sessions:#{identity.actor_id}"
|
||||
Web.Endpoint.subscribe(live_socket_id)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_session(:live_socket_id, live_socket_id)
|
||||
|> get(~p"/#{account}/sign_out")
|
||||
|
||||
assert redirected_to(conn) == "/#{account.id}/sign_in"
|
||||
|
||||
assert_receive %Phoenix.Socket.Broadcast{event: "disconnect", topic: ^live_socket_id}
|
||||
end
|
||||
|
||||
test "works even if user is already logged out", %{conn: conn} do
|
||||
account = AccountsFixtures.create_account()
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_session(:preferred_locale, "en_US")
|
||||
|> get(~p"/#{account}/sign_out")
|
||||
|
||||
assert redirected_to(conn) == "/#{account.id}/sign_in"
|
||||
assert conn.private.plug_session == %{"preferred_locale" => "en_US"}
|
||||
end
|
||||
end
|
||||
end
|
||||
15
elixir/apps/web/test/web/live/auth_live/email_live_test.exs
Normal file
15
elixir/apps/web/test/web/live/auth_live/email_live_test.exs
Normal file
@@ -0,0 +1,15 @@
|
||||
defmodule Web.Auth.EmailLiveTest do
|
||||
use Web.ConnCase, async: true
|
||||
alias Domain.{AccountsFixtures, AuthFixtures}
|
||||
|
||||
test "renders email page", %{conn: conn} do
|
||||
account = AccountsFixtures.create_account()
|
||||
provider = AuthFixtures.create_email_provider(account: account)
|
||||
|
||||
{:ok, lv, html} = live(conn, ~p"/#{account}/sign_in/providers/email/#{provider}")
|
||||
|
||||
assert html =~ "Please check your email"
|
||||
assert has_element?(lv, ~s|a[href="https://mail.google.com/mail/"]|, "Open Gmail")
|
||||
assert has_element?(lv, ~s|a[href="https://outlook.live.com/mail/"]|, "Open Outlook")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,48 @@
|
||||
defmodule Web.Auth.ProvidersLiveTest do
|
||||
use Web.ConnCase, async: true
|
||||
alias Domain.{AccountsFixtures, AuthFixtures}
|
||||
|
||||
test "renders active providers on the page", %{conn: conn} do
|
||||
account = AccountsFixtures.create_account()
|
||||
|
||||
email_provider = AuthFixtures.create_email_provider(account: account)
|
||||
|
||||
{:ok, _lv, html} = live(conn, ~p"/#{account}/sign_in")
|
||||
|
||||
assert html =~ "Sign in with a magic link"
|
||||
refute html =~ "Sign in with username and password"
|
||||
|
||||
userpass_provider = AuthFixtures.create_userpass_provider(account: account)
|
||||
|
||||
{:ok, _lv, html} = live(conn, ~p"/#{account}/sign_in")
|
||||
|
||||
assert html =~ "Sign in with username and password"
|
||||
refute html =~ "Vault"
|
||||
|
||||
AuthFixtures.start_openid_providers(["vault"])
|
||||
|> AuthFixtures.create_openid_connect_provider(name: "Vault", account: account)
|
||||
|
||||
{:ok, _lv, html} = live(conn, ~p"/#{account}/sign_in")
|
||||
|
||||
assert html =~ "Vault"
|
||||
|
||||
identity =
|
||||
AuthFixtures.create_identity(
|
||||
actor_default_type: :account_admin_user,
|
||||
account: account,
|
||||
provider: email_provider
|
||||
)
|
||||
|
||||
subject = AuthFixtures.create_subject(identity)
|
||||
|
||||
{:ok, _provider} = Domain.Auth.disable_provider(userpass_provider, subject)
|
||||
{:ok, _lv, html} = live(conn, ~p"/#{account}/sign_in")
|
||||
refute html =~ "Sign in with username and password"
|
||||
|
||||
{:ok, _provider} = Domain.Auth.delete_provider(email_provider, subject)
|
||||
{:ok, _lv, html} = live(conn, ~p"/#{account}/sign_in")
|
||||
refute html =~ "Sign in with a magic link"
|
||||
|
||||
assert html =~ "Vault"
|
||||
end
|
||||
end
|
||||
@@ -86,6 +86,13 @@ config :web,
|
||||
external_trusted_proxies: [],
|
||||
private_clients: [%{__struct__: Postgrex.INET, address: {172, 28, 0, 0}, netmask: 16}]
|
||||
|
||||
config :web, Web.Plugs.SecureHeaders,
|
||||
csp_policy: [
|
||||
"default-src 'self' 'nonce-${nonce}'",
|
||||
"img-src 'self' data: https://www.gravatar.com",
|
||||
"style-src 'self' 'unsafe-inline'"
|
||||
]
|
||||
|
||||
###############################
|
||||
##### API #####################
|
||||
###############################
|
||||
|
||||
@@ -48,6 +48,14 @@ config :phoenix_live_reload, :dirs, [
|
||||
Path.join([root_path, "apps", "api"])
|
||||
]
|
||||
|
||||
config :web, Web.Plugs.SecureHeaders,
|
||||
csp_policy: [
|
||||
"default-src 'self' 'nonce-${nonce}'",
|
||||
"img-src 'self' data: https://www.gravatar.com",
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"script-src 'self' 'unsafe-inline'"
|
||||
]
|
||||
|
||||
###############################
|
||||
##### API #####################
|
||||
###############################
|
||||
|
||||
@@ -27,7 +27,7 @@ config :domain, Domain.ConnectivityChecks, enabled: false
|
||||
###############################
|
||||
|
||||
config :web, Web.Endpoint,
|
||||
http: [port: 14_000],
|
||||
http: [port: 13_000],
|
||||
server: true
|
||||
|
||||
###############################
|
||||
@@ -35,15 +35,15 @@ config :web, Web.Endpoint,
|
||||
###############################
|
||||
|
||||
config :api, API.Endpoint,
|
||||
http: [port: 14_001],
|
||||
http: [port: 13_001],
|
||||
server: true
|
||||
|
||||
###############################
|
||||
##### Third-party configs #####
|
||||
###############################
|
||||
config :web, Web.Mailer, adapter: Web.MailerTestAdapter
|
||||
config :web, Web.Mailer, adapter: Web.Mailer.TestAdapter
|
||||
|
||||
config :logger, level: :warn
|
||||
config :logger, level: :warning
|
||||
|
||||
config :argon2_elixir, t_cost: 1, m_cost: 8
|
||||
|
||||
|
||||
@@ -43,9 +43,7 @@ defmodule Firezone.MixProject do
|
||||
{:jason, "~> 1.2"},
|
||||
|
||||
# Shared test deps
|
||||
{:excoveralls, "~> 0.14", only: :test},
|
||||
{:credo, "~> 1.5", only: [:dev, :test], runtime: false},
|
||||
{:mix_test_watch, "~> 1.0", only: :dev, runtime: false},
|
||||
{:dialyxir, "~> 1.1", only: [:dev], runtime: false},
|
||||
{:junit_formatter, "~> 3.3", only: [:test]},
|
||||
{:mix_audit, "~> 2.1", only: [:dev, :test]},
|
||||
@@ -53,7 +51,7 @@ defmodule Firezone.MixProject do
|
||||
|
||||
# Formatter doesn't track dependencies of children applications
|
||||
{:phoenix, "~> 1.7.0"},
|
||||
{:phoenix_live_view, "~> 0.18.8"}
|
||||
{:phoenix_live_view, "~> 0.19.3"}
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
|
||||
"bureaucrat": {:hex, :bureaucrat, "0.2.9", "d98e4d2b9bdbf22e4a45c2113ce8b38b5b63278506c6ff918e3b943a4355d85b", [:mix], [{:inflex, ">= 1.10.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.2.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, ">= 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0 or ~> 4.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "111c8dd84382a62e1026ae011d592ceee918553e5203fe8448d9ba6ccbdfff7d"},
|
||||
"bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"},
|
||||
"castore": {:hex, :castore, "1.0.2", "0c6292ecf3e3f20b7c88408f00096337c4bfd99bd46cc2fe63413ddbe45b3573", [:mix], [], "hexpm", "40b2dd2836199203df8500e4a270f10fc006cc95adc8a319e148dc3077391d96"},
|
||||
"castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"},
|
||||
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
|
||||
"chatterbox": {:hex, :ts_chatterbox, "0.13.0", "6f059d97bcaa758b8ea6fffe2b3b81362bd06b639d3ea2bb088335511d691ebf", [:rebar3], [{:hpack, "~> 0.2.3", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "b93d19104d86af0b3f2566c4cba2a57d2e06d103728246ba1ac6c3c0ff010aa7"},
|
||||
"cidr": {:git, "https://github.com/firezone/cidr-elixir.git", "a32125127a7910f476734f45391ba6d37036ee11", []},
|
||||
@@ -20,21 +20,20 @@
|
||||
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
||||
"dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"},
|
||||
"ecto": {:hex, :ecto, "3.10.1", "c6757101880e90acc6125b095853176a02da8f1afe056f91f1f90b80c9389822", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d2ac4255f1601bdf7ac74c0ed971102c6829dc158719b94bd30041bbad77f87a"},
|
||||
"ecto": {:hex, :ecto, "3.10.2", "6b887160281a61aa16843e47735b8a266caa437f80588c3ab80a8a960e6abe37", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6a895778f0d7648a4b34b486af59a1c8009041fbdf2b17f1ac215eb829c60235"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.10.1", "6ea6b3036a0b0ca94c2a02613fd9f742614b5cfe494c41af2e6571bb034dd94c", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6a25bdbbd695f12c8171eaff0851fa4c8e72eec1e98c7364402dda9ce11c56b"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.7.6", "67716309dc5d43e16b5abbd00c01b8df6a0c2ab54a8f595468035a50189f9169", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5a0569756b0f7873a77687800c164cca6dfc03a09418e6fcf853d78991f49940"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"},
|
||||
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
|
||||
"esaml": {:git, "https://github.com/firezone/esaml.git", "4294a3ac5262582144e117c10a1537287b6c1fe8", []},
|
||||
"esbuild": {:hex, :esbuild, "0.7.0", "ce3afb13cd2c5fd63e13c0e2d0e0831487a97a7696cfa563707342bb825d122a", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "4ae9f4f237c5ebcb001390b8ada65a12fb2bb04f3fe3d1f1692b7a06fbfe8752"},
|
||||
"esbuild": {:hex, :esbuild, "0.7.1", "fa0947e8c3c3c2f86c9bf7e791a0a385007ccd42b86885e8e893bdb6631f5169", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "66661cdf70b1378ee4dc16573fcee67750b59761b2605a0207c267ab9d19f13c"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.29.1", "b1c652fa5f92ee9cf15c75271168027f92039b3877094290a75abcaac82a9f77", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "b7745fa6374a36daf484e2a2012274950e084815b936b1319aeebcf7809574f6"},
|
||||
"excoveralls": {:hex, :excoveralls, "0.16.1", "0bd42ed05c7d2f4d180331a20113ec537be509da31fed5c8f7047ce59ee5a7c5", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "dae763468e2008cf7075a64cb1249c97cb4bc71e236c5c2b5e5cdf1cfa2bf138"},
|
||||
"expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"},
|
||||
"file_size": {:hex, :file_size, "3.0.1", "ad447a69442a82fc701765a73992d7b1110136fa0d4a9d3190ea685d60034dcd", [:mix], [{:decimal, ">= 1.0.0 and < 3.0.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:number, "~> 1.0", [hex: :number, repo: "hexpm", optional: false]}], "hexpm", "64dd665bc37920480c249785788265f5d42e98830d757c6a477b3246703b8e20"},
|
||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
||||
"finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"},
|
||||
"floki": {:hex, :floki, "0.34.2", "5fad07ef153b3b8ec110b6b155ec3780c4b2c4906297d0b4be1a7162d04a7e02", [:mix], [], "hexpm", "26b9d50f0f01796bc6be611ca815c5e0de034d2128e39cc9702eee6b66a4d1c8"},
|
||||
"floki": {:hex, :floki, "0.34.3", "5e2dcaec5d7c228ce5b1d3501502e308b2d79eb655e4191751a1fe491c37feac", [:mix], [], "hexpm", "9577440eea5b97924b4bf3c7ea55f7b8b6dce589f9b28b096cc294a8dc342341"},
|
||||
"gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"},
|
||||
"gettext": {:hex, :gettext, "0.22.1", "e7942988383c3d9eed4bdc22fc63e712b655ae94a672a27e4900e3d4a2c43581", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "ad105b8dab668ee3f90c0d3d94ba75e9aead27a62495c101d94f2657a190ac5d"},
|
||||
"gettext": {:hex, :gettext, "0.22.2", "6bfca374de34ecc913a28ba391ca184d88d77810a3e427afa8454a71a51341ac", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "8a2d389673aea82d7eae387e6a2ccc12660610080ae7beb19452cfdc1ec30f60"},
|
||||
"gproc": {:hex, :gproc, "0.8.0", "cea02c578589c61e5341fce149ea36ccef236cc2ecac8691fba408e7ea77ec2f", [:rebar3], [], "hexpm", "580adafa56463b75263ef5a5df4c86af321f68694e7786cb057fd805d1e2a7de"},
|
||||
"grpcbox": {:hex, :grpcbox, "0.16.0", "b83f37c62d6eeca347b77f9b1ec7e9f62231690cdfeb3a31be07cd4002ba9c82", [:rebar3], [{:acceptor_pool, "~> 1.0.0", [hex: :acceptor_pool, repo: "hexpm", optional: false]}, {:chatterbox, "~> 0.13.0", [hex: :ts_chatterbox, repo: "hexpm", optional: false]}, {:ctx, "~> 0.6.0", [hex: :ctx, repo: "hexpm", optional: false]}, {:gproc, "~> 0.8.0", [hex: :gproc, repo: "hexpm", optional: false]}], "hexpm", "294df743ae20a7e030889f00644001370a4f7ce0121f3bbdaf13cf3169c62913"},
|
||||
"guardian": {:hex, :guardian, "2.3.1", "2b2d78dc399a7df182d739ddc0e566d88723299bfac20be36255e2d052fd215d", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bbe241f9ca1b09fad916ad42d6049d2600bbc688aba5b3c4a6c82592a54274c3"},
|
||||
@@ -49,42 +48,41 @@
|
||||
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
|
||||
"jose": {:hex, :jose, "1.11.5", "3bc2d75ffa5e2c941ca93e5696b54978323191988eb8d225c2e663ddfefd515e", [:mix, :rebar3], [], "hexpm", "dcd3b215bafe02ea7c5b23dafd3eb8062a5cd8f2d904fd9caa323d37034ab384"},
|
||||
"junit_formatter": {:hex, :junit_formatter, "3.3.1", "c729befb848f1b9571f317d2fefa648e9d4869befc4b2980daca7c1edc468e40", [:mix], [], "hexpm", "761fc5be4b4c15d8ba91a6dafde0b2c2ae6db9da7b8832a55b5a1deb524da72b"},
|
||||
"libcluster": {:hex, :libcluster, "3.3.2", "84c6ebfdc72a03805955abfb5ff573f71921a3e299279cc3445445d5af619ad1", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8b691ce8185670fc8f3fc0b7ed59eff66c6889df890d13411f8f1a0e6871d8a5"},
|
||||
"libcluster": {:hex, :libcluster, "3.3.3", "a4f17721a19004cfc4467268e17cff8b1f951befe428975dd4f6f7b84d927fe0", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "7c0a2275a0bb83c07acd17dab3c3bfb4897b145106750eeccc62d302e3bdfee5"},
|
||||
"logger_json": {:hex, :logger_json, "5.1.2", "7dde5f6dff814aba033f045a3af9408f5459bac72357dc533276b47045371ecf", [:mix], [{:ecto, "~> 2.1 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.5.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "ed42047e5c57a60d0fa1450aef36bc016d0f9a5e6c0807ebb0c03d8895fb6ebc"},
|
||||
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
|
||||
"makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"},
|
||||
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
|
||||
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
|
||||
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
|
||||
"mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"},
|
||||
"mix_audit": {:hex, :mix_audit, "2.1.0", "3c0dafb29114dffcdb508164a3d35311a9ac2c5baeba6495c9cd5315c25902b9", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "14c57a23e0a5f652c1e7f6e8dab93f166f66d63bd0c85f97278f5972b14e2be0"},
|
||||
"mix_test_watch": {:hex, :mix_test_watch, "1.1.0", "330bb91c8ed271fe408c42d07e0773340a7938d8a0d281d57a14243eae9dc8c3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "52b6b1c476cbb70fd899ca5394506482f12e5f6b0d6acff9df95c7f1e0812ec3"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"},
|
||||
"number": {:hex, :number, "1.0.3", "932c8a2d478a181c624138958ca88a78070332191b8061717270d939778c9857", [:mix], [{:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "dd397bbc096b2ca965a6a430126cc9cf7b9ef7421130def69bcf572232ca0f18"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
|
||||
"number": {:hex, :number, "1.0.4", "3e6e6032a3c1d4c3760e77a42c580a57a15545dd993af380809da30fe51a032c", [:mix], [{:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "16f7516584ef2be812af4f33f2eaf3f9b9f6ed8892f45853eb93113f83721e42"},
|
||||
"observer_cli": {:hex, :observer_cli, "1.7.4", "3c1bfb6d91bf68f6a3d15f46ae20da0f7740d363ee5bc041191ce8722a6c4fae", [:mix, :rebar3], [{:recon, "~> 2.5.1", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "50de6d95d814f447458bd5d72666a74624eddb0ef98bdcee61a0153aae0865ff"},
|
||||
"openid_connect": {:git, "https://github.com/firezone/openid_connect.git", "13320ed8b0d347330d07e1375a9661f3089b9c03", [branch: "master"]},
|
||||
"opentelemetry": {:hex, :opentelemetry, "1.3.0", "988ac3c26acac9720a1d4fb8d9dc52e95b45ecfec2d5b5583276a09e8936bc5e", [:rebar3], [{:opentelemetry_api, "~> 1.2.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "8e09edc26aad11161509d7ecad854a3285d88580f93b63b0b1cf0bac332bfcc0"},
|
||||
"opentelemetry_api": {:hex, :opentelemetry_api, "1.2.1", "7b69ed4f40025c005de0b74fce8c0549625d59cb4df12d15c32fe6dc5076ff42", [:mix, :rebar3], [{:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "6d7a27b7cad2ad69a09cabf6670514cafcec717c8441beb5c96322bac3d05350"},
|
||||
"opentelemetry_cowboy": {:hex, :opentelemetry_cowboy, "0.2.1", "feb09d4abe48c6d983fd46ea7b500cdf31b0f77c80702e175fe1fd86f8a52445", [:rebar3], [{:cowboy_telemetry, "~> 0.4", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_telemetry, "~> 1.0", [hex: :opentelemetry_telemetry, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "21ba198dd51294211a498dee720a30d2c2cb4d35ddc843d84f2d4e0a9681be49"},
|
||||
"opentelemetry_ecto": {:hex, :opentelemetry_ecto, "1.1.1", "218b791d2883becaf28d3fe25627b48f862ad63d4982dd0d10d307861eafa847", [:mix], [{:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e5f4c76aa9385cefa099a88e19eba90a7a19ef82deec43e0c03c987528bdd826"},
|
||||
"opentelemetry_exporter": {:hex, :opentelemetry_exporter, "1.5.0", "7f866236d7018c20de28ebc379c02b4b0d4fd6cfd058cd15351412e7b390a733", [:rebar3], [{:grpcbox, ">= 0.0.0", [hex: :grpcbox, repo: "hexpm", optional: false]}, {:opentelemetry, "~> 1.3", [hex: :opentelemetry, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.2", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:tls_certificate_check, "~> 1.18", [hex: :tls_certificate_check, repo: "hexpm", optional: false]}], "hexpm", "662fac229eba0114b3a9d1538fdf564bb46ca037cdb6d0e5fdc4c5d0da7a21be"},
|
||||
"opentelemetry_exporter": {:hex, :opentelemetry_exporter, "1.6.0", "f4fbf69aa9f1541b253813221b82b48a9863bc1570d8ecc517bc510c0d1d3d8c", [:rebar3], [{:grpcbox, ">= 0.0.0", [hex: :grpcbox, repo: "hexpm", optional: false]}, {:opentelemetry, "~> 1.3", [hex: :opentelemetry, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.2", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:tls_certificate_check, "~> 1.18", [hex: :tls_certificate_check, repo: "hexpm", optional: false]}], "hexpm", "1802d1dca297e46f21e5832ecf843c451121e875f73f04db87355a6cb2ba1710"},
|
||||
"opentelemetry_finch": {:hex, :opentelemetry_finch, "0.2.0", "55ddfb96082dda59a64214f2d4640d2fb1323ca45bbb4b40d32599a0e8087a05", [:mix], [{:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7364f70822ec282853cade12953f40d7b94e03967608a52fd406e3b080f18d5e"},
|
||||
"opentelemetry_liveview": {:hex, :opentelemetry_liveview, "1.0.0-rc.4", "52915a83809100f31f7b6ea42e00b964a66032b75cc56e5b4cbcf7e21d4a45da", [:mix], [{:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_telemetry, "~> 1.0.0-beta.7", [hex: :opentelemetry_telemetry, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e06ab69da7ee46158342cac42f1c22886bdeab53e8d8c4e237c3b3c2cf7b815d"},
|
||||
"opentelemetry_phoenix": {:hex, :opentelemetry_phoenix, "1.1.0", "60c8b3f23d16f17103532f6f16003e1ef76eac67d4e5f8a206091fe59dcac263", [:mix], [{:nimble_options, "~> 0.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: false]}, {:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}, {:opentelemetry_telemetry, "~> 1.0", [hex: :opentelemetry_telemetry, repo: "hexpm", optional: false]}, {:plug, ">= 1.11.0", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5a38537aedc5d568590e8be9ffe481d668cba4ffd25f06fe2d33c11296d7855f"},
|
||||
"opentelemetry_phoenix": {:hex, :opentelemetry_phoenix, "1.1.1", "b6ab632d39138c2cc9b6e52b65d560545904f659c42647856d669e593110521f", [:mix], [{:nimble_options, "~> 0.5 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: false]}, {:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}, {:opentelemetry_telemetry, "~> 1.0", [hex: :opentelemetry_telemetry, repo: "hexpm", optional: false]}, {:plug, ">= 1.11.0", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "942850ce28fe21f98d98a743b94163820ce5ba6488333a806dbd1e8161a653d8"},
|
||||
"opentelemetry_process_propagator": {:hex, :opentelemetry_process_propagator, "0.2.2", "85244a49f0c32ae1e2f3d58c477c265bd6125ee3480ade82b0fa9324b85ed3f0", [:mix, :rebar3], [{:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}], "hexpm", "04db13302a34bea8350a13ed9d49c22dfd32c4bc590d8aa88b6b4b7e4f346c61"},
|
||||
"opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "0.2.0", "b67fe459c2938fcab341cb0951c44860c62347c005ace1b50f8402576f241435", [:mix, :rebar3], [], "hexpm", "d61fa1f5639ee8668d74b527e6806e0503efc55a42db7b5f39939d84c07d6895"},
|
||||
"opentelemetry_telemetry": {:hex, :opentelemetry_telemetry, "1.0.0", "d5982a319e725fcd2305b306b65c18a86afdcf7d96821473cf0649ff88877615", [:mix, :rebar3], [{:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.3.0", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "3401d13a1d4b7aa941a77e6b3ec074f0ae77f83b5b2206766ce630123a9291a9"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.2", "c375ffb482beb4e3d20894f84dd7920442884f5f5b70b9f4528cbe0cedefec63", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.4", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "1ebca94b32b4d0e097ab2444a9742ed8ff3361acad17365e4e6b2e79b4792159"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.1", "fe7a02387a7d26002a46b97e9879591efee7ebffe5f5e114fd196632e6e4a08d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ddccf8b4966180afe7630b105edb3402b1ca485e7468109540d262e842048ba4"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.6", "61f0625af7c1d1923d582470446de29b008c0e07ae33d7a3859ede247ddaf59a", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "f6b4be7780402bb060cbc6e83f1b6d3f5673b674ba73cc4a7dd47db0322dfb88"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.2", "b21bd01fdeffcfe2fab49e4942aa938b6d3e89e93a480d4aee58085560a0bc0d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "70242edd4601d50b69273b057ecf7b684644c19ee750989fd555625ae4ce8f5d"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.18", "1f38fbd7c363723f19aad1a04b5490ff3a178e37daaf6999594d5f34796c47fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a5810d0472f3189ede6d2a95bda7f31c6113156b91784a3426cb0ab6a6d85214"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.19.3", "3918c1b34df8ac71a9a636806ba5b7f053349a0392b312e16f35b0bf4d070aab", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "545626887948495fd8ea23d83b75bd7aaf9dc4221563e158d2c4b52ea1dd7e00"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
|
||||
"phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.0", "a544d83fde4a767efb78f45404a74c9e37b2a9c5ea3339692e65a6966731f935", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "e88d117251e89a16b92222415a6d87b99a96747ddf674fc5c7631de734811dba"},
|
||||
"phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"},
|
||||
"phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"},
|
||||
@@ -99,10 +97,10 @@
|
||||
"rustler_precompiled": {:hex, :rustler_precompiled, "0.5.5", "a075a92c8e748ce5c4f7b2cf573a072d206a6d8d99c53f627e81d3f2b10616a3", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "e8a7f1abfec8d68683bb25d14efc88496f091ef113f7f4c45d39f3606f7223f6"},
|
||||
"samly": {:git, "https://github.com/firezone/samly.git", "4603438ed4a95ed74d6c0232676c24d097e2feec", []},
|
||||
"sobelow": {:hex, :sobelow, "0.12.2", "45f4d500e09f95fdb5a7b94c2838d6b26625828751d9f1127174055a78542cf5", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2f0b617dce551db651145662b84c8da4f158e7abe049a76daaaae2282df01c5d"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||
"sweet_xml": {:hex, :sweet_xml, "0.7.3", "debb256781c75ff6a8c5cbf7981146312b66f044a2898f453709a53e5031b45b", [:mix], [], "hexpm", "e110c867a1b3fe74bfc7dd9893aa851f0eed5518d0d7cad76d7baafd30e4f5ba"},
|
||||
"swoosh": {:hex, :swoosh, "1.10.2", "77acdc1261de404b893e24224d47459d1b42deb02577c7b31514e0a720f949d6", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1736faf374ed49c6091845cdfd5b3a68c88c5f2bfd989447d12bffafc0dda03a"},
|
||||
"tailwind": {:hex, :tailwind, "0.2.0", "95f9e4a32020c5bec480f1d6a43a49ac8030b13183127b577605f506d6e13a66", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "385e939fcd7fe4654be5130b187e358aaabade385513f9d200ffecdbb9552a9e"},
|
||||
"swoosh": {:hex, :swoosh, "1.11.2", "39dd1e44f75bc03a34366d5f830599d248de2b9caaf05704dc76c0507a58c6a1", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4c43f4591503e7d5bf028314af8ac7c06d1c4d340aa23faeefabfa2543fa726e"},
|
||||
"tailwind": {:hex, :tailwind, "0.2.1", "83d8eadbe71a8e8f67861fe7f8d51658ecfb258387123afe4d9dc194eddc36b0", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "e8a13f6107c95f73e58ed1b4221744e1eb5a093cd1da244432067e19c8c9a277"},
|
||||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
|
||||
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
|
||||
@@ -114,8 +112,8 @@
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
||||
"wallaby": {:hex, :wallaby, "0.30.3", "9213ebf6e22e34544ede60e435bdbf807b67119ba4548f7b9bdbbd53a359767f", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:web_driver_client, "~> 0.2.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "40844afbf3bf6933f21406bdba2c59042ea0983b7a2533a51f46d372d79bc400"},
|
||||
"web_driver_client": {:hex, :web_driver_client, "0.2.0", "63b76cd9eb3b0716ec5467a0f8bead73d3d9612e63f7560d21357f03ad86e31a", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "83cc6092bc3e74926d1c8455f0ce927d5d1d36707b74d9a65e38c084aab0350f"},
|
||||
"websock": {:hex, :websock, "0.5.1", "c496036ce95bc26d08ba086b2a827b212c67e7cabaa1c06473cd26b40ed8cf10", [:mix], [], "hexpm", "b9f785108b81cd457b06e5f5dabe5f65453d86a99118b2c0a515e1e296dc2d2c"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.1", "292e6c56724e3457e808e525af0e9bcfa088cc7b9c798218e78658c7f9b85066", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "8e2e1544bfde5f9d0442f9cec2f5235398b224f75c9e06b60557debf64248ec1"},
|
||||
"websock": {:hex, :websock, "0.5.2", "b3c08511d8d79ed2c2f589ff430bd1fe799bb389686dafce86d28801783d8351", [:mix], [], "hexpm", "925f5de22fca6813dfa980fb62fd542ec43a2d1a1f83d2caec907483fe66ff05"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.3", "4908718e42e4a548fc20e00e70848620a92f11f7a6add8cf0886c4232267498d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "cbe5b814c1f86b6ea002b52dd99f345aeecf1a1a6964e209d208fb404d930d3d"},
|
||||
"wireguardex": {:hex, :wireguardex, "0.3.6", "163b72693ecb710473c40d3dec3c4c150ba1e7c85ac137114feb93033198c935", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.5.1", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "1bbe747178265b5f5182aa46a61994635a31a6548cf5a965efbf827e7263c673"},
|
||||
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
|
||||
"yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"},
|
||||
|
||||
Reference in New Issue
Block a user