Add policies (#1850)

Why:

* Policies are needed to make sure devices are allowed to connect to a
      given resource.

---------

Signed-off-by: bmanifold <bmanifold@users.noreply.github.com>
Co-authored-by: Andrew Dryga <andrew@dryga.com>
This commit is contained in:
bmanifold
2023-08-08 14:55:53 -05:00
committed by GitHub
parent a7a05af2d2
commit 06394ab3c9
14 changed files with 1019 additions and 274 deletions

View File

@@ -10,13 +10,14 @@ defmodule Domain.Auth.Roles do
defp list_authorizers do
[
Domain.Accounts.Authorizer,
Domain.Actors.Authorizer,
Domain.Auth.Authorizer,
Domain.Config.Authorizer,
Domain.Accounts.Authorizer,
Domain.Devices.Authorizer,
Domain.Gateways.Authorizer,
Domain.Policies.Authorizer,
Domain.Relays.Authorizer,
Domain.Actors.Authorizer,
Domain.Resources.Authorizer
]
end

View File

@@ -0,0 +1,90 @@
defmodule Domain.Policies do
alias Domain.{Auth, Repo, Validator}
alias Domain.Policy
alias Domain.Policies.{Authorizer, Policy}
def fetch_policy_by_id(id, %Auth.Subject{} = subject, opts \\ []) do
{preload, _opts} = Keyword.pop(opts, :preload, [])
required_permissions =
{:one_of,
[
Authorizer.manage_policies_permission(),
Authorizer.view_available_policies_permission()
]}
with :ok <- Auth.ensure_has_permissions(subject, required_permissions),
true <- Validator.valid_uuid?(id) do
Policy.Query.by_id(id)
|> Authorizer.for_subject(subject)
|> Repo.fetch()
|> case do
{:ok, policy} -> {:ok, Repo.preload(policy, preload)}
{:error, reason} -> {:error, reason}
end
else
false -> {:error, :not_found}
other -> other
end
end
def list_policies(%Auth.Subject{} = subject, opts \\ []) do
{preload, _opts} = Keyword.pop(opts, :preload, [])
required_permissions =
{:one_of,
[
Authorizer.manage_policies_permission(),
Authorizer.view_available_policies_permission()
]}
with :ok <- Auth.ensure_has_permissions(subject, required_permissions) do
{:ok, policies} =
Policy.Query.all()
|> Authorizer.for_subject(subject)
|> Repo.list()
{:ok, Repo.preload(policies, preload)}
end
end
def create_policy(attrs, %Auth.Subject{} = subject) do
required_permissions =
{:one_of, [Authorizer.manage_policies_permission()]}
with :ok <- Auth.ensure_has_permissions(subject, required_permissions) do
Policy.Changeset.create_changeset(attrs, subject)
|> Repo.insert()
end
end
def update_policy(%Policy{} = policy, attrs, %Auth.Subject{} = subject) do
required_permissions =
{:one_of, [Authorizer.manage_policies_permission()]}
with :ok <- Auth.ensure_has_permissions(subject, required_permissions),
:ok <- ensure_has_access_to(subject, policy) do
Policy.Changeset.update_changeset(policy, attrs)
|> Repo.update()
end
end
def delete_policy(%Policy{} = policy, %Auth.Subject{} = subject) do
required_permissions =
{:one_of, [Authorizer.manage_policies_permission()]}
with :ok <- Auth.ensure_has_permissions(subject, required_permissions) do
Policy.Query.by_id(policy.id)
|> Authorizer.for_subject(subject)
|> Repo.fetch_and_update(with: &Policy.Changeset.delete_changeset/1)
end
end
def ensure_has_access_to(%Auth.Subject{} = subject, %Policy{} = policy) do
if subject.account.id == policy.account_id do
:ok
else
{:error, :unauthorized}
end
end
end

View File

@@ -0,0 +1,37 @@
defmodule Domain.Policies.Authorizer do
use Domain.Auth.Authorizer
alias Domain.Policies.Policy
def manage_policies_permission, do: build(Policy, :manage)
def view_available_policies_permission, do: build(Policy, :view_available_policies)
@impl Domain.Auth.Authorizer
def list_permissions_for_role(:account_admin_user) do
[
manage_policies_permission()
]
end
def list_permissions_for_role(:account_user) do
[
view_available_policies_permission()
]
end
def list_permissions_for_role(_) do
[]
end
@impl Domain.Auth.Authorizer
def for_subject(queryable, %Subject{} = subject) do
cond do
has_permission?(subject, manage_policies_permission()) ->
Policy.Query.by_account_id(queryable, subject.account.id)
has_permission?(subject, view_available_policies_permission()) ->
queryable
|> Policy.Query.by_account_id(subject.account.id)
|> Policy.Query.by_actor_id(subject.actor.id)
end
end
end

View File

@@ -0,0 +1,17 @@
defmodule Domain.Policies.Policy do
use Domain, :schema
schema "policies" do
field :name, :string
belongs_to :actor_group, Domain.Actors.Group
belongs_to :resource, Domain.Resources.Resource
belongs_to :account, Domain.Accounts.Account
field :created_by, Ecto.Enum, values: ~w[identity]a
belongs_to :created_by_identity, Domain.Auth.Identity
field :deleted_at, :utc_datetime_usec
timestamps()
end
end

View File

@@ -0,0 +1,58 @@
defmodule Domain.Policies.Policy.Changeset do
use Domain, :changeset
alias Domain.Auth
alias Domain.Policies.Policy
@fields ~w[name actor_group_id resource_id]a
@update_fields ~w[name]a
@required_fields @fields
def create_changeset(attrs, %Auth.Subject{} = subject) do
%Policy{}
|> cast(attrs, @fields)
|> validate_required(@required_fields)
|> changeset()
|> put_change(:account_id, subject.account.id)
|> put_change(:created_by, :identity)
|> put_change(:created_by_identity_id, subject.identity.id)
end
def update_changeset(%Policy{} = policy, attrs) do
policy
|> cast(attrs, @update_fields)
|> validate_required(@required_fields)
|> changeset()
end
def delete_changeset(%Policy{} = policy) do
policy
|> change()
|> put_change(:deleted_at, DateTime.utc_now())
end
defp changeset(changeset) do
changeset
|> validate_length(:name, min: 1, max: 255)
|> unique_constraint([:account_id, :name],
message: "Policy Name already exists",
error_key: :name
)
|> unique_constraint(
:base,
name: :policies_account_id_resource_id_actor_group_id_index,
message: "Policy with Group and Resource already exists"
)
|> assoc_constraint(:resource)
|> assoc_constraint(:actor_group)
|> unique_constraint(
:base,
name: :policies_actor_group_id_fkey,
message: "Not allowed to create policies for groups outside of your account"
)
|> unique_constraint(
:base,
name: :policies_resource_id_fkey,
message: "Not allowed to create policies for resources outside of your account"
)
end
end

View File

@@ -0,0 +1,25 @@
defmodule Domain.Policies.Policy.Query do
use Domain, :query
def all do
from(policies in Domain.Policies.Policy, as: :policies)
|> where([policies: policies], is_nil(policies.deleted_at))
end
def by_id(queryable \\ all(), id) do
where(queryable, [policies: policies], policies.id == ^id)
end
def by_account_id(queryable \\ all(), account_id) do
where(queryable, [policies: policies], policies.account_id == ^account_id)
end
def by_actor_id(queryable \\ all(), actor_id) do
queryable
|> join(:inner, [policies: policies], ag in assoc(policies, :actor_group), as: :actor_groups)
|> join(:inner, [actor_groups: actor_groups], a in assoc(actor_groups, :memberships),
as: :memberships
)
|> where([memberships: memberships], memberships.actor_id == ^actor_id)
end
end

View File

@@ -0,0 +1,44 @@
defmodule Domain.Repo.Migrations.CreatePolicies do
use Ecto.Migration
def change do
create(index(:resources, [:account_id, :id], unique: true))
create(index(:actor_groups, [:account_id, :id], unique: true))
create table(:policies, primary_key: false) do
add(:id, :uuid, primary_key: true)
add(:name, :string, null: false)
add(
:actor_group_id,
references(:actor_groups,
type: :binary_id,
on_delete: :delete_all,
with: [account_id: :account_id]
),
null: false
)
add(
:resource_id,
references(:resources,
type: :binary_id,
on_delete: :delete_all,
with: [account_id: :account_id]
),
null: false
)
add(:account_id, references(:accounts, type: :binary_id), null: false)
add(:created_by, :string, null: false)
add(:created_by_identity_id, references(:auth_identities, type: :binary_id))
add(:deleted_at, :utc_datetime_usec)
timestamps(type: :utc_datetime_usec)
end
create(index(:policies, [:account_id, :name], unique: true))
create(index(:policies, [:account_id, :resource_id, :actor_group_id], unique: true))
end
end

View File

@@ -1,4 +1,4 @@
alias Domain.{Repo, Accounts, Auth, Actors, Relays, Gateways, Resources}
alias Domain.{Repo, Accounts, Auth, Actors, Relays, Gateways, Resources, Policies}
# Seeds can be run both with MIX_ENV=prod and MIX_ENV=test, for test env we don't have
# an adapter configured and creation of email provider will fail, so we will override it here.
@@ -56,6 +56,20 @@ IO.puts("")
adapter_config: %{}
})
{:ok, other_email_provider} =
Auth.create_provider(other_account, %{
name: "email",
adapter: :email,
adapter_config: %{}
})
{:ok, other_userpass_provider} =
Auth.create_provider(other_account, %{
name: "UserPass",
adapter: :userpass,
adapter_config: %{}
})
unprivileged_actor_email = "firezone-unprivileged-1@localhost"
admin_actor_email = "firezone@localhost"
@@ -88,6 +102,39 @@ unprivileged_actor_userpass_identity =
id: "7da7d1cd-111c-44a7-b5ac-4027b9d230e5"
)
# Other Account Users
other_unprivileged_actor_email = "other-unprivileged-1@localhost"
other_admin_actor_email = "other@localhost"
{:ok, other_unprivileged_actor} =
Actors.create_actor(other_email_provider, other_unprivileged_actor_email, %{
type: :account_user,
name: "Other Unprivileged"
})
{:ok, other_admin_actor} =
Actors.create_actor(other_email_provider, other_admin_actor_email, %{
type: :account_admin_user,
name: "Other Admin"
})
{:ok, other_unprivileged_actor_userpass_identity} =
Auth.create_identity(
other_unprivileged_actor,
other_userpass_provider,
other_unprivileged_actor_email,
%{
"password" => "Firezone1234",
"password_confirmation" => "Firezone1234"
}
)
{:ok, _other_admin_actor_userpass_identity} =
Auth.create_identity(other_admin_actor, other_userpass_provider, other_admin_actor_email, %{
"password" => "Firezone1234",
"password_confirmation" => "Firezone1234"
})
if client_secret = System.get_env("SEEDS_GOOGLE_OIDC_CLIENT_SECRET") do
{:ok, google_provider} =
Auth.create_provider(account, %{
@@ -171,6 +218,41 @@ admin_iphone =
IO.puts("Devices created")
IO.puts("")
IO.puts("Created Actor Groups: ")
{:ok, eng_group} = Actors.create_group(%{name: "Engineering"}, admin_subject)
{:ok, finance_group} = Actors.create_group(%{name: "Finance"}, admin_subject)
{:ok, all_group} = Actors.create_group(%{name: "All Employees"}, admin_subject)
for group <- [eng_group, finance_group, all_group] do
IO.puts(" Name: #{group.name} ID: #{group.id}")
end
Actors.update_group(
eng_group,
%{memberships: [%{actor_id: admin_subject.actor.id}]},
admin_subject
)
Actors.update_group(
finance_group,
%{memberships: [%{actor_id: unprivileged_subject.actor.id}]},
admin_subject
)
Actors.update_group(
all_group,
%{
memberships: [
%{actor_id: admin_subject.actor.id},
%{actor_id: unprivileged_subject.actor.id}
]
},
admin_subject
)
IO.puts("")
relay_group =
account
|> Relays.Group.Changeset.create_changeset(
@@ -262,7 +344,7 @@ IO.puts(" Public Key: #{gateway2.public_key}")
IO.puts(" IPv4: #{gateway2.ipv4} IPv6: #{gateway2.ipv6}")
IO.puts("")
{:ok, dns_resource} =
{:ok, dns_google_resource} =
Resources.create_resource(
%{
type: :dns,
@@ -272,7 +354,7 @@ IO.puts("")
admin_subject
)
{:ok, dns_resource} =
{:ok, dns_gitlab_resource} =
Resources.create_resource(
%{
type: :dns,
@@ -299,10 +381,39 @@ IO.puts("")
)
IO.puts("Created resources:")
IO.puts(" #{dns_resource.address} - DNS - #{dns_resource.ipv4} - gateways: #{gateway_name}")
IO.puts(
" #{dns_google_resource.address} - DNS - #{dns_google_resource.ipv4} - gateways: #{gateway_name}"
)
IO.puts(
" #{dns_gitlab_resource.address} - DNS - #{dns_gitlab_resource.ipv4} - gateways: #{gateway_name}"
)
IO.puts(" #{cidr_resource.address} - CIDR - gateways: #{gateway_name}")
IO.puts("")
Policies.create_policy(
%{
name: "Eng Access To Gitlab",
actor_group_id: eng_group.id,
resource_id: dns_gitlab_resource.id
},
admin_subject
)
Policies.create_policy(
%{
name: "All Access To Network",
actor_group_id: all_group.id,
resource_id: cidr_resource.id
},
admin_subject
)
IO.puts("Policies Created")
IO.puts("")
{:ok, unprivileged_subject_session_token} =
Auth.create_session_token_from_subject(unprivileged_subject)

View File

@@ -0,0 +1,374 @@
defmodule Domain.PoliciesTest do
alias Web.Policies
use Domain.DataCase, async: true
import Domain.Policies
alias Domain.{
AccountsFixtures,
ActorsFixtures,
AuthFixtures,
PoliciesFixtures,
ResourcesFixtures
}
alias Domain.Policies
setup do
account = AccountsFixtures.create_account()
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
identity = AuthFixtures.create_identity(account: account, actor: actor)
subject = AuthFixtures.create_subject(identity)
%{
account: account,
actor: actor,
identity: identity,
subject: subject
}
end
describe "fetch_policy_by_id/2" do
test "returns error when policy does not exist", %{subject: subject} do
assert fetch_policy_by_id(Ecto.UUID.generate(), subject) == {:error, :not_found}
end
test "returns error when UUID is invalid", %{subject: subject} do
assert fetch_policy_by_id("foo", subject) == {:error, :not_found}
end
test "returns policy when policy exists", %{account: account, subject: subject} do
policy = PoliciesFixtures.create_policy(account: account)
assert {:ok, fetched_policy} = fetch_policy_by_id(policy.id, subject)
assert fetched_policy.id == policy.id
end
test "does not return deleted policy", %{account: account, subject: subject} do
{:ok, policy} =
PoliciesFixtures.create_policy(account: account)
|> delete_policy(subject)
assert fetch_policy_by_id(policy.id, subject) == {:error, :not_found}
end
test "does not return policies in other accounts", %{subject: subject} do
policy = PoliciesFixtures.create_policy()
assert fetch_policy_by_id(policy.id, subject) == {:error, :not_found}
end
test "returns error when subject has no permission to view policies", %{subject: subject} do
subject = AuthFixtures.remove_permissions(subject)
assert fetch_policy_by_id(Ecto.UUID.generate(), subject) ==
{:error,
{:unauthorized,
[
missing_permissions: [
{:one_of,
[
Policies.Authorizer.manage_policies_permission(),
Policies.Authorizer.view_available_policies_permission()
]}
]
]}}
end
# TODO: add a test that soft-deleted assocs are not preloaded
test "associations are preloaded when opts given", %{account: account, subject: subject} do
policy = PoliciesFixtures.create_policy(account: account)
{:ok, policy} = fetch_policy_by_id(policy.id, subject, preload: [:actor_group, :resource])
assert Ecto.assoc_loaded?(policy.actor_group)
assert Ecto.assoc_loaded?(policy.resource)
end
end
describe "list_policies/1" do
test "returns empty list when there are no policies", %{subject: subject} do
assert list_policies(subject) == {:ok, []}
end
test "does not list policies from other accounts", %{subject: subject} do
PoliciesFixtures.create_policy()
assert list_policies(subject) == {:ok, []}
end
test "does not list deleted policies", %{account: account, subject: subject} do
PoliciesFixtures.create_policy(account: account)
|> delete_policy(subject)
assert list_policies(subject) == {:ok, []}
end
test "returns all policies for account admin subject", %{account: account} do
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
identity = AuthFixtures.create_identity(account: account, actor: actor)
subject = AuthFixtures.create_subject(identity)
PoliciesFixtures.create_policy(account: account)
PoliciesFixtures.create_policy(account: account)
PoliciesFixtures.create_policy()
assert {:ok, policies} = list_policies(subject)
assert length(policies) == 2
end
test "returns select policies for non-admin subject", %{account: account, subject: subject} do
unprivileged_actor = ActorsFixtures.create_actor(type: :account_user, account: account)
unpriviledged_identity =
AuthFixtures.create_identity(account: account, actor: unprivileged_actor)
unprivileged_subject = AuthFixtures.create_subject(unpriviledged_identity)
actor_group = ActorsFixtures.create_group(account: account, subject: subject)
Domain.Actors.update_group(
actor_group,
%{memberships: [%{actor_id: unprivileged_actor.id}]},
subject
)
PoliciesFixtures.create_policy(account: account, actor_group: actor_group)
PoliciesFixtures.create_policy(account: account)
PoliciesFixtures.create_policy()
assert {:ok, policies} = list_policies(unprivileged_subject)
assert length(policies) == 1
end
test "returns error when subject has no permission to view policies", %{subject: subject} do
subject = AuthFixtures.remove_permissions(subject)
assert list_policies(subject) ==
{:error,
{:unauthorized,
[
missing_permissions: [
{:one_of,
[
Policies.Authorizer.manage_policies_permission(),
Policies.Authorizer.view_available_policies_permission()
]}
]
]}}
end
end
describe "create_policy/2" do
test "returns changeset error on empty params", %{subject: subject} do
assert {:error, changeset} = create_policy(%{}, subject)
assert errors_on(changeset) == %{
name: ["can't be blank"],
actor_group_id: ["can't be blank"],
resource_id: ["can't be blank"]
}
end
test "returns changeset error on invalid params", %{subject: subject} do
assert {:error, changeset} =
create_policy(
%{name: 1, actor_group_id: "foo", resource_id: "bar"},
subject
)
assert errors_on(changeset) == %{name: ["is invalid"]}
end
test "returns error when subject has no permission to manage policies", %{subject: subject} do
subject = AuthFixtures.remove_permissions(subject)
assert create_policy(%{}, subject) ==
{:error,
{:unauthorized,
[
missing_permissions: [
{:one_of, [Policies.Authorizer.manage_policies_permission()]}
]
]}}
end
test "returns changeset error when trying to create policy with another account actor_group",
%{
account: account,
subject: subject
} do
other_account = AccountsFixtures.create_account()
resource = ResourcesFixtures.create_resource(account: account)
other_actor_group = ActorsFixtures.create_group(account: other_account)
attrs = %{
account_id: account.id,
name: "yikes",
actor_group_id: other_actor_group.id,
resource_id: resource.id
}
assert {:error, changeset} = create_policy(attrs, subject)
assert errors_on(changeset) == %{actor_group: ["does not exist"]}
end
test "returns changeset error when trying to create policy with another account resource", %{
account: account,
subject: subject
} do
other_account = AccountsFixtures.create_account()
other_resource = ResourcesFixtures.create_resource(account: other_account)
actor_group = ActorsFixtures.create_group(account: account)
attrs = %{
account_id: account.id,
name: "yikes",
actor_group_id: actor_group.id,
resource_id: other_resource.id
}
assert {:error, changeset} =
create_policy(attrs, subject)
assert errors_on(changeset) == %{resource: ["does not exist"]}
end
end
describe "update_policy/3" do
setup context do
policy =
PoliciesFixtures.create_policy(
account: context.account,
subject: context.subject
)
Map.put(context, :policy, policy)
end
test "does nothing on empty params", %{policy: policy, subject: subject} do
assert {:ok, _policy} = update_policy(policy, %{}, subject)
end
test "returns changeset error on invalid params", %{account: account, subject: subject} do
policy = PoliciesFixtures.create_policy(account: account, subject: subject)
assert {:error, changeset} =
update_policy(
policy,
%{name: 1, actor_group_id: "foo", resource_id: "bar"},
subject
)
assert errors_on(changeset) == %{name: ["is invalid"]}
end
test "allows update to name", %{policy: policy, subject: subject} do
assert {:ok, updated_policy} =
update_policy(policy, %{name: "updated policy name"}, subject)
assert updated_policy.name == "updated policy name"
end
test "does not allow update to actor_group_id", %{
policy: policy,
account: account,
subject: subject
} do
new_actor_group = ActorsFixtures.create_group(account: account)
assert {:ok, updated_policy} =
update_policy(policy, %{actor_group_id: new_actor_group.id}, subject)
assert updated_policy.actor_group_id == policy.actor_group_id
end
test "does not allow update to resource_id", %{
policy: policy,
account: account,
subject: subject
} do
new_resource = ResourcesFixtures.create_resource(account: account)
assert {:ok, updated_policy} =
update_policy(policy, %{resource_id: new_resource.id}, subject)
assert updated_policy.resource_id == policy.resource_id
end
test "returns error when subject has no permission to update policies", %{
policy: policy,
subject: subject
} do
subject = AuthFixtures.remove_permissions(subject)
assert update_policy(policy, %{name: "Name Change Attempt"}, subject) ==
{:error,
{:unauthorized,
[
missing_permissions: [
{:one_of, [Policies.Authorizer.manage_policies_permission()]}
]
]}}
end
test "return error when subject is outside of account", %{policy: policy} do
other_account = AccountsFixtures.create_account()
other_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: other_account)
other_identity = AuthFixtures.create_identity(account: other_account, actor: other_actor)
other_subject = AuthFixtures.create_subject(other_identity)
assert {:error, :unauthorized} =
update_policy(policy, %{name: "Should not be allowed"}, other_subject)
end
end
describe "delete_policy/2" do
setup context do
policy =
PoliciesFixtures.create_policy(
account: context.account,
subject: context.subject
)
Map.put(context, :policy, policy)
end
test "deletes policy", %{policy: policy, subject: subject} do
assert {:ok, deleted_policy} = delete_policy(policy, subject)
assert deleted_policy.deleted_at != nil
end
test "returns error when subject has no permission to delete policies", %{
policy: policy,
subject: subject
} do
subject = AuthFixtures.remove_permissions(subject)
assert delete_policy(policy, subject) ==
{:error,
{:unauthorized,
[
missing_permissions: [
{:one_of, [Policies.Authorizer.manage_policies_permission()]}
]
]}}
end
test "returns error on state conflict", %{policy: policy, subject: subject} do
assert {:ok, deleted_policy} = delete_policy(policy, subject)
assert delete_policy(deleted_policy, subject) == {:error, :not_found}
assert delete_policy(policy, subject) == {:error, :not_found}
end
test "returns error when subject attempts to delete policy outside of account", %{
policy: policy
} do
other_account = AccountsFixtures.create_account()
other_actor = ActorsFixtures.create_actor(type: :account_admin_user, account: other_account)
other_identity = AuthFixtures.create_identity(account: other_account, actor: other_actor)
other_subject = AuthFixtures.create_subject(other_identity)
assert delete_policy(policy, other_subject) == {:error, :not_found}
end
end
end

View File

@@ -0,0 +1,61 @@
defmodule Domain.PoliciesFixtures do
alias Domain.{AccountsFixtures, ActorsFixtures, AuthFixtures, ResourcesFixtures}
def policy_attrs(attrs \\ %{}) do
name = "policy-#{counter()}"
Enum.into(attrs, %{
name: name,
actor_group_id: nil,
resource_id: nil
})
end
def create_policy(attrs \\ %{}) do
attrs = policy_attrs(attrs)
{account, attrs} =
Map.pop_lazy(attrs, :account, fn ->
AccountsFixtures.create_account()
end)
{subject, attrs} =
Map.pop_lazy(attrs, :subject, fn ->
actor = ActorsFixtures.create_actor(type: :account_admin_user, account: account)
identity = AuthFixtures.create_identity(account: account, actor: actor)
AuthFixtures.create_subject(identity)
end)
{actor_group, attrs} =
Map.pop_lazy(attrs, :actor_group, fn ->
ActorsFixtures.create_group(account: account, subject: subject)
end)
{actor_group_id, attrs} =
Map.pop_lazy(attrs, :actor_group, fn ->
actor_group.id
end)
{resource, attrs} =
Map.pop_lazy(attrs, :resource, fn ->
ResourcesFixtures.create_resource(account: account, subject: subject)
end)
{resource_id, attrs} =
Map.pop_lazy(attrs, :resource, fn ->
resource.id
end)
{:ok, policy} =
attrs
|> Map.put(:actor_group_id, actor_group_id)
|> Map.put(:resource_id, resource_id)
|> Domain.Policies.create_policy(subject)
policy
end
defp counter do
System.unique_integer([:positive])
end
end

View File

@@ -1,20 +1,30 @@
defmodule Web.Policies.Edit do
use Web, :live_view
alias Domain.Policies
def mount(%{"id" => id} = _params, _session, socket) do
with {:ok, policy} <- Policies.fetch_policy_by_id(id, socket.assigns.subject) do
{:ok, assign(socket, policy: policy)}
else
_other -> raise Web.LiveErrors.NotFoundError
end
end
def render(assigns) do
~H"""
<.breadcrumbs home_path={~p"/#{@account}/dashboard"}>
<.breadcrumb path={~p"/#{@account}/policies"}>Policies</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/policies/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}>
Engineering access to GitLab
<.breadcrumb path={~p"/#{@account}/policies/#{@policy}"}>
<%= @policy.name %>
</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/policies/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89/edit"}>
<.breadcrumb path={~p"/#{@account}/policies/#{@policy}/edit"}>
Edit
</.breadcrumb>
</.breadcrumbs>
<.header>
<:title>
Edit Policy <code>Engineering access to GitLab</code>
Edit Policy <code><%= @policy.name %></code>
</:title>
</.header>
<!-- Edit Policy -->
@@ -27,24 +37,21 @@ defmodule Web.Policies.Edit do
<.label for="name">
Name
</.label>
<input
<.input
autocomplete="off"
type="text"
name="name"
id="policy-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="Engineering access to GitLab"
placeholder="Name of Policy"
value={@policy.name}
required
/>
</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"
>
<.button type="submit" class="btn btn-primary">
Save
</button>
</.button>
</div>
</form>
</div>

View File

@@ -1,5 +1,15 @@
defmodule Web.Policies.Index do
use Web, :live_view
alias Domain.Policies
def mount(_params, _session, socket) do
with {:ok, policies} <-
Policies.list_policies(socket.assigns.subject, preload: [:actor_group, :resource]) do
{:ok, assign(socket, policies: policies)}
else
_other -> raise Web.LiveErrors.NotFoundError
end
end
def render(assigns) do
~H"""
@@ -18,171 +28,64 @@ defmodule Web.Policies.Index do
</.header>
<!-- Policies 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"
/>
</div>
</form>
</div>
</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">
Group
<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">
Resource
<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"/#{@account}/policies/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
Engineering access to Gitlab
</.link>
</th>
<td class="px-4 py-3">
<.link
class="inline-block"
navigate={~p"/#{@account}/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">
<.link
class="text-blue-600 dark:text-blue-500 hover:underline"
navigate={~p"/#{@account}/resources/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
>
GitLab
</.link>
</td>
<td class="px-4 py-3 flex items-center justify-end">
<.link navigate="#" class="text-blue-600 dark:text-blue-500 hover:underline">
Delete
</.link>
</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"/#{@account}/policies/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
IT access to Staging VPC
</.link>
</th>
<td class="px-4 py-3">
<.link
class="inline-block"
navigate={~p"/#{@account}/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">
<.link
class="text-blue-600 dark:text-blue-500 hover:underline"
navigate={~p"/#{@account}/resources/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
>
Staging VPC
</.link>
</td>
<td class="px-4 py-3 flex items-center justify-end">
<.link navigate="#" class="text-blue-600 dark:text-blue-500 hover:underline">
Delete
</.link>
</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"/#{@account}/policies/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
Admin access to Jira
</.link>
</th>
<td class="px-4 py-3">
<.link
class="inline-block"
navigate={~p"/#{@account}/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">
Admin
</span>
</.link>
</td>
<td class="px-4 py-3">
<.link
class="text-blue-600 dark:text-blue-500 hover:underline"
navigate={~p"/#{@account}/resources/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
>
Jira
</.link>
</td>
<td class="px-4 py-3 flex items-center justify-end">
<.link navigate="#" class="text-blue-600 dark:text-blue-500 hover:underline">
Delete
</.link>
</td>
</tr>
</tbody>
</table>
</div>
<.resource_filter />
<.table id="policies" rows={@policies} row_id={&"policies-#{&1.id}"}>
<:col :let={policy} label="NAME">
<.link
navigate={~p"/#{@account}/policies/#{policy}"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
<%= policy.name %>
</.link>
</:col>
<:col :let={policy} label="GROUP">
<.badge>
<%= policy.actor_group.name %>
</.badge>
</:col>
<:col :let={policy} label="RESOURCE">
<.link
navigate={~p"/#{@account}/resources/#{policy.resource_id}"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
<%= 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"
>
Delete
</a>
</:action>
</.table>
<.paginator page={3} total_pages={100} collection_base_path={~p"/#{@account}/gateways"} />
</div>
"""
end
def resource_filter(assigns) do
~H"""
<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>
</div>
"""
end
end

View File

@@ -1,6 +1,23 @@
defmodule Web.Policies.New do
use Web, :live_view
alias Domain.{Resources, Actors}
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
socket =
assign(socket,
resources: resources,
actor_groups: actor_groups
)
{:ok, socket}
else
_other -> raise Web.LiveErrors.NotFoundError
end
end
def render(assigns) do
~H"""
<.breadcrumbs home_path={~p"/#{@account}/dashboard"}>
@@ -18,59 +35,47 @@ defmodule Web.Policies.New do
<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>
<select
id="group"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
>
<option>Everyone</option>
<option>DevOps</option>
<option selected>Engineering</option>
<option>IT</option>
<option>Admin</option>
</select>
<.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>
<select
id="resource"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
>
<option>GitLab</option>
<option>Jira</option>
<option>10.0.0.0/24</option>
<option>24.119.103.223</option>
<option>fc00::1</option>
</select>
</div>
<div>
<.label for="policy-name">
Name
</.label>
<input
autocomplete="off"
type="text"
name="name"
value="Engineering access to GitLab"
id="policy-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="Enter a name for this policy"
<.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"
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"
>
<.button type="submit">
Save
</button>
</.button>
</div>
</form>
</div>

View File

@@ -1,80 +1,92 @@
defmodule Web.Policies.Show do
use Web, :live_view
alias Domain.Policies
def mount(%{"id" => id} = _params, _session, socket) do
with {:ok, policy} <-
Policies.fetch_policy_by_id(id, socket.assigns.subject,
preload: [:actor_group, :resource, [created_by_identity: :actor]]
) do
{:ok, assign(socket, policy: policy)}
else
_other -> raise Web.LiveErrors.NotFoundError
end
end
defp pretty_print_date(date) do
"#{date.month}/#{date.day}/#{date.year} #{date.hour}:#{date.minute}:#{date.second}"
end
def render(assigns) do
~H"""
<.breadcrumbs home_path={~p"/#{@account}/dashboard"}>
<.breadcrumb path={~p"/#{@account}/policies"}>Policies</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/policies/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}>
Engineering access to GitLab
<.breadcrumb path={~p"/#{@account}/policies/#{@policy}"}>
<%= @policy.name %>
</.breadcrumb>
</.breadcrumbs>
<.header>
<:title>
Viewing Policy <code>Engineering access to GitLab</code>
Viewing Policy <code><%= @policy.name %></code>
</:title>
<:actions>
<.edit_button navigate={~p"/#{@account}/policies/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89/edit"}>
<.edit_button navigate={~p"/#{@account}/policies/#{@policy}/edit"}>
Edit Policy
</.edit_button>
</:actions>
</.header>
<!-- Show Policy -->
<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"
<.vertical_table>
<.vertical_table_row>
<:label>
Name
</:label>
<:value>
<%= @policy.name %>
</:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>
Group
</:label>
<:value>
<.link
navigate={~p"/#{@account}/groups/#{@policy.actor_group_id}"}
class="text-blue-600 dark:text-blue-500 hover:underline"
>
Name
</th>
<td class="px-6 py-4">
Engineering access to GitLab
</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"
<%= @policy.actor_group.name %>
</.link>
</:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>
Resource
</:label>
<:value>
<.link
navigate={~p"/#{@account}/resources/#{@policy.resource_id}"}
class="text-blue-600 dark:text-blue-500 hover:underline"
>
Group
</th>
<td class="px-6 py-4">
Engineering
</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"
<%= @policy.resource.name %>
</.link>
</:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>
Created
</:label>
<:value>
<%= pretty_print_date(@policy.inserted_at) %> by
<.link
navigate={~p"/#{@account}/actors/#{@policy.created_by_identity.actor.id}"}
class="text-blue-600 dark:text-blue-500 hover:underline"
>
Resource
</th>
<td class="px-6 py-4">
GitLab
</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"/#{@account}/actors/DF43E951-7DFB-4921-8F7F-BF0F8D31FA89"}
>
Andrew Dryga
</.link>
</td>
</tr>
</tbody>
</table>
<%= @policy.created_by_identity.actor.name %>
</.link>
</:value>
</.vertical_table_row>
</.vertical_table>
</div>
<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">