Packages
phoenix_gen_api
2.20.1
2.22.0
2.21.1
2.21.0
2.20.1
2.20.0
2.19.0
2.18.1
2.18.0
2.17.1
2.17.0
2.16.0
2.15.0
2.14.0
2.13.0
2.12.0
2.11.0
2.10.0
2.9.1
2.9.0
2.8.0
2.7.0
2.6.1
2.6.0
2.5.0
2.4.0
2.3.0
2.2.0
2.1.1
2.1.0
2.0.1
2.0.0
1.2.0
1.1.3
1.1.2
1.1.1
1.1.0
1.0.1
1.0.0
0.2.1
0.2.0
0.1.1
0.1.0
0.0.16
0.0.15
0.0.14
0.0.13
0.0.12
0.0.11
0.0.10
0.0.9
0.0.8
0.0.7
0.0.6
0.0.5
0.0.4
0.0.3
0.0.2
0.0.1
A library for fast develop APIs in Elixir cluster, using Phoenix Channels for transport data, auto pull api configs from service nodes.
Current section
Files
Jump to
Current section
Files
lib/phoenix_gen_api/security.ex
defmodule PhoenixGenApi.Security do
@moduledoc """
Provides security utilities for PhoenixGenApi.
## Features
1. **Admin gate** — fail-closed authorization for dangerous runtime operations
(toggling `detail_error`, updating rate limit config, pushing configs).
2. **Push token validation** — constant-time comparison to authenticate
push requests from remote nodes.
3. **MFA allowlist validation** — restricts which `{module, function, args}`
tuples can be registered as function configurations, preventing a
compromised node from registering dangerous MFAs (e.g. `:os.cmd`).
## Configuration
All checks are **opt-in and backward compatible**. If the relevant application
environment variables are not set, the checks are skipped:
config :phoenix_gen_api,
# Admin actions allowlist (fail-closed: default denies everything)
admin_actions: [:push_config],
# Push token — when set, push requests must include a matching token
push_token: "my-secret-token",
# MFA allowlist — when set, only listed {module, function} pairs are allowed.
# Module-level entries (just an atom) allow all functions in that module.
mfa_allowlist: [
MyApp.UserService,
{MyApp.OrderService, :create_order}
]
## Hardcoded Denylist
The following modules are **always blocked** unless explicitly allowed:
`:os`, `:file`, `:code`, `:erlang`, `:net`, `:rpc`, `:global`, `:inet`.
## Environment Recommendations
- **Development**: You may enable all admin actions for convenience.
- **Production**: Enable only what's needed. Configure `push_token` and
`mfa_allowlist` to restrict push sources and allowed function targets.
"""
import Bitwise
require Logger
@type admin_action ::
:change_detail_error
| :update_rate_limit_config
| :push_config
| :enable_tracing
| :disable_tracing
# ---------------------------------------------------------------------------
# Admin Gate
# ---------------------------------------------------------------------------
@doc """
Checks whether a given admin action is currently permitted.
Returns `true` if the action is in the configured allowlist, `false` otherwise.
When denied, a warning is logged.
## Examples
iex> PhoenixGenApi.Security.admin_action_allowed?(:update_rate_limit_config)
false
iex> PhoenixGenApi.Security.admin_action_allowed?(:change_detail_error)
false
"""
@spec admin_action_allowed?(admin_action()) :: boolean()
def admin_action_allowed?(action)
when action in [
:change_detail_error,
:update_rate_limit_config,
:push_config,
:enable_tracing,
:disable_tracing
] do
allowed = Application.get_env(:phoenix_gen_api, :admin_actions, [])
if action in allowed do
Logger.info("[Security] admin action allowed: #{action}")
true
else
Logger.warning("[Security] admin action denied: #{action}, not in allowlist")
false
end
end
# ---------------------------------------------------------------------------
# Push Token Validation
# ---------------------------------------------------------------------------
@doc """
Validates a push token using constant-time comparison.
Returns `true` if:
- No `:push_token` is configured (backward compatible — push allowed without token)
- The provided token matches the configured token
Returns `false` if a token is configured but the provided token doesn't match
or is missing.
Uses constant-time comparison to prevent timing attacks.
"""
@spec valid_push_token?(nil | String.t() | binary()) :: boolean()
def valid_push_token?(nil) do
case Application.get_env(:phoenix_gen_api, :push_token) do
nil -> true
_configured -> false
end
end
def valid_push_token?(token) when is_binary(token) do
case Application.get_env(:phoenix_gen_api, :push_token) do
nil ->
true
configured when is_binary(configured) ->
constant_time_compare(token, configured)
_ ->
false
end
end
def valid_push_token?(_), do: false
# Constant-time binary comparison to prevent timing attacks.
# Compares length first, then all bytes regardless of early mismatch.
defp constant_time_compare(a, b) when is_binary(a) and is_binary(b) do
if byte_size(a) == byte_size(b) do
constant_time_compare_bin(a, b, 0) == 0
else
# Still do a dummy comparison to avoid leaking length info via timing
:crypto.hash(:sha256, a)
:crypto.hash(:sha256, b)
false
end
end
defp constant_time_compare_bin(<<>>, <<>>, acc), do: acc
defp constant_time_compare_bin(<<x, rest_a::binary>>, <<y, rest_b::binary>>, acc) do
constant_time_compare_bin(rest_a, rest_b, acc ||| Bitwise.bxor(x, y))
end
# ---------------------------------------------------------------------------
# MFA Allowlist Validation
# ---------------------------------------------------------------------------
@hardcoded_denylist [
:os,
:file,
:code,
:erlang,
:net,
:rpc,
:global,
:inet
]
@doc """
Validates that an MFA tuple is allowed by the configured allowlist and not
in the hardcoded denylist.
## Parameters
- `mfa` - A `{module, function, args}` tuple
## Returns
- `:ok` if the MFA is allowed
- `{:error, {:mfa_not_allowed, mfa}}` if the MFA is not allowed
## Behavior
- If no `:mfa_allowlist` is configured, all MFAs pass the allowlist check
(backward compatible) — but the hardcoded denylist is still enforced.
- If `:mfa_allowlist` IS configured, the `{module, function}` pair must match
an entry. Entries can be:
- A module atom (e.g. `MyApp.UserService`) — allows all functions in that module
- A `{module, function}` tuple (e.g. `{MyApp.OrderService, :create_order}`)
- Modules in the hardcoded denylist (`:os`, `:file`, `:code`, `:erlang`,
`:net`, `:rpc`, `:global`, `:inet`) are ALWAYS blocked unless the
allowlist explicitly includes them.
"""
@spec validate_mfa({module(), atom(), list()}) :: :ok | {:error, {:mfa_not_allowed, term()}}
def validate_mfa({module, function, _args} = mfa) do
# Check hardcoded denylist first — always enforced
if module in @hardcoded_denylist do
Logger.warning(
"[Security] MFA denied by hardcoded denylist: module=#{inspect(module)}, function=#{inspect(function)}"
)
{:error, {:mfa_not_allowed, mfa}}
else
check_mfa_allowlist(mfa)
end
end
def validate_mfa(invalid) do
{:error, {:mfa_not_allowed, invalid}}
end
# MFA allowlist check
defp check_mfa_allowlist(mfa = {module, function, _args}) do
allowlist = Application.get_env(:phoenix_gen_api, :mfa_allowlist)
if is_nil(allowlist) do
# No allowlist configured — allow everything (backward compatible)
:ok
else
if mfa_in_allowlist?({module, function}, allowlist) do
:ok
else
Logger.warning(
"[Security] MFA not in allowlist: module=#{inspect(module)}, function=#{inspect(function)}"
)
{:error, {:mfa_not_allowed, mfa}}
end
end
end
# Check if {module, function} matches an allowlist entry
defp mfa_in_allowlist?({mod, fun}, allowlist) when is_list(allowlist) do
Enum.any?(allowlist, fn
# Module-level entry — allows all functions in that module
^mod -> true
# Specific {module, function} entry
{^mod, ^fun} -> true
# No match
_ -> false
end)
end
defp mfa_in_allowlist?(_, _), do: false
end