Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ Key starting points:
- [DG-0001: Architecture Overview](/Users/Pascal/code/ash/ash_ui/guides/developer/DG-0001-architecture-overview.md)
- [Example: basic dashboard](/Users/Pascal/code/ash/ash_ui/examples/basic_dashboard/README.md)

## Current Phase
## Current Status

The project is in Phase 8, focused on governance gates and release readiness. CI, conformance coverage, observability, and documentation are now first-class parts of the repo instead of placeholders.
Phase 8 governance work is complete, but the repo is still closing feature gaps in earlier phases. The current implementation is strongest in resource storage, compilation, runtime wiring, observability, and governance, while real Ash-backed binding execution and full external renderer integration remain open in reopened Phase 1, 3, 4, 5, and 7 workstreams.

## Development Notes

Expand Down
2 changes: 2 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ config :ash_ui, :rendering,
default_renderer: :liveview,
# Enable automatic renderer detection based on context
auto_detect: true,
# Allow adapter fallback when external renderer packages are not installed
allow_adapter_fallback: true,
# Fallback renderer if primary is unavailable
fallback_renderer: nil,
# Renderer-specific options
Expand Down
114 changes: 93 additions & 21 deletions lib/ash_ui/authorization/binding_policy.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,69 @@ defmodule AshUI.Authorization.BindingPolicy do
"""

alias AshUI.Authorization.Policies
alias AshUI.Authorization.ScreenPolicy

@doc """
Defines policies for binding resource access.
"""
def policies do
[
%Ash.Policy.Policy{description: "Bindings are evaluable if parent screen is accessible", policies: []},
%Ash.Policy.Policy{description: "Can create bindings if can modify parent screen", policies: []},
%Ash.Policy.Policy{description: "Can update bindings if can modify parent screen", policies: []},
%Ash.Policy.Policy{description: "Can delete bindings if can modify parent screen", policies: []},
%Ash.Policy.Policy{
description: "Bindings are evaluable if parent screen is accessible",
policies: []
},
%Ash.Policy.Policy{
description: "Can create bindings if can modify parent screen",
policies: []
},
%Ash.Policy.Policy{
description: "Can update bindings if can modify parent screen",
policies: []
},
%Ash.Policy.Policy{
description: "Can delete bindings if can modify parent screen",
policies: []
},
%Ash.Policy.Policy{description: "Must have access to binding source data", policies: []}
]
end

@doc """
Checks whether the actor can read a binding.
"""
def can_read?(user, binding), do: can_evaluate?(user, binding)

@doc """
Checks whether the actor can create, update, or delete a binding.
"""
def can_manage?(user, binding) do
cond do
Policies.runtime_authorization_bypass?() -> true
not Policies.user_active(user) -> false
Policies.user_role(user, :admin) -> true
not Policies.role_allowed?(user, binding) -> false
screen_owned?(user, binding) -> true
Policies.screen_owner(user, binding) -> true
Policies.unrestricted_resource?(binding) -> true
true -> false
end
end

@doc """
Check if user can evaluate a binding.
"""
def can_evaluate?(user, binding) do
cond do
Policies.runtime_authorization_bypass?() -> true

not Policies.resource_active?(binding) -> false
# Admins can evaluate all bindings
Policies.user_role(user, :admin) -> true

# User must be active
not Policies.user_active(user) -> false

not Policies.role_allowed?(user, binding) -> false
not screen_accessible?(user, binding) -> false
# Check data source access
not has_data_access?(binding, user) -> false

# Default allow
true -> true
end
Expand All @@ -47,19 +80,16 @@ defmodule AshUI.Authorization.BindingPolicy do
def can_write?(user, binding) do
cond do
Policies.runtime_authorization_bypass?() -> true

# Admins can write to all bindings
Policies.user_role(user, :admin) -> true

# User must be active
not Policies.user_active(user) -> false

not Policies.role_allowed?(user, binding) -> false
# Check if binding is read-only
Map.get(binding, :read_only, false) -> false

read_only?(binding) -> false
not screen_owned?(user, binding) -> false
# Check write access to data source
not has_write_access?(binding, user) -> false

# Default allow
true -> true
end
Expand All @@ -82,47 +112,89 @@ defmodule AshUI.Authorization.BindingPolicy do
@doc """
Check if binding source resource is accessible.
"""
def source_accessible?(_user, binding) do
def source_accessible?(user, binding) do
source = normalize_source(binding)

cond do
# No source means no restriction
map_size(source) == 0 -> true

# Check resource-level access
not Policies.can_read_source(binding) -> false

# Check field-level access
not field_accessible?(user, binding) -> false
# Default allow
true -> true
end
end

# Private functions

defp has_data_access?(binding, _user) do
defp screen_accessible?(user, binding) do
case loaded_screen(binding) do
%{} = screen -> ScreenPolicy.can_read?(user, screen)
_ -> true
end
end

defp screen_owned?(user, binding) do
case loaded_screen(binding) do
%{} = screen ->
ScreenPolicy.can_manage?(user, screen)

_ ->
Policies.screen_owner(user, binding) || Policies.unrestricted_resource?(binding)
end
end

defp loaded_screen(resource) do
case Map.get(resource, :screen) || Map.get(resource, "screen") do
%Ash.NotLoaded{} -> nil
screen -> screen
end
end

defp has_data_access?(binding, user) do
source = normalize_source(binding)

cond do
map_size(source) == 0 -> true
not Policies.can_read_source(binding) -> false
not Policies.can_read_source(binding, user) -> false
not Policies.can_access_field(binding, Map.get(source, "field")) -> false
true -> true
end
end

defp has_write_access?(binding, _user) do
defp has_write_access?(binding, user) do
source = normalize_source(binding)

cond do
map_size(source) == 0 -> true
not Policies.can_write_source(binding) -> false
not Policies.can_write_source(binding, user) -> false
true -> true
end
end

defp field_accessible?(_user, binding) do
source = normalize_source(binding)
field = Map.get(source, "field")

case field do
nil -> true
_ -> Policies.can_access_field(binding, field)
end
end

defp normalize_source(binding) do
case Map.get(binding, :source) do
source when is_map(source) -> source
nil -> Map.get(binding, "source") || %{}
_ -> %{}
end
end

defp read_only?(binding) do
Map.get(binding, :read_only) ||
Map.get(binding, "read_only") ||
false
end
end
33 changes: 33 additions & 0 deletions lib/ash_ui/authorization/checks/binding_access.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
defmodule AshUI.Authorization.Checks.BindingAccess do
@moduledoc """
Ash policy check that routes binding authorization through `BindingPolicy`.
"""

use Ash.Policy.SimpleCheck

alias AshUI.Authorization.BindingPolicy
alias AshUI.Authorization.Subject

@impl true
@doc """
Describes the binding access mode being evaluated.
"""
def describe(opts), do: "binding #{Keyword.get(opts, :mode, :read)} access"

@impl true
@doc """
Evaluates binding access for the supplied actor and policy subject.
"""
def match?(actor, %{subject: subject}, opts) do
binding = Subject.to_data(subject)

allowed =
case Keyword.get(opts, :mode, :read) do
:read -> BindingPolicy.can_read?(actor, binding)
:manage -> BindingPolicy.can_manage?(actor, binding)
_ -> false
end

{:ok, allowed}
end
end
33 changes: 33 additions & 0 deletions lib/ash_ui/authorization/checks/element_access.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
defmodule AshUI.Authorization.Checks.ElementAccess do
@moduledoc """
Ash policy check that routes element authorization through `ElementPolicy`.
"""

use Ash.Policy.SimpleCheck

alias AshUI.Authorization.ElementPolicy
alias AshUI.Authorization.Subject

@impl true
@doc """
Describes the element access mode being evaluated.
"""
def describe(opts), do: "element #{Keyword.get(opts, :mode, :read)} access"

@impl true
@doc """
Evaluates element access for the supplied actor and policy subject.
"""
def match?(actor, %{subject: subject}, opts) do
element = Subject.to_data(subject)

allowed =
case Keyword.get(opts, :mode, :read) do
:read -> ElementPolicy.can_read?(actor, element)
:manage -> ElementPolicy.can_manage?(actor, element)
_ -> false
end

{:ok, allowed}
end
end
34 changes: 34 additions & 0 deletions lib/ash_ui/authorization/checks/screen_access.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
defmodule AshUI.Authorization.Checks.ScreenAccess do
@moduledoc """
Ash policy check that routes screen authorization through `ScreenPolicy`.
"""

use Ash.Policy.SimpleCheck

alias AshUI.Authorization.ScreenPolicy
alias AshUI.Authorization.Subject

@impl true
@doc """
Describes the screen access mode being evaluated.
"""
def describe(opts), do: "screen #{Keyword.get(opts, :mode, :read)} access"

@impl true
@doc """
Evaluates screen access for the supplied actor and policy subject.
"""
def match?(actor, %{subject: subject}, opts) do
screen = Subject.to_data(subject)

allowed =
case Keyword.get(opts, :mode, :read) do
:mount -> ScreenPolicy.can_mount?(actor, screen)
:read -> ScreenPolicy.can_read?(actor, screen)
:manage -> ScreenPolicy.can_manage?(actor, screen)
_ -> false
end

{:ok, allowed}
end
end
Loading
Loading