mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-03-22 07:41:51 +00:00
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:
@@ -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
|
||||
|
||||
90
elixir/apps/domain/lib/domain/policies.ex
Normal file
90
elixir/apps/domain/lib/domain/policies.ex
Normal 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
|
||||
37
elixir/apps/domain/lib/domain/policies/authorizer.ex
Normal file
37
elixir/apps/domain/lib/domain/policies/authorizer.ex
Normal 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
|
||||
17
elixir/apps/domain/lib/domain/policies/policy.ex
Normal file
17
elixir/apps/domain/lib/domain/policies/policy.ex
Normal 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
|
||||
58
elixir/apps/domain/lib/domain/policies/policy/changeset.ex
Normal file
58
elixir/apps/domain/lib/domain/policies/policy/changeset.ex
Normal 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
|
||||
25
elixir/apps/domain/lib/domain/policies/policy/query.ex
Normal file
25
elixir/apps/domain/lib/domain/policies/policy/query.ex
Normal 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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
|
||||
374
elixir/apps/domain/test/domain/policies_test.exs
Normal file
374
elixir/apps/domain/test/domain/policies_test.exs
Normal 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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user