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
208 changes: 208 additions & 0 deletions lib/ash_ui/authorization/binding_policy.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
defmodule AshUI.Authorization.BindingPolicy do
@moduledoc """
Policy definitions for AshUI.Binding resource.

Defines access control for binding evaluation and modification.
"""

alias AshUI.Authorization.Policies

@doc """
Defines policies for binding resource access.
"""
def policies do
[
# Read/evaluation policy - bindings inherit from parent
%Ash.Policy.Policy{
description: "Bindings are evaluable if parent screen is accessible",
policies: [
Ash.Policy.Authorizer.expr(
Policies.user_active(@actor) and
can_access_binding?(@actor, @resource)
)
]
},

# Create policy - inherit from screen
%Ash.Policy.Policy{
description: "Can create bindings if can modify parent screen",
policies: [
Ash.Policy.Authorizer.expr(
Policies.user_role(@actor, :admin) or
(Policies.user_active(@actor) and
screen_owned?(@actor, @resource))
)
]
},

# Update policy - inherit from screen
%Ash.Policy.Policy{
description: "Can update bindings if can modify parent screen",
policies: [
Ash.Policy.Authorizer.expr(
Policies.user_role(@actor, :admin) or
(Policies.user_active(@actor) and
screen_owned?(@actor, @resource))
)
]
},

# Destroy policy - inherit from screen
%Ash.Policy.Policy{
description: "Can delete bindings if can modify parent screen",
policies: [
Ash.Policy.Authorizer.expr(
Policies.user_role(@actor, :admin) or
(Policies.user_active(@actor) and
screen_owned?(@actor, @resource))
)
]
},

# Data source access policy
%Ash.Policy.Policy{
description: "Must have access to binding source data",
policies: [
Ash.Policy.Authorizer.expr(
has_data_access?(@resource, @actor)
)
]
},

# Development environment bypass
%Ash.Policy.Policy{
description: "Development environment bypass",
policies: [
Ash.Policy.Authorizer.expr(
Policies.environment([:dev, :test])
)
]
}
]
end

@doc """
Check if user can evaluate a binding.
"""
def can_evaluate?(user, binding) do
cond do
# Development bypass
Policies.environment([:dev, :test]) -> true

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

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

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

# Default allow
true -> true
end
end

@doc """
Check if user can write to a binding.
"""
def can_write?(user, binding) do
cond do
# Development bypass
Policies.environment([:dev, :test]) -> true

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

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

# Check if binding is read-only
Map.get(binding, :read_only, false) -> false

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

# Default allow
true -> true
end
end

@doc """
Get redacted value for binding if user is unauthorized.

Returns a placeholder value instead of actual data.
"""
def redacted_value(binding) do
case Map.get(binding, :binding_type) do
:value -> "[PROTECTED]"
:list -> []
:action -> nil
_ -> nil
end
end

@doc """
Check if binding source resource is accessible.
"""
def source_accessible?(user, binding) do
source = Map.get(binding, :source, %{})

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 can_access_binding?(user, binding) do
# In production, would check parent screen access
Policies.user_active(user)
end

defp screen_owned?(user, binding) do
# In production, would check parent screen ownership
true
end

defp has_data_access?(binding, user) do
source = Map.get(binding, :source, %{})

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

defp has_write_access?(binding, user) do
source = Map.get(binding, :source, %{})

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

defp field_accessible?(user, binding) do
source = Map.get(binding, :source, %{})
field = Map.get(source, "field")

case field do
nil -> true
_ -> Policies.can_access_field(binding, field)
end
end
end
168 changes: 168 additions & 0 deletions lib/ash_ui/authorization/element_policy.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
defmodule AshUI.Authorization.ElementPolicy do
@moduledoc """
Policy definitions for AshUI.Element resource.

Defines access control for element visibility and modification.
"""

alias AshUI.Authorization.Policies

@doc """
Defines policies for element resource access.
"""
def policies do
[
# Read/visibility policy - elements inherit screen policies
%Ash.Policy.Policy{
description: "Elements are visible if parent screen is accessible",
policies: [
Ash.Policy.Authorizer.expr(
# Can see element if can access parent screen
Policies.user_active(@actor) and
screen_accessible?(@actor, @resource)
)
]
},

# Create policy - inherit from screen
%Ash.Policy.Policy{
description: "Can create elements if can modify parent screen",
policies: [
Ash.Policy.Authorizer.expr(
Policies.user_role(@actor, :admin) or
(Policies.user_active(@actor) and
screen_owned?(@actor, @resource))
)
]
},

# Update policy - inherit from screen
%Ash.Policy.Policy{
description: "Can update elements if can modify parent screen",
policies: [
Ash.Policy.Authorizer.expr(
Policies.user_role(@actor, :admin) or
(Policies.user_active(@actor) and
screen_owned?(@actor, @resource))
)
]
},

# Destroy policy - inherit from screen
%Ash.Policy.Policy{
description: "Can delete elements if can modify parent screen",
policies: [
Ash.Policy.Authorizer.expr(
Policies.user_role(@actor, :admin) or
(Policies.user_active(@actor) and
screen_owned?(@actor, @resource))
)
]
},

# Element-specific visibility policies
%Ash.Policy.Policy{
description: "Respects element visibility conditions",
policies: [
Ash.Policy.Authorizer.expr(
# Element is visible if condition is met or no condition
element_visible?(@resource)
)
]
},

# Development environment bypass
%Ash.Policy.Policy{
description: "Development environment bypass",
policies: [
Ash.Policy.Authorizer.expr(
Policies.environment([:dev, :test])
)
]
}
]
end

@doc """
Check if element should be visible to user.
"""
def visible?(user, element) do
cond do
# Development bypass
Policies.environment([:dev, :test]) -> true

# Admins see all elements
Policies.user_role(user, :admin) -> true

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

# Check element visibility conditions
not meets_visibility_condition?(element, user) -> false

# Check parent screen access
not screen_accessible?(user, element) -> false

# Default visible
true -> true
end
end

@doc """
Check if element is editable by user.
"""
def editable?(user, element) do
cond do
# Development bypass
Policies.environment([:dev, :test]) -> true

# Admins can edit all elements
Policies.user_role(user, :admin) -> true

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

# Check if element is explicitly read-only
Map.get(element, :read_only, false) -> false

# Must own parent screen
not screen_owned?(user, element) -> false

# Default editable
true -> true
end
end

# Private functions

defp screen_accessible?(user, element) do
# In production, would check if user can access parent screen
true
end

defp screen_owned?(user, element) do
# In production, would check if user owns parent screen
true
end

defp element_visible?(element) do
# Check if element has visibility condition
case Map.get(element, :visible_when) do
nil -> true
condition when is_function(condition, 0) -> condition.()
condition when is_boolean(condition) -> condition
_ -> true
end
end

defp meets_visibility_condition?(element, user) do
case Map.get(element, :visible_when) do
nil -> true
{field, value} ->
# Check user field matches required value
Map.get(user, field) == value
condition when is_function(condition, 1) -> condition.(user)
_ -> true
end
end
end
Loading
Loading