env_bro 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +118 -0
  4. data/Rakefile +3 -0
  5. data/app/controllers/env_bro/application_controller.rb +51 -0
  6. data/app/controllers/env_bro/audit_controller.rb +10 -0
  7. data/app/controllers/env_bro/home_controller.rb +13 -0
  8. data/app/controllers/env_bro/proposals_controller.rb +65 -0
  9. data/app/controllers/env_bro/reviews_controller.rb +63 -0
  10. data/app/helpers/env_bro/application_helper.rb +59 -0
  11. data/app/models/env_bro/application_record.rb +5 -0
  12. data/app/models/env_bro/audit_event.rb +59 -0
  13. data/app/views/env_bro/audit/index.html.erb +36 -0
  14. data/app/views/env_bro/proposals/_new_key_row.html.erb +7 -0
  15. data/app/views/env_bro/proposals/new.html.erb +82 -0
  16. data/app/views/env_bro/reviews/new.html.erb +32 -0
  17. data/app/views/env_bro/reviews/show.html.erb +134 -0
  18. data/app/views/env_bro/shared/_script.html.erb +113 -0
  19. data/app/views/env_bro/shared/_styles.html.erb +151 -0
  20. data/app/views/env_bro/shared/forbidden.html.erb +5 -0
  21. data/app/views/env_bro/shared/not_ready.html.erb +13 -0
  22. data/app/views/layouts/env_bro/application.html.erb +38 -0
  23. data/config/routes.rb +11 -0
  24. data/db/migrate/20261029000000_create_env_bro_audit_events.rb +24 -0
  25. data/lib/env_bro/audit.rb +17 -0
  26. data/lib/env_bro/configuration.rb +44 -0
  27. data/lib/env_bro/credentials.rb +135 -0
  28. data/lib/env_bro/deployments.rb +41 -0
  29. data/lib/env_bro/differ.rb +58 -0
  30. data/lib/env_bro/editor.rb +104 -0
  31. data/lib/env_bro/encrypted_blob.rb +18 -0
  32. data/lib/env_bro/engine.rb +15 -0
  33. data/lib/env_bro/environment.rb +74 -0
  34. data/lib/env_bro/version.rb +3 -0
  35. data/lib/env_bro.rb +40 -0
  36. data/lib/generators/env_bro/install/install_generator.rb +41 -0
  37. data/lib/generators/env_bro/install/templates/env_bro.rb +25 -0
  38. metadata +97 -0
@@ -0,0 +1,135 @@
1
+ require "yaml"
2
+
3
+ module EnvBro
4
+ # Decrypted credentials as a nested hash, addressed with dotted key paths
5
+ # ("aws.access_key_id"). The EnvBro metadata key is kept apart from the rest.
6
+ class Credentials
7
+ SEGMENT = /\A[A-Za-z0-9_\-]+\z/
8
+
9
+ attr_reader :data
10
+
11
+ def self.parse(yaml)
12
+ data = YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(yaml) : YAML.load(yaml)
13
+ data = {} if data.blank?
14
+ raise Error, "Credentials must be a YAML mapping at the top level" unless data.is_a?(Hash)
15
+ new(data)
16
+ end
17
+
18
+ def self.split(path)
19
+ segments = path.to_s.strip.split(".")
20
+ if segments.empty? || segments.any? { |s| !s.match?(SEGMENT) }
21
+ raise InvalidChange, "Invalid key #{path.inspect}. Use letters, digits, _ and -, nested with dots (aws.secret_key)."
22
+ end
23
+ segments
24
+ end
25
+
26
+ def initialize(data)
27
+ @data = data
28
+ end
29
+
30
+ def metadata
31
+ key = EnvBro.config.metadata_key
32
+ key ? (data[key] || data[key.to_sym]) : nil
33
+ end
34
+
35
+ def metadata=(value)
36
+ key = EnvBro.config.metadata_key or return
37
+ data.delete(key.to_sym)
38
+ data[key] = value
39
+ end
40
+
41
+ # One line of the YAML-like outline shown to proposers: a group ("aws:") or a key.
42
+ Line = Struct.new(:path, :name, :depth, :group, keyword_init: true)
43
+
44
+ # { "aws.access_key_id" => "AKIA...", "secret_key_base" => "..." }, without metadata.
45
+ def flatten
46
+ flatten_hash(without_metadata)
47
+ end
48
+
49
+ # Groups and keys in file order, with their nesting depth. No values.
50
+ def outline(hash = without_metadata, prefix = nil, depth = 0, lines = [])
51
+ hash.each do |key, value|
52
+ path = [ prefix, key.to_s ].compact.join(".")
53
+ group = value.is_a?(Hash) && value.any?
54
+ lines << Line.new(path: path, name: key.to_s, depth: depth, group: group)
55
+ outline(value, path, depth + 1, lines) if group
56
+ end
57
+ lines
58
+ end
59
+
60
+ def key_paths
61
+ flatten.keys
62
+ end
63
+
64
+ def key?(path)
65
+ flatten.key?(path)
66
+ end
67
+
68
+ def set(path, value)
69
+ *parents, leaf = self.class.split(path)
70
+ node = data
71
+ parents.each do |segment|
72
+ key = find_key(node, segment) || segment
73
+ if node[key].nil?
74
+ node = node[key] = {}
75
+ elsif node[key].is_a?(Hash)
76
+ node = node[key]
77
+ else
78
+ raise InvalidChange, "Cannot set #{path}: #{segment} already holds a value, not a group of keys."
79
+ end
80
+ end
81
+ existing_leaf = find_key(node, leaf)
82
+ if existing_leaf && node[existing_leaf].is_a?(Hash)
83
+ raise InvalidChange, "Cannot set #{path}: it is a group of keys, set one of its children instead."
84
+ end
85
+ node[existing_leaf || leaf] = value
86
+ end
87
+
88
+ def delete(path)
89
+ *parents, leaf = self.class.split(path)
90
+ chain = [ data ]
91
+ parents.each do |segment|
92
+ child = chain.last[find_key(chain.last, segment)]
93
+ raise InvalidChange, "Cannot remove #{path}: key does not exist." unless child.is_a?(Hash)
94
+ chain << child
95
+ end
96
+ existing_leaf = find_key(chain.last, leaf)
97
+ raise InvalidChange, "Cannot remove #{path}: key does not exist." if existing_leaf.nil?
98
+ chain.last.delete(existing_leaf)
99
+
100
+ # Drop groups left empty by the removal.
101
+ parents.reverse.each_with_index do |segment, i|
102
+ parent = chain[-(i + 2)]
103
+ key = find_key(parent, segment)
104
+ parent.delete(key) if parent[key].is_a?(Hash) && parent[key].empty?
105
+ end
106
+ end
107
+
108
+ def to_yaml
109
+ YAML.dump(data)
110
+ end
111
+
112
+ private
113
+ def without_metadata
114
+ data.except(EnvBro.config.metadata_key, EnvBro.config.metadata_key&.to_sym)
115
+ end
116
+
117
+ # Credentials keys are usually strings but may be symbols (":foo:" in YAML).
118
+ def find_key(hash, segment)
119
+ if hash.key?(segment) then segment
120
+ elsif hash.key?(segment.to_sym) then segment.to_sym
121
+ end
122
+ end
123
+
124
+ def flatten_hash(hash, prefix = nil)
125
+ hash.each_with_object({}) do |(key, value), out|
126
+ path = [ prefix, key.to_s ].compact.join(".")
127
+ if value.is_a?(Hash) && value.any?
128
+ out.merge!(flatten_hash(value, path))
129
+ else
130
+ out[path] = value
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,41 @@
1
+ module EnvBro
2
+ # Rails reads credentials once, at boot. On boot we look at the credentials file
3
+ # this app is running with and, if it came from an EnvBro proposal, record when
4
+ # that proposal went live.
5
+ module Deployments
6
+ module_function
7
+
8
+ # Returns the proposal_deployed event created, or nil. Never raises: this
9
+ # runs during boot and must not take the app down (no DB during
10
+ # assets:precompile, migrations not run yet, ...).
11
+ def track
12
+ return unless EnvBro.config.track_deployments
13
+ return unless AuditEvent.table_exists?
14
+
15
+ environment = Environment.current
16
+ return unless environment.ready?
17
+
18
+ digest = Environment.digest(environment.current_cipher_text)
19
+ proposal = AuditEvent.proposals.find_by(file_digest: digest)
20
+ modified = false
21
+
22
+ # The file was re-encrypted after review but still carries the proposal's id.
23
+ if proposal.nil?
24
+ token = environment.current_credentials.metadata.to_h["proposal_id"]
25
+ proposal = AuditEvent.proposals.find_by(proposal_token: token) if token.present?
26
+ modified = proposal.present?
27
+ end
28
+
29
+ return if proposal.nil? || AuditEvent.deployments.exists?(proposal_id: proposal.id)
30
+
31
+ Audit.record(:proposal_deployed, user: "deploy",
32
+ columns: { proposal: proposal, file_digest: digest },
33
+ modified_after_review: modified)
34
+ rescue ActiveRecord::RecordNotUnique
35
+ nil # another process recorded it first
36
+ rescue StandardError => e
37
+ Rails.logger&.warn("[EnvBro] could not check deployed credentials: #{e.class}: #{e.message}")
38
+ nil
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,58 @@
1
+ module EnvBro
2
+ # Compares two decrypted credentials (base vs proposed) key by key.
3
+ class Differ
4
+ Entry = Struct.new(:key, :status, :old_value, :new_value, keyword_init: true) do
5
+ def changed?
6
+ status != :unchanged
7
+ end
8
+ end
9
+
10
+ STATUS_ORDER = { added: 0, changed: 1, removed: 2, unchanged: 3 }.freeze
11
+
12
+ attr_reader :base, :proposed
13
+
14
+ def initialize(base, proposed)
15
+ @base = base
16
+ @proposed = proposed
17
+ end
18
+
19
+ def entries
20
+ @entries ||= begin
21
+ old_flat = base.flatten
22
+ new_flat = proposed.flatten
23
+ keys = (old_flat.keys | new_flat.keys)
24
+
25
+ keys.map do |key|
26
+ status =
27
+ if !old_flat.key?(key) then :added
28
+ elsif !new_flat.key?(key) then :removed
29
+ elsif old_flat[key] == new_flat[key] then :unchanged
30
+ else :changed
31
+ end
32
+ Entry.new(key: key, status: status, old_value: old_flat[key], new_value: new_flat[key])
33
+ end.sort_by { |e| [ STATUS_ORDER[e.status], e.key ] }
34
+ end
35
+ end
36
+
37
+ def changes
38
+ entries.select(&:changed?)
39
+ end
40
+
41
+ def counts
42
+ entries.group_by(&:status).transform_values(&:size)
43
+ end
44
+
45
+ def proposed_metadata
46
+ proposed.metadata.is_a?(Hash) ? proposed.metadata : nil
47
+ end
48
+
49
+ # :current - proposal was built on the base we compare against
50
+ # :stale - proposal was built on a different version of the file
51
+ # :unknown - proposal carries no EnvBro metadata (made by hand, or an old file)
52
+ def base_status(base_digest)
53
+ recorded = proposed_metadata&.fetch("base_digest", nil)
54
+ return :unknown if recorded.blank?
55
+ recorded == base_digest ? :current : :stale
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,104 @@
1
+ require "time"
2
+ require "securerandom"
3
+
4
+ module EnvBro
5
+ # Applies a list of changes to an environment's current credentials, in memory,
6
+ # and returns the re-encrypted file. Nothing is written to disk.
7
+ class Editor
8
+ ACTIONS = %w[set delete].freeze
9
+
10
+ Change = Struct.new(:action, :key, :value, keyword_init: true) do
11
+ def description
12
+ action == "delete" ? "remove #{key}" : "set #{key}"
13
+ end
14
+ end
15
+
16
+ Result = Struct.new(:cipher_text, :filename, :changes, :base_digest, :proposal_token, keyword_init: true) do
17
+ # SHA-256 of the file handed out; seeing it deployed means the proposal is live.
18
+ def file_digest
19
+ Environment.digest(cipher_text)
20
+ end
21
+ end
22
+
23
+ attr_reader :environment
24
+
25
+ def initialize(environment)
26
+ @environment = environment
27
+ end
28
+
29
+ # changes: array of hashes/Changes with action ("set"/"delete"), key and value.
30
+ def apply(changes, author:)
31
+ changes = normalize(changes)
32
+ raise InvalidChange, "Add at least one change." if changes.empty?
33
+
34
+ base_cipher_text = environment.current_cipher_text
35
+ credentials = environment.decrypt(base_cipher_text)
36
+
37
+ changes.each do |change|
38
+ if change.action == "delete"
39
+ credentials.delete(change.key)
40
+ else
41
+ credentials.set(change.key, change.value)
42
+ end
43
+ end
44
+
45
+ base_digest = Environment.digest(base_cipher_text)
46
+ proposal_token = SecureRandom.uuid
47
+ credentials.metadata = {
48
+ "proposal_id" => proposal_token,
49
+ "base_digest" => base_digest,
50
+ "proposed_by" => author.to_s,
51
+ "proposed_at" => Time.now.utc.iso8601,
52
+ "changes" => changes.map(&:description)
53
+ }
54
+
55
+ Result.new(
56
+ cipher_text: environment.encrypt(credentials),
57
+ filename: environment.filename,
58
+ changes: changes,
59
+ base_digest: base_digest,
60
+ proposal_token: proposal_token
61
+ )
62
+ end
63
+
64
+ private
65
+ def normalize(changes)
66
+ changes = Array(changes).map { |c| c.is_a?(Change) ? c : Change.new(**c.to_h.symbolize_keys.slice(:action, :key, :value)) }
67
+ changes = changes.reject { |c| c.key.blank? && c.value.blank? }
68
+
69
+ changes.each do |change|
70
+ change.action = change.action.presence || "set"
71
+ change.key = change.key.to_s.strip
72
+ raise InvalidChange, "Unknown action #{change.action.inspect}." unless ACTIONS.include?(change.action)
73
+ Credentials.split(change.key)
74
+ if metadata_key?(change.key)
75
+ raise InvalidChange, "#{change.key} is reserved for EnvBro metadata."
76
+ end
77
+
78
+ if change.action == "set"
79
+ change.value = clean_value(change.value)
80
+ raise InvalidChange, "#{change.key} needs a value." if change.value.empty?
81
+ else
82
+ change.value = nil
83
+ end
84
+ end
85
+
86
+ duplicates = changes.map(&:key).tally.select { |_, n| n > 1 }.keys
87
+ raise InvalidChange, "Each key can only be changed once per proposal: #{duplicates.join(', ')}." if duplicates.any?
88
+
89
+ changes
90
+ end
91
+
92
+ # Browsers send textarea newlines as CRLF. Single-line values are stripped
93
+ # because a stray space or newline in an API key is a classic bug.
94
+ def clean_value(value)
95
+ value = value.to_s.gsub("\r\n", "\n").sub(/\n+\z/, "")
96
+ value.include?("\n") ? value : value.strip
97
+ end
98
+
99
+ def metadata_key?(key)
100
+ meta = EnvBro.config.metadata_key
101
+ meta && Credentials.split(key).first == meta
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,18 @@
1
+ require "active_support/encrypted_file"
2
+
3
+ module EnvBro
4
+ # Reuses Rails' own EncryptedFile (cipher, serializer, key lookup) but lets us
5
+ # encrypt/decrypt arbitrary strings in memory instead of writing to disk.
6
+ class EncryptedBlob < ActiveSupport::EncryptedFile
7
+ def encrypt_string(plain)
8
+ encrypt(plain)
9
+ end
10
+
11
+ def decrypt_string(cipher_text)
12
+ decrypt(cipher_text.to_s.strip)
13
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage, ArgumentError
14
+ raise DecryptionError, "This file could not be decrypted with the #{content_path.basename} key. " \
15
+ "It is either for another environment or corrupted."
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ module EnvBro
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace EnvBro
4
+
5
+ # Secret values and uploaded files must never end up in the host's logs.
6
+ initializer "env_bro.filter_parameters" do |app|
7
+ app.config.filter_parameters += %i[secret_value proposed_file base_file proposed_enc base_enc]
8
+ end
9
+
10
+ # Credentials are read at boot, so this is when a merged proposal takes effect.
11
+ config.after_initialize do
12
+ EnvBro::Deployments.track
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,74 @@
1
+ require "digest"
2
+ require "pathname"
3
+
4
+ module EnvBro
5
+ # The credentials file of the environment the app runs in, located exactly the
6
+ # way Rails finds it (config.credentials.content_path, RAILS_MASTER_KEY, ...).
7
+ class Environment
8
+ attr_reader :name, :content_path, :key_path, :env_key
9
+
10
+ def self.current
11
+ credentials = Rails.application.credentials
12
+ new(name: Rails.env, content_path: credentials.content_path, key_path: credentials.key_path, env_key: credentials.env_key)
13
+ end
14
+
15
+ def initialize(name:, content_path:, key_path:, env_key:)
16
+ @name = name.to_s
17
+ @content_path = Pathname.new(content_path.to_s)
18
+ @key_path = Pathname.new(key_path.to_s)
19
+ @env_key = env_key
20
+ end
21
+
22
+ def filename
23
+ content_path.basename.to_s
24
+ end
25
+
26
+ def exists?
27
+ content_path.exist?
28
+ end
29
+
30
+ def key?
31
+ blob(raise_if_missing_key: false).key?.present?
32
+ end
33
+
34
+ # :ready, :missing_file, :missing_key or :wrong_key.
35
+ def status
36
+ return :missing_file unless exists?
37
+ return :missing_key unless key?
38
+ current_credentials
39
+ :ready
40
+ rescue DecryptionError, ActiveSupport::EncryptedFile::InvalidKeyLengthError
41
+ :wrong_key
42
+ end
43
+
44
+ def ready?
45
+ status == :ready
46
+ end
47
+
48
+ # Encrypted bytes as deployed on this server.
49
+ def current_cipher_text
50
+ content_path.binread.strip
51
+ end
52
+
53
+ def current_credentials
54
+ decrypt(current_cipher_text)
55
+ end
56
+
57
+ def decrypt(cipher_text)
58
+ Credentials.parse(blob.decrypt_string(cipher_text))
59
+ end
60
+
61
+ def encrypt(credentials)
62
+ blob.encrypt_string(credentials.to_yaml)
63
+ end
64
+
65
+ def self.digest(cipher_text)
66
+ Digest::SHA256.hexdigest(cipher_text.to_s.strip)
67
+ end
68
+
69
+ private
70
+ def blob(raise_if_missing_key: true)
71
+ EncryptedBlob.new(content_path: content_path, key_path: key_path, env_key: env_key, raise_if_missing_key: raise_if_missing_key)
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,3 @@
1
+ module EnvBro
2
+ VERSION = "0.1.0"
3
+ end
data/lib/env_bro.rb ADDED
@@ -0,0 +1,40 @@
1
+ require "env_bro/version"
2
+ require "env_bro/configuration"
3
+ require "env_bro/environment"
4
+ require "env_bro/encrypted_blob"
5
+ require "env_bro/credentials"
6
+ require "env_bro/editor"
7
+ require "env_bro/differ"
8
+ require "env_bro/audit"
9
+ require "env_bro/deployments"
10
+ require "env_bro/engine"
11
+
12
+ module EnvBro
13
+ class Error < StandardError; end
14
+ class DecryptionError < Error; end
15
+ class InvalidChange < Error; end
16
+ class Forbidden < StandardError; end
17
+
18
+ # Problems we show to the user instead of raising a 500.
19
+ USER_FACING_ERRORS = [
20
+ Error,
21
+ ActiveSupport::EncryptedFile::MissingKeyError,
22
+ ActiveSupport::EncryptedFile::InvalidKeyLengthError,
23
+ Errno::ENOENT,
24
+ Psych::SyntaxError
25
+ ].freeze
26
+
27
+ class << self
28
+ def config
29
+ @config ||= Configuration.new
30
+ end
31
+
32
+ def configure
33
+ yield config
34
+ end
35
+
36
+ def reset_config!
37
+ @config = nil
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,41 @@
1
+ require "rails/generators"
2
+
3
+ module EnvBro
4
+ module Generators
5
+ # bin/rails generate env_bro:install
6
+ class InstallGenerator < Rails::Generators::Base
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ desc "Adds the EnvBro initializer, mounts the engine and copies its migrations."
10
+
11
+ class_option :mount_path, type: :string, default: "/admin/env", desc: "Where to mount EnvBro"
12
+
13
+ def copy_initializer
14
+ copy_file "env_bro.rb", "config/initializers/env_bro.rb"
15
+ end
16
+
17
+ def mount_engine
18
+ routes = File.join(destination_root, "config/routes.rb")
19
+ if File.exist?(routes) && File.read(routes).include?("EnvBro::Engine")
20
+ say_status :skip, "EnvBro::Engine is already mounted", :yellow
21
+ else
22
+ route %(mount EnvBro::Engine => "#{options[:mount_path]}")
23
+ end
24
+ end
25
+
26
+ def copy_migrations
27
+ rails_command "env_bro:install:migrations", inline: true
28
+ end
29
+
30
+ def next_steps
31
+ say <<~TEXT
32
+
33
+ EnvBro is installed. Next:
34
+ 1. bin/rails db:migrate
35
+ 2. Set who may propose, review and reveal in config/initializers/env_bro.rb
36
+ 3. Visit #{options[:mount_path]}
37
+ TEXT
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,25 @@
1
+ # EnvBro lets people propose changes to this environment's encrypted credentials
2
+ # without holding the key.
3
+ EnvBro.configure do |config|
4
+ # Who may do what. Each lambda gets the signed in user (nil when signed out).
5
+ # Everything is denied until you change these.
6
+ config.can_propose = ->(user) { false } # e.g. user&.developer?
7
+ config.can_review = ->(user) { false } # e.g. user&.admin?
8
+ config.can_reveal = ->(user) { false } # see actual values in a review (audited)
9
+
10
+ # How EnvBro finds the signed in user. Engine controllers inherit from
11
+ # parent_controller, so its authentication (before_actions) applies too.
12
+ # config.parent_controller = "::ApplicationController"
13
+ # config.current_user = ->(controller) { controller.send(:current_user) }
14
+ # config.user_label = ->(user) { user.email }
15
+
16
+ # Reject plain HTTP (secret values travel through the browser).
17
+ # config.require_ssl = Rails.env.production?
18
+
19
+ # Top-level credentials key holding proposal metadata. nil disables it, along
20
+ # with the outdated-base warning and deployment tracking by proposal id.
21
+ # config.metadata_key = "env_bro"
22
+
23
+ # Record on boot when the running credentials came from an EnvBro proposal.
24
+ # config.track_deployments = true
25
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: env_bro
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ruby Dev SRL
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.1'
26
+ description: 'A mountable Rails engine: team members with permission build a re-encrypted
27
+ credentials file with their changes and open a PR; reviewers upload the file and
28
+ see a masked diff.'
29
+ email:
30
+ - office@rubydev.ro
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - MIT-LICENSE
36
+ - README.md
37
+ - Rakefile
38
+ - app/controllers/env_bro/application_controller.rb
39
+ - app/controllers/env_bro/audit_controller.rb
40
+ - app/controllers/env_bro/home_controller.rb
41
+ - app/controllers/env_bro/proposals_controller.rb
42
+ - app/controllers/env_bro/reviews_controller.rb
43
+ - app/helpers/env_bro/application_helper.rb
44
+ - app/models/env_bro/application_record.rb
45
+ - app/models/env_bro/audit_event.rb
46
+ - app/views/env_bro/audit/index.html.erb
47
+ - app/views/env_bro/proposals/_new_key_row.html.erb
48
+ - app/views/env_bro/proposals/new.html.erb
49
+ - app/views/env_bro/reviews/new.html.erb
50
+ - app/views/env_bro/reviews/show.html.erb
51
+ - app/views/env_bro/shared/_script.html.erb
52
+ - app/views/env_bro/shared/_styles.html.erb
53
+ - app/views/env_bro/shared/forbidden.html.erb
54
+ - app/views/env_bro/shared/not_ready.html.erb
55
+ - app/views/layouts/env_bro/application.html.erb
56
+ - config/routes.rb
57
+ - db/migrate/20261029000000_create_env_bro_audit_events.rb
58
+ - lib/env_bro.rb
59
+ - lib/env_bro/audit.rb
60
+ - lib/env_bro/configuration.rb
61
+ - lib/env_bro/credentials.rb
62
+ - lib/env_bro/deployments.rb
63
+ - lib/env_bro/differ.rb
64
+ - lib/env_bro/editor.rb
65
+ - lib/env_bro/encrypted_blob.rb
66
+ - lib/env_bro/engine.rb
67
+ - lib/env_bro/environment.rb
68
+ - lib/env_bro/version.rb
69
+ - lib/generators/env_bro/install/install_generator.rb
70
+ - lib/generators/env_bro/install/templates/env_bro.rb
71
+ homepage: https://www.rubydev.ro
72
+ licenses:
73
+ - MIT
74
+ metadata:
75
+ allowed_push_host: https://rubygems.org
76
+ rubygems_mfa_required: 'true'
77
+ homepage_uri: https://www.rubydev.ro
78
+ source_code_uri: https://github.com/rubydevro/env_bro
79
+ bug_tracker_uri: https://github.com/rubydevro/env_bro/issues
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '3.2'
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubygems_version: 4.0.22
95
+ specification_version: 4
96
+ summary: Propose changes to Rails encrypted credentials without holding the keys.
97
+ test_files: []