redaction 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8111bc43a6fc2d4dbd6224b80664d7410bf62780c6d609b5532c9043ee3032fa
4
+ data.tar.gz: 8ab8dcfa4d70432f19c179eb77e4f9093a12b1f8261c7aad1fe0800e372bda30
5
+ SHA512:
6
+ metadata.gz: d2479196f571dfc27a21f48147d488b08140eaa4afa0821467d0d5d04b4b3f9ae3295c8db4b316d70fe7be39848de8925a6ebb84cc5533deed1c6171ddf79e14
7
+ data.tar.gz: 135321f897acb08d60cac36b3c8413ea9bd2c42280cee09bc30ac9ee999099009c7c4f8d735c128f8b1326e64d9beac1107e8d959748ea36eaba956373a9abb2
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022 Drew Bragg
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # Redaction
2
+ [![Tests](https://github.com/DRBragg/redaction/actions/workflows/ci.yml/badge.svg)](https://github.com/DRBragg/redaction/actions/workflows/ci.yml)
3
+
4
+ Easily redact your ActiveRecord Models. Great for use when you use production data in staging or dev. Simply set the redaction type of the attributes you want to redact and run via the [console](#via-the-rails-console) or the included [rake task](#via-rake-task).
5
+
6
+ `redaction` uses [Faker](https://github.com/faker-ruby/faker) under the hood to generate redacted data.
7
+
8
+ ## Installation
9
+ **NOTE:** This is currently an unreleased library very much in beta. Use at your own risk.
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem "redaction", git: "https://github.com/drbragg/redaction.git"
15
+ ```
16
+
17
+ And then execute:
18
+ ```bash
19
+ $ bundle
20
+ ```
21
+
22
+ ## Usage
23
+ ### Redacting a Model
24
+ To "redact" a models attribute add:
25
+ ```ruby
26
+ class Model < ApplicationRecord
27
+ redacts :<attribute>, with: :<redactor_type>
28
+ end
29
+ ```
30
+ `<redactor_type>` can be a symbol, proc, or custom class. See [Redactor Types](#redactor-types) for more information.
31
+
32
+ `redacts` accepts multiple attributes, provided they all use the same redactor type. i.e.:
33
+ ```ruby
34
+ class User < ApplicationRecord
35
+ redacts :first_name, :last_name, with: :name
36
+ end
37
+ ```
38
+ ### Redactor Types
39
+
40
+ #### Built in
41
+ `redaction` comes with a few different redactor types:
42
+ | Type | Generates |
43
+ |:------------:|:------------:|
44
+ | `:basic` | A Sentence |
45
+ | `:basic_html`| An HTML snippet with `strong` and `em` tags wrapping some of the words |
46
+ | `:email` | A safe (will not send) email address |
47
+ | `:html` | Multiple HTML Paragraphs with a random amount of link tags, `strong` tags, and `em` tags |
48
+ | `:name` | A person first/last name |
49
+ | `:phone` | A phone number |
50
+ | `:text` | Multiple paragraphs |
51
+
52
+ To use a built in redactor type set the `with:` option of a `redacts` call to the appropriate symbol.
53
+
54
+ #### Using a Proc
55
+ A Proc `:with` value is given two arguments: the record being redacted, and a hash with the :attribute key-value pair.
56
+ ```ruby
57
+ class Model < ApplicationRecord
58
+ redacts :attribute, with: -> (record, data) { record.id }
59
+ end
60
+ ```
61
+ would cause `Model#attribute` to be set to `Model#id` after redaction
62
+ #### Using a custom class
63
+ Add a folder in `app/`, `redactors/` is suggested, and put custom redactors in there. A custom redactor should inherit from `Redaction::Types::Base` and should define a `content` method. Like so:
64
+ ```ruby
65
+ # app/redactors/custom_redactor.rb
66
+ class CustomRedactor < Redaction::Types::Base
67
+ def content
68
+ "Some Custom Value"
69
+ end
70
+ end
71
+ ```
72
+ and then to use it:
73
+ ```ruby
74
+ class Model < ApplicationRecord
75
+ redacts :attribute, with: CustomRedactor
76
+ end
77
+ ```
78
+ would cause `Model#attribute` to be set to "Some Custom Value" after redaction.
79
+ Custom redactor types also get access to the record being redacted via `record`, and a hash with the `:attribute` key-value pair via `data`
80
+
81
+ ### Preforming a Redaction
82
+ There are two ways to preform the redaction.
83
+
84
+ #### Via Rake Task
85
+ ```bash
86
+ rails redaction:redact
87
+ ```
88
+ This will target **all** the models with redacted attributes. To target specific models run:
89
+ ```bash
90
+ rails redaction:redact MODELS=User,Post
91
+ ```
92
+ This will only redact the `User` and `Post` Models
93
+
94
+ #### Via the Rails Console
95
+ ```ruby
96
+ Redaction.redact!
97
+ ```
98
+ This will target **all** the models with redacted attributes. To target specific models run:
99
+ ```ruby
100
+ Redaction.redact!(models: ["User", "Post"])
101
+ ```
102
+ This will only redact the `User` and `Post` Models
103
+
104
+ #### Validations and Callbacks
105
+ By default, preforming a redaction does not trigger validations or update the `updated_at` attribute.
106
+
107
+ Callbacks can be skipped with the `:redacting?` method. i.e.:
108
+ ```ruby
109
+ class User < ApplicationRecord
110
+ after_save :do_something, unless: :redacting?
111
+
112
+ redacts :first_name, :last_name, with: :name
113
+ end
114
+ ```
115
+
116
+ ## Roadmap
117
+ - [ ] Raise Error or at least a message when skipping a passed in Model
118
+ - [ ] Configuration (touch, email domains, etc)
119
+ - [ ] Better Documentation
120
+ - [ ] More types
121
+ - [ ] Release v1.0 as a real gem
122
+
123
+ ## Contributing
124
+ Bug reports and pull requests are welcome on GitHub at [drbragg/redaction](https://github.com/drbragg/redaction). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/DRBragg/redaction/blob/main/CODE_OF_CONDUCT.md).
125
+
126
+ ## License
127
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
128
+
129
+ ## Acknowledgments
130
+ `redaction` leans heavily on the awesome [Faker gem](https://github.com/faker-ruby/faker). If not for their hard work this would be a much different and probably more complex project. If you like `redaction` please consider sending them a thank you or contributing to the gem.
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
4
+
5
+ load "tasks/redaction.rake"
@@ -0,0 +1,13 @@
1
+ module Redaction
2
+ class Railtie < ::Rails::Railtie
3
+ initializer "redaction.initialize" do
4
+ ActiveSupport.on_load(:active_record) do
5
+ include Redaction::Redactable
6
+ end
7
+ end
8
+
9
+ rake_tasks do
10
+ load "tasks/redaction.rake"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,44 @@
1
+ module Redaction
2
+ module Redactable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :redacted_attributes, instance_writer: false
7
+
8
+ after_commit do
9
+ @_redacting = false
10
+ end
11
+
12
+ def redact!
13
+ @_redacting = true
14
+ redacted_attributes.each_pair do |redactor_type, attributes|
15
+ redactor = Redaction.find(redactor_type)
16
+
17
+ attributes.each do |attribute|
18
+ if send(attribute).present?
19
+ send("#{attribute}=", redactor.call(self, {attribute: attribute}))
20
+ end
21
+ end
22
+ end
23
+
24
+ save!(validate: false, touch: false, context: :redaction)
25
+ end
26
+
27
+ def redacting?
28
+ !!@_redacting
29
+ end
30
+ end
31
+
32
+ class_methods do
33
+ def redacts(*attributes, with: :basic)
34
+ self.redacted_attributes ||= Hash.new { |hash, key| hash[key] = [] }
35
+
36
+ self.redacted_attributes[with] += attributes
37
+ end
38
+
39
+ def has_redacted_content?
40
+ self.redacted_attributes&.any?
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,46 @@
1
+ require "ruby-progressbar"
2
+
3
+ module Redaction
4
+ class Redactor
5
+ attr_reader :models_to_redact
6
+
7
+ def initialize(models: nil)
8
+ @models_to_redact = set_models(models)
9
+ end
10
+
11
+ def redact!
12
+ models_to_redact.each do |model|
13
+ next if model.redacted_attributes.empty?
14
+
15
+ model_progress = progress_bar(model.name, model.count)
16
+
17
+ model.all.unscoped.find_each do |record|
18
+ record.redact!
19
+ model_progress.increment
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def set_models(models)
27
+ if models.present?
28
+ models
29
+ .map { |model| model.constantize }
30
+ .select { |model| model.has_redacted_content? }
31
+ else
32
+ Redaction.redactable_models
33
+ end
34
+ end
35
+
36
+ def progress_bar(title, total)
37
+ ProgressBar.create(
38
+ format: "%t %b\u{15E7}%i %p%%",
39
+ progress_mark: " ",
40
+ remainder_mark: "\u{FF65}",
41
+ title: title,
42
+ total: total
43
+ )
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,20 @@
1
+ module Redaction
2
+ module Types
3
+ class Base
4
+ attr_reader :record, :data
5
+
6
+ def self.call(record, data)
7
+ new(record, data).content
8
+ end
9
+
10
+ def initialize(record, data)
11
+ @record = record
12
+ @data = data
13
+ end
14
+
15
+ def content
16
+ "[REDACTED]"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ require "faker"
2
+
3
+ module Redaction
4
+ module Types
5
+ class Basic < Base
6
+ def content
7
+ Faker::Lorem.sentence(word_count: rand(3..10)).gsub(/\.$/, "")
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ require "faker"
2
+
3
+ module Redaction
4
+ module Types
5
+ class BasicHtml < Base
6
+ def content
7
+ [nil, "em", "strong"].shuffle.map! do |tag|
8
+ text = Faker::Lorem.sentence(word_count: rand(1..3)).sub(/\.$/, "")
9
+ tag ? "<#{tag}>#{text}</#{tag}>" : text
10
+ end.join(" ")
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ require "faker"
2
+
3
+ module Redaction
4
+ module Types
5
+ class Email < Base
6
+ def content
7
+ Faker::Internet.safe_email
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,44 @@
1
+ require "faker"
2
+
3
+ module Redaction
4
+ module Types
5
+ class Html < Base
6
+ include ActionView::Helpers::TagHelper
7
+
8
+ TAGS = %i[em strong a]
9
+ private_constant :TAGS
10
+
11
+ def content
12
+ 1.upto(rand(1..3)).map { content_tag(:p, generate_paragraph.html_safe) }.join("\n")
13
+ end
14
+
15
+ private
16
+
17
+ def generate_paragraph
18
+ sentences = Faker::Lorem.sentences(number: rand(1..5)).map! do |sentence|
19
+ words = sentence.split(/\s+/)
20
+
21
+ words.map! do |word|
22
+ case rand(10)
23
+ when 0, 1, 2
24
+ create_html_word(word)
25
+ else
26
+ word
27
+ end
28
+ end
29
+
30
+ words.join(" ")
31
+ end
32
+
33
+ sentences.join(" ")
34
+ end
35
+
36
+ def create_html_word(word)
37
+ tag = TAGS.sample
38
+ options = tag == :a ? {href: "http://example.com"} : {}
39
+
40
+ content_tag(tag, word, options)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,11 @@
1
+ require "faker"
2
+
3
+ module Redaction
4
+ module Types
5
+ class Name < Base
6
+ def content
7
+ Faker::Name.first_name
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ require "faker"
2
+
3
+ module Redaction
4
+ module Types
5
+ class Phone < Base
6
+ def content
7
+ # Use cell_phone so no extension is added
8
+ # see: https://github.com/faker-ruby/faker/blob/master/doc/default/phone_number.md
9
+ Faker::PhoneNumber.cell_phone
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ require "faker"
2
+
3
+ module Redaction
4
+ module Types
5
+ class Text < Base
6
+ def content
7
+ paragraphs = 1.upto(rand(1..3)).map do
8
+ Faker::Lorem.paragraph(sentence_count: rand(1..5))
9
+ end
10
+
11
+ "#{paragraphs.join("\n\n")}\n"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module Redaction
2
+ VERSION = "0.1.0"
3
+ end
data/lib/redaction.rb ADDED
@@ -0,0 +1,34 @@
1
+ require "redaction/version"
2
+ require "redaction/railtie"
3
+
4
+ module Redaction
5
+ module Types
6
+ autoload :Base, "redaction/types/base"
7
+ autoload :Basic, "redaction/types/basic"
8
+ autoload :BasicHtml, "redaction/types/basic_html"
9
+ autoload :Email, "redaction/types/email"
10
+ autoload :Html, "redaction/types/html"
11
+ autoload :Name, "redaction/types/name"
12
+ autoload :Phone, "redaction/types/phone"
13
+ autoload :Text, "redaction/types/text"
14
+ end
15
+
16
+ autoload :Redactable, "redaction/redactable"
17
+ autoload :Redactor, "redaction/redactor"
18
+
19
+ def self.find(redactor_type)
20
+ if redactor_type.respond_to?(:call)
21
+ redactor_type
22
+ else
23
+ "Redaction::Types::#{redactor_type.to_s.camelize}".safe_constantize || Redaction::Types::Base
24
+ end
25
+ end
26
+
27
+ def self.redactable_models
28
+ ApplicationRecord.subclasses.select { |descendant| descendant.has_redacted_content? }
29
+ end
30
+
31
+ def self.redact!(models: nil)
32
+ Redactor.new(models: models).redact!
33
+ end
34
+ end
@@ -0,0 +1,15 @@
1
+ namespace :redaction do
2
+ desc "Update Models with redacted data"
3
+ task redact: :environment do
4
+ if Rails.env.production?
5
+ abort "Cannot redact in production"
6
+ end
7
+
8
+ Rails.application.eager_load!
9
+
10
+ model_names = (ENV["MODELS"] || "").split(",").map(&:strip)
11
+ puts model_names.empty? ? "Redacting all models" : "Redacting models: #{model_names.join(", ")}"
12
+
13
+ Redaction.redact!(models: model_names)
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redaction
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Drew Bragg
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-04-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 5.1.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 5.1.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: faker
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: ruby-progressbar
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: standard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: appraisal
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Easily redact your ActiveRecord Models.
84
+ email:
85
+ - drbragg@hey.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - MIT-LICENSE
91
+ - README.md
92
+ - Rakefile
93
+ - lib/redaction.rb
94
+ - lib/redaction/railtie.rb
95
+ - lib/redaction/redactable.rb
96
+ - lib/redaction/redactor.rb
97
+ - lib/redaction/types/base.rb
98
+ - lib/redaction/types/basic.rb
99
+ - lib/redaction/types/basic_html.rb
100
+ - lib/redaction/types/email.rb
101
+ - lib/redaction/types/html.rb
102
+ - lib/redaction/types/name.rb
103
+ - lib/redaction/types/phone.rb
104
+ - lib/redaction/types/text.rb
105
+ - lib/redaction/version.rb
106
+ - lib/tasks/redaction.rake
107
+ homepage: https://github.com/drbragg/redaction
108
+ licenses:
109
+ - MIT
110
+ metadata:
111
+ homepage_uri: https://github.com/drbragg/redaction
112
+ source_code_uri: https://github.com/drbragg/redaction
113
+ changelog_uri: https://github.com/drbragg/redaction/blob/main/CHANGELOG.md
114
+ post_install_message:
115
+ rdoc_options: []
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ required_rubygems_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ requirements: []
129
+ rubygems_version: 3.2.3
130
+ signing_key:
131
+ specification_version: 4
132
+ summary: Easily redact your ActiveRecord Models.
133
+ test_files: []