Skip to content

Commit

Permalink
Task/redacting fallback (#16)
Browse files Browse the repository at this point in the history
* [fix] add fallbacks for center redacter

* [change] rename old 'mask' conceptù

* [add] allow fallback value in base redacters

* [change] update documentation

* [add] tests for custom fallback fixed value

* [change] total renaming of endemic typo
  • Loading branch information
zoten authored Oct 19, 2022
1 parent f20ff48 commit f1f45c6
Show file tree
Hide file tree
Showing 20 changed files with 152 additions and 131 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ to your understanding of the system, and at the same moment it does not expose
## Usage

See [RedactEx](./lib/redact_ex.ex) for general information about this library and its intended use
See [RedactEx.Redacter](./lib/redact_ex/redacter.ex) for generating fast redacters for strings
See [RedactEx.Redactor](./lib/redact_ex/redactor.ex) for generating fast redactors for strings
See [RedactEx.Redactable](./lib/redact_ex/redactable.ex) protocol for deriving redactable structs

## Contributing
Expand All @@ -40,7 +40,7 @@ cp .tool-versions.recommended .tool-versions

## Open Points

* Other ways of masking (e.g. first X letters and last Y exposed)
* maybe-email masker (with fallback)?
* Other ways of redacting (e.g. first X letters and last Y exposed)
* maybe-email redactor (with fallback)?
* define a default strict implementation for `redact` in the `Any` implementation?
* some other default magic configuration in derive, e.g. a default module+function?
16 changes: 8 additions & 8 deletions lib/redact_ex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule RedactEx do
RedactEx provides protocols and derivatoin utilities to work with
sensitive data that should be redacted in logs and external facing tools
* `RedactEx.Redacter` module gives you automagical generation of redacting
* `RedactEx.Redactor` module gives you automagical generation of redacting
functions under a desired module namespace
* `RedactEx.Redactable` is the protocol for which implementation and
derivation should be created to ensure the best possible practices
Expand All @@ -13,13 +13,13 @@ defmodule RedactEx do
RedactEx usual protection consists in two or more steps:
## Generate your redacter functions
## Generate your redactor functions
You can generate functions to redact your specific data using `RedactEx.Redacter` module.
You can generate functions to redact your specific data using `RedactEx.Redactor` module.
iex> defmodule MyApp.Redacting do
...> @moduledoc false
...> use RedactEx.Redacter, redacters: [
...> use RedactEx.Redactor, redactors: [
...> {"redact_three", length: 3, algorithm: :simple},
...> {"redact", lengths: 1..3, algorithm: :simple}
...> ]
Expand All @@ -42,17 +42,17 @@ defmodule RedactEx do
You can use the `@derive` macro from the `RedactEx.Redactable` protocol,
configured based on your needs and optionally using functions from a module generated with
`RedactEx.Redacter` helpers, or manually define an implementation of choice, e.g.
`RedactEx.Redactor` helpers, or manually define an implementation of choice, e.g.
iex> defmodule MyRedacterModule do
iex> defmodule MyRedactorModule do
...> def redact_function_one(_), do: "(redacted1)"
...> def redact_function_two(_), do: "(redacted2)"
...> end
...> defmodule MyApp.RedactStruct do
...> @derive {RedactEx.Redactable,
...> fields: [
...> myfield1: {MyRedacterModule, :redact_function_one},
...> myfield2: {MyRedacterModule, :redact_function_two},
...> myfield1: {MyRedactorModule, :redact_function_one},
...> myfield2: {MyRedactorModule, :redact_function_two},
...> ]}
...> defstruct [:myfield1, :myfield2]
...> end
Expand Down
17 changes: 11 additions & 6 deletions lib/redact_ex/algorithms/center.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,39 @@ defmodule RedactEx.Algorithms.Center do
"""
@behaviour RedactEx.Algorithms.Algorithm

alias RedactEx.Configuration.Context

@impl RedactEx.Algorithms.Algorithm
def generate_ast(%{
def generate_ast(%Context{
plaintext_length: nil,
redacted_length: nil,
keep: keep,
name: name,
redacter: redacter
redactor: redactor,
fallback_value: fallback_value
}) do
quote do
def unquote(name)(value) do
def unquote(name)(value) when is_binary(value) do
string_length = String.length(value)
keep_size = string_length * unquote(keep) / 100 / 2
head_size = ceil(keep_size)
tail_size = floor(keep_size)
center_size = max(string_length - head_size - tail_size, 0)
center_content = for _ <- 1..center_size, do: unquote(redacter), into: ""
center_content = for _ <- 1..center_size, do: unquote(redactor), into: ""

if center_size <= 0 do
unquote(redacter)
unquote(redactor)
else
String.slice(value, 0..(head_size - 1)) <>
center_content <> String.slice(value, (string_length - tail_size)..string_length)
end
end

def unquote(name)(_value), do: unquote(fallback_value)
end
end

def generate_ast(%{
def generate_ast(%Context{
plaintext_length: plaintext_length,
redacted_length: redacted_length,
name: name,
Expand Down
11 changes: 6 additions & 5 deletions lib/redact_ex/algorithms/simple.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ defmodule RedactEx.Algorithms.Simple do
length: :*,
keep: keep,
name: name,
redacter: redacter,
redacted_size: redacted_size
redactor: redactor,
redacted_size: redacted_size,
fallback_value: fallback_value
}) do
quote do
def unquote(name)(value) when is_binary(value) do
Expand All @@ -31,7 +32,7 @@ defmodule RedactEx.Algorithms.Simple do
unquote(redacted_size)
)

redacted = Context.get_redacter_string(redacted_length, unquote(redacter))
redacted = Context.get_redactor_string(redacted_length, unquote(redactor))

case max(0, plaintext_length - 1) do
0 ->
Expand All @@ -42,11 +43,11 @@ defmodule RedactEx.Algorithms.Simple do
end
end

def unquote(name)(_value), do: "(fully redacted as not a string)"
def unquote(name)(_value), do: unquote(fallback_value)
end
end

def generate_ast(%{
def generate_ast(%Context{
plaintext_length: plaintext_length,
redacted_length: redacted_length,
name: name,
Expand Down
42 changes: 23 additions & 19 deletions lib/redact_ex/configuration.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@ defmodule RedactEx.Configuration do

@default_redacting_keep 25
@default_redacted_size :auto
@default_redacter "*"
@default_redactor "*"
@default_redacting_algorithm RedactEx.Algorithms.algorithm_simple()
@default_lengths [:*]
@default_except []
@default_fallback_value "(redacted)"

@default_configuration [
keep: @default_redacting_keep,
redacted_size: @default_redacted_size,
redacter: @default_redacter,
redactor: @default_redactor,
algorithm: @default_redacting_algorithm,
lengths: @default_lengths,
except: @default_except
except: @default_except,
fallback_value: @default_fallback_value
]

alias RedactEx.Algorithms
Expand All @@ -32,63 +34,64 @@ defmodule RedactEx.Configuration do
def parse(configuration, current_env, macro_env) when is_list(configuration),
do:
configuration
|> Keyword.fetch!(:redacters)
|> Enum.flat_map(&parse_redacter(&1, macro_env))
|> Keyword.fetch!(:redactors)
|> Enum.flat_map(&parse_redactor(&1, macro_env))
|> reject_by_env(current_env)
|> Enum.group_by(fn %{name: name} = _config -> name end)
|> Enum.map(&add_fallback_redacter!(&1, macro_env))
|> Enum.map(&add_fallback_redactor!(&1, macro_env))
|> Enum.into(%{})

defp reject_by_env(redacters, current_env),
defp reject_by_env(redactors, current_env),
do:
Enum.reject(redacters, fn %Context{except: refute_envs} ->
Enum.reject(redactors, fn %Context{except: refute_envs} ->
current_env in refute_envs
end)

defp parse_redacter({aliases, config}, macro_env) when is_list(aliases),
defp parse_redactor({aliases, config}, macro_env) when is_list(aliases),
do: Enum.flat_map(aliases, &map_lengths_and_parse(&1, config, macro_env))

# Single name: all defaults apply
# Name will be normalized later
defp parse_redacter(name, macro_env) when is_atom(name) or is_binary(name),
defp parse_redactor(name, macro_env) when is_atom(name) or is_binary(name),
do: map_lengths_and_parse(name, @default_configuration, macro_env)

defp parse_redacter({name, config}, macro_env),
defp parse_redactor({name, config}, macro_env),
do: map_lengths_and_parse(name, config, macro_env)

defp map_lengths_and_parse(name, config, macro_env) do
config
|> get_lengths_from_config(name)
|> Enum.map(&do_parse_redacter(&1, name, config, macro_env))
|> Enum.map(&do_parse_redactor(&1, name, config, macro_env))
end

defp do_parse_redacter(string_length, given_name, given_config, macro_env)
defp do_parse_redactor(string_length, given_name, given_config, macro_env)
when is_integer(string_length) or string_length == :* do
config = Keyword.merge(@default_configuration, given_config)

except = Keyword.fetch!(config, :except)
keep = Keyword.fetch!(config, :keep)
redacted_size = Keyword.fetch!(config, :redacted_size)
fallback_value = Keyword.fetch!(config, :fallback_value)
needs_fallback_function = needs_fallback_function?(string_length)

algorithm =
config |> Keyword.fetch!(:algorithm) |> Macro.expand(macro_env) |> validate_algorithm()

name = alias_name(given_name)

redacter = Keyword.fetch!(config, :redacter)
redactor = Keyword.fetch!(config, :redactor)

{plaintext_length, redacted_length} =
Context.get_plaintext_length_redacted_length(string_length, keep, redacted_size)

redacted = Context.get_redacter_string(redacted_length, redacter)
redacted = Context.get_redactor_string(redacted_length, redactor)

extra = algorithm.parse_extra_configuration!(config)

%Context{
redacted_size: redacted_size,
length: string_length,
redacter: redacter,
redactor: redactor,
keep: keep,
plaintext_length: plaintext_length,
redacted_length: redacted_length,
Expand All @@ -98,7 +101,8 @@ defmodule RedactEx.Configuration do
algorithm: algorithm,
needs_fallback_function: needs_fallback_function,
extra: extra,
except: except
except: except,
fallback_value: fallback_value
}
end

Expand Down Expand Up @@ -143,12 +147,12 @@ defmodule RedactEx.Configuration do
end
end

defp add_fallback_redacter!({key, contexts}, macro_env) do
defp add_fallback_redactor!({key, contexts}, macro_env) do
case Enum.split_with(contexts, fn %Context{length: len} -> len != :* end) do
{non_fallback_contexts, []} ->
{key,
Enum.concat(non_fallback_contexts, [
do_parse_redacter(:*, key, @default_configuration, macro_env)
do_parse_redactor(:*, key, @default_configuration, macro_env)
])}

{non_fallback_contexts, [fallback_context]} ->
Expand Down
11 changes: 6 additions & 5 deletions lib/redact_ex/configuration/context.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule RedactEx.Configuration.Context do
alias __MODULE__

defstruct length: :*,
redacter: nil,
redactor: nil,
keep: nil,
plaintext_length: nil,
redacted_size: nil,
Expand All @@ -16,6 +16,7 @@ defmodule RedactEx.Configuration.Context do
string_length: nil,
algorithm: nil,
needs_fallback_function: nil,
fallback_value: nil,
extra: nil,
except: []

Expand All @@ -42,12 +43,12 @@ defmodule RedactEx.Configuration.Context do
@doc """
Gets the actual redacted part of the string given configuration
"""
@spec get_redacter_string(size :: nil | integer(), redacter :: char() | any()) ::
@spec get_redactor_string(size :: nil | integer(), redactor :: char() | any()) ::
nil | String.t()
def get_redacter_string(nil, _redacter), do: nil
def get_redactor_string(nil, _redactor), do: nil

def get_redacter_string(size, redacter) do
1..size |> Enum.map(fn _ -> redacter end) |> List.to_string()
def get_redactor_string(size, redactor) do
1..size |> Enum.map(fn _ -> redactor end) |> List.to_string()
end

# Get the length of the redacted part of data
Expand Down
16 changes: 8 additions & 8 deletions lib/redact_ex/redactable.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defprotocol RedactEx.Redactable do
@moduledoc """
# Redactable
Protocol for defining a redact-able item, e.g. an item whose internal elements could be masked
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
Expand Down Expand Up @@ -98,7 +98,7 @@ defimpl RedactEx.Redactable, for: Any do
fields = fields_to_redact(struct, opts)

case fields do
{redacter_module, function} -> redact_all_ast(module, redacter_module, function)
{redactor_module, function} -> redact_all_ast(module, redactor_module, function)
fields when is_list(fields) -> redact_ast(module, fields)
end
end
Expand All @@ -110,11 +110,11 @@ defimpl RedactEx.Redactable, for: Any do
description: "RedactEx.Redactable protocol must always be explicitly implemented"
end

defp redact_all_ast(module, redacter_module, function) do
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(redacter_module), unquote(function), [value])
:erlang.apply(unquote(redactor_module), unquote(function), [value])
end
end
end
Expand Down Expand Up @@ -162,10 +162,10 @@ defimpl RedactEx.Redactable, for: Any do
{_, action} when action in [:redact, :drop] ->
true

{_, {redacter_module, redacter_function}} ->
Code.ensure_loaded!(redacter_module)
# true = Module.defines?(redacter_module, {redacter_function, 1})
true = Kernel.function_exported?(redacter_module, redacter_function, 1)
{_, {redactor_module, redactor_function}} ->
Code.ensure_loaded!(redactor_module)
# true = Module.defines?(redactor_module, {redactor_function, 1})
true = Kernel.function_exported?(redactor_module, redactor_function, 1)
end)
end

Expand Down
Loading

0 comments on commit f1f45c6

Please sign in to comment.