├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── SECURITY.md ├── bin ├── console ├── rspec ├── rubocop └── setup ├── kreds.gemspec ├── lib ├── kreds.rb └── kreds │ ├── fetch.rb │ ├── inputs.rb │ ├── show.rb │ └── version.rb └── spec ├── kreds_spec.rb ├── spec_helper.rb └── support ├── credentials.yml.enc ├── master.key └── test_app.rb /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [brownboxdev] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "bundler" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | 8 | linting: 9 | runs-on: ubuntu-latest 10 | 11 | name: RuboCop 12 | permissions: 13 | contents: read 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Ruby 17 | uses: ruby/setup-ruby@v1 18 | with: 19 | ruby-version: 3.2 20 | - name: Bundle install 21 | run: bundle install 22 | - name: Run RuboCop 23 | run: bundle exec rubocop --color 24 | 25 | todo: 26 | runs-on: ubuntu-latest 27 | 28 | name: ToDo 29 | permissions: 30 | contents: read 31 | steps: 32 | - uses: actions/checkout@v4 33 | - name: Set up Ruby 34 | uses: ruby/setup-ruby@v1 35 | with: 36 | ruby-version: 3.2 37 | - name: Bundle install 38 | run: bundle install 39 | - name: Run grepfruit 40 | run: bundle exec grepfruit -r TODO -e 'vendor,tmp,.yardoc,.git,ci.yml:40' --search-hidden 41 | 42 | tests: 43 | runs-on: ubuntu-latest 44 | 45 | name: Ruby ${{ matrix.ruby }} - Rails ${{ matrix.rails }} 46 | permissions: 47 | contents: read 48 | strategy: 49 | matrix: 50 | ruby: 51 | - "3.2" 52 | - "3.3" 53 | - "3.4" 54 | rails: 55 | - "~> 7.1.0" 56 | - "~> 7.2.0" 57 | - "~> 8.0.0" 58 | 59 | env: 60 | RAILS_VERSION: "${{ matrix.rails }}" 61 | 62 | steps: 63 | - uses: actions/checkout@v4 64 | - name: Set up Ruby 65 | uses: ruby/setup-ruby@v1 66 | with: 67 | ruby-version: ${{ matrix.ruby }} 68 | - name: Bundle install 69 | run: bundle install 70 | - name: Run RSpec 71 | run: bundle exec rspec 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /log/ 10 | Gemfile.lock 11 | 12 | # rspec failure tracking 13 | .rspec_status 14 | 15 | # macOS 16 | .DS_Store 17 | 18 | # rbenv 19 | .ruby-version 20 | 21 | # byebug 22 | .byebug_history 23 | 24 | # RubyMine 25 | .idea 26 | 27 | # VS Code 28 | .vscode 29 | 30 | # solargraph 31 | .solargraph.yml 32 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --tty 3 | --require spec_helper 4 | --order random 5 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - rubocop-md 3 | - rubocop-packaging 4 | - rubocop-performance 5 | - rubocop-rails 6 | - rubocop-rake 7 | - rubocop-rspec 8 | - rubocop-rspec_rails 9 | - rubocop-thread_safety 10 | 11 | AllCops: 12 | TargetRailsVersion: 7.1 13 | TargetRubyVersion: 3.2 14 | NewCops: enable 15 | 16 | Layout/IndentationConsistency: 17 | Exclude: 18 | - 'README.md' 19 | 20 | Layout/LineLength: 21 | Enabled: false 22 | 23 | Metrics/AbcSize: 24 | Enabled: false 25 | 26 | Metrics/CyclomaticComplexity: 27 | Enabled: false 28 | 29 | Metrics/MethodLength: 30 | Enabled: false 31 | 32 | Metrics/PerceivedComplexity: 33 | Enabled: false 34 | 35 | RSpec/ExampleLength: 36 | Enabled: false 37 | 38 | RSpec/MessageSpies: 39 | Enabled: false 40 | 41 | RSpec/MultipleExpectations: 42 | Enabled: false 43 | 44 | RSpec/NamedSubject: 45 | Enabled: false 46 | 47 | RSpec/NestedGroups: 48 | Enabled: false 49 | 50 | Style/Documentation: 51 | Enabled: false 52 | 53 | Style/FrozenStringLiteralComment: 54 | Enabled: false 55 | 56 | Style/MutableConstant: 57 | Enabled: false 58 | 59 | Style/ParallelAssignment: 60 | Enabled: false 61 | 62 | Style/StringLiterals: 63 | EnforcedStyle: double_quotes 64 | 65 | Style/StringLiteralsInInterpolation: 66 | EnforcedStyle: double_quotes 67 | 68 | Style/SymbolArray: 69 | EnforcedStyle: brackets 70 | 71 | Style/WordArray: 72 | EnforcedStyle: brackets 73 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.1.2 2 | 3 | - Added input validation using `dry-types` for user-facing API 4 | - Internal code optimizations 5 | 6 | ## v1.1.1 7 | 8 | - Updated gemspec metadata to include the correct homepage URL 9 | 10 | ## v1.1.0 11 | 12 | - Added block support for fallback behavior in all fetching methods 13 | 14 | ## v1.0.0 15 | 16 | - Dropped support for Rails 7.0 17 | - Dropped support for Ruby 3.1 18 | - Replaced `Kreds::BlankValueError` and `Kreds::UnknownKeyError` with `Kreds::BlankCredentialsError` and `Kreds::UnknownCredentialsError`, respectively 19 | - Added optional fallback to environment variables for `Kreds.fetch!` method 20 | - Added shortcut method `Kreds.var!` for fetching values directly from environment variables 21 | - Added `Kreds.env_fetch!` method for fetching values from credentials per Rails environment 22 | - Added `Kreds.show` method for displaying all credentials as a hash 23 | 24 | ## v0.1.0 25 | 26 | - Initial release 27 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This Code of Conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 45 | version 1.3.0, available at https://www.contributor-covenant.org/version/1/3/0/code-of-conduct.html 46 | 47 | [homepage]: https://www.contributor-covenant.org 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Issues 4 | 5 | Issues should be created only for bugs. 6 | 7 | When opening an issue: 8 | 9 | - Describe the steps to reproduce the problem. 10 | - Include the stack trace if there is an error. 11 | - Specify the versions of Kreds, Ruby, Rails, etc. 12 | 13 | Don't hesitate to add any other information that you think might be relevant. 14 | 15 | ## Pull Requests 16 | 17 | If you want to contribute an enhancement or a fix: 18 | 19 | - Fork the project on GitHub. 20 | - Make your changes, including tests. 21 | - Update the documentation if necessary. 22 | - Commit the changes and submit a pull request. 23 | 24 | By submitting a pull request, you indicate that you waive any rights or claims to the modifications made in the Kreds project and transfer the copyright of those changes to the Kreds gem copyright owners. 25 | 26 | If you are unable or unwilling to transfer these rights, please refrain from submitting a pull request. 27 | 28 | ## Local Development 29 | 30 | Use the Ruby version specified in the `kreds.gemspec` file. 31 | 32 | To install the development dependencies, run: `bin/setup` 33 | 34 | To start the developer console, run: `bin/console` 35 | 36 | To run the tests: `bin/rspec` 37 | 38 | To run the linter: `bin/rubocop` 39 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | rails_version = ENV.fetch("RAILS_VERSION", "~> 7") 6 | 7 | gem "byebug" 8 | gem "dry-types" 9 | gem "grepfruit" 10 | gem "rails", rails_version 11 | gem "rake" 12 | gem "rspec" 13 | gem "rspec-rails" 14 | gem "rubocop" 15 | gem "rubocop-md" 16 | gem "rubocop-packaging" 17 | gem "rubocop-performance" 18 | gem "rubocop-rails" 19 | gem "rubocop-rake" 20 | gem "rubocop-rspec" 21 | gem "rubocop-rspec_rails" 22 | gem "rubocop-thread_safety" 23 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2025 enjaku4 (https://github.com/enjaku4) 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kreds: Streamlined Rails Credentials Access 2 | 3 | [![Gem Version](https://badge.fury.io/rb/kreds.svg)](http://badge.fury.io/rb/kreds) 4 | [![Github Actions badge](https://github.com/brownboxdev/kreds/actions/workflows/ci.yml/badge.svg)](https://github.com/brownboxdev/kreds/actions/workflows/ci.yml) 5 | 6 | Kreds is a simpler, shorter, and safer way to access Rails credentials, with a few extra features built in. Rails credentials are a convenient way to store secrets, but retrieving them could be more intuitive — that's where Kreds comes in. 7 | 8 | **Key Features:** 9 | 10 | - Simplified credential access with clear error messages 11 | - Environment variable fallback support 12 | - Environment-scoped credentials access (production, staging, development) 13 | - Automatic blank value detection and prevention 14 | 15 | **Before and After:** 16 | 17 | ```ruby 18 | # Instead of this (long, silent failures if value is missing): 19 | Rails.application.credentials[:recaptcha][:site_key] 20 | # => nil 21 | 22 | # Or this (long, unclear errors): 23 | Rails.application.credentials[:captcha][:site_key] 24 | # => undefined method `[]' for nil:NilClass (NoMethodError) 25 | 26 | # Or even this (longer, still unclear errors): 27 | Rails.application.credentials.fetch(:recaptcha).fetch(:key) 28 | # => key not found: :key (KeyError) 29 | 30 | # You can write this (shorter, human-readable errors): 31 | Kreds.fetch!(:recaptcha, :site_key) 32 | # => Blank value in credentials: [:recaptcha][:site_key] (Kreds::BlankCredentialsError) 33 | Kreds.fetch!(:captcha, :site_key) 34 | # => Credentials key not found: [:captcha] (Kreds::UnknownCredentialsError) 35 | ``` 36 | 37 | ## Table of Contents 38 | 39 | **Gem Usage:** 40 | - [Installation](#installation) 41 | - [Credential Fetching](#credential-fetching) 42 | - [Environment-Scoped Credentials](#environment-scoped-credentials) 43 | - [Environment Variables](#environment-variables) 44 | - [Debug and Inspection](#debug-and-inspection) 45 | 46 | **Community Resources:** 47 | - [Contributing](#contributing) 48 | - [License](#license) 49 | - [Code of Conduct](#code-of-conduct) 50 | 51 | ## Installation 52 | 53 | Add Kreds to your Gemfile: 54 | 55 | ```ruby 56 | gem "kreds" 57 | ``` 58 | 59 | And then execute: 60 | 61 | ```bash 62 | bundle install 63 | ``` 64 | 65 | ## Credential Fetching 66 | 67 | **`Kreds.fetch!(*keys, var: nil, &block)`** 68 | 69 | Fetches credentials from the Rails credentials store. 70 | 71 | **Parameters:** 72 | - `*keys` - Variable number of symbols representing the key path 73 | - `var` - Optional environment variable name as fallback 74 | - `&block` - Optional block to execute if fetch fails 75 | 76 | **Returns:** The credential value 77 | 78 | **Raises:** 79 | - `Kreds::UnknownCredentialsError` - if the key path doesn't exist 80 | - `Kreds::BlankCredentialsError` - if the value exists but is blank 81 | 82 | ```ruby 83 | # Basic usage 84 | Kreds.fetch!(:aws, :s3, :credentials, :access_key_id) 85 | 86 | # With environment variable fallback 87 | Kreds.fetch!(:aws, :access_key_id, var: "AWS_ACCESS_KEY_ID") 88 | 89 | # With block 90 | Kreds.fetch!(:api_key) do 91 | raise MyCustomError, "API key not configured" 92 | end 93 | ``` 94 | 95 | ## Environment-Scoped Credentials 96 | 97 | **`Kreds.env_fetch!(*keys, var: nil, &block)`** 98 | 99 | Fetches credentials scoped by the current Rails environment (e.g., `:production`, `:staging`, `:development`). 100 | 101 | **Parameters:** Same as `fetch!` 102 | 103 | **Returns:** The credential value from `Rails.application.credentials[Rails.env]` followed by the provided key path 104 | 105 | **Raises:** Same exceptions as `fetch!` 106 | 107 | ```ruby 108 | # Looks in credentials[:production][:recaptcha][:site_key] in production 109 | Kreds.env_fetch!(:recaptcha, :site_key) 110 | ``` 111 | 112 | ## Environment Variables 113 | 114 | **`Kreds.var!(name, &block)`** 115 | 116 | Fetches a value directly from environment variables. 117 | 118 | **Parameters:** 119 | - `name` - Environment variable name 120 | - `&block` - Optional block to execute if variable is missing/blank 121 | 122 | **Returns:** The environment variable value 123 | 124 | **Raises:** 125 | - `Kreds::UnknownEnvironmentVariableError` - if the variable doesn't exist 126 | - `Kreds::BlankEnvironmentVariableError` - if the variable exists but is blank 127 | 128 | ```ruby 129 | # Direct environment variable access 130 | Kreds.var!("AWS_ACCESS_KEY_ID") 131 | 132 | # With block 133 | Kreds.var!("THREADS") { 1 } 134 | ``` 135 | 136 | ## Debug and Inspection 137 | 138 | **`Kreds.show`** 139 | 140 | Useful for debugging and exploring available credentials in the Rails console. 141 | 142 | **Returns:** Hash containing all credentials 143 | 144 | ```ruby 145 | Kreds.show 146 | # => { aws: { access_key_id: "...", secret_access_key: "..." }, ... } 147 | ``` 148 | 149 | ## Contributing 150 | 151 | ### Getting Help 152 | Have a question or need assistance? Open a discussion in our [discussions section](https://github.com/brownboxdev/kreds/discussions) for: 153 | - Usage questions 154 | - Implementation guidance 155 | - Feature suggestions 156 | 157 | ### Reporting Issues 158 | Found a bug? Please [create an issue](https://github.com/brownboxdev/kreds/issues) with: 159 | - A clear description of the problem 160 | - Steps to reproduce the issue 161 | - Your environment details (Rails version, Ruby version, etc.) 162 | 163 | ### Contributing Code 164 | Ready to contribute? You can: 165 | - Fix bugs by submitting pull requests 166 | - Improve documentation 167 | - Add new features (please discuss first in our [discussions section](https://github.com/brownboxdev/kreds/discussions)) 168 | 169 | Before contributing, please read the [contributing guidelines](https://github.com/brownboxdev/kreds/blob/master/CONTRIBUTING.md) 170 | 171 | ## License 172 | 173 | The gem is available as open source under the terms of the [MIT License](https://github.com/brownboxdev/kreds/blob/master/LICENSE.txt). 174 | 175 | ## Code of Conduct 176 | 177 | Everyone interacting in the Kreds project is expected to follow the [code of conduct](https://github.com/brownboxdev/kreds/blob/master/CODE_OF_CONDUCT.md). 178 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | require "rubocop/rake_task" 7 | 8 | RuboCop::RakeTask.new 9 | 10 | task default: [:spec, :rubocop] 11 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Kreds provides security updates only for the latest major version. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | If you discover a vulnerability in Kreds, you can report it via the [GitHub security advisories page](https://github.com/brownboxdev/kreds/security/advisories) 10 | 11 | To ensure user safety, please do not publicly disclose the vulnerability until it has been resolved and a patched version released. 12 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "kreds" 5 | 6 | require_relative "../spec/spec_helper" 7 | 8 | require "irb" 9 | IRB.start(__FILE__) 10 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 4 | 5 | bundle_binstub = File.expand_path("bundle", __dir__) 6 | 7 | if File.file?(bundle_binstub) 8 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 9 | load(bundle_binstub) 10 | else 11 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 12 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 13 | end 14 | end 15 | 16 | require "rubygems" 17 | require "bundler/setup" 18 | 19 | load Gem.bin_path("rspec-core", "rspec") 20 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 4 | 5 | bundle_binstub = File.expand_path("bundle", __dir__) 6 | 7 | if File.file?(bundle_binstub) 8 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 9 | load(bundle_binstub) 10 | else 11 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 12 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 13 | end 14 | end 15 | 16 | require "rubygems" 17 | require "bundler/setup" 18 | 19 | load Gem.bin_path("rubocop", "rubocop") 20 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | -------------------------------------------------------------------------------- /kreds.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/kreds/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "kreds" 5 | spec.version = Kreds::VERSION 6 | spec.authors = ["enjaku4"] 7 | spec.homepage = "https://github.com/brownboxdev/kreds" 8 | spec.metadata["homepage_uri"] = spec.homepage 9 | spec.metadata["source_code_uri"] = spec.homepage 10 | spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md" 11 | spec.metadata["rubygems_mfa_required"] = "true" 12 | spec.summary = "The missing shorthand for Rails credentials" 13 | spec.license = "MIT" 14 | spec.required_ruby_version = ">= 3.2", "< 3.5" 15 | 16 | spec.files = [ 17 | "kreds.gemspec", "README.md", "CHANGELOG.md", "LICENSE.txt" 18 | ] + Dir.glob("lib/**/*") 19 | 20 | spec.require_paths = ["lib"] 21 | 22 | spec.add_dependency "dry-types", "~> 1.8" 23 | spec.add_dependency "rails", ">= 7.1", "< 8.1" 24 | end 25 | -------------------------------------------------------------------------------- /lib/kreds.rb: -------------------------------------------------------------------------------- 1 | require_relative "kreds/version" 2 | 3 | require "rails" 4 | 5 | require_relative "kreds/inputs" 6 | require_relative "kreds/fetch" 7 | require_relative "kreds/show" 8 | 9 | module Kreds 10 | class Error < StandardError; end 11 | class InvalidArgumentError < Error; end 12 | class BlankCredentialsError < Error; end 13 | class BlankEnvironmentVariableError < Error; end 14 | class UnknownCredentialsError < Error; end 15 | class UnknownEnvironmentVariableError < Error; end 16 | 17 | extend ::Kreds::Show 18 | extend ::Kreds::Fetch 19 | end 20 | -------------------------------------------------------------------------------- /lib/kreds/fetch.rb: -------------------------------------------------------------------------------- 1 | module Kreds 2 | module Fetch 3 | def fetch!(*keys, var: nil, &) 4 | symbolized_keys = Kreds::Inputs.process(keys, as: :symbol_array) 5 | 6 | path = [] 7 | 8 | symbolized_keys.reduce(Kreds.show) do |hash, key| 9 | path << key 10 | fetch_key(hash, key, path, symbolized_keys) 11 | end 12 | rescue Kreds::BlankCredentialsError, Kreds::UnknownCredentialsError => e 13 | fallback_to_var(e, Kreds::Inputs.process(var, as: :string, optional: true), &) 14 | end 15 | 16 | def env_fetch!(*keys, var: nil, &) 17 | fetch!(Rails.env, *keys, var:, &) 18 | end 19 | 20 | def var!(var, &) 21 | value = ENV.fetch(Kreds::Inputs.process(var, as: :string)) 22 | 23 | return raise_or_yield(Kreds::BlankEnvironmentVariableError.new("Blank value in environment variable: #{var.inspect}"), &) if value.blank? 24 | 25 | value 26 | rescue KeyError 27 | raise_or_yield(Kreds::UnknownEnvironmentVariableError.new("Environment variable not found: #{var.inspect}"), &) 28 | end 29 | 30 | private 31 | 32 | def fetch_key(hash, key, path, keys) 33 | value = hash.fetch(key) 34 | 35 | raise Kreds::BlankCredentialsError, "Blank value in credentials: #{path_to_s(path)}" if value.blank? 36 | raise Kreds::UnknownCredentialsError, "Credentials key not found: #{path_to_s(path)}[:#{keys[path.size]}]" unless value.is_a?(Hash) || keys == path 37 | 38 | value 39 | rescue KeyError 40 | raise Kreds::UnknownCredentialsError, "Credentials key not found: #{path_to_s(path)}" 41 | end 42 | 43 | def fallback_to_var(error, var, &) 44 | return raise_or_yield(error, &) if var.blank? 45 | 46 | var!(var, &) 47 | rescue Kreds::BlankEnvironmentVariableError, Kreds::UnknownEnvironmentVariableError => e 48 | raise_or_yield(Kreds::Error.new("#{error.message}, #{e.message}"), &) 49 | end 50 | 51 | def raise_or_yield(error, &) 52 | block_given? ? yield : raise(error) 53 | end 54 | 55 | def path_to_s(path) 56 | "[:#{path.join("][:")}]" 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/kreds/inputs.rb: -------------------------------------------------------------------------------- 1 | require "dry-types" 2 | 3 | module Kreds 4 | module Inputs 5 | extend self 6 | 7 | include Dry.Types() 8 | 9 | TYPES = { 10 | symbol_array: -> { self::Array.of(self::Coercible::Symbol).constrained(min_size: 1) }, 11 | string: -> { self::Strict::String }, 12 | boolean: -> { self::Strict::Bool } 13 | }.freeze 14 | 15 | def process(value, as:, optional: false) 16 | checker = type_for(as) 17 | checker = checker.optional if optional 18 | 19 | checker[value] 20 | rescue Dry::Types::CoercionError => e 21 | raise Kreds::InvalidArgumentError, e 22 | end 23 | 24 | private 25 | 26 | def type_for(name) = Kreds::Inputs::TYPES.fetch(name).call 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/kreds/show.rb: -------------------------------------------------------------------------------- 1 | module Kreds 2 | module Show 3 | def show 4 | Rails.application.credentials.as_json.deep_symbolize_keys 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/kreds/version.rb: -------------------------------------------------------------------------------- 1 | module Kreds 2 | VERSION = "1.1.2" 3 | end 4 | -------------------------------------------------------------------------------- /spec/kreds_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Kreds do 2 | describe ".show" do 3 | let(:credentials) do 4 | { 5 | foo: { bar: { baz: 42 } }, 6 | bad: nil, 7 | secret_key_base: "dummy_secret_key_base", 8 | test: { foo: [1, 2, 3] } 9 | } 10 | end 11 | 12 | it "returns the credentials structure" do 13 | expect(described_class.show).to eq(credentials) 14 | end 15 | end 16 | 17 | describe ".fetch!" do 18 | describe "input validation" do 19 | it "raises error for empty keys" do 20 | expect { described_class.fetch!([]) }.to raise_error(Kreds::InvalidArgumentError, /to_sym/) 21 | end 22 | 23 | it "raises error for invalid key types" do 24 | expect { described_class.fetch!(:foo, 42) }.to raise_error(Kreds::InvalidArgumentError, /to_sym/) 25 | end 26 | end 27 | 28 | context "without environment variable fallback" do 29 | it "returns value for symbol keys" do 30 | expect(described_class.fetch!(:foo, :bar, :baz)).to eq(42) 31 | end 32 | 33 | it "returns value for string keys" do 34 | expect(described_class.fetch!("foo", "bar", "baz")).to eq(42) 35 | end 36 | 37 | it "raises error for missing end key" do 38 | expect { described_class.fetch!(:foo, :bar, :bad) } 39 | .to raise_error(Kreds::UnknownCredentialsError, "Credentials key not found: [:foo][:bar][:bad]") 40 | end 41 | 42 | it "raises error for missing middle key" do 43 | expect { described_class.fetch!(:foo, :bad, :baz) } 44 | .to raise_error(Kreds::UnknownCredentialsError, "Credentials key not found: [:foo][:bad]") 45 | end 46 | 47 | it "raises error for blank value" do 48 | expect { described_class.fetch!(:bad) } 49 | .to raise_error(Kreds::BlankCredentialsError, "Blank value in credentials: [:bad]") 50 | end 51 | end 52 | 53 | context "with environment variable fallback" do 54 | it "raises error for invalid var type when fallback is needed" do 55 | expect { described_class.fetch!(:bad, var: 42) }.to raise_error(Kreds::InvalidArgumentError, /type\?\(String/) 56 | end 57 | 58 | context "when env var exists" do 59 | it "returns credentials when available" do 60 | expect(described_class.fetch!(:foo, :bar, :baz, var: "RAILS_ENV")).to eq(42) 61 | end 62 | 63 | it "returns env var when credentials blank" do 64 | expect(described_class.fetch!(:bad, var: "RAILS_ENV")).to eq("test") 65 | end 66 | 67 | it "returns env var when credentials missing" do 68 | expect(described_class.fetch!(:foo, :bar, :bad, var: "RAILS_ENV")).to eq("test") 69 | end 70 | end 71 | 72 | context "when env var missing" do 73 | it "returns credentials when available" do 74 | expect(described_class.fetch!(:foo, :bar, :baz, var: "MISSING_VAR")).to eq(42) 75 | end 76 | 77 | it "raises combined error for blank credentials" do 78 | expect { described_class.fetch!(:bad, var: "MISSING_VAR") } 79 | .to raise_error(Kreds::Error, /Blank value in credentials.*Environment variable not found/) 80 | end 81 | 82 | it "raises combined error for missing credentials" do 83 | expect { described_class.fetch!(:foo, :bar, :bad, var: "MISSING_VAR") } 84 | .to raise_error(Kreds::Error, /Credentials key not found.*Environment variable not found/) 85 | end 86 | end 87 | 88 | context "when env var blank" do 89 | around do |example| 90 | ENV["BLANK_VAR"] = "" 91 | example.run 92 | ENV.delete("BLANK_VAR") 93 | end 94 | 95 | it "returns credentials when available" do 96 | expect(described_class.fetch!(:foo, :bar, :baz, var: "BLANK_VAR")).to eq(42) 97 | end 98 | 99 | it "raises combined error for blank credentials" do 100 | expect { described_class.fetch!(:bad, var: "BLANK_VAR") } 101 | .to raise_error(Kreds::Error, /Blank value in credentials.*Blank value in environment variable/) 102 | end 103 | end 104 | end 105 | 106 | context "with block" do 107 | it "returns block value when both credentials and env var fail" do 108 | result = described_class.fetch!(:missing, var: "MISSING_VAR") { "default" } 109 | expect(result).to eq("default") 110 | end 111 | end 112 | 113 | context "when handling edge cases" do 114 | it "raises error when trying to access non-hash as hash" do 115 | expect { described_class.fetch!(:foo, :bar, :baz, :extra) } 116 | .to raise_error(Kreds::UnknownCredentialsError, "Credentials key not found: [:foo][:bar][:baz][:extra]") 117 | end 118 | 119 | it "works with single key" do 120 | expect(described_class.fetch!(:foo)).to eq({ bar: { baz: 42 } }) 121 | end 122 | end 123 | end 124 | 125 | describe ".env_fetch!" do 126 | it "raises error for invalid key types" do 127 | expect { described_class.env_fetch!(:foo, 42) }.to raise_error(Kreds::InvalidArgumentError, /to_sym/) 128 | end 129 | 130 | it "raises error for invalid var type when fallback is needed" do 131 | expect { described_class.env_fetch!(:bad, var: 42) }.to raise_error(Kreds::InvalidArgumentError, /type\?\(String/) 132 | end 133 | 134 | context "without var" do 135 | it "returns entire test environment hash when no keys provided" do 136 | expect(described_class.env_fetch!).to eq({ foo: [1, 2, 3] }) 137 | end 138 | 139 | it "returns specific value from test environment" do 140 | expect(described_class.env_fetch!(:foo)).to eq([1, 2, 3]) 141 | end 142 | 143 | it "raises error for missing key in test environment" do 144 | expect { described_class.env_fetch!(:foo, :bar, :bad) } 145 | .to raise_error(Kreds::UnknownCredentialsError, "Credentials key not found: [:test][:foo][:bar]") 146 | end 147 | end 148 | 149 | context "with var" do 150 | it "works like regular fetch! with environment prefix" do 151 | expect(described_class.env_fetch!(:foo, var: "RAILS_ENV")).to eq([1, 2, 3]) 152 | end 153 | 154 | it "falls back to env var when credentials missing" do 155 | expect(described_class.env_fetch!(:missing, var: "RAILS_ENV")).to eq("test") 156 | end 157 | end 158 | 159 | context "with block" do 160 | it "returns block value when both fail" do 161 | result = described_class.env_fetch!(:missing, var: "MISSING_VAR") { "default" } 162 | expect(result).to eq("default") 163 | end 164 | end 165 | end 166 | 167 | describe ".var!" do 168 | it "raises error for nil var" do 169 | expect { described_class.var!(nil) }.to raise_error(Kreds::InvalidArgumentError, /type\?\(String/) 170 | end 171 | 172 | it "raises error for non-string var" do 173 | expect { described_class.var!(42) }.to raise_error(Kreds::InvalidArgumentError, /type\?\(String/) 174 | end 175 | 176 | it "returns environment variable value" do 177 | expect(described_class.var!("RAILS_ENV")).to eq("test") 178 | end 179 | 180 | it "raises error for missing variable" do 181 | expect { described_class.var!("MISSING_VAR") } 182 | .to raise_error(Kreds::UnknownEnvironmentVariableError, /Environment variable not found/) 183 | end 184 | 185 | it "raises error for blank variable" do 186 | ENV["BLANK_VAR"] = "" 187 | expect { described_class.var!("BLANK_VAR") } 188 | .to raise_error(Kreds::BlankEnvironmentVariableError, /Blank value in environment variable/) 189 | ENV.delete("BLANK_VAR") 190 | end 191 | 192 | context "with block" do 193 | it "returns block value for missing variable" do 194 | result = described_class.var!("MISSING_VAR") { "default" } 195 | expect(result).to eq("default") 196 | end 197 | end 198 | end 199 | end 200 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] = "test" 2 | 3 | require "kreds" 4 | require "byebug" 5 | require "rspec" 6 | 7 | RSpec.configure do |config| 8 | config.example_status_persistence_file_path = ".rspec_status" 9 | 10 | config.disable_monkey_patching! 11 | 12 | config.expect_with :rspec do |c| 13 | c.syntax = :expect 14 | end 15 | end 16 | 17 | require "#{File.dirname(__FILE__)}/support/test_app" 18 | -------------------------------------------------------------------------------- /spec/support/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | E4UfZSxRc4Gnt0GE1g6nS6vuSw7+d/BHiEoTfw1QhDkIH1q/jTV6q6DBOFqD+R1/RhPh/MS1oDS40ntMGaSWi6GqXlsLWBh562oZPPfeHoG3mfUN1ydvuvOjLyW6OTHBTf8M/29+0hbQvgZvtgGwdUql4MkTBzjEQikNjaAu+nRxiuUodDqB2Ie5WUUolaRCcqVZhFwbIV8q8FnJQR4XlLbuMQtOfrGR+fUOv3ju5OYgPQC+C+QCwjVW00oypNQQwYY6dA==--k/J2j87iAV0Uhe7V--vEKZG5xUym2xts8bPknnXg== -------------------------------------------------------------------------------- /spec/support/master.key: -------------------------------------------------------------------------------- 1 | 88772818bc7292d3de0efce993e097b3 2 | -------------------------------------------------------------------------------- /spec/support/test_app.rb: -------------------------------------------------------------------------------- 1 | class TestApp < Rails::Application 2 | config.eager_load = false 3 | config.secret_key_base = "dummy_secret_key_base" 4 | 5 | config.credentials.content_path = Rails.root.join("spec/support/credentials.yml.enc") 6 | config.credentials.key_path = Rails.root.join("spec/support/master.key") 7 | end 8 | 9 | Rails.application ||= TestApp.initialize! 10 | --------------------------------------------------------------------------------