Last active
September 4, 2024 18:01
-
-
Save aks/e9ce8222abb64bff2f8ce4c2b1ac6910 to your computer and use it in GitHub Desktop.
Script to help manage, create, list, and cycle Rails environment credentials and their keys
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# rails-cred [show|edit|cycle|init|check|list] [env|all] | |
# | |
# Makes showing, editing, checking, and cycling Rails credentials | |
# and their secrets easier | |
# | |
# Copyright 2023 Alan K. Stebbens <[email protected]> | |
PROG="${0##*/}" | |
DIR="${0%/*}" | |
ALL_ENV_NAMES="/tmp/$PROG-all-env-names" | |
ALL_CRED_NAMES="/tmp/$PROG-all-cred-names" | |
NO_CRED_ENV_NAMES="/tmp/$PROG-no-cred-env-names" | |
NO_ENV_CRED_NAMES="/tmp/$PROG-no-env-cred-names" | |
usage() { | |
cat 1>&2 <<USAGE | |
usage: $PROG [OPTIONS] [ACTION] [ENV] | |
Applies ACTION to the Rails credentials file for the given or current environment. | |
ACTION can be one of: | |
show (s): show the credentials file contents | |
edit (e): open the EDITOR on the current crentials file contents | |
list (l): list the environment names (from config/environments) | |
check (c): check correspondence between environment names and credentials | |
cycle : cycles the credentials file using a new secret. | |
init : initialize the credentials file for the given environment. | |
The "cycle" or "init" actions require confirmation before changes | |
are made. The confirmation is interactive, unless '-y' is given. | |
If the ACTION is not provided, "show" is used by default. | |
The environment can be named on the command line, or will be taken | |
from RAILS_ENV. If RAILS_ENV is not defined, "development" is the | |
ultimate default. | |
The ACTION and ENV can be given in any order, and unambiguously | |
abbreviated, except for 'cycle' and 'init', which must be spelled | |
out because they should not be "convenient". | |
a all environments | |
d development | |
p production | |
q qa | |
sa sandbox | |
st staging | |
te test | |
tu tugboat | |
The options can be used to affect the process and result: | |
-h show the help and exit | |
-k KEY Use KEY for the new encryption (must be exactly 32 characters) | |
-n show the commands but do not run them | |
-v be verbose | |
-y assume "yes" to confirmation prompts (when making changes) | |
Examples: | |
$PROG show dev # or s d | |
$PROG show all # or s a | |
$PROG edit prod # or e p | |
$PROG edit all # or e a | |
$PROG init qa | |
$PROG cycle test | |
$PROG cycle all | |
USAGE | |
exit | |
} | |
warn() { | |
case "$*" in | |
"* ") echo 1>&2 -e -n "$*" ;; | |
*) echo 1>&2 -e "$*" ;; | |
esac | |
} | |
warnf() { printf 1>&2 "$@" ; } | |
vwarn() { (( verbose )) && warn "$*" ; } | |
vwarnf() { (( verbose )) && warnf "$@" ; } | |
error() { warn "$*" ; exit -1 ; } | |
run() { | |
if (( norun )) ; then | |
warn "(norun) $*" | |
else | |
vwarn "--> $*" | |
eval "$*" | |
fi | |
} | |
set_cmd() { | |
[[ -z "$cmd" ]] || error "Only one action can be given at a time!" | |
cmd="$1" | |
} | |
set_env() { | |
[[ -z "$env" ]] || error "Only one environment can be given at a time" | |
env="$1" | |
} | |
list_environments() { | |
warn "Environments configured:" | |
for env in `all_env_names` ; do | |
if (( verbose )) ; then | |
ls -l config/environments/$env.rb | |
else | |
warn " $env" | |
fi | |
done | |
warn '' | |
warn "Environments with credentials:" | |
for cred in `all_cred_names` ; do | |
if (( verbose )); then | |
ls -l config/credentials/$cred.{key,yml.enc} | |
else | |
warn " $cred" | |
fi | |
done | |
warn '' | |
check_environments | |
} | |
check_environments() { | |
errors=0 | |
no_cred_envs=(`no_cred_env_names`) | |
if (( ${#no_cred_envs[*]} > 0 )); then | |
warn "Warning: these environents have no credentials:" | |
for env in "${no_cred_envs[@]}" ; do | |
warn " $env" | |
let errors+=1 | |
done | |
warn '' | |
fi | |
no_env_creds=(`no_env_cred_names`) | |
if (( ${#no_env_creds[@]} > 0 )) ; then | |
warn "Warning: these credentials have no corresponding environment:" | |
for cred in "${no_env_creds[@]}" ; do | |
warn " $cred" | |
let errors+=1 | |
done | |
warn '' | |
fi | |
for cred in `all_cred_names` ; do | |
key_path="config/credentials/$cred.key" | |
if [[ ! -e $key_path ]] ; then | |
warn "The credential key '$key_path' is missing!" | |
let errors+=1 | |
fi | |
done | |
if (( errors > 0 )) ; then | |
warn "$errors errors" | |
else | |
warn "no errors" | |
vwarn "Each environment has a corresponding credential" | |
fi | |
exit $errors | |
} | |
all_env_names() { | |
cache_names $ALL_ENV_NAMES 'get_all_env_names' | |
} | |
all_cred_names() { | |
cache_names $ALL_CRED_NAMES 'get_all_cred_names' | |
} | |
no_cred_env_names() { | |
cache_names $NO_CRED_ENV_NAMES 'diff_names -23' | |
} | |
no_env_cred_names() { | |
cache_names $NO_ENV_CRED_NAMES 'diff_names -13' | |
} | |
get_all_env_names() { | |
get_names "config/environments/*.rb" '.rb' | |
} | |
get_all_cred_names() { | |
get_names "config/credentials/*.yml.enc" '.yml.enc' | |
} | |
# PATH EXT | |
get_names() { | |
eval "\\ls -1 $1" | xargs -I % basename % $2 | sort | |
} | |
diff_names() { | |
[[ -e $ALL_ENV_NAMES ]] || all_env_names >/dev/null | |
[[ -e $ALL_CRED_NAMES ]] || all_cred_names >/dev/null | |
\comm $1 $ALL_ENV_NAMES $ALL_CRED_NAMES | |
} | |
# args: cache_file get_names_func | |
cache_names() { | |
$2 | tee $1 | |
} | |
show_credentials() { | |
apply_func "Show" show_env_credential | |
} | |
show_env_credential() { | |
run "bundle exec rails credentials:show -e $env" | |
} | |
edit_credentials() { | |
apply_func "Edit" edit_env_credential "$1" | |
} | |
edit_env_credential() { | |
editor="${1:-${EDITOR:-vim}}" | |
case "$env" in | |
development) | |
# when cycling development (default) environment, we must tell rails to use another | |
# environment beforehand, because we could be doing "init" or "cycle", in which case | |
# the credentials file is temporarily missing | |
other_env=`some_non_dev_env` | |
run "RAILS_ENV=$other_env EDITOR=\"$editor\" bundle exec rails credentials:edit -e $env" | |
;; | |
*) | |
run "EDITOR=\"$editor\" bundle exec rails credentials:edit -e $env" | |
;; | |
esac | |
} | |
some_non_dev_env() { | |
all_env_names | grep -v development | head -1 | |
} | |
cred_file_path() { | |
echo "config/credentials/$env.yml.enc" | |
} | |
cred_key_path() { | |
echo "config/credentials/$env.key" | |
} | |
init_credentials() { | |
apply_func "Init" init_env_credential | |
} | |
init_env_credential() { | |
local cfp=`cred_file_path` | |
local ckp=`cred_key_path` | |
confirm_file_change_or_quit 'Initialize' "$cfp" || return 1 | |
remove_existing_file "$cfp" | |
remove_existing_file "$ckp" | |
new_secret_key "$ckp" | |
edit_credentials "vim -c ':x'" | |
} | |
cycle_credentials() { | |
apply_func "Cycle" cycle_env_credential | |
} | |
cycle_env_credential() { | |
local cfp=`cred_file_path` | |
local ckp=`cred_key_path` | |
confirm_file_change_or_quit 'Cycle secrets on' "$cfp" || return 1 | |
remove_existing_file "$cfp.new" | |
remove_existing_file "$ckp.new" | |
if [[ -e "$cfp" ]] ; then | |
run "show_credentials > $cfp.new" | |
run "mv $cfp $cfp.old" | |
run "mv $ckp $ckp.old" | |
else | |
touch "$cfp.new" # ensure empty file at least | |
fi | |
new_secret_key "$ckp" | |
edit_credentials "vim -c ':%d' -c ':read $cfp.new' -c '1:d' -c ':x' " | |
if (( $? == 0 )) ; then | |
run "rm -f '$cfp.new' '$cfp.old' '$ckp.old'" | |
fi | |
} | |
remove_existing_file() { | |
local file="$1" | |
[[ -e "$file" ]] && run "rm -f '$file'" | |
} | |
# args: ACTION_MSG ACTION_FUNC ACTION_FUNC_ARGS | |
apply_func() { | |
local action_msg="$1" | |
local action_func="$2" | |
local action_func_args="$3" | |
case "$env" in | |
all) | |
local all_env_names=( `all_env_names` ) | |
warn "$action_msg all environments: ${all_env_names[@]}" | |
local num_names=${#all_env_names[*]} | |
local namex=0 | |
for env in "${all_env_names[@]}" ; do | |
let namex+=1 | |
vwarn "$action_msg $env" | |
$action_func "$action_func_args" | |
if (( namex < num_names )) ; then | |
warn '' | |
warn "-----------------------------------------------------" | |
fi | |
done | |
;; | |
*) | |
$action_func "$action_func_args" | |
;; | |
esac | |
} | |
# new_secret_key FILE | |
new_secret_key() { | |
local key_file="$1" | |
if [[ -n "$new_key" ]] ; then | |
run "echo \"$new_key\" >$key_file" | |
warn "Using given secret '$new_key' for $key_file" | |
else | |
run "bundle exec rails secret | ( head -c 32 ; echo '' ) >$key_file" | |
warn "Created new secret '$new_key' for $key_file" | |
fi | |
run "chmod 600 $key_file" | |
} | |
# confirm_file_change_or_quit ACTION FILE | |
confirm_file_change_or_quit() { | |
confirm_it "$1 $2" || { warn "$2 unchanged\n" ; return 1 ; } | |
} | |
confirm_it() { | |
(( confirmed )) && return 0 | |
local ans | |
(( $# > 0 )) && warn "$1; " | |
while read -p "proceed? [yN]" ans ; do | |
case "${ans:-no}" in | |
yes|y) return 0 ;; | |
no|n) return 1 ;; | |
*) warn "Please answer yes or no." ;; | |
esac | |
done || error "nothing done!" | |
} | |
norun= verbose= confirmed= all_envs= new_key= | |
while getopts 'hk:nvy' opt ; do | |
case "$opt" in | |
h) usage ;; | |
k) new_key="$OPTARG" ;; | |
n) norun=1 ;; | |
v) verbose=1 ;; | |
y) confirmed=1 ;; | |
esac | |
done | |
shift $(( OPTIND - 1)) | |
while (( $# > 0 )) ; do | |
arg="$1" | |
shift | |
case "$arg" in | |
# actions | |
show|sh|s) set_cmd 'show' ;; | |
edit|ed|e) set_cmd 'edit' ;; | |
list|lis|ls|l) set_cmd 'list' ;; | |
check|ch|c) set_cmd 'check' ;; | |
cycle) set_cmd 'cycle' ;; | |
init) set_cmd 'init' ;; | |
# environments | |
development|devel|dev|d) set_env 'development' ;; | |
test|te) set_env 'test' ;; | |
qa|q) set_env 'qa' ;; | |
tugboat|tu) set_env 'tugboat' ;; | |
sandbox|sa) set_env 'sandbox' ;; | |
staging|stag|stg|st) set_env 'staging' ;; | |
production|prod|p) set_env 'production' ;; | |
all|al|a) set_env 'all' ; all_envs=1 ;; | |
*) error "Unknown action or environment: '$arg'" ;; | |
esac | |
done | |
[[ -n "$cmd" ]] || cmd='show' | |
[[ -n "$env" ]] || env="${RAILS_ENV:-development}" | |
[[ -n "$new_key" ]] && warn "Using key '$new_key' for the new secret(s)" | |
case "$cmd" in | |
show) show_credentials ;; | |
edit) edit_credentials ;; | |
list) list_environments ;; | |
check) check_environments ;; | |
init) init_credentials ;; | |
cycle) cycle_credentials ;; | |
*) error "Unknown action: $cmd !!" | |
esac | |
exit |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment