Skip to content

Instantly share code, notes, and snippets.

@aks
Last active February 17, 2023 00:11
Show Gist options
  • Save aks/e9ce8222abb64bff2f8ce4c2b1ac6910 to your computer and use it in GitHub Desktop.
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
#!/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 <aks@stebbens.org>
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".
d development
t test
q qa
st staging
p production
a all environments
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|t) set_env 'test' ;;
qa|q) set_env 'qa' ;;
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