Update new/edit policy pages (#1946)

Why:

* The new and edit policy pages had previously only been pulling live
data from the DB, but had not been able to use the forms to create or
update policies. This commit allows the forms to function as intended.
This commit is contained in:
bmanifold
2023-09-12 19:43:02 -04:00
committed by GitHub
parent 1a6f0efec0
commit 34fd5693d7
22 changed files with 754 additions and 123 deletions

View File

@@ -80,6 +80,10 @@ defmodule Domain.Policies do
end
end
def new_policy(attrs, %Auth.Subject{} = subject) do
Policy.Changeset.create(attrs, subject)
end
def ensure_has_access_to(%Auth.Subject{} = subject, %Policy{} = policy) do
if subject.account.id == policy.account_id do
:ok

View File

@@ -0,0 +1,17 @@
defmodule Domain.Repo.Migrations.UpdatePolicyIndexes do
use Ecto.Migration
def change do
drop(index(:policies, [:account_id, :name], unique: true))
drop(index(:policies, [:account_id, :resource_id, :actor_group_id], unique: true))
create(index(:policies, [:account_id, :name], unique: true, where: "deleted_at IS NULL"))
create(
index(:policies, [:account_id, :resource_id, :actor_group_id],
unique: true,
where: "deleted_at IS NULL"
)
)
end
end

View File

@@ -3,8 +3,11 @@ defmodule Domain.Fixtures.Accounts do
alias Domain.Accounts
def account_attrs(attrs \\ %{}) do
unique_num = unique_integer()
Enum.into(attrs, %{
name: "acc-#{unique_integer()}"
name: "acc-#{unique_num}",
slug: "acc_#{unique_num}"
})
end

View File

@@ -1,5 +1,6 @@
defmodule Domain.Fixtures.Policies do
use Domain.Fixture
alias Domain.Policies
def policy_attrs(attrs \\ %{}) do
Enum.into(attrs, %{
@@ -42,4 +43,17 @@ defmodule Domain.Fixtures.Policies do
policy
end
def delete_policy(policy) do
policy = Repo.preload(policy, :account)
subject =
Fixtures.Auth.create_subject(
account: policy.account,
actor: [type: :account_admin_user]
)
{:ok, policy} = Policies.delete_policy(policy, subject)
policy
end
end

View File

@@ -365,11 +365,12 @@ defmodule Web.FormComponents do
def delete_button(assigns) do
~H"""
<button
type="button"
class="text-red-600 inline-flex items-center hover:text-white border border-red-600 hover:bg-red-600 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:border-red-500 dark:text-red-500 dark:hover:text-white dark:hover:bg-red-600 dark:focus:ring-red-900"
{@rest}
>
<button type="button" class={~w[
text-red-600 inline-flex items-center hover:text-white border border-red-600 hover:bg-red-600
focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5
text-center dark:border-red-500 dark:text-red-500 dark:hover:text-white dark:hover:bg-red-600
dark:focus:ring-red-900
]} {@rest}>
<!-- XXX: Fix icon for dark mode -->
<!-- <.icon name="hero-trash-solid" class="text-red-600 w-5 h-5 mr-1 -ml-1" /> -->
<%= render_slot(@inner_block) %>

View File

@@ -314,4 +314,22 @@ defmodule Web.TableComponents do
</tr>
"""
end
@doc ~S"""
This component is meant to be used with the table component. It renders a
<.link> component that has a specific style for actions in a table.
"""
attr :navigate, :string, required: true
slot :inner_block
def action_link(assigns) do
~H"""
<.link
navigate={@navigate}
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
<%= render_slot(@inner_block) %>
</.link>
"""
end
end

View File

@@ -191,7 +191,7 @@ defmodule Web.AuthController do
conn = put_session(conn, :client_csrf_token, params["client_csrf_token"])
redirect_url =
url(~p"/#{provider.account_id}/sign_in/providers/#{provider.id}/handle_callback")
url(~p"/#{account_id_or_slug}/sign_in/providers/#{provider.id}/handle_callback")
redirect_to_idp(conn, redirect_url, provider)
else

View File

@@ -54,6 +54,7 @@ defmodule Web.Auth.SignIn do
<.providers_group_form
adapter="openid_connect"
providers={@providers_by_adapter[:openid_connect]}
account={@account}
params={@params}
/>
</:item>
@@ -66,6 +67,7 @@ defmodule Web.Auth.SignIn do
<.providers_group_form
adapter="userpass"
provider={List.first(@providers_by_adapter[:userpass])}
account={@account}
flash={@flash}
params={@params}
/>
@@ -79,6 +81,7 @@ defmodule Web.Auth.SignIn do
<.providers_group_form
adapter="email"
provider={List.first(@providers_by_adapter[:email])}
account={@account}
flash={@flash}
params={@params}
/>
@@ -104,7 +107,12 @@ defmodule Web.Auth.SignIn do
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} params={@params} />
<.openid_connect_button
:for={provider <- @providers}
provider={provider}
account={@account}
params={@params}
/>
</div>
"""
end
@@ -117,7 +125,7 @@ defmodule Web.Auth.SignIn do
~H"""
<.simple_form
for={@userpass_form}
action={~p"/#{@provider.account_id}/sign_in/providers/#{@provider.id}/verify_credentials"}
action={~p"/#{@account}/sign_in/providers/#{@provider.id}/verify_credentials"}
class="space-y-4 lg:space-y-6"
id="userpass_form"
phx-update="ignore"
@@ -157,7 +165,7 @@ defmodule Web.Auth.SignIn do
~H"""
<.simple_form
for={@email_form}
action={~p"/#{@provider.account_id}/sign_in/providers/#{@provider.id}/request_magic_link"}
action={~p"/#{@account}/sign_in/providers/#{@provider.id}/request_magic_link"}
class="space-y-4 lg:space-y-6"
id="email_form"
phx-update="ignore"
@@ -183,9 +191,7 @@ defmodule Web.Auth.SignIn do
def openid_connect_button(assigns) do
~H"""
<a
href={~p"/#{@provider.account_id}/sign_in/providers/#{@provider}/redirect?#{@params}"}
class={~w[
<a href={~p"/#{@account}/sign_in/providers/#{@provider}/redirect?#{@params}"} class={~w[
w-full inline-flex items-center justify-center py-2.5 px-5
bg-white rounded-lg
text-sm font-medium text-gray-900
@@ -194,8 +200,7 @@ defmodule Web.Auth.SignIn do
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]}
>
dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700]}>
Log in with <%= @provider.name %>
</a>
"""

View File

@@ -5,7 +5,9 @@ defmodule Web.Policies.Edit do
def mount(%{"id" => id}, _session, socket) do
with {:ok, policy} <- Policies.fetch_policy_by_id(id, socket.assigns.subject) do
{:ok, assign(socket, policy: policy)}
form = to_form(Policies.Policy.Changeset.update(policy, %{}))
socket = assign(socket, policy: policy, form: form)
{:ok, socket, temporary_assigns: [form: %Phoenix.HTML.Form{}]}
else
_other -> raise Web.LiveErrors.NotFoundError
end
@@ -31,31 +33,46 @@ defmodule Web.Policies.Edit do
<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 Policy details</h2>
<form action="#">
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
<div>
<.label for="name">
Name
</.label>
<.input
autocomplete="off"
type="text"
name="name"
id="policy-name"
placeholder="Name of Policy"
value={@policy.name}
required
/>
</div>
</div>
<div class="flex items-center space-x-4">
<.button type="submit" class="btn btn-primary">
Save
<.simple_form
for={@form}
class="space-y-4 lg:space-y-6"
phx-submit="submit"
phx-change="validate"
>
<.input
field={@form[:name]}
type="text"
label="Policy Name"
placeholder="Enter a Policy Name here"
required
phx-debounce="300"
/>
<:actions>
<.button phx-disable-with="Updating Policy..." class="w-full">
Update Policy
</.button>
</div>
</form>
</:actions>
</.simple_form>
</div>
</section>
"""
end
def handle_event("validate", %{"policy" => policy_params}, socket) do
changeset =
Policies.Policy.Changeset.update(socket.assigns.policy, policy_params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, form: to_form(changeset))}
end
def handle_event("submit", %{"policy" => policy_params}, socket) do
with {:ok, policy} <-
Policies.update_policy(socket.assigns.policy, policy_params, socket.assigns.subject) do
{:noreply, redirect(socket, to: ~p"/#{socket.assigns.account}/policies/#{policy}")}
else
{:error, changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
end

View File

@@ -21,9 +21,7 @@ defmodule Web.Policies.Index do
All Policies
</:title>
<:actions>
<.add_button navigate={~p"/#{@account}/policies/new"}>
Add a new Policy
</.add_button>
<.add_button navigate={~p"/#{@account}/policies/new"}>Add Policy</.add_button>
</:actions>
</.header>
<!-- Policies table -->
@@ -51,13 +49,19 @@ defmodule Web.Policies.Index do
<%= policy.resource.name %>
</.link>
</:col>
<:action>
<a
href="#"
class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
<:action :let={policy}>
<.action_link navigate={~p"/#{@account}/policies/#{policy}/edit"}>
Edit
</.action_link>
</:action>
<:action :let={policy}>
<div
phx-click="delete"
phx-value-id={policy.id}
class="block py-2 px-4 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
Delete
</a>
</div>
</:action>
</.table>
<.paginator page={3} total_pages={100} collection_base_path={~p"/#{@account}/gateway_groups"} />
@@ -88,4 +92,10 @@ defmodule Web.Policies.Index do
</div>
"""
end
def handle_event("delete", %{"id" => id}, socket) do
{:ok, policy} = Policies.fetch_policy_by_id(id, socket.assigns.subject)
{:ok, _} = Policies.delete_policy(policy, socket.assigns.subject)
{:noreply, push_navigate(socket, to: ~p"/#{socket.assigns.account}/policies")}
end
end

View File

@@ -1,18 +1,21 @@
defmodule Web.Policies.New do
use Web, :live_view
alias Domain.{Resources, Actors}
alias Domain.{Resources, Actors, Policies}
def mount(_params, _session, socket) do
with {:ok, resources} <- Resources.list_resources(socket.assigns.subject),
{:ok, actor_groups} <- Actors.list_groups(socket.assigns.subject) do
form = to_form(Policies.new_policy(%{}, socket.assigns.subject))
socket =
assign(socket,
resources: resources,
actor_groups: actor_groups
actor_groups: actor_groups,
form: form
)
{:ok, socket}
{:ok, socket, temporary_assigns: [form: %Phoenix.HTML.Form{}]}
else
_other -> raise Web.LiveErrors.NotFoundError
end
@@ -33,53 +36,59 @@ defmodule Web.Policies.New do
<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">Policy details</h2>
<form action="#">
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
<div>
<.label for="policy-name">
Name
</.label>
<.input
autocomplete="off"
type="text"
name="name"
value=""
id="policy-name"
placeholder="Enter a name for this policy"
/>
</div>
<div>
<.label for="group">
Group
</.label>
<.input
type="select"
options={Enum.map(@actor_groups, fn g -> [key: g.name, value: g.id] end)}
name="actor_group"
value=""
/>
</div>
<div>
<.label for="resource">
Resource
</.label>
<.input
type="select"
options={Enum.map(@resources, fn r -> [key: r.name, value: r.id] end)}
name="resource"
value=""
/>
</div>
</div>
<div class="flex items-center space-x-4">
<.button type="submit">
Save
<.simple_form for={@form} phx-submit="submit" phx-change="validate">
<.input
field={@form[:name]}
type="text"
label="Policy Name"
placeholder="Enter a Policy Name here"
required
phx-debounce="300"
/>
<.input
field={@form[:actor_group_id]}
label="Group"
type="select"
options={Enum.map(@actor_groups, fn g -> [key: g.name, value: g.id] end)}
value={@form[:actor_group_id].value}
required
/>
<.input
field={@form[:resource_id]}
label="Resource"
type="select"
options={Enum.map(@resources, fn r -> [key: r.name, value: r.id] end)}
value={@form[:resource_id].value}
required
/>
<.base_error form={@form} field={:base} />
<:actions>
<.button phx-disable-with="Creating Policy..." class="w-full">
Create Policy
</.button>
</div>
</form>
</:actions>
</.simple_form>
</div>
</section>
"""
end
def handle_event("validate", %{"policy" => policy_params}, socket) do
form =
Policies.new_policy(policy_params, socket.assigns.subject)
|> Map.put(:action, :validate)
|> to_form()
{:noreply, assign(socket, form: form)}
end
def handle_event("submit", %{"policy" => policy_params}, socket) do
with {:ok, policy} <- Policies.create_policy(policy_params, socket.assigns.subject) do
{:noreply, redirect(socket, to: ~p"/#{socket.assigns.account}/policies/#{policy}")}
else
{:error, %Ecto.Changeset{} = changeset} ->
form = to_form(changeset)
{:noreply, assign(socket, form: form)}
end
end
end

View File

@@ -37,7 +37,7 @@ defmodule Web.Policies.Show do
</.header>
<!-- Show Policy -->
<div class="bg-white dark:bg-gray-800 overflow-hidden">
<.vertical_table>
<.vertical_table id="policy">
<.vertical_table_row>
<:label>
Name
@@ -144,11 +144,16 @@ defmodule Web.Policies.Show do
Danger zone
</:title>
<:actions>
<.delete_button>
<.delete_button phx-click="delete" phx-value-id={@policy.id}>
Delete Policy
</.delete_button>
</:actions>
</.header>
"""
end
def handle_event("delete", %{"id" => _policy_id}, socket) do
{:ok, _} = Policies.delete_policy(socket.assigns.policy, socket.assigns.subject)
{:noreply, push_navigate(socket, to: ~p"/#{socket.assigns.account}/policies")}
end
end

View File

@@ -79,7 +79,7 @@ defmodule Web.Settings.IdentityProviders.GoogleWorkspace.Show do
</.button>
<% end %>
<.edit_button navigate={
~p"/#{@provider.account_id}/settings/identity_providers/google_workspace/#{@provider}/redirect"
~p"/#{@account}/settings/identity_providers/google_workspace/#{@provider}/redirect"
}>
Reconnect Identity Provider
</.edit_button>

View File

@@ -77,7 +77,7 @@ defmodule Web.Settings.IdentityProviders.OpenIDConnect.Show do
Disable Identity Provider
</.button>
<.edit_button navigate={
~p"/#{@provider.account_id}/settings/identity_providers/openid_connect/#{@provider}/redirect"
~p"/#{@account}/settings/identity_providers/openid_connect/#{@provider}/redirect"
}>
Reconnect Identity Provider
</.edit_button>

View File

@@ -169,7 +169,7 @@ defmodule Web.AuthTest do
describe "ensure_authenticated/2" do
setup context do
%{conn: %{context.conn | path_params: %{"account_id_or_slug" => context.account.id}}}
%{conn: %{context.conn | path_params: %{"account_id_or_slug" => context.account.slug}}}
end
test "redirects if user is not authenticated", %{account: account, conn: conn} do
@@ -179,7 +179,7 @@ defmodule Web.AuthTest do
|> ensure_authenticated([])
assert conn.halted
assert redirected_to(conn) == ~p"/#{account}/sign_in"
assert redirected_to(conn) == ~p"/#{account.slug}/sign_in"
assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
"You must log in to access this page."
@@ -342,14 +342,14 @@ defmodule Web.AuthTest do
} do
session_token = "invalid_token"
session = conn |> put_session(:session_token, session_token) |> get_session()
params = %{"account_id_or_slug" => subject.account.id}
params = %{"account_id_or_slug" => subject.account.slug}
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"}}
assert updated_socket.redirected == {:redirect, %{to: ~p"/#{subject.account.slug}/sign_in"}}
end
test "redirects to login page if there isn't a session_token", %{
@@ -358,14 +358,14 @@ defmodule Web.AuthTest do
admin_subject: subject
} do
session = conn |> get_session()
params = %{"account_id_or_slug" => subject.account.id}
params = %{"account_id_or_slug" => subject.account.slug}
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"}}
assert updated_socket.redirected == {:redirect, %{to: ~p"/#{subject.account.slug}/sign_in"}}
end
end

View File

@@ -153,7 +153,7 @@ defmodule Web.AuthControllerTest do
conn =
conn
|> post(
~p"/#{provider.account_id}/sign_in/providers/#{provider.id}/verify_credentials",
~p"/#{account}/sign_in/providers/#{provider.id}/verify_credentials",
%{
"userpass" => %{
"provider_identifier" => identity.provider_identifier,
@@ -162,7 +162,7 @@ defmodule Web.AuthControllerTest do
}
)
assert redirected_to(conn) == "/#{account.slug}/dashboard"
assert redirected_to(conn) == ~p"/#{account.slug}/dashboard"
end
test "renews the session when credentials are valid", %{conn: conn} do
@@ -372,7 +372,7 @@ defmodule Web.AuthControllerTest do
assert email.subject == "Firezone Sign In Link"
verify_sign_in_token_path =
"/#{account.id}/sign_in/providers/#{provider.id}/verify_sign_in_token"
~p"/#{account.id}/sign_in/providers/#{provider.id}/verify_sign_in_token"
assert email.text_body =~ "#{verify_sign_in_token_path}"
assert email.text_body =~ "identity_id=#{identity.id}"
@@ -493,7 +493,7 @@ defmodule Web.AuthControllerTest do
"secret" => "bar"
})
assert redirected_to(conn) == "/#{account.id}/sign_in"
assert redirected_to(conn) == ~p"/#{account}/sign_in"
assert flash(conn, :error) == "The sign in link is invalid or expired."
end
@@ -524,7 +524,7 @@ defmodule Web.AuthControllerTest do
}
)
assert redirected_to(conn) == "/#{account.id}/sign_in"
assert redirected_to(conn) == ~p"/#{account}/sign_in"
assert flash(conn, :error) == "The sign in link is invalid or expired."
end
@@ -554,7 +554,7 @@ defmodule Web.AuthControllerTest do
}
)
assert redirected_to(conn) == "/#{account.id}/sign_in"
assert redirected_to(conn) == ~p"/#{account}/sign_in"
assert flash(conn, :error) == "The sign in link is invalid or expired."
end
@@ -640,12 +640,12 @@ defmodule Web.AuthControllerTest do
conn =
conn
|> put_session(:sign_in_nonce, sign_in_nonce)
|> get(~p"/#{account}/sign_in/providers/#{provider}/verify_sign_in_token", %{
|> get(~p"/#{account.id}/sign_in/providers/#{provider}/verify_sign_in_token", %{
"identity_id" => identity.id,
"secret" => email_token
})
assert redirected_to(conn) == "/#{account.slug}/dashboard"
assert redirected_to(conn) == ~p"/#{account.slug}/dashboard"
end
test "redirects to the platform link when credentials are valid for account users", %{
@@ -970,7 +970,7 @@ defmodule Web.AuthControllerTest do
"code" => "MyFakeCode"
})
assert redirected_to(conn) == "/#{account.slug}/dashboard"
assert redirected_to(conn) == ~p"/#{account.slug}/dashboard"
assert %{
"live_socket_id" => "actors_sessions:" <> socket_id,
@@ -1085,7 +1085,7 @@ defmodule Web.AuthControllerTest do
|> put_session(:preferred_locale, "en_US")
|> get(~p"/#{account}/sign_out")
assert redirected_to(conn) =~ "/#{account.id}/sign_in"
assert redirected_to(conn) == url(~p"/#{account}/sign_in")
assert conn.private.plug_session == %{"preferred_locale" => "en_US"}
assert %{"fz_recent_account_ids" => fz_recent_account_ids} = conn.cookies
@@ -1129,7 +1129,7 @@ defmodule Web.AuthControllerTest do
|> put_session(:live_socket_id, live_socket_id)
|> get(~p"/#{account}/sign_out")
assert redirected_to(conn) == "/#{account.id}/sign_in"
assert redirected_to(conn) == ~p"/#{account}/sign_in"
assert_receive %Phoenix.Socket.Broadcast{event: "disconnect", topic: ^live_socket_id}
end
@@ -1172,7 +1172,7 @@ defmodule Web.AuthControllerTest do
|> put_session(:preferred_locale, "en_US")
|> get(~p"/#{account}/sign_out")
assert redirected_to(conn) =~ "/#{account.id}/sign_in"
assert redirected_to(conn) == url(~p"/#{account}/sign_in")
assert conn.private.plug_session == %{"preferred_locale" => "en_US"}
assert %{"fz_recent_account_ids" => fz_recent_account_ids} = conn.cookies
@@ -1187,7 +1187,7 @@ defmodule Web.AuthControllerTest do
|> put_session(:preferred_locale, "en_US")
|> get(~p"/#{account}/sign_out")
assert redirected_to(conn) == "/#{account.id}/sign_in"
assert redirected_to(conn) == ~p"/#{account}/sign_in"
assert conn.private.plug_session == %{"preferred_locale" => "en_US"}
refute Map.has_key?(conn.cookies, "fz_recent_account_ids")

View File

@@ -0,0 +1,148 @@
defmodule Web.Live.Policies.EditTest do
use Web.ConnCase, async: true
setup do
account = Fixtures.Accounts.create_account()
actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
identity = Fixtures.Auth.create_identity(account: account, actor: actor)
policy = Fixtures.Policies.create_policy(account: account)
%{
account: account,
actor: actor,
identity: identity,
policy: policy
}
end
test "redirects to sign in page for unauthorized user", %{
account: account,
policy: policy,
conn: conn
} do
assert live(conn, ~p"/#{account}/policies/#{policy}/edit") ==
{:error,
{:redirect,
%{
to: ~p"/#{account}/sign_in",
flash: %{"error" => "You must log in to access this page."}
}}}
end
test "renders not found error when policy is deleted", %{
account: account,
identity: identity,
policy: policy,
conn: conn
} do
Fixtures.Policies.delete_policy(policy)
assert_raise Web.LiveErrors.NotFoundError, fn ->
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/policies/#{policy}/edit")
end
end
test "renders breadcrumbs item", %{
account: account,
identity: identity,
policy: policy,
conn: conn
} do
{:ok, _lv, html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/policies/#{policy}/edit")
assert item = Floki.find(html, "[aria-label='Breadcrumb']")
breadcrumbs = String.trim(Floki.text(item))
assert breadcrumbs =~ "Policies"
assert breadcrumbs =~ policy.name
assert breadcrumbs =~ "Edit"
end
test "renders form", %{
account: account,
identity: identity,
policy: policy,
conn: conn
} do
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/policies/#{policy}/edit")
form = form(lv, "form")
assert find_inputs(form) == ["policy[name]"]
end
test "renders changeset errors on input change", %{
account: account,
identity: identity,
policy: policy,
conn: conn
} do
attrs = Fixtures.Policies.policy_attrs() |> Map.take([:name])
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/policies/#{policy}/edit")
lv
|> form("form", policy: attrs)
|> validate_change(%{policy: %{name: String.duplicate("a", 256)}}, fn form, _html ->
assert form_validation_errors(form) == %{
"policy[name]" => ["should be at most 255 character(s)"]
}
end)
|> validate_change(%{policy: %{name: ""}}, fn form, _html ->
assert form_validation_errors(form) == %{"policy[name]" => ["can't be blank"]}
end)
end
test "renders changeset errors on submit", %{
account: account,
identity: identity,
policy: policy,
conn: conn
} do
other_policy = Fixtures.Policies.create_policy(account: account)
attrs = %{name: other_policy.name}
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/policies/#{policy}/edit")
assert lv
|> form("form", policy: attrs)
|> render_submit()
|> form_validation_errors() == %{"policy[name]" => ["Policy Name already exists"]}
end
test "updates a policy on valid attrs", %{
account: account,
identity: identity,
policy: policy,
conn: conn
} do
attrs = Fixtures.Policies.policy_attrs() |> Map.take([:name])
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/policies/#{policy}/edit")
assert lv
|> form("form", policy: attrs)
|> render_submit() ==
{:error, {:redirect, %{to: ~p"/#{account}/policies/#{policy}"}}}
assert policy = Repo.get_by(Domain.Policies.Policy, id: policy.id)
assert policy.name == attrs.name
end
end

View File

@@ -0,0 +1,78 @@
defmodule Web.Live.Policies.IndexTest do
use Web.ConnCase, async: true
setup do
account = Fixtures.Accounts.create_account()
identity = Fixtures.Auth.create_identity(account: account, actor: [type: :account_admin_user])
%{
account: account,
identity: identity
}
end
test "redirects to sign in page for unauthorized user", %{account: account, conn: conn} do
assert live(conn, ~p"/#{account}/policies") ==
{:error,
{:redirect,
%{
to: ~p"/#{account}/sign_in",
flash: %{"error" => "You must log in to access this page."}
}}}
end
test "renders breadcrumbs item", %{
account: account,
identity: identity,
conn: conn
} do
{:ok, _lv, html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/policies")
assert item = Floki.find(html, "[aria-label='Breadcrumb']")
breadcrumbs = String.trim(Floki.text(item))
assert breadcrumbs =~ "Policies"
end
test "renders add policy button", %{
account: account,
identity: identity,
conn: conn
} do
{:ok, _lv, html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/policies")
assert button = Floki.find(html, "a[href='/#{account.id}/policies/new']")
assert Floki.text(button) =~ "Add Policy"
end
test "renders policies table", %{
account: account,
identity: identity,
conn: conn
} do
policy =
Fixtures.Policies.create_policy(account: account)
|> Domain.Repo.preload(:actor_group)
|> Domain.Repo.preload(:resource)
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/policies")
[rendered_policy | _] =
lv
|> element("#policies")
|> render()
|> table_to_map()
assert rendered_policy["name"] =~ policy.name
assert rendered_policy["group"] =~ policy.actor_group.name
assert rendered_policy["resource"] =~ policy.resource.name
end
end

View File

@@ -0,0 +1,158 @@
defmodule Web.Live.Policies.NewTest do
use Web.ConnCase, async: true
setup do
account = Fixtures.Accounts.create_account()
actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
identity = Fixtures.Auth.create_identity(account: account, actor: actor)
%{
account: account,
actor: actor,
identity: identity
}
end
test "redirects to sign in page for unauthorized user", %{
account: account,
conn: conn
} do
assert live(conn, ~p"/#{account}/policies/new") ==
{:error,
{:redirect,
%{
to: ~p"/#{account}/sign_in",
flash: %{"error" => "You must log in to access this page."}
}}}
end
test "renders breadcrumbs item", %{
account: account,
identity: identity,
conn: conn
} do
{:ok, _lv, html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/policies/new")
assert item = Floki.find(html, "[aria-label='Breadcrumb']")
breadcrumbs = String.trim(Floki.text(item))
assert breadcrumbs =~ "Policies"
assert breadcrumbs =~ "Add"
end
test "renders form", %{
account: account,
identity: identity,
conn: conn
} do
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/policies/new")
form = form(lv, "form")
assert find_inputs(form) == [
"policy[actor_group_id]",
"policy[name]",
"policy[resource_id]"
]
end
test "renders changeset errors on input change", %{
account: account,
identity: identity,
conn: conn
} do
group = Fixtures.Actors.create_group(account: account)
resource = Fixtures.Resources.create_resource(account: account)
attrs =
Fixtures.Policies.policy_attrs()
|> Map.take([:name])
|> Map.put(:actor_group_id, group.id)
|> Map.put(:resource_id, resource.id)
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/policies/new")
lv
|> form("form", policy: attrs)
|> validate_change(%{policy: %{name: String.duplicate("a", 256)}}, fn form, _html ->
assert form_validation_errors(form) == %{
"policy[name]" => ["should be at most 255 character(s)"]
}
end)
|> validate_change(%{policy: %{name: ""}}, fn form, _html ->
assert form_validation_errors(form) == %{
"policy[name]" => ["can't be blank"]
}
end)
end
test "renders changeset errors on submit", %{
account: account,
identity: identity,
conn: conn
} do
other_policy = Fixtures.Policies.create_policy(account: account)
attrs = %{name: other_policy.name}
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/policies/new")
assert lv
|> form("form", policy: attrs)
|> render_submit()
|> form_validation_errors() == %{
"policy[name]" => ["Policy Name already exists"]
}
attrs = %{
name: "unique",
actor_group_id: other_policy.actor_group_id,
resource_id: other_policy.resource_id
}
assert lv
|> form("form", policy: attrs)
|> render_submit()
|> form_validation_errors() == %{
"policy[base]" => ["Policy with Group and Resource already exists"]
}
end
test "creates a new policy on valid attrs and redirects", %{
account: account,
identity: identity,
conn: conn
} do
group = Fixtures.Actors.create_group(account: account)
resource = Fixtures.Resources.create_resource(account: account)
attrs =
Fixtures.Policies.policy_attrs()
|> Map.take([:name])
|> Map.put(:actor_group_id, group.id)
|> Map.put(:resource_id, resource.id)
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/policies/new")
assert lv
|> form("form", policy: attrs)
|> render_submit()
policy = Repo.get_by(Domain.Policies.Policy, attrs)
assert assert_redirect(lv, ~p"/#{account}/policies/#{policy}")
end
end

View File

@@ -0,0 +1,146 @@
defmodule Web.Live.Policies.ShowTest do
use Web.ConnCase, async: true
setup do
account = Fixtures.Accounts.create_account()
actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
identity = Fixtures.Auth.create_identity(account: account, actor: actor)
subject = Fixtures.Auth.create_subject(account: account, actor: actor, identity: identity)
policy = Fixtures.Policies.create_policy(account: account, subject: subject)
%{
account: account,
actor: actor,
identity: identity,
subject: subject,
policy: policy
}
end
test "redirects to sign in page for unauthorized user", %{
account: account,
policy: policy,
conn: conn
} do
assert live(conn, ~p"/#{account}/policies/#{policy}") ==
{:error,
{:redirect,
%{
to: ~p"/#{account}/sign_in",
flash: %{"error" => "You must log in to access this page."}
}}}
end
test "renders not found error when gateway is deleted", %{
account: account,
policy: policy,
identity: identity,
conn: conn
} do
policy = Fixtures.Policies.delete_policy(policy)
assert_raise Web.LiveErrors.NotFoundError, fn ->
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/policies/#{policy}")
end
end
test "renders breadcrumbs item", %{
account: account,
policy: policy,
identity: identity,
conn: conn
} do
{:ok, _lv, html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/policies/#{policy}")
assert item = Floki.find(html, "[aria-label='Breadcrumb']")
breadcrumbs = String.trim(Floki.text(item))
assert breadcrumbs =~ "Policies"
assert breadcrumbs =~ policy.name
end
test "allows editing policy", %{
account: account,
policy: policy,
identity: identity,
conn: conn
} do
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/policies/#{policy}")
assert lv
|> element("a", "Edit Policy")
|> render_click() ==
{:error,
{:live_redirect, %{to: ~p"/#{account}/policies/#{policy}/edit", kind: :push}}}
end
test "renders policy details", %{
account: account,
actor: actor,
identity: identity,
policy: policy,
conn: conn
} do
policy =
policy
|> Domain.Repo.preload(:actor_group)
|> Domain.Repo.preload(:resource)
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/policies/#{policy}")
table =
lv
|> element("#policy")
|> render()
|> vertical_table_to_map()
assert table["name"] =~ policy.name
assert table["group"] =~ policy.actor_group.name
assert table["resource"] =~ policy.resource.name
assert table["created"] =~ actor.name
end
# TODO: Finish this test when logs are implemented
# test "renders logs table", %{
# account: account,
# actor: actor,
# identity: identity,
# policy: policy,
# conn: conn
# } do
# {:ok, lv, _html} =
# conn
# |> authorize_conn(identity)
# |> live(~p"/#{account}/policies/#{policy}")
# end
test "allows deleting policy", %{
account: account,
policy: policy,
identity: identity,
conn: conn
} do
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/policies/#{policy}")
assert lv
|> element("button", "Delete Policy")
|> render_click() ==
{:error, {:live_redirect, %{to: ~p"/#{account}/policies", kind: :push}}}
assert Repo.get(Domain.Policies.Policy, policy.id).deleted_at
end
end

View File

@@ -48,7 +48,7 @@ defmodule Web.Live.Settings.IdentityProviders.GoogleWorkspace.Connect do
~p"/#{account.id}/settings/identity_providers/google_workspace/#{provider_id}/redirect"
)
assert redirected_to(conn) == "/#{account.id}/settings/identity_providers"
assert redirected_to(conn) == ~p"/#{account}/settings/identity_providers"
assert flash(conn, :error) == "Provider does not exist."
end
@@ -74,7 +74,7 @@ defmodule Web.Live.Settings.IdentityProviders.GoogleWorkspace.Connect do
callback_url =
url(
~p"/#{account.id}/settings/identity_providers/google_workspace/#{provider.id}/handle_callback"
~p"/#{account}/settings/identity_providers/google_workspace/#{provider.id}/handle_callback"
)
{state, verifier} = conn.cookies["fz_auth_state_#{provider.id}"] |> :erlang.binary_to_term()

View File

@@ -44,11 +44,9 @@ defmodule Web.Live.Settings.IdentityProviders.OpenIDConnect.Connect do
conn
|> authorize_conn(identity)
|> assign(:account, account)
|> get(
~p"/#{account.id}/settings/identity_providers/openid_connect/#{provider_id}/redirect"
)
|> get(~p"/#{account}/settings/identity_providers/openid_connect/#{provider_id}/redirect")
assert redirected_to(conn) == "/#{account.id}/settings/identity_providers"
assert redirected_to(conn) == ~p"/#{account}/settings/identity_providers"
assert flash(conn, :error) == "Provider does not exist."
end
@@ -74,7 +72,7 @@ defmodule Web.Live.Settings.IdentityProviders.OpenIDConnect.Connect do
callback_url =
url(
~p"/#{account.id}/settings/identity_providers/openid_connect/#{provider.id}/handle_callback"
~p"/#{account}/settings/identity_providers/openid_connect/#{provider.id}/handle_callback"
)
{state, verifier} = conn.cookies["fz_auth_state_#{provider.id}"] |> :erlang.binary_to_term()