Packages

Utilities and best practices to redact potentially sensitive data

Current section

Files

Jump to
redact_ex lib redact_ex redactable.ex
Raw

lib/redact_ex/redactable.ex

defprotocol RedactEx.Redactable do
@moduledoc """
# Redactable
Protocol for defining a redact-able item, e.g. an item whose internal elements could be redacted
It shall return a redact-ed item of the same type as the input item
You can derive Redactable protocol for a struct by using
``` elixir
defmodule MyApp.RedactStruct do
@derive {RedactEx.Redactable,
fields: [
myfield1: {MyModule, :redact_function_one},
myfield2: {MyModule, :redact_function_two},
]}
defstruct [:myfield1, :myfield2]
end
```
options
* `:fields` possible values of `fields` are a keyword list of configurations, where configuration can be
* `{module, function}`, e.g.
``` elixir
fields: [
myfield1: {MyModule, :redact_function_one},
myfield2: {MyModule, :redact_function_two},
]
```
In this case redacting will be applied to `myfield1` and `myfield2` using module.function as configured
* `:redact`, e.g.
``` elixir
fields: [
field: :redact
]
```
In this case it is expected for value in `field` to have an Impl for `RedactEx.Redactable`, and `RedactEx.Redactable.redact/1` will be called
* `:drop`, e.g.
``` elixir
fields: [
field: :drop
]
```
In this case `:field` key is entirely dropped. **NOTE** that this also means
struct information will be lost, and the value will become a map
e.g. suppose you define a module like
```
# ./test/support/derive/redact_struct.ex
defmodule MyApp.RedactStruct do
@moduledoc false
# For usage in RedactEx.Redactable doctests
@derive {RedactEx.Redactable,
fields: [
myfield: {String, :reverse}
]}
defstruct [:myfield]
end
```
then you can expect it to have derived the `RedactEx.Redactable` protocol
```
iex> %MyApp.RedactStruct{myfield: "reverseme"} |> RedactEx.Redactable.redact() |> Map.get(:myfield)
"emesrever"
```
## Best Practices
Good practices could be to implement the Inspect protocol for sensitive data and use the derived `redact`
capabilities of the inspected module inside its implementation.
You can both `derive` single fields in your struct, or define whatever implementation fits best
for your structs as `defimpl RedactEx.Redactable, for: MyStruct`
Then you can enforce `inspect`ing structures in your logs or export functions. Unluckily this is not a simple practice
to enforce in an automatic flavour.
"""
@fallback_to_any true
@type container :: struct() | map()
@type binary_container :: String.t() | binary()
@type any_container :: any()
@type t :: container() | binary_container() | any_container()
@dialyzer {:nowarn_function, redact: 1}
@doc """
Redacts `value` hiding sensitive information
"""
@spec redact(value :: t()) :: any()
def redact(value)
end
defimpl RedactEx.Redactable, for: Any do
# inspired by Jason.Encoder protocol implementation https://github.com/michalmuskala/jason/blob/e860f7c6a99be17b32ec7f8862d779a7bdddbe2b/lib/encoder.ex
defmacro __deriving__(module, struct, opts) do
fields = fields_to_redact(struct, opts)
case fields do
{redactor_module, function} -> redact_all_ast(module, redactor_module, function)
fields when is_list(fields) -> redact_ast(module, fields)
end
end
def redact(value) do
raise Protocol.UndefinedError,
protocol: @protocol,
value: value,
description: "RedactEx.Redactable protocol must always be explicitly implemented"
end
defp redact_all_ast(module, redactor_module, function) do
quote do
defimpl RedactEx.Redactable, for: unquote(module) do
def redact(%_{} = value) do
:erlang.apply(unquote(redactor_module), unquote(function), [value])
end
end
end
end
defp redact_ast(module, fields) do
quote do
defimpl RedactEx.Redactable, for: unquote(module) do
def redact(%_{} = value) do
Enum.reduce(
unquote(fields),
value,
fn
{key, :drop}, acc ->
acc |> Map.from_struct() |> Map.drop([key])
{key, :redact}, acc ->
Map.update!(acc, key, &RedactEx.Redactable.redact/1)
{key, {module, function}}, acc ->
Map.update!(acc, key, fn val -> :erlang.apply(module, function, [val]) end)
end
)
end
end
end
end
defp fields_to_redact(struct, opts) do
struct_fields = Map.keys(struct)
fields_to_redact = Keyword.fetch!(opts, :fields)
:ok = check_fields_to_redact!(struct_fields, fields_to_redact)
:ok = check_fields_to_redact_configuration!(fields_to_redact)
fields_to_redact
end
defp keys_from_opts(opts) do
Enum.map(opts, fn {key, _config} -> key end)
end
defp check_fields_to_redact_configuration!(fields_to_redact) do
Enum.each(fields_to_redact, fn
{_, action} when action in [:redact, :drop] ->
true
{_, {redactor_module, redactor_function}} ->
Code.ensure_loaded!(redactor_module)
# true = Module.defines?(redactor_module, {redactor_function, 1})
function_exported!(redactor_module, redactor_function, 1)
end)
end
defp function_exported!(module, function, arity) do
if Kernel.function_exported?(module, function, arity) == true do
:ok
else
raise "Module [#{inspect(module)}] does not implement [#{inspect(function)}/#{inspect(arity)}]"
end
end
# redact all fields
defp check_fields_to_redact!(_struct_fields, :redact), do: :ok
defp check_fields_to_redact!(struct_fields, fields_to_redact) do
case keys_from_opts(fields_to_redact) -- struct_fields do
[] ->
:ok
error_keys ->
raise ArgumentError,
"`:fields` specified keys (#{inspect(error_keys)}) that are not defined in defstruct: " <>
"#{inspect(struct_fields -- [:__struct__])}"
end
end
end