522/allow disabling of config creation (#559)

* Checkpoint

* Optionally hide device mgmt buttons
This commit is contained in:
Jamil
2022-04-27 15:20:33 -07:00
committed by GitHub
parent 2eb5a24a13
commit 908cfc7dff
13 changed files with 145 additions and 30 deletions

View File

@@ -18,7 +18,7 @@ defmodule FzHttp.Devices.Device do
import FzHttp.Queries.INET
alias FzHttp.{Devices, Users.User}
alias FzHttp.{Devices, Users, Users.User}
@description_max_length 2048
@@ -58,6 +58,7 @@ defmodule FzHttp.Devices.Device do
|> put_next_ip(:ipv6)
|> shared_changeset()
|> validate_max_devices()
|> validate_device_management()
end
def update_changeset(device, attrs) do
@@ -154,6 +155,21 @@ defmodule FzHttp.Devices.Device do
end
end
defp validate_device_management(changeset) do
user_id = changeset.changes.user_id || changeset.data.user_id
if Users.get_user!(user_id).role == :admin ||
Application.fetch_env!(:fz_http, :allow_unprivileged_device_management) do
changeset
else
add_error(
changeset,
:base,
"Must be an administrator to manage devices."
)
end
end
defp validate_omitted_if_site(changeset, fields) when is_list(fields) do
validate_omitted(changeset, filter_site_fields(changeset, fields, use_site: true))
end

View File

@@ -60,11 +60,13 @@
<% end %>
</div>
<div class="block">
<%= live_patch(to: Routes.device_unprivileged_index_path(@socket, :new), class: "button") do %>
Add Device
<% end %>
</div>
<%= if FzHttpWeb.DeviceView.can_manage_devices?(@current_user) do %>
<div class="block">
<%= live_patch(to: Routes.device_unprivileged_index_path(@socket, :new), class: "button") do %>
Add Device
<% end %>
</div>
<% end %>
<h4 class="title is-4">VPN Session</h4>

View File

@@ -43,7 +43,9 @@ defmodule FzHttpWeb.DeviceLive.Unprivileged.Show do
end
def delete_device(device, socket) do
if socket.assigns.current_user.id == device.user_id do
if socket.assigns.current_user.id == device.user_id &&
(has_role?(socket.assigns.current_user, :admin) ||
Application.fetch_env!(:fz_http, :allow_unprivileged_device_management)) do
Devices.delete_device(device)
else
{:not_authorized}

View File

@@ -6,18 +6,20 @@
<%= render(FzHttpWeb.SharedView, "device_details.html", assigns) %>
</section>
<section class="section is-main-section">
<h4 class="title is-4">
Danger Zone
</h4>
<%= if FzHttpWeb.DeviceView.can_manage_devices?(@current_user) do %>
<section class="section is-main-section">
<h4 class="title is-4">
Danger Zone
</h4>
<button class="button is-danger"
phx-click="delete_device"
phx-value-device_id={@device.id}
data-confirm="Are you sure? This will immediately disconnect this device and remove all associated data.">
<span class="icon is-small">
<i class="fas fa-trash"></i>
</span>
<span>Delete Device <%= @device.name %></span>
</button>
</section>
<button class="button is-danger"
phx-click="delete_device"
phx-value-device_id={@device.id}
data-confirm="Are you sure? This will immediately disconnect this device and remove all associated data.">
<span class="icon is-small">
<i class="fas fa-trash"></i>
</span>
<span>Delete Device <%= @device.name %></span>
</button>
</section>
<% end %>

View File

@@ -1,3 +1,8 @@
defmodule FzHttpWeb.DeviceView do
use FzHttpWeb, :view
def can_manage_devices?(user) do
has_role?(user, :admin) ||
Application.fetch_env!(:fz_http, :allow_unprivileged_device_management)
end
end

View File

@@ -1,13 +1,14 @@
defmodule FzHttpWeb.DeviceLive.Unprivileged.IndexTest do
use FzHttpWeb.ConnCase, async: false
# alias FzHttp.{Devices, Devices.Device}
describe "authenticated/device list" do
setup :create_devices
test "includes the device name in the list", %{
unprivileged_user: user,
unprivileged_conn: conn
} do
{:ok, devices: devices} = create_devices(user_id: user.id)
test "includes the device name in the list", %{admin_conn: conn, devices: devices} do
path = Routes.device_admin_index_path(conn, :index)
path = Routes.device_unprivileged_index_path(conn, :index)
{:ok, _view, html} = live(conn, path)
for device <- devices do
@@ -25,6 +26,32 @@ defmodule FzHttpWeb.DeviceLive.Unprivileged.IndexTest do
end
end
describe "authenticated device management disabled" do
setup do
restore_env(:allow_unprivileged_device_management, false, &on_exit/1)
end
test "omits Add Device button", %{unprivileged_conn: conn} do
path = Routes.device_unprivileged_index_path(conn, :index)
{:ok, _view, html} = live(conn, path)
refute html =~ "Add Device"
end
test "prevents creating a device", %{unprivileged_conn: conn} do
path = Routes.device_unprivileged_index_path(conn, :new)
{:ok, view, _html} = live(conn, path)
new_view =
view
|> element("#create-device")
|> render_submit(%{"device" => %{"public_key" => "test-pubkey", "name" => "test-tunnel"}})
assert new_view =~ "Must be an administrator to manage devices."
refute new_view =~ "Device added!"
end
end
describe "authenticated/creates device" do
test "shows new form", %{unprivileged_conn: conn} do
path = Routes.device_unprivileged_index_path(conn, :index)
@@ -43,7 +70,7 @@ defmodule FzHttpWeb.DeviceLive.Unprivileged.IndexTest do
"""
end
test "creates tunnel", %{unprivileged_conn: conn} do
test "creates device", %{unprivileged_conn: conn} do
path = Routes.device_unprivileged_index_path(conn, :new)
{:ok, view, _html} = live(conn, path)

View File

@@ -0,0 +1,44 @@
defmodule FzHttpWeb.DeviceLive.Unprivileged.ShowTest do
use FzHttpWeb.ConnCase, async: true
describe "unauthenticated" do
setup :create_device
@tag :unauthed
test "mount redirects to session path", %{unauthed_conn: conn, device: device} do
path = Routes.device_admin_show_path(conn, :show, device)
expected_path = Routes.root_path(conn, :index)
assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path)
end
end
describe "authenticated; device management disabled" do
test "prevents deleting a device; doesn't show button", %{
unprivileged_user: user,
unprivileged_conn: conn
} do
{:ok, device: device} = create_device(user_id: user.id)
restore_env(:allow_unprivileged_device_management, false, &on_exit/1)
path = Routes.device_unprivileged_show_path(conn, :show, device)
{:ok, _view, html} = live(conn, path)
refute html =~ "Delete Device"
end
end
# XXX: Revisit this when RBAC is more fleshed out. Admins can now view other admins' devices.
# describe "authenticated as other user" do
# setup [:create_device, :create_other_user_device]
#
# test "mount redirects to session path", %{
# admin_conn: conn,
# device: _device,
# other_device: other_device
# } do
# path = Routes.device_admin_show_path(conn, :show, other_device)
# expected_path = Routes.auth_path(conn, :request)
# assert {:error, {:redirect, %{to: ^expected_path}}} = live(conn, path)
# end
# end
end

View File

@@ -76,8 +76,14 @@ defmodule FzHttp.TestHelpers do
{:ok, devices: devices}
end
def create_user(_) do
user = UsersFixtures.user()
def create_user(tags) do
user =
if tags[:unprivileged] do
UsersFixtures.user(%{role: :unprivileged})
else
UsersFixtures.user()
end
{:ok, user: user}
end

View File

@@ -49,6 +49,7 @@ config :fz_http, FzHttpWeb.Authentication,
config :fz_http,
sandbox: true,
allow_unprivileged_device_management: true,
telemetry_id: "543aae08-5a2b-428d-b704-2956dd3f5a57",
wireguard_endpoint: nil,
wireguard_dns: "1.1.1.1, 1.0.0.1",

View File

@@ -40,6 +40,9 @@ telemetry_id = System.fetch_env!("TELEMETRY_ID")
guardian_secret_key = System.fetch_env!("GUARDIAN_SECRET_KEY")
external_url = System.fetch_env!("EXTERNAL_URL")
allow_unprivileged_device_management =
FzString.to_boolean(System.fetch_env!("ALLOW_UNPRIVILEGED_DEVICE_MANAGEMENT"))
# Local auth
local_auth_enabled = FzString.to_boolean(System.fetch_env!("LOCAL_AUTH_ENABLED"))
@@ -154,6 +157,7 @@ config :fz_http, FzHttpWeb.Authentication,
secret_key: guardian_secret_key
config :fz_http,
allow_unprivileged_device_management: allow_unprivileged_device_management,
max_devices_per_user: max_devices_per_user,
local_auth_enabled: local_auth_enabled,
okta_auth_enabled: okta_auth_enabled,

View File

@@ -26,6 +26,8 @@ Shown below is a complete listing of the configuration options available in
| `default['firezone']['user']` | Name of unprivileged Linux user most services and files will belong to. | `'firezone'` |
| `default['firezone']['group']` | Name of Linux group most services and files will belong to. | `'firezone'` |
| `default['firezone']['admin_email']` | Email address for initial Firezone user. | `"firezone@localhost"` |
| `default['firezone']['max_devices_per_user']` | Maximum number of devices a user can have. | `10` |
| `default['firezone']['allow_unprivileged_device_management']` | Allows non-admin users to create and manage devices. | `true` |
| `default['firezone']['egress_interface']` | Interface name where tunneled traffic will exit. If nil, the default route interface will be used. | `nil` |
| `default['firezone']['fips_enabled']` | Enable or disable OpenSSL FIPs mode. | `nil` |
| `default['firezone']['logging']['enabled']` | Enable or disable logging across Firezone. Set to `false` to disable logging entirely. | `true` |
@@ -143,7 +145,6 @@ Shown below is a complete listing of the configuration options available in
| `default['firezone']['wireguard']['ipv6']['enabled']` | Enable or disable IPv6 for WireGuard network. | `true` |
| `default['firezone']['wireguard']['ipv6']['network']` | WireGuard network IPv6 address pool. | `'fd00::3:2:0/120'` |
| `default['firezone']['wireguard']['ipv6']['address']` | WireGuard interface IPv6 address. Must be within IPv6 address pool. | `'fd00::3:2:1'` |
| `default['firezone']['wireguard']['max_devices_per_user']` | Maximum number of devices a user can have. | `10` |
| `default['firezone']['runit']['svlogd_bin']` | Runit svlogd bin location. | `"#{node['firezone']['install_directory']}/embedded/bin/svlogd"` |
| `default['firezone']['ssl']['directory']` | SSL directory for storing generated certs. | `'/var/opt/firezone/ssl'` |
| `default['firezone']['ssl']['enabled']` | Enable or disable SSL for nginx. | `true` |

View File

@@ -41,6 +41,10 @@ default['firezone']['admin_email'] = 'firezone@localhost'
# Default: 10
default['firezone']['max_devices_per_user'] = 10
# Allow users to create (and download) their own devices. Set to false
# if you only want administrators to create and manage devices.
default['firezone']['allow_unprivileged_device_management'] = true
default['firezone']['config_directory'] = '/etc/firezone'
default['firezone']['install_directory'] = '/opt/firezone'
default['firezone']['app_directory'] = "#{node['firezone']['install_directory']}/embedded/service/firezone"

View File

@@ -243,6 +243,7 @@ class Firezone
'WIREGUARD_IPV6_NETWORK' => attributes['wireguard']['ipv6']['network'],
'WIREGUARD_IPV6_ADDRESS' => attributes['wireguard']['ipv6']['address'],
'MAX_DEVICES_PER_USER' => attributes['max_devices_per_user'].to_s,
'ALLOW_UNPRIVILEGED_DEVICE_MANAGEMENT' => attributes['allow_unprivileged_device_management'].to_s,
# Allow env var to override config
'TELEMETRY_ENABLED' => ENV.fetch('TELEMETRY_ENABLED',
attributes['telemetry']['enabled'] == false ? 'false' : 'true'),