Current section
Files
Jump to
Current section
Files
lib/frontier/rules.ex
defmodule Frontier.Rules do
@moduledoc false
alias Frontier.Classifier
alias Frontier.Store
@doc """
Check whether a call from `caller` to `callee` is allowed.
Returns `:ok` or `{:violation, type, details}`.
"""
def check(caller, callee) do
callee_classification = Classifier.classify(callee)
caller_context = Classifier.owning_context(caller)
callee_context = Classifier.owning_context(callee)
cond do
caller == callee ->
:ok
application_module?(caller) ->
:ok
callee_classification in [:global, :ignored] ->
:ok
# Same-context calls are always allowed — before reaches checks
caller_context != nil and caller_context == callee_context ->
:ok
callee_classification == :context_root ->
check_reaches(caller_context, callee_context, caller, callee)
callee_classification == :schema ->
check_reaches(caller_context, callee_context, caller, callee)
callee_classification == :exported ->
check_reaches(caller_context, callee_context, caller, callee)
calls_enforcement_disabled?(caller_context) ->
:ok
callers_enforcement_disabled?(callee_context) ->
:ok
skip_violation?(caller_context, callee) ->
:ok
callee_classification == :internal ->
{:violation, :internal_module,
%{
caller: caller,
callee: callee,
caller_context: caller_context,
callee_context: callee_context
}}
callee_classification == :reclassified ->
# Treat as part of target context — check same-context or reaches
if caller_context == callee_context do
:ok
else
check_reaches(caller_context, callee_context, caller, callee)
end
callee_classification == :unowned ->
:ok
true ->
:ok
end
end
@doc """
Check whether a call from `caller_context` to an external package `app` is allowed.
Returns `:ok` or `{:violation, :externals_restricted, details}`.
"""
def check_external(caller_context, app) when is_atom(app) do
case Store.context(caller_context) do
nil ->
:ok
%{externals: nil} ->
:ok
%{externals: externals} ->
if app in externals do
:ok
else
{:violation, :externals_restricted,
%{
caller_context: caller_context,
app: app,
allowed_externals: externals
}}
end
end
end
defp check_reaches(caller_context, callee_context, caller, callee) do
if reaches_violation?(caller_context, callee_context) do
{:violation, :reaches_restricted,
%{
caller: caller,
callee: callee,
caller_context: caller_context,
callee_context: callee_context,
allowed_reaches: Store.context(caller_context).reaches
}}
else
:ok
end
end
defp calls_enforcement_disabled?(nil), do: false
defp calls_enforcement_disabled?(context) do
case Store.context(context) do
nil -> false
%{enforce: %{calls: false}} -> true
_ -> false
end
end
defp callers_enforcement_disabled?(nil), do: false
defp callers_enforcement_disabled?(context) do
case Store.context(context) do
nil -> false
%{enforce: %{callers: false}} -> true
_ -> false
end
end
defp skip_violation?(nil, _callee), do: false
defp skip_violation?(caller_context, callee) do
case Store.context(caller_context) do
nil -> false
%{skip_violations: skip_list} -> callee in skip_list
end
end
defp application_module?(module) do
Code.ensure_loaded?(module) &&
function_exported?(module, :start, 2) &&
Application in (module.module_info(:attributes)[:behaviour] || [])
end
defp reaches_violation?(nil, _callee_context), do: false
defp reaches_violation?(_caller_context, nil), do: false
defp reaches_violation?(caller_context, callee_context) do
case Store.context(caller_context) do
nil -> false
%{reaches: nil} -> false
%{reaches: reaches} -> callee_context not in reaches
end
end
end