├── log
└── test.log
├── lib
├── rails-creds.rb
├── creds
│ ├── version.rb
│ └── railtie.rb
├── tasks
│ └── creds.rake
└── creds.rb
├── .rspec
├── config
└── master.key
├── Rakefile
├── Gemfile
├── .gitignore
├── spec
├── spec_helper.rb
└── creds_spec.rb
├── creds.gemspec
├── README.md
├── LICENSE.txt
├── .github
└── workflows
│ └── ci.yml
└── CHANGELOG.md
/log/test.log:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/rails-creds.rb:
--------------------------------------------------------------------------------
1 | require "creds"
2 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --require spec_helper
3 |
--------------------------------------------------------------------------------
/config/master.key:
--------------------------------------------------------------------------------
1 | 69104fb1cb0f1be6b0a0019f1311bb00
2 |
--------------------------------------------------------------------------------
/lib/creds/version.rb:
--------------------------------------------------------------------------------
1 | module Creds
2 | VERSION = "0.5.1".freeze
3 | end
4 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require "rspec/core/rake_task"
3 |
4 | RSpec::Core::RakeTask.new(:spec)
5 |
6 | task(default: :spec)
7 |
--------------------------------------------------------------------------------
/lib/tasks/creds.rake:
--------------------------------------------------------------------------------
1 | namespace(:creds) do
2 | desc("Print an example credentials file")
3 | task(:example => :environment) do
4 | puts(Creds::EXAMPLE_CONFIG)
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4 |
5 | # Specify your gem's dependencies in creds.gemspec
6 | gemspec
7 |
--------------------------------------------------------------------------------
/lib/creds/railtie.rb:
--------------------------------------------------------------------------------
1 | module Creds
2 | class Railtie < ::Rails::Railtie
3 | rake_tasks do
4 | Dir[File.join(__dir__, "../tasks/**/*.rake")].each { |f| load f }
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /_yardoc/
4 | /coverage/
5 | /doc/
6 | /pkg/
7 | /spec/reports/
8 | /tmp/
9 |
10 | # rspec failure tracking
11 | .rspec_status
12 |
13 | config/credentials.yml.enc
14 | Gemfile.lock
15 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require "bundler/setup"
2 | require "creds"
3 |
4 | RSpec.configure do |config|
5 | # Enable flags like --only-failures and --next-failure
6 | config.example_status_persistence_file_path = ".rspec_status"
7 |
8 | # Disable RSpec exposing methods globally on `Module` and `main`
9 | config.disable_monkey_patching!
10 |
11 | config.expect_with(:rspec) do |c|
12 | c.syntax = :expect
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/creds.gemspec:
--------------------------------------------------------------------------------
1 | lib = File.expand_path("lib", __dir__)
2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3 | require "creds/version"
4 |
5 | Gem::Specification.new do |spec|
6 | spec.name = "rails-creds"
7 | spec.version = Creds::VERSION
8 | spec.authors = ["Mikkel Malmberg"]
9 | spec.email = ["mikkel@brnbw.com"]
10 |
11 | spec.summary = "Shorter, env-scoped version of Rails' credentials"
12 | spec.description = ""
13 | spec.homepage = "https://github.com/mikker/rails-creds"
14 | spec.metadata = {"source_code_uri" => "https://github.com/mikker/rails-creds"}
15 | spec.license = "MIT"
16 |
17 | spec.files = `git ls-files -z`.split("\x0").reject do |f|
18 | f.match(%r{^(test|spec|features)/})
19 | end
20 |
21 | spec.bindir = "exe"
22 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23 | spec.require_paths = ["lib"]
24 |
25 | spec.add_dependency("rails", ">= 5.2")
26 |
27 | spec.add_development_dependency("bundler", "~> 2.0")
28 | spec.add_development_dependency("rake", "~> 12.3")
29 | spec.add_development_dependency("rspec", "~> 3.0")
30 | end
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 |
3 | [](https://rubygems.org/gems/rails-creds)
4 |
5 | `Creds` is …
6 |
7 | 1. a shortcut for the dreadfully long `Rails.application.credentials` and …
8 | 2. environment scoped by default
9 |
10 | ## Usage
11 |
12 | Given encrypted credentials looking like:
13 |
14 | ```yaml
15 | ---
16 | secret_key_base: "abc123"
17 |
18 | shared: &shared
19 | secret: 123
20 |
21 | test:
22 | <<: *shared
23 |
24 | development:
25 | <<: *shared
26 |
27 | production:
28 | <<: *shared
29 | secret: 456
30 | ```
31 |
32 | You can access those super secret things like:
33 |
34 | ```ruby
35 | # development, test:
36 | Creds.secret # => 123
37 |
38 | # production
39 | Creds.secret # => 456
40 |
41 | # staging
42 | Creds.secret # => raises Creds::MissingEnvError
43 |
44 | # any
45 | Creds.missing_secret # => raises Creds::MissingKeyError
46 | ```
47 |
48 | ## Installation
49 |
50 | ```sh
51 | $ bundle add rails-creds
52 | $ bundle install
53 | ```
54 |
55 | ## License
56 |
57 | MIT
58 |
59 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Mikkel Malmberg
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches: [main]
7 |
8 | jobs:
9 | test:
10 | runs-on: ubuntu-latest
11 |
12 | strategy:
13 | fail-fast: false
14 | matrix:
15 | ruby:
16 | - "3.0"
17 | - "3.1"
18 | - "3.2"
19 | - "3.3"
20 | - "3.4"
21 |
22 | steps:
23 | - name: Install packages
24 | run: sudo apt-get update && sudo apt-get install --no-install-recommends -y build-essential git pkg-config google-chrome-stable
25 |
26 | - name: Checkout code
27 | uses: actions/checkout@v4
28 |
29 | - name: Set up Ruby
30 | uses: ruby/setup-ruby@v1
31 | with:
32 | ruby-version: ruby-3.4.1
33 | bundler-cache: true
34 |
35 | - name: Run tests
36 | env:
37 | RAILS_ENV: test
38 | run: bin/rails test
39 |
40 | - name: Keep screenshots from failed system tests
41 | uses: actions/upload-artifact@v4
42 | if: failure()
43 | with:
44 | name: screenshots
45 | path: ${{ github.workspace }}/tmp/screenshots
46 | if-no-files-found: ignore
47 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 0.5.1
4 |
5 | - Noop values if `SECRET_KEY_BASE_DUMMY` is `"1"`
6 |
7 | ## 0.5.0
8 |
9 | **NB:** Probably breaking change.
10 |
11 | We now rely on YAML for merging credentials. See `bin/rails creds:example` for an example configuration.
12 |
13 | - New: Missing keys always raise.
14 | - Added example task.
15 |
16 | ## 0.4.0 (2023-07-08)
17 |
18 | ### Added
19 |
20 | - Return `nil` and don't read credentials nor complain when SECRET_KEY_BASE_DUMMY=1
21 |
22 | ## 0.3.0 (2019-09-19)
23 |
24 | ### Breaking changes
25 |
26 | - _(Possibly breaking)_ Scoped creds are now merged with top level creds ([#23](https://github.com/mikker/rails-creds/pull/23))
27 |
28 | ## 0.2.3 (2019-08-19)
29 |
30 | ### Fixed:
31 |
32 | - Require strip_heredoc extension before use
33 |
34 | ## 0.2.1
35 |
36 | ## Changed:
37 |
38 | - Credentials are now memoized after successful read.
39 |
40 | ## 0.2.0
41 |
42 | ### Changed:
43 |
44 | - Creds will now warn about missing credentials when the encrypted file isn't
45 | found. It will afterwards be a Null Object and return `nil` on every key.
46 | - When encrypted credentials are found but the master key file AND env
47 | variable is missing, Creds will return a special error with explanation.
48 |
49 | ## 0.1.0
50 |
51 | Initial version
52 |
--------------------------------------------------------------------------------
/lib/creds.rb:
--------------------------------------------------------------------------------
1 | require "rails"
2 | require "active_support/core_ext/string/strip"
3 |
4 | require "creds/version"
5 | require "creds/railtie"
6 |
7 | # The main module of rails-creds
8 | module Creds
9 | EXAMPLE_CONFIG = <<-YAML
10 | ---
11 | secret_key_base: "abc123"
12 |
13 | shared: &shared
14 | secret: 123
15 |
16 | test:
17 | <<: *shared
18 |
19 | development:
20 | <<: *shared
21 |
22 | production:
23 | <<: *shared
24 | secret: 456
25 | YAML
26 | .strip_heredoc
27 | .freeze
28 |
29 | class MissingKeyError < StandardError
30 | MESSAGE = "Key :%s missing from credentials in \"%s\" env".freeze
31 |
32 | def initialize(key, env)
33 | super(format(MESSAGE, key: key, env: env))
34 | end
35 | end
36 |
37 | class MissingEnvError < StandardError
38 | MESSAGE = <<-MSG
39 | It seems you are missing a scope for the environment "%s".
40 |
41 | Here's an example of how your credentials could look:
42 |
43 | #{Creds::EXAMPLE_CONFIG.gsub(/^([^\n]+)$/m, " \\1")}
44 | MSG
45 | .strip_heredoc
46 | .freeze
47 |
48 | def initialize(env)
49 | super(format(MESSAGE, env: env))
50 | end
51 | end
52 |
53 | def self.method_missing(mth, *args, &block)
54 | # If this is set, we're likely building a Docker image and so just noop
55 | return nil if ENV["SECRET_KEY_BASE_DUMMY"] == "1"
56 |
57 | @cache ||= Rails.application.credentials[Rails.env].tap do |scoped|
58 | raise MissingEnvError.new(Rails.env) unless scoped.is_a?(Hash)
59 | raise MissingKeyError.new(mth, Rails.env) unless scoped.key?(mth.to_sym)
60 |
61 | scoped
62 | end
63 |
64 | @cache.fetch(mth.to_sym)
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/spec/creds_spec.rb:
--------------------------------------------------------------------------------
1 | ENV["RAILS_ENV"] = "test"
2 |
3 | require "action_controller/railtie"
4 |
5 | RSpec.describe Creds do
6 | before do
7 | class RailsTestApp < Rails::Application
8 | config.secret_key_base = "__secret_key_base"
9 | config.logger = Logger.new(STDOUT)
10 | config.eager_load = false
11 | config.require_master_key = true
12 |
13 | # Silence warning
14 | config.active_support.to_time_preserves_timezone = :zone
15 | end
16 |
17 | RailsTestApp.initialize!
18 |
19 | # reset cache
20 | Creds.instance_variable_set(:@cache, nil)
21 |
22 | write_config({})
23 | end
24 |
25 | after do
26 | %i[RailsTestApp].each do |const|
27 | Object.send(:remove_const, const)
28 | end
29 |
30 | Rails.application = nil
31 |
32 | # For Rails 7, which freezes these in an initializer.
33 | # Can't start a new instance of the application when these are frozen.
34 | ActiveSupport::Dependencies.autoload_paths = []
35 | ActiveSupport::Dependencies.autoload_once_paths = []
36 | end
37 |
38 | it "returns Rails credentials scoped to env" do
39 | write_config(
40 | <<-YAML
41 | test:
42 | super_secret: "shh!"
43 | YAML
44 | )
45 | expect(Creds.super_secret).to(eq("shh!"))
46 | end
47 |
48 | it "raises MissingKeyError on missing keys" do
49 | write_config(
50 | <<-YAML
51 | test:
52 | super_secret: "shh!"
53 | YAML
54 | )
55 | expect { Creds.non_existing_key }.to(raise_error(Creds::MissingKeyError))
56 | end
57 |
58 | it "raises MissingEnvError on missing env" do
59 | write_config(
60 | <<-YAML
61 | development:
62 | super_secret: "shh!"
63 | YAML
64 | )
65 | expect { Creds.super_secret }.to(raise_error(Creds::MissingEnvError))
66 | end
67 |
68 | def write_config(yaml)
69 | Rails.application.credentials.change do |path|
70 | File.open(path, "w") { |f| f.write(yaml) }
71 | end
72 | end
73 | end
74 |
--------------------------------------------------------------------------------