Implement the permission hierachy #2

Closed
krista wants to merge 4 commits from permission-expansion into main
7 changed files with 198 additions and 5 deletions

View file

@ -6,5 +6,5 @@ S3_URL=
S3_BUCKET= S3_BUCKET=
POSTGRES_DB=holder POSTGRES_DB=holder
POSTGRES_USER=holder POSTGRES_USER=holder
POSTGRES_PASSWORD= POSTGRES_PASSWORD=boobs
POSTGRES_HOSTNAME=localhost POSTGRES_HOSTNAME=localhost

View file

@ -1,2 +1,2 @@
elixir 1.18.2-otp-27 elixir 1.18.4-otp-27
erlang 27.2.2 erlang 27.3.4.2

View file

@ -2,5 +2,5 @@
"[elixir]": { "[elixir]": {
"editor.formatOnSave": true "editor.formatOnSave": true
}, },
"elixirLS.dialyzerEnabled": false "elixirLS.dialyzerEnabled": true
} }

View file

@ -6,4 +6,6 @@ config :nostrum,
youtubedl: nil, youtubedl: nil,
streamlink: nil streamlink: nil
config :logger, level: :info config :logger, :console,
level: :info,
format: "$time $metadata[$level]\t$message\n"

View file

@ -49,4 +49,13 @@ defmodule Holder.Hold.Controller do
def permissions(%Schema.Hold.Controller{id: id}), def permissions(%Schema.Hold.Controller{id: id}),
do: Hold.Permission.aggregate_by(controller_id: id) do: Hold.Permission.aggregate_by(controller_id: id)
def trustee?(nil), do: false
def trustee?(id) do
case Repo.get(Schema.Hold.Controller, id) do
nil -> false
c -> c.trustee
end
end
end end

View file

@ -2,6 +2,8 @@ defmodule Holder.Hold.Permission do
alias Holder.Schema alias Holder.Schema
alias Holder.Repo alias Holder.Repo
alias Holder.Hold.Permission.Flags alias Holder.Hold.Permission.Flags
alias Holder.Hold.Permission.Role
import Ecto.Query
def aggregate_by(owner) when is_struct(owner), do: resolve_owner(owner) |> aggregate_by() def aggregate_by(owner) when is_struct(owner), do: resolve_owner(owner) |> aggregate_by()
@ -47,4 +49,162 @@ defmodule Holder.Hold.Permission do
|> List.wrap() |> List.wrap()
|> Keyword.from_keys(id) |> Keyword.from_keys(id)
end end
def all_of(actor_id) do
Repo.all(
from p in Schema.Hold.Permission,
where: p.controller_id == ^actor_id or p.participant_id == ^actor_id
)
end
def actor_permissions(actor_id) do
has =
all_of(actor_id)
|> Enum.filter(& &1.has)
if Enum.empty?(has) do
{:error, "Actor not found."}
else
{:ok,
has
|> Enum.map(& &1.bitfield)
|> Enum.reduce(&Bitwise.bor/2)
|> Flags.deserialize()}
end
end
def get_actor(actor_id) do
case all_of(actor_id) do
[] ->
{:error, "Actor not found."}
ac ->
hd(ac)
|> Repo.preload([:controller, :participant])
|> then(fn
%Schema.Hold.Permission{controller: c} when not is_nil(c) -> {:ok, c}
%Schema.Hold.Permission{participant: p} when not is_nil(p) -> {:ok, p}
_ -> {:error, "Actor not found."}
end)
end
end
def actionable(%Schema.Hold.Controller{} = actor, on) do
actor
|> Repo.preload([:permissions])
|> Map.get(:permissions)
|> Enum.filter(& &1.has)
|> Enum.map(&{Role.deserialize(&1 |> Map.get(on)), Flags.deserialize(&1.bitfield)})
end
def actionable(%Schema.Hold.Participant{} = actor, on) do
actor
|> Repo.preload([:permissions])
|> Map.get(:permissions)
|> Enum.filter(& &1.has)
|> Enum.map(&{Role.deserialize(&1 |> Map.get(on)), Flags.deserialize(&1.bitfield)})
end
def actionable(actor_id, on) when is_integer(actor_id) do
all_of(actor_id)
|> Enum.filter(& &1.has)
|> Enum.map(&{Role.deserialize(&1 |> Map.get(on)), Flags.deserialize(&1.bitfield)})
end
def grantable(actor_id) do
actionable(actor_id, :may_grant_to)
end
def revokeable(actor_id) do
actionable(actor_id, :may_revoke_from)
end
def revokeable_by(actor_id) do
actionable(actor_id, :may_be_revoked_by)
end
def verify_action([], _, _) do
{:error, "Actor has no actionable permissions."}
end
def verify_action(action, target, permission) do
action
|> Enum.map(fn {roles, perms} ->
Role.from(target) |> Bitfield.difference(roles) |> Enum.empty?() and
Bitfield.new([permission]) |> Bitfield.difference(perms) |> Enum.empty?()
end)
|> then(&{:ok, Enum.all?(&1) and not Enum.empty?(&1)})
end
def check(permission, actor_id) do
case actor_permissions(actor_id) do
{:ok, perms} ->
{:ok,
Bitfield.new([permission])
|> Bitfield.difference(perms)
|> Enum.empty?()}
e ->
e
end
end
def may(permission, actor_id, target_id \\ nil)
def may(permission, actor_id, nil) do
check(permission, actor_id)
end
def may(:hold_controllers_remove = p, actor_id, target_id) do
a = get_actor(actor_id)
t = get_actor(target_id)
different_holds =
case {a, t} do
{{:ok, actor}, {:ok, target}} ->
actor.hold_id !== target.hold_id
_ ->
false
end
actor_has = may(p, actor_id, nil)
target_has = Holder.Hold.Controller.trustee?(target_id)
case {actor_has, target_has, different_holds} do
{_, _, true} ->
{:error, "The actor and target are on different holds."}
{{:error, _}, _, _} ->
actor_has
{{:ok, true}, true, _} ->
{:error, "Trustees cannot be removed as hold controllers."}
_ ->
{:ok, Kernel.and(actor_has |> elem(1), target_has)}
end
end
def may_grant(permission, actor_id, target_id) do
case get_actor(target_id) do
{:ok, target} ->
grantable(actor_id)
|> verify_action(target, permission)
e ->
e
end
end
def may_revoke(permission, actor_id, target_id) do
case get_actor(target_id) do
{:ok, target} ->
revokeable(actor_id)
|> verify_action(target, permission)
e ->
e
end
end
end end

View file

@ -0,0 +1,22 @@
defmodule Holder.Hold.Permission.Role do
alias Holder.Schema
use Bitfield, flags: [:participant, :controller, :trustee]
def participant, do: Bitfield.new([:participant])
def controller, do: Bitfield.new([:controller])
def trustee, do: Bitfield.new([:trustee])
def from(%Schema.Hold.Controller{trustee: t}) do
if t do
[:controller, :trustee]
else
[:controller]
end
|> Bitfield.new()
end
def from(%Schema.Hold.Participant{}) do
participant()
end
end