├── 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 | [![Gem version](https://img.shields.io/gem/v/rails-creds.svg)](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 | --------------------------------------------------------------------------------