= attr_masker
:source-highlighter: pygments
:pygments-style: native
:pygments-linenums-mode: inline

image:https://img.shields.io/gem/v/attr_masker.svg["Gem Version", link="https://rubygems.org/gems/attr_masker"]
image:https://img.shields.io/travis/riboseinc/attr_masker/master.svg["Build Status", link="https://travis-ci.org/riboseinc/attr_masker"]

Mask ActiveRecord data with ease!

== Introduction

This gem is intended to mask sensitive data so that production database dumps
can be used in staging or test environments.  It works with Active Record 4+
and modern Rubies.

== Getting started

=== Installation

Add attr_masker to your gemfile:

[source,ruby]
----
  gem "attr_masker", github: "riboseinc/attr_masker"
----


Then install the gem:

[source,sh]
----
  bundle install
----

=== Basic usage

In your models, define attributes which should be masked:

[source,ruby]
----
  class User
    attr_masker :email, :first_name, :last_name
  end
----

Then, when you want to mask the data, run the `db:mask` Rake task in some
Rails environment other than production, for example:

[source,sh]
----
  bundle exec rake db:mask RAILS_ENV=staging
----

WARNING: Data are destructively overwritten.  Run `rake db:mask` with care!

=== Masking records selectively

You can use `:if` and `:unless` options to prevent some records from being
altered.

[source,ruby]
----
# evaluates given proc for each record, and the record is passed as a proc's
# argument
attr_masker :email :unless => ->(record) { ! record.tester_user? }

# calls #tester_user? method on each record
attr_masker :first_name, :if => :tester_user?
----

The ActiveRecord's `::default_scope` method has no effect on masking.  All
table records are updated, provided that :if and :unless filters allow that.
For example, if you're using a Paranoia[https://github.com/rubysherpas/paranoia]
gem to soft-delete your data, records marked as deleted will be masked as well.

=== Using custom maskers

By default, data is maksed with `AttrMasker::Maskers::SIMPLE` masker which
always returns `"(redacted)"` string.  But anything what responds to `#call`
can be used instead: a lambda, `Method` instance, and more.  You can specify it
by setting the `:masker` option.

For instance, you may want to use https://github.com/ffaker/ffaker[ffaker] or
https://github.com/skalee/well_read_faker[Well Read Faker] to generate random
replacement values:

[source,ruby]
----
require "ffaker"

attr_masker :first_name, :masker => ->(_hash) { FFaker::Name.first_name }
----

A hash is passed as an argument, which includes some information about the
record being masked and the attribute value.  It can be used to further
customize masker's behaviour.

=== Built-in maskers

Attr Masker comes with several built-in maskers.

`AttrMasker::Maskers::SIMPLE`::
+
Simply replaces any value with the `"(redacted)"`.  Only useful for columns
containing textual data.
+
This is a default masker.  It is used when `:masker` option is unspecified.
+
Example:
+
[source,ruby]
----
attr_masker :first_name
attr_masker :last_name, :masker => AttrMasker::Maskers::SIMPLE
----
+
Would set both `first_name` and `last_name` attributes to `"(redacted)"`.

`AttrMasker::Maskers::Replacing`::
+
Replaces characters with some masker string (single asterisk by default).
Can be initialized with options.
+
[options="header"]
|===============================================================================
|Name|Default|Description
|`replacement`|`"*"`|Replacement string, can be empty.
|`alphanum_only`|`false`|When true, only alphanumeric charaters are replaced.
|===============================================================================
+
Example:
+
[source,ruby]
----
rm = AttrMasker::Maskers::Replacing.new(character: "X", alphanum_only: true)
attr_masker :phone, :masker => rm
----
+
Would mask "123-456-7890" as "XXX-XXX-XXXX".

== Roadmap & TODOs
- documentation
- spec tests
- Make the `Rails.env` (in which `db:mask` could be run) configurable
** maybe by passing `ENV` vars
- more masking options!
** default scrambling algorithms?
** structured text preserving algorithms
*** _e.g._, keeping an HTML snippet valid HTML, but with masked inner text
** structured *Object* preserving algorithms
*** _i.e._ generalization of the above HTML scenario
- I18n of the default `"(redacted)"` phrase
- …

== Acknowledgements

https://github.com/attr-encrypted/attr_encrypted[attr_encrypted] for the initial
code structure